From cdf2d22243d935dd46df9a75da73d6b62d2ebe51 Mon Sep 17 00:00:00 2001 From: Yahnis Elsts Date: Tue, 27 Dec 2016 18:03:06 +0200 Subject: [PATCH] Refactoring GitHub and BitBucket support. Move most GitHub and BitBucket stuff to a general "VCS checker" class and put service-specific logic in API classes that follow a common interface. Rationale: Upon further reflection, there's no need to have different theme & plugin checker implementations for each Git hosting service. The overall update detection algorithm stays the same. Only the API and authentication are different. Not entirely happy with the code duplication in Vcs_PluginUpdateChecker and Vcs_ThemeUpdateChecker. Traits would be one solution, but can't use that in PHP 5.2. There's probably a "good enough" way to achieve the same thing through composition, but I haven't figured it out yet. For private GH repositories, use setAuthentication('access_token_here') instead of setAccessToken(). --- Puc/v4/Factory.php | 66 +++++- Puc/v4/GitHub/PluginUpdateChecker.php | 224 ------------------ Puc/v4/GitHub/ThemeUpdateChecker.php | 99 -------- Puc/v4/{VcsApi.php => Vcs/Api.php} | 91 ++++++- Puc/v4/Vcs/BaseChecker.php | 22 ++ .../Api.php => Vcs/BitBucketApi.php} | 100 +++++--- Puc/v4/{GitHub/Api.php => Vcs/GitHubApi.php} | 69 ++++-- .../PluginUpdateChecker.php | 109 ++++----- .../{VcsReference.php => Vcs/Reference.php} | 4 +- Puc/v4/Vcs/ThemeUpdateChecker.php | 99 ++++++++ plugin-update-checker.php | 8 +- 11 files changed, 434 insertions(+), 457 deletions(-) delete mode 100644 Puc/v4/GitHub/PluginUpdateChecker.php delete mode 100644 Puc/v4/GitHub/ThemeUpdateChecker.php rename Puc/v4/{VcsApi.php => Vcs/Api.php} (62%) create mode 100644 Puc/v4/Vcs/BaseChecker.php rename Puc/v4/{BitBucket/Api.php => Vcs/BitBucketApi.php} (65%) rename Puc/v4/{GitHub/Api.php => Vcs/GitHubApi.php} (75%) rename Puc/v4/{BitBucket => Vcs}/PluginUpdateChecker.php (68%) rename Puc/v4/{VcsReference.php => Vcs/Reference.php} (91%) create mode 100644 Puc/v4/Vcs/ThemeUpdateChecker.php diff --git a/Puc/v4/Factory.php b/Puc/v4/Factory.php index 7293299..147150e 100644 --- a/Puc/v4/Factory.php +++ b/Puc/v4/Factory.php @@ -23,13 +23,13 @@ if ( !class_exists('Puc_v4_Factory', false) ): * * @see PluginUpdateChecker::__construct() * - * @param string $metadataUrl The URL of the metadata file, or a GitHub repository, etc. + * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source. * @param string $fullPath Full path to the main plugin file or to the theme directory. * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory. * @param int $checkPeriod How often to check for updates (in hours). * @param string $optionName Where to store book-keeping info about update checks. * @param string $muPluginFile The plugin filename relative to the mu-plugins directory. - * @return Puc_v4_Plugin_UpdateChecker|Puc_v4_Theme_UpdateChecker + * @return Puc_v4_Plugin_UpdateChecker|Puc_v4_Theme_UpdateChecker|Puc_v4_Vcs_BaseChecker */ public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') { $fullPath = wp_normalize_path($fullPath); @@ -68,31 +68,63 @@ if ( !class_exists('Puc_v4_Factory', false) ): } } - $class = null; + $checkerClass = null; + $apiClass = null; if ( empty($service) ) { //The default is to get update information from a remote JSON file. - $class = $type . '_UpdateChecker'; + $checkerClass = $type . '_UpdateChecker'; } else { - $class = $service . '_' . $type . 'UpdateChecker'; + //You can also use a VCS repository like GitHub. + $checkerClass = 'Vcs_' . $type . 'UpdateChecker'; + $apiClass = $service . 'Api'; } - if ( !isset(self::$classVersions[$class][self::$greatestCompatVersion]) ) { + $checkerClass = self::getCompatibleClass($checkerClass); + if ( !$checkerClass ) { trigger_error( sprintf( - 'PUC %s does not support updates for %ss hosted on %s', + 'PUC %s does not support updates for %ss %s', htmlentities(self::$greatestCompatVersion), strtolower($type), - $service + $service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata' ), E_USER_ERROR ); return null; } - $class = self::$classVersions[$class][self::$greatestCompatVersion]; - return new $class($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile); + if ( !isset($apiClass) ) { + //Plain old update checker. + return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile); + } else { + //VCS checker + an API client. + $apiClass = self::getCompatibleClass($apiClass); + if ( !$apiClass ) { + trigger_error(sprintf( + 'PUC %s does not support %s', + htmlentities(self::$greatestCompatVersion), + htmlentities($service) + ), E_USER_ERROR); + return null; + } + + return new $checkerClass( + new $apiClass($metadataUrl), + $id, + $slug, + $checkPeriod, + $optionName, + $muPluginFile + ); + } } + /** + * Check if the path points to something inside the "plugins" or "mu-plugins" directories. + * + * @param string $absolutePath + * @return bool + */ protected static function isPluginFile($absolutePath) { $pluginDir = wp_normalize_path(WP_PLUGIN_DIR); $muPluginDir = wp_normalize_path(WPMU_PLUGIN_DIR); @@ -101,6 +133,20 @@ if ( !class_exists('Puc_v4_Factory', false) ): return (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0); } + /** + * Get the latest version of the specified class that has the same major version number + * as this factory class. + * + * @param string $class Partial class name. + * @return string|null Full class name. + */ + protected static function getCompatibleClass($class) { + if ( isset(self::$classVersions[$class][self::$greatestCompatVersion]) ) { + return self::$classVersions[$class][self::$greatestCompatVersion]; + } + return null; + } + /** * Get the specific class name for the latest available version of a class. * diff --git a/Puc/v4/GitHub/PluginUpdateChecker.php b/Puc/v4/GitHub/PluginUpdateChecker.php deleted file mode 100644 index 31c0c9c..0000000 --- a/Puc/v4/GitHub/PluginUpdateChecker.php +++ /dev/null @@ -1,224 +0,0 @@ -repositoryUrl = $repositoryUrl; - parent::__construct($repositoryUrl, $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile); - } - - /** - * Set the GitHub branch to use for updates. Defaults to 'master'. - * - * @param string $branch - * @return $this - */ - public function setBranch($branch) { - $this->branch = empty($branch) ? 'master' : $branch; - return $this; - } - - /** - * Retrieve details about the latest plugin version from GitHub. - * - * @param array $unusedQueryArgs Unused. - * @return Puc_v4_Plugin_Info - */ - public function requestInfo($unusedQueryArgs = array()) { - $api = $this->api = new Puc_v4_GitHub_Api($this->repositoryUrl, $this->accessToken); - - $info = new Puc_v4_Plugin_Info(); - $info->filename = $this->pluginFile; - $info->slug = $this->slug; - - $this->setInfoFromHeader($this->getPluginHeader(), $info); - - //Figure out which reference (tag or branch) we'll use to get the latest version of the plugin. - $updateSource = $this->chooseReference(); - if ( $updateSource ) { - $ref = $updateSource->name; - $info->version = $updateSource->version; - $info->last_updated = $updateSource->updated; - $info->download_url = $updateSource->downloadUrl; - - if ( !empty($updateSource->changelog) ) { - $info->sections['changelog'] = $updateSource->changelog; - } - if ( isset($updateSource->downloadCount) ) { - $info->downloaded = $updateSource->downloadCount; - } - } else { - return null; - } - - if ( !empty($info->download_url) && !empty($this->accessToken) ) { - $info->download_url = add_query_arg('access_token', $this->accessToken, $info->download_url); - } - - //Get headers from the main plugin file in this branch/tag. Its "Version" header and other metadata - //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags. - $mainPluginFile = basename($this->pluginFile); - $remotePlugin = $api->getRemoteFile($mainPluginFile, $ref); - if ( !empty($remotePlugin) ) { - $remoteHeader = $this->getFileHeader($remotePlugin); - $this->setInfoFromHeader($remoteHeader, $info); - } - - //Try parsing readme.txt. If it's formatted according to WordPress.org standards, it will contain - //a lot of useful information like the required/tested WP version, changelog, and so on. - if ( $this->readmeTxtExistsLocally() ) { - $this->setInfoFromRemoteReadme($ref, $info); - } - - //The changelog might be in a separate file. - if ( empty($info->sections['changelog']) ) { - $info->sections['changelog'] = $api->getRemoteChangelog($ref, dirname($this->getAbsolutePath())); - if ( empty($info->sections['changelog']) ) { - $info->sections['changelog'] = __('There is no changelog available.', 'plugin-update-checker'); - } - } - - if ( empty($info->last_updated) ) { - //Fetch the latest commit that changed the tag/branch and use it as the "last_updated" date. - $info->last_updated = $api->getLatestCommitTime($ref); - } - - $info = apply_filters($this->getUniqueName('request_info_result'), $info, null); - return $info; - } - - /** - * @return Puc_v4_VcsReference|null - */ - protected function chooseReference() { - $api = $this->api; - $updateSource = null; - - if ( $this->branch === 'master' ) { - //Use the latest release. - $updateSource = $api->getLatestRelease(); - if ( $updateSource === null ) { - //Failing that, use the tag with the highest version number. - $updateSource = $api->getLatestTag(); - } - } - //Alternatively, just use the branch itself. - if ( empty($ref) ) { - $updateSource = $api->getBranch($this->branch); - } - - return $updateSource; - } - - /** - * Set the access token that will be used to make authenticated GitHub API requests. - * - * @param string $accessToken - * @return $this - */ - public function setAccessToken($accessToken) { - $this->accessToken = $accessToken; - return $this; - } - - /** - * Copy plugin metadata from a file header to a PluginInfo object. - * - * @param array $fileHeader - * @param Puc_v4_Plugin_Info $pluginInfo - */ - protected function setInfoFromHeader($fileHeader, $pluginInfo) { - $headerToPropertyMap = array( - 'Version' => 'version', - 'Name' => 'name', - 'PluginURI' => 'homepage', - 'Author' => 'author', - 'AuthorName' => 'author', - 'AuthorURI' => 'author_homepage', - - 'Requires WP' => 'requires', - 'Tested WP' => 'tested', - 'Requires at least' => 'requires', - 'Tested up to' => 'tested', - ); - foreach ($headerToPropertyMap as $headerName => $property) { - if ( isset($fileHeader[$headerName]) && !empty($fileHeader[$headerName]) ) { - $pluginInfo->$property = $fileHeader[$headerName]; - } - } - - if ( !empty($fileHeader['Description']) ) { - $pluginInfo->sections['description'] = $fileHeader['Description']; - } - } - - /** - * Copy plugin metadata from the remote readme.txt file. - * - * @param string $ref GitHub tag or branch where to look for the readme. - * @param Puc_v4_Plugin_Info $pluginInfo - */ - protected function setInfoFromRemoteReadme($ref, $pluginInfo) { - $readme = $this->api->getRemoteReadme($ref); - if ( empty($readmeTxt) ) { - return; - } - - if ( isset($readme['sections']) ) { - $pluginInfo->sections = array_merge($pluginInfo->sections, $readme['sections']); - } - if ( !empty($readme['tested_up_to']) ) { - $pluginInfo->tested = $readme['tested_up_to']; - } - if ( !empty($readme['requires_at_least']) ) { - $pluginInfo->requires = $readme['requires_at_least']; - } - - if ( isset($readme['upgrade_notice'], $readme['upgrade_notice'][$pluginInfo->version]) ) { - $pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version]; - } - } - - /** - * Check if the currently installed version has a readme.txt file. - * - * @return bool - */ - protected function readmeTxtExistsLocally() { - $pluginDirectory = dirname($this->pluginAbsolutePath); - if ( empty($this->pluginAbsolutePath) || !is_dir($pluginDirectory) || ($pluginDirectory === '.') ) { - return false; - } - return is_file($pluginDirectory . '/readme.txt'); - } -} - -endif; \ No newline at end of file diff --git a/Puc/v4/GitHub/ThemeUpdateChecker.php b/Puc/v4/GitHub/ThemeUpdateChecker.php deleted file mode 100644 index 4721759..0000000 --- a/Puc/v4/GitHub/ThemeUpdateChecker.php +++ /dev/null @@ -1,99 +0,0 @@ -repositoryUrl = $repositoryUrl; - parent::__construct($repositoryUrl, $stylesheet, $customSlug, $checkPeriod, $optionName); - } - - public function requestUpdate() { - $api = new Puc_v4_GitHub_Api($this->repositoryUrl, $this->accessToken); - - $update = new Puc_v4_Theme_Update(); - $update->slug = $this->slug; - - //Figure out which reference (tag or branch) we'll use to get the latest version of the theme. - $ref = $this->branch; - if ( $this->branch === 'master' ) { - //Use the latest release. - $release = $api->getLatestRelease(); - if ( $release !== null ) { - $ref = $release->name; - $update->version = ltrim($release->name, 'v'); //Remove the "v" prefix from "v1.2.3". - $update->download_url = $release->downloadUrl; - } else { - //Failing that, use the tag with the highest version number. - $tag = $api->getLatestTag(); - if ( $tag !== null ) { - $ref = $tag->name; - $update->version = $tag->version; - $update->download_url = $tag->downloadUrl; - } - } - } - - if ( empty($update->download_url) ) { - $update->download_url = $api->buildArchiveDownloadUrl($ref); - } else if ( !empty($this->accessToken) ) { - $update->download_url = add_query_arg('access_token', $this->accessToken, $update->download_url); - } - - //Get headers from the main stylesheet in this branch/tag. Its "Version" header and other metadata - //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags. - $remoteStylesheet = $api->getRemoteFile('style.css', $ref); - if ( !empty($remoteStylesheet) ) { - $remoteHeader = $this->getFileHeader($remoteStylesheet); - if ( !empty($remoteHeader['Version']) ) { - $update->version = $remoteHeader['Version']; - } - if ( !empty($remoteHeader['ThemeURI']) ) { - $update->details_url = $remoteHeader['ThemeURI']; - } - } - - //The details URL defaults to the Theme URI header or the repository URL. - if ( empty($update->details_url) ) { - $update->details_url = $this->theme->get('ThemeURI'); - } - if ( empty($update->details_url) ) { - $update->details_url = $this->repositoryUrl; - } - - if ( empty($update->version) ) { - //It looks like we didn't find a valid update after all. - $update = null; - } - - $update = $this->filterUpdateResult($update); - return $update; - } - - /** - * Set the GitHub branch to use for updates. Defaults to 'master'. - * - * @param string $branch - * @return $this - */ - public function setBranch($branch) { - $this->branch = empty($branch) ? 'master' : $branch; - return $this; - } - - /** - * Set the access token that will be used to make authenticated GitHub API requests. - * - * @param string $accessToken - * @return $this - */ - public function setAccessToken($accessToken) { - $this->accessToken = $accessToken; - return $this; - } - } - -endif; \ No newline at end of file diff --git a/Puc/v4/VcsApi.php b/Puc/v4/Vcs/Api.php similarity index 62% rename from Puc/v4/VcsApi.php rename to Puc/v4/Vcs/Api.php index ecfafff..7d5dd32 100644 --- a/Puc/v4/VcsApi.php +++ b/Puc/v4/Vcs/Api.php @@ -1,9 +1,46 @@ repositoryUrl = $repositoryUrl; + $this->setAuthentication($credentials); + } + + /** + * @return string + */ + public function getRepositoryUrl() { + return $this->repositoryUrl; + } + + /** + * Figure out which reference (i.e tag or branch) contains the latest version. + * + * @param string $configBranch Start looking in this branch. + * @param bool $useStableTag + * @return null|Puc_v4_Vcs_Reference + */ + abstract public function chooseReference($configBranch, $useStableTag = true); + /** * Get the readme.txt file from the remote repository and parse it * according to the plugin readme standard. @@ -25,7 +62,7 @@ if ( !class_exists('Puc_v4_VcsApi') ): * Get a branch. * * @param string $branchName - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ abstract public function getBranch($branchName); @@ -33,7 +70,7 @@ if ( !class_exists('Puc_v4_VcsApi') ): * Get a specific tag. * * @param string $tagName - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ abstract public function getTag($tagName); @@ -41,7 +78,7 @@ if ( !class_exists('Puc_v4_VcsApi') ): * Get the tag that looks like the highest version number. * (Implementations should skip pre-release versions if possible.) * - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ abstract public function getLatestTag(); @@ -64,6 +101,33 @@ if ( !class_exists('Puc_v4_VcsApi') ): return (preg_match('@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name) === 1); } + /** + * Check if a tag appears to be named like a version number. + * + * @param stdClass $tag + * @return bool + */ + protected function isVersionTag($tag) { + $property = $this->tagNameProperty; + return isset($tag->$property) && $this->looksLikeVersion($tag->$property); + } + + /** + * Sort a list of tags as if they were version numbers. + * Tags that don't look like version number will be removed. + * + * @param stdClass[] $tags Array of tag objects. + * @return stdClass[] Filtered array of tags sorted in descending order. + */ + protected function sortTagsByVersion($tags) { + //Keep only those tags that look like version numbers. + $versionTags = array_filter($tags, array($this, 'isVersionTag')); + //Sort them in descending order. + usort($versionTags, array($this, 'compareTagNames')); + + return $versionTags; + } + /** * Compare two tags as if they were version number. * @@ -141,6 +205,23 @@ if ( !class_exists('Puc_v4_VcsApi') ): } return null; } + + /** + * Set authentication credentials. + * + * @param $credentials + */ + public function setAuthentication($credentials) { + $this->credentials = $credentials; + } + + /** + * @param string $url + * @return string + */ + public function signDownloadUrl($url) { + return $url; + } } endif; diff --git a/Puc/v4/Vcs/BaseChecker.php b/Puc/v4/Vcs/BaseChecker.php new file mode 100644 index 0000000..77a8919 --- /dev/null +++ b/Puc/v4/Vcs/BaseChecker.php @@ -0,0 +1,22 @@ +repositoryUrl = $repositoryUrl; - $path = @parse_url($repositoryUrl, PHP_URL_PATH); if ( preg_match('@^/?(?P[^/]+?)/(?P[^/#?&]+?)/?$@', $path, $matches) ) { $this->username = $matches['username']; @@ -33,12 +26,45 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): throw new InvalidArgumentException('Invalid BitBucket repository URL: "' . $repositoryUrl . '"'); } - if ( !empty($credentials) && !empty($credentials['consumer_key']) ) { - $this->oauth = new Puc_v4_OAuthSignature( - $credentials['consumer_key'], - $credentials['consumer_secret'] - ); + parent::__construct($repositoryUrl, $credentials); + } + + /** + * Figure out which reference (i.e tag or branch) contains the latest version. + * + * @param string $configBranch Start looking in this branch. + * @param bool $useStableTag + * @return null|Puc_v4_Vcs_Reference + */ + public function chooseReference($configBranch, $useStableTag = true) { + $updateSource = null; + + //Check if there's a "Stable tag: 1.2.3" header that points to a valid tag. + if ( $useStableTag ) { + $remoteReadme = $this->getRemoteReadme($configBranch); + if ( !empty($remoteReadme['stable_tag']) ) { + $tag = $remoteReadme['stable_tag']; + + //You can explicitly opt out of using tags by setting "Stable tag" to + //"trunk" or the name of the current branch. + if ( ($tag === $configBranch) || ($tag === 'trunk') ) { + return $this->getBranch($configBranch); + } + + $updateSource = $this->getTag($tag); + } } + + //Look for version-like tags. + if ( !$updateSource && ($configBranch === 'master') ) { + $updateSource = $this->getLatestTag(); + } + //If all else fails, use the specified branch itself. + if ( !$updateSource ) { + $updateSource = $this->getBranch($configBranch); + } + + return $updateSource; } public function getBranch($branchName) { @@ -47,7 +73,7 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): return null; } - return new Puc_v4_VcsReference(array( + return new Puc_v4_Vcs_Reference(array( 'name' => $branch->name, 'updated' => $branch->target->date, 'downloadUrl' => $this->getDownloadUrl($branch->name), @@ -58,7 +84,7 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): * Get a specific tag. * * @param string $tagName - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ public function getTag($tagName) { $tag = $this->api('/refs/tags/' . $tagName); @@ -66,7 +92,7 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): return null; } - return new Puc_v4_VcsReference(array( + return new Puc_v4_Vcs_Reference(array( 'name' => $tag->name, 'version' => ltrim($tag->name, 'v'), 'updated' => $tag->target->date, @@ -77,7 +103,7 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): /** * Get the tag that looks like the highest version number. * - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ public function getLatestTag() { $tags = $this->api('/refs/tags'); @@ -85,15 +111,13 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): return null; } - //Keep only those tags that look like version numbers. - $versionTags = array_filter($tags->values, array($this, 'isVersionTag')); - //Sort them in descending order. - usort($versionTags, array($this, 'compareTagNames')); + //Filter and sort the list of tags. + $versionTags = $this->sortTagsByVersion($tags->values); //Return the first result. if ( !empty($versionTags) ) { $tag = $versionTags[0]; - return new Puc_v4_VcsReference(array( + return new Puc_v4_Vcs_Reference(array( 'name' => $tag->name, 'version' => ltrim($tag->name, 'v'), 'updated' => $tag->target->date, @@ -111,10 +135,6 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): return trailingslashit($this->repositoryUrl) . 'get/' . $ref . '.zip'; } - protected function isVersionTag($tag) { - return isset($tag->name) && $this->looksLikeVersion($tag->name); - } - /** * Get the contents of a file from a specific branch or tag. * @@ -185,6 +205,32 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ): 'BitBucket API error. HTTP status: ' . $code ); } + + /** + * @param array $credentials + */ + public function setAuthentication($credentials) { + parent::setAuthentication($credentials); + + if ( !empty($credentials) && !empty($credentials['consumer_key']) ) { + $this->oauth = new Puc_v4_OAuthSignature( + $credentials['consumer_key'], + $credentials['consumer_secret'] + ); + } else { + $this->oauth = null; + } + } + + public function signDownloadUrl($url) { + //Add authentication data to download URLs. Since OAuth signatures incorporate + //timestamps, we have to do this immediately before inserting the update. Otherwise + //authentication could fail due to a stale timestamp. + if ( $this->oauth ) { + $url = $this->oauth->sign($url); + } + return $url; + } } endif; \ No newline at end of file diff --git a/Puc/v4/GitHub/Api.php b/Puc/v4/Vcs/GitHubApi.php similarity index 75% rename from Puc/v4/GitHub/Api.php rename to Puc/v4/Vcs/GitHubApi.php index 2dc3558..3e3e74d 100644 --- a/Puc/v4/GitHub/Api.php +++ b/Puc/v4/Vcs/GitHubApi.php @@ -1,8 +1,8 @@ repositoryUrl = $repositoryUrl; - $this->accessToken = $accessToken; - $path = @parse_url($repositoryUrl, PHP_URL_PATH); if ( preg_match('@^/?(?P[^/]+?)/(?P[^/#?&]+?)/?$@', $path, $matches) ) { $this->userName = $matches['username']; @@ -33,12 +30,14 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): } else { throw new InvalidArgumentException('Invalid GitHub repository URL: "' . $repositoryUrl . '"'); } + + parent::__construct($repositoryUrl, $accessToken); } /** * Get the latest release from GitHub. * - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ public function getLatestRelease() { $release = $this->api('/repos/:user/:repo/releases/latest'); @@ -46,10 +45,10 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): return null; } - $reference = new Puc_v4_VcsReference(array( + $reference = new Puc_v4_Vcs_Reference(array( 'name' => $release->tag_name, 'version' => ltrim($release->tag_name, 'v'), //Remove the "v" prefix from "v1.2.3". - 'downloadUrl' => $release->zipball_url, + 'downloadUrl' => $this->signDownloadUrl($release->zipball_url), 'updated' => $release->created_at, )); @@ -67,7 +66,7 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): /** * Get the tag that looks like the highest version number. * - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ public function getLatestTag() { $tags = $this->api('/repos/:user/:repo/tags'); @@ -76,13 +75,16 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): return null; } - usort($tags, array($this, 'compareTagNames')); //Sort from highest to lowest. + $versionTags = $this->sortTagsByVersion($tags); + if ( empty($versionTags) ) { + return null; + } - $tag = $tags[0]; - return new Puc_v4_VcsReference(array( + $tag = $versionTags[0]; + return new Puc_v4_Vcs_Reference(array( 'name' => $tag->name, 'version' => ltrim($tag->name, 'v'), - 'downloadUrl' => $tag->zipball_url, + 'downloadUrl' => $this->signDownloadUrl($tag->zipball_url), )); } @@ -90,7 +92,7 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): * Get a branch by name. * * @param string $branchName - * @return null|Puc_v4_VcsReference + * @return null|Puc_v4_Vcs_Reference */ public function getBranch($branchName) { $branch = $this->api('/repos/:user/:repo/branches/' . $branchName); @@ -98,7 +100,7 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): return null; } - $reference = new Puc_v4_VcsReference(array( + $reference = new Puc_v4_Vcs_Reference(array( 'name' => $branch->name, 'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name), )); @@ -218,7 +220,7 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): urlencode($ref) ); if ( !empty($this->accessToken) ) { - $url = add_query_arg('access_token', $this->accessToken, $url); + $url = $this->signDownloadUrl($url); } return $url; } @@ -227,12 +229,43 @@ if ( !class_exists('Puc_v4_GitHub_Api', false) ): * Get a specific tag. * * @param string $tagName - * @return Puc_v4_VcsReference|null + * @return Puc_v4_Vcs_Reference|null */ public function getTag($tagName) { - //The current GitHub update checker doesn't use getTag, so didn't bother to implement it. + //The current GitHub update checker doesn't use getTag, so I didn't bother to implement it. throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.'); } + + public function setAuthentication($credentials) { + parent::setAuthentication($credentials); + $this->accessToken = is_string($credentials) ? $credentials : null; + } + + /** + * Figure out which reference (i.e tag or branch) contains the latest version. + * + * @param string $configBranch Start looking in this branch. + * @param bool $useStableTag Ignored. The GitHub client doesn't use the "Stable tag" header. + * @return null|Puc_v4_Vcs_Reference + */ + public function chooseReference($configBranch, $useStableTag = false) { + $updateSource = null; + + if ( $configBranch === 'master' ) { + //Use the latest release. + $updateSource = $this->getLatestRelease(); + if ( $updateSource === null ) { + //Failing that, use the tag with the highest version number. + $updateSource = $this->getLatestTag(); + } + } + //Alternatively, just use the branch itself. + if ( empty($updateSource) ) { + $updateSource = $this->getBranch($configBranch); + } + + return $updateSource; + } } endif; \ No newline at end of file diff --git a/Puc/v4/BitBucket/PluginUpdateChecker.php b/Puc/v4/Vcs/PluginUpdateChecker.php similarity index 68% rename from Puc/v4/BitBucket/PluginUpdateChecker.php rename to Puc/v4/Vcs/PluginUpdateChecker.php index a3dae8c..da2ba5f 100644 --- a/Puc/v4/BitBucket/PluginUpdateChecker.php +++ b/Puc/v4/Vcs/PluginUpdateChecker.php @@ -1,25 +1,38 @@ api = $api; + parent::__construct($api->getRepositoryUrl(), $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile); + } public function requestInfo($queryArgs = array()) { //We have to make several remote API requests to gather all the necessary info //which can take a while on slow networks. set_time_limit(60); - $api = $this->api = new Puc_v4_BitBucket_Api($this->metadataUrl, $this->credentials); + $api = $this->api; $info = new Puc_v4_Plugin_Info(); $info->filename = $this->pluginFile; @@ -28,12 +41,19 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ): $this->setInfoFromHeader($this->getPluginHeader(), $info); //Pick a branch or tag. - $updateSource = $this->chooseReference(); + $updateSource = $api->chooseReference($this->branch); if ( $updateSource ) { $ref = $updateSource->name; $info->version = $updateSource->version; $info->last_updated = $updateSource->updated; $info->download_url = $updateSource->downloadUrl; + + if ( !empty($updateSource->changelog) ) { + $info->sections['changelog'] = $updateSource->changelog; + } + if ( isset($updateSource->downloadCount) ) { + $info->downloaded = $updateSource->downloadCount; + } } else { //There's probably a network problem or an authentication error. return null; @@ -74,56 +94,6 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ): return $info; } - /** - * Figure out which reference (tag or branch) we'll use to get the latest version of the plugin. - * - * @return Puc_v4_VcsReference|null - */ - protected function chooseReference() { - $api = $this->api; - $updateSource = null; - - //Check if there's a "Stable tag: v1.2.3" header that points to a valid tag. - $remoteReadme = $api->getRemoteReadme($this->branch); - if ( !empty($remoteReadme['stable_tag']) ) { - $tag = $remoteReadme['stable_tag']; - - //You can explicitly opt out of using tags by setting "Stable tag" to - //"trunk" or the name of the current branch. - if ( ($tag === $this->branch) || ($tag === 'trunk') ) { - return $api->getBranch($this->branch); - } - - $updateSource = $api->getTag($tag); - } - //Look for version-like tags. - if ( !$updateSource && ($this->branch === 'master') ) { - $updateSource = $api->getLatestTag(); - } - //If all else fails, use the specified branch itself. - if ( !$updateSource ) { - $updateSource = $api->getBranch($this->branch); - } - - return $updateSource; - } - - public function setAuthentication($credentials) { - $this->credentials = array_merge( - array( - 'consumer_key' => '', - 'consumer_secret' => '', - ), - $credentials - ); - return $this; - } - - public function setBranch($branchName = 'master') { - $this->branch = $branchName; - return $this; - } - /** * Check if the currently installed version has a readme.txt file. * @@ -195,20 +165,21 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ): } } + public function setBranch($branch) { + $this->branch = $branch; + return $this; + } + + public function setAuthentication($credentials) { + $this->api->setAuthentication($credentials); + return $this; + } + public function getUpdate() { $update = parent::getUpdate(); - //Add authentication data to download URLs. Since OAuth signatures incorporate - //timestamps, we have to do this immediately before inserting the update. Otherwise - //authentication could fail due to a stale timestamp. - if ( isset($update, $update->download_url) && !empty($update->download_url) && !empty($this->credentials) ) { - if ( !empty($this->credentials['consumer_key']) ) { - $oauth = new Puc_v4_OAuthSignature( - $this->credentials['consumer_key'], - $this->credentials['consumer_secret'] - ); - $update->download_url = $oauth->sign($update->download_url); - } + if ( isset($update) && !empty($update->download_url) ) { + $update->download_url = $this->api->signDownloadUrl($update->download_url); } return $update; diff --git a/Puc/v4/VcsReference.php b/Puc/v4/Vcs/Reference.php similarity index 91% rename from Puc/v4/VcsReference.php rename to Puc/v4/Vcs/Reference.php index e7047fa..501955f 100644 --- a/Puc/v4/VcsReference.php +++ b/Puc/v4/Vcs/Reference.php @@ -1,5 +1,5 @@ api = $api; + parent::__construct($api->getRepositoryUrl(), $stylesheet, $customSlug, $checkPeriod, $optionName); + } + + public function requestUpdate() { + $api = $this->api; + + $update = new Puc_v4_Theme_Update(); + $update->slug = $this->slug; + + //Figure out which reference (tag or branch) we'll use to get the latest version of the theme. + $updateSource = $api->chooseReference($this->branch, false); + if ( $updateSource ) { + $ref = $updateSource->name; + $update->version = $updateSource->version; + $update->download_url = $updateSource->downloadUrl; + } else { + $ref = $this->branch; + } + + //Get headers from the main stylesheet in this branch/tag. Its "Version" header and other metadata + //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags. + $remoteStylesheet = $api->getRemoteFile('style.css', $ref); + if ( !empty($remoteStylesheet) ) { + $remoteHeader = $this->getFileHeader($remoteStylesheet); + if ( !empty($remoteHeader['Version']) ) { + $update->version = $remoteHeader['Version']; + } + if ( !empty($remoteHeader['ThemeURI']) ) { + $update->details_url = $remoteHeader['ThemeURI']; + } + } + + //The details URL defaults to the Theme URI header or the repository URL. + if ( empty($update->details_url) ) { + $update->details_url = $this->theme->get('ThemeURI'); + } + if ( empty($update->details_url) ) { + $update->details_url = $this->metadataUrl; + } + + if ( empty($update->version) ) { + //It looks like we didn't find a valid update after all. + $update = null; + } + + $update = $this->filterUpdateResult($update); + return $update; + } + + public function setBranch($branch) { + $this->branch = $branch; + return $this; + } + + public function setAuthentication($credentials) { + $this->api->setAuthentication($credentials); + return $this; + } + + public function getUpdate() { + $update = parent::getUpdate(); + + if ( isset($update) && !empty($update->download_url) ) { + $update->download_url = $this->api->signDownloadUrl($update->download_url); + } + + return $update; + } + + + } + +endif; \ No newline at end of file diff --git a/plugin-update-checker.php b/plugin-update-checker.php index 81dbd87..642809a 100644 --- a/plugin-update-checker.php +++ b/plugin-update-checker.php @@ -13,7 +13,9 @@ new Puc_v4_Autoloader(); //Register classes defined in this file with the factory. Puc_v4_Factory::addVersion('Plugin_UpdateChecker', 'Puc_v4_Plugin_UpdateChecker', '4.0'); Puc_v4_Factory::addVersion('Theme_UpdateChecker', 'Puc_v4_Theme_UpdateChecker', '4.0'); -Puc_v4_Factory::addVersion('GitHub_PluginUpdateChecker', 'Puc_v4_GitHub_PluginUpdateChecker', '4.0'); -Puc_v4_Factory::addVersion('GitHub_ThemeUpdateChecker', 'Puc_v4_GitHub_ThemeUpdateChecker', '4.0'); -Puc_v4_Factory::addVersion('BitBucket_PluginUpdateChecker', 'Puc_v4_BitBucket_PluginUpdateChecker', '4.0'); +Puc_v4_Factory::addVersion('Vcs_PluginUpdateChecker', 'Puc_v4_Vcs_PluginUpdateChecker', '4.0'); +Puc_v4_Factory::addVersion('Vcs_ThemeUpdateChecker', 'Puc_v4_Vcs_ThemeUpdateChecker', '4.0'); + +Puc_v4_Factory::addVersion('GitHubApi', 'Puc_v4_Vcs_GitHubApi', '4.0'); +Puc_v4_Factory::addVersion('BitBucketApi', 'Puc_v4_Vcs_BitBucketApi', '4.0');