BitBucket support is now semi-usable.

More testing required. Could probably refactor to reduce duplication; lots of overlap with GitHub integration.
This commit is contained in:
Yahnis Elsts 2016-12-22 19:10:05 +02:00
parent 3692f5201e
commit f64c3170cc
5 changed files with 276 additions and 23 deletions

View File

@ -2,16 +2,50 @@
if ( !class_exists('Puc_v4_BitBucket_Api', false) ):
class Puc_v4_BitBucket_Api {
public function __construct($repositoryUrl, $credentials = array()) {
/**
* @var Puc_v4_OAuthSignature
*/
private $oauth = null;
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $repository;
public function __construct($repositoryUrl, $credentials = array()) {
$path = @parse_url($repositoryUrl, PHP_URL_PATH);
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
$this->username = $matches['username'];
$this->repository = $matches['repository'];
} else {
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']
);
}
}
/**
* @param string $ref
* @return array
*/
public function getRemoteReadme($ref) {
return array();
public function getRemoteReadme($ref = 'master') {
$fileContents = $this->getRemoteFile('readme.txt', $ref);
if ( empty($fileContents) ) {
return array();
}
$parser = new PucReadmeParser();
return $parser->parse_readme_contents($fileContents);
}
/**
@ -21,7 +55,11 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ):
* @return stdClass|null
*/
public function getTag($tagName) {
return null;
$tag = $this->api('/refs/tags/' . $tagName);
if ( is_wp_error($tag) || empty($tag) ) {
return null;
}
return $tag;
}
/**
@ -30,9 +68,63 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ):
* @return stdClass|null
*/
public function getLatestTag() {
$tags = $this->api('/refs/tags');
if ( !isset($tags, $tags->values) || !is_array($tags->values) ) {
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'));
//Return the first result.
if ( !empty($versionTags) ) {
return $versionTags[0];
}
return null;
}
protected function isVersionTag($tag) {
return isset($tag->name) && $this->looksLikeVersion($tag->name);
}
/**
* Check if a tag name string looks like a version number.
*
* @param string $name
* @return bool
*/
protected function looksLikeVersion($name) {
//Tag names may be prefixed with "v", e.g. "v1.2.3".
$name = ltrim($name, 'v');
//The version string must start with a number.
if ( !is_numeric($name) ) {
return false;
}
//The goal is to accept any SemVer-compatible or "PHP-standardized" version number.
return (preg_match('@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name) === 1);
}
/**
* Compare two BitBucket tags as if they were version number.
*
* @param string $tag1
* @param string $tag2
* @return int
*/
protected function compareTagNames($tag1, $tag2) {
if ( !isset($tag1->name) ) {
return 1;
}
if ( !isset($tag2->name) ) {
return -1;
}
return -version_compare(ltrim($tag1->name, 'v'), ltrim($tag2->name, 'v'));
}
/**
* Get the contents of a file from a specific branch or tag.
*
@ -41,7 +133,11 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ):
* @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
*/
public function getRemoteFile($path, $ref = 'master') {
return null;
$response = $this->api('src/' . $ref . '/' . ltrim($path), '1.0');
if ( is_wp_error($response) || !isset($response, $response->data) ) {
return null;
}
return $response->data;
}
/**
@ -51,6 +147,10 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ):
* @return string|null
*/
public function getLatestCommitTime($ref) {
$response = $this->api('commits/' . $ref);
if ( isset($response->values, $response->values[0], $response->values[0]->date) ) {
return $response->values[0]->date;
}
return null;
}
@ -84,6 +184,48 @@ if ( !class_exists('Puc_v4_BitBucket_Api', false) ):
}
return null;
}
/**
* Perform a BitBucket API 2.0 request.
*
* @param string $url
* @param string $version
* @return mixed|WP_Error
*/
public function api($url, $version = '2.0') {
//printf('Requesting %s<br>' . "\n", $url);
$url = implode('/', array(
'https://api.bitbucket.org',
$version,
'repositories',
$this->username,
$this->repository,
ltrim($url, '/')
));
if ( $this->oauth ) {
$url = $this->oauth->sign($url,'GET');
}
$response = wp_remote_get($url, array('timeout' => 10));
//var_dump($response);
if ( is_wp_error($response) ) {
return $response;
}
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
if ( $code === 200 ) {
$document = json_decode($body);
return $document;
}
return new WP_Error(
'puc-bitbucket-http-error',
'BitBucket API error. HTTP status: ' . $code
);
}
}
endif;

View File

@ -5,24 +5,17 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ):
/**
* @var string
*/
protected $repositoryUrl;
/**
* @var string
*/
protected $branch;
protected $branch = 'master';
/**
* @var Puc_v4_BitBucket_Api
*/
protected $api;
protected $credentials = array();
public function requestInfo($queryArgs = array()) {
//TODO: BitBucket support
$api = $this->api = new Puc_v4_BitBucket_Api(
$this->repositoryUrl,
array()
);
$api = $this->api = new Puc_v4_BitBucket_Api($this->metadataUrl, $this->credentials);
$info = new Puc_v4_Plugin_Info();
$info->filename = $this->pluginFile;
@ -42,7 +35,6 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ):
$ref = $tag->name;
$info->version = ltrim($tag->name, 'v');
$info->last_updated = $tag->target->date;
//TODO: Download url
$foundVersion = true;
}
}
@ -54,7 +46,6 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ):
$ref = $tag->name;
$info->version = ltrim($tag->name, 'v');
$info->last_updated = $tag->target->date;
//TODO: Download url
$foundVersion = true;
}
}
@ -62,9 +53,10 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ):
//If all else fails, use the specified branch itself.
if ( !$foundVersion ) {
$ref = $this->branch;
//TODO: Download url for this branch.
}
$info->download_url = trailingslashit($this->metadataUrl) . 'get/' . $ref . '.zip';
//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);
@ -76,7 +68,7 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ):
//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() ) {
if ( $this->readmeTxtExistsLocally() || !empty($remoteReadme) ) {
$this->setInfoFromRemoteReadme($ref, $info);
}
@ -100,6 +92,16 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ):
return $info;
}
public function setAuthentication($credentials) {
$this->credentials = array_merge(
array(
'consumer_key' => '',
'consumer_secret' => '',
),
$credentials
);
}
/**
* Check if the currently installed version has a readme.txt file.
*
@ -170,6 +172,26 @@ if ( !class_exists('Puc_v4_BitBucket_PluginUpdateChecker') ):
$pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version];
}
}
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);
}
}
return $update;
}
}
endif;

View File

@ -31,7 +31,7 @@ if ( !class_exists('Puc_v4_Factory', false) ):
* @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_UpdateChecker
* @return Puc_v4_Plugin_UpdateChecker|Puc_v4_Theme_UpdateChecker
*/
public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
$fullPath = wp_normalize_path($fullPath);

87
Puc/v4/OAuthSignature.php Normal file
View File

@ -0,0 +1,87 @@
<?php
if ( !class_exists('Puc_v4_OAuthSignature', false) ):
/**
* A basic signature generator for zero-legged OAuth 1.0.
*/
class Puc_v4_OAuthSignature {
private $consumerKey = '';
private $consumerSecret = '';
public function __construct($consumerKey, $consumerSecret) {
$this->consumerKey = $consumerKey;
$this->consumerSecret = $consumerSecret;
}
/**
* Sign a URL using OAuth 1.0.
*
* @param string $url The URL to be signed. It may contain query parameters.
* @param string $method HTTP method such as "GET", "POST" and so on.
* @return string The signed URL.
*/
public function sign($url, $method = 'GET') {
$parameters = array();
//Parse query parameters.
$query = @parse_url($url, PHP_URL_QUERY);
if ( !empty($query) ) {
parse_str($query, $parsedParams);
if ( is_array($parameters) ) {
$parameters = $parsedParams;
}
//Remove the query string from the URL. We'll replace it later.
$url = substr($url, 0, strpos($url, '?'));
}
$parameters = array_merge(
$parameters,
array(
'oauth_consumer_key' => $this->consumerKey,
'oauth_nonce' => $this->nonce(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
)
);
//Parameters must be sorted alphabetically before signing.
ksort($parameters);
//The most complicated part of the request - generating the signature.
//The string to sign contains the HTTP method, the URL path, and all of
//our query parameters. Everything is URL encoded. Then we concatenate
//them with ampersands into a single string to hash.
$encodedVerb = urlencode($method);
$encodedUrl = urlencode($url);
$encodedParams = urlencode(http_build_query($parameters, '', '&'));
$stringToSign = $encodedVerb . '&' . $encodedUrl . '&' . $encodedParams;
//Since we only have one OAuth token (the consumer secret) we only have
//to use it as our HMAC key. However, we still have to append an & to it
//as if we were using it with additional tokens.
$secret = urlencode($this->consumerSecret) . '&';
//The signature is a hash of the consumer key and the base string. Note
//that we have to get the raw output from hash_hmac and base64 encode
//the binary data result.
$parameters['oauth_signature'] = base64_encode(hash_hmac('sha1', $stringToSign, $secret, true));
return ($url . '?' . http_build_query($parameters));
}
/**
* Generate a random nonce.
*
* @return string
*/
private function nonce() {
$mt = microtime();
$rand = mt_rand();
return md5($mt . '_' . $rand);
}
}
endif;

View File

@ -1,11 +1,13 @@
<?php
if ( !class_exists('PucReadmeParser', false) ):
/**
* This is a slightly modified version of github.com/markjaquith/WordPress-Plugin-Readme-Parser
* It uses Parsedown instead of the "Markdown Extra" parser.
*/
Class PucReadmeParser {
class PucReadmeParser {
function __construct() {
// This space intentionally blank
@ -328,4 +330,4 @@ Class PucReadmeParser {
} // end class
Class Automattic_Readme extends PucReadmeParser {}
endif;