mirror of
https://github.com/WenPai-org/lelms-copyright.git
synced 2025-08-08 06:56:01 +08:00
源码提交
This commit is contained in:
parent
2955d0dde5
commit
3fd3701567
131 changed files with 13972 additions and 0 deletions
379
lib/plugin-update-checker/Puc/v5p3/Vcs/Api.php
Normal file
379
lib/plugin-update-checker/Puc/v5p3/Vcs/Api.php
Normal file
|
@ -0,0 +1,379 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
use Parsedown;
|
||||
use PucReadmeParser;
|
||||
|
||||
if ( !class_exists(Api::class, false) ):
|
||||
|
||||
abstract class Api {
|
||||
const STRATEGY_LATEST_RELEASE = 'latest_release';
|
||||
const STRATEGY_LATEST_TAG = 'latest_tag';
|
||||
const STRATEGY_STABLE_TAG = 'stable_tag';
|
||||
const STRATEGY_BRANCH = 'branch';
|
||||
|
||||
/**
|
||||
* Consider all releases regardless of their version number or prerelease/upcoming
|
||||
* release status.
|
||||
*/
|
||||
const RELEASE_FILTER_ALL = 3;
|
||||
|
||||
/**
|
||||
* Exclude releases that have the "prerelease" or "upcoming release" flag.
|
||||
*
|
||||
* This does *not* look for prerelease keywords like "beta" in the version number.
|
||||
* It only uses the data provided by the API. For example, on GitHub, you can
|
||||
* manually mark a release as a prerelease.
|
||||
*/
|
||||
const RELEASE_FILTER_SKIP_PRERELEASE = 1;
|
||||
|
||||
/**
|
||||
* If there are no release assets or none of them match the configured filter,
|
||||
* fall back to the automatically generated source code archive.
|
||||
*/
|
||||
const PREFER_RELEASE_ASSETS = 1;
|
||||
/**
|
||||
* Skip releases that don't have any matching release assets.
|
||||
*/
|
||||
const REQUIRE_RELEASE_ASSETS = 2;
|
||||
|
||||
protected $tagNameProperty = 'name';
|
||||
protected $slug = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $repositoryUrl = '';
|
||||
|
||||
/**
|
||||
* @var mixed Authentication details for private repositories. Format depends on service.
|
||||
*/
|
||||
protected $credentials = null;
|
||||
|
||||
/**
|
||||
* @var string The filter tag that's used to filter options passed to wp_remote_get.
|
||||
* For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug".
|
||||
*/
|
||||
protected $httpFilterName = '';
|
||||
|
||||
/**
|
||||
* @var string The filter applied to the list of update detection strategies that
|
||||
* are used to find the latest version.
|
||||
*/
|
||||
protected $strategyFilterName = '';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $localDirectory = null;
|
||||
|
||||
/**
|
||||
* Api constructor.
|
||||
*
|
||||
* @param string $repositoryUrl
|
||||
* @param array|string|null $credentials
|
||||
*/
|
||||
public function __construct($repositoryUrl, $credentials = null) {
|
||||
$this->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.
|
||||
* @return null|Reference
|
||||
*/
|
||||
public function chooseReference($configBranch) {
|
||||
$strategies = $this->getUpdateDetectionStrategies($configBranch);
|
||||
|
||||
if ( !empty($this->strategyFilterName) ) {
|
||||
$strategies = apply_filters(
|
||||
$this->strategyFilterName,
|
||||
$strategies,
|
||||
$this->slug
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($strategies as $strategy) {
|
||||
$reference = call_user_func($strategy);
|
||||
if ( !empty($reference) ) {
|
||||
return $reference;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ordered list of strategies that can be used to find the latest version.
|
||||
*
|
||||
* The update checker will try each strategy in order until one of them
|
||||
* returns a valid reference.
|
||||
*
|
||||
* @param string $configBranch
|
||||
* @return array<callable> Array of callables that return Vcs_Reference objects.
|
||||
*/
|
||||
abstract protected function getUpdateDetectionStrategies($configBranch);
|
||||
|
||||
/**
|
||||
* Get the readme.txt file from the remote repository and parse it
|
||||
* according to the plugin readme standard.
|
||||
*
|
||||
* @param string $ref Tag or branch name.
|
||||
* @return array Parsed readme.
|
||||
*/
|
||||
public function getRemoteReadme($ref = 'master') {
|
||||
$fileContents = $this->getRemoteFile($this->getLocalReadmeName(), $ref);
|
||||
if ( empty($fileContents) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$parser = new PucReadmeParser();
|
||||
return $parser->parse_readme_contents($fileContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the case-sensitive name of the local readme.txt file.
|
||||
*
|
||||
* In most cases it should just be called "readme.txt", but some plugins call it "README.txt",
|
||||
* "README.TXT", or even "Readme.txt". Most VCS are case-sensitive so we need to know the correct
|
||||
* capitalization.
|
||||
*
|
||||
* Defaults to "readme.txt" (all lowercase).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalReadmeName() {
|
||||
static $fileName = null;
|
||||
if ( $fileName !== null ) {
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
$fileName = 'readme.txt';
|
||||
if ( isset($this->localDirectory) ) {
|
||||
$files = scandir($this->localDirectory);
|
||||
if ( !empty($files) ) {
|
||||
foreach ($files as $possibleFileName) {
|
||||
if ( strcasecmp($possibleFileName, 'readme.txt') === 0 ) {
|
||||
$fileName = $possibleFileName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a branch.
|
||||
*
|
||||
* @param string $branchName
|
||||
* @return Reference|null
|
||||
*/
|
||||
abstract public function getBranch($branchName);
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return Reference|null
|
||||
*/
|
||||
abstract public function getTag($tagName);
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
* (Implementations should skip pre-release versions if possible.)
|
||||
*
|
||||
* @return Reference|null
|
||||
*/
|
||||
abstract public function getLatestTag();
|
||||
|
||||
/**
|
||||
* 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(substr($name, 0, 1)) ) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param \stdClass $tag1 Tag object.
|
||||
* @param \stdClass $tag2 Another tag object.
|
||||
* @return int
|
||||
*/
|
||||
protected function compareTagNames($tag1, $tag2) {
|
||||
$property = $this->tagNameProperty;
|
||||
if ( !isset($tag1->$property) ) {
|
||||
return 1;
|
||||
}
|
||||
if ( !isset($tag2->$property) ) {
|
||||
return -1;
|
||||
}
|
||||
return -version_compare(ltrim($tag1->$property, 'v'), ltrim($tag2->$property, 'v'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
|
||||
*/
|
||||
abstract public function getRemoteFile($path, $ref = 'master');
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return string|null
|
||||
*/
|
||||
abstract public function getLatestCommitTime($ref);
|
||||
|
||||
/**
|
||||
* Get the contents of the changelog file from the repository.
|
||||
*
|
||||
* @param string $ref
|
||||
* @param string $localDirectory Full path to the local plugin or theme directory.
|
||||
* @return null|string The HTML contents of the changelog.
|
||||
*/
|
||||
public function getRemoteChangelog($ref, $localDirectory) {
|
||||
$filename = $this->findChangelogName($localDirectory);
|
||||
if ( empty($filename) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$changelog = $this->getRemoteFile($filename, $ref);
|
||||
if ( $changelog === null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Parsedown::instance()->text($changelog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess the name of the changelog file.
|
||||
*
|
||||
* @param string $directory
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findChangelogName($directory = null) {
|
||||
if ( !isset($directory) ) {
|
||||
$directory = $this->localDirectory;
|
||||
}
|
||||
if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
|
||||
$files = scandir($directory);
|
||||
$foundNames = array_intersect($possibleNames, $files);
|
||||
|
||||
if ( !empty($foundNames) ) {
|
||||
return reset($foundNames);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication credentials.
|
||||
*
|
||||
* @param $credentials
|
||||
*/
|
||||
public function setAuthentication($credentials) {
|
||||
$this->credentials = $credentials;
|
||||
}
|
||||
|
||||
public function isAuthenticationEnabled() {
|
||||
return !empty($this->credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public function signDownloadUrl($url) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filterName
|
||||
*/
|
||||
public function setHttpFilterName($filterName) {
|
||||
$this->httpFilterName = $filterName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filterName
|
||||
*/
|
||||
public function setStrategyFilterName($filterName) {
|
||||
$this->strategyFilterName = $filterName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
*/
|
||||
public function setLocalDirectory($directory) {
|
||||
if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
|
||||
$this->localDirectory = null;
|
||||
} else {
|
||||
$this->localDirectory = $directory;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
*/
|
||||
public function setSlug($slug) {
|
||||
$this->slug = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
29
lib/plugin-update-checker/Puc/v5p3/Vcs/BaseChecker.php
Normal file
29
lib/plugin-update-checker/Puc/v5p3/Vcs/BaseChecker.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
if ( !interface_exists(BaseChecker::class, false) ):
|
||||
|
||||
interface BaseChecker {
|
||||
/**
|
||||
* Set the repository branch to use for updates. Defaults to 'master'.
|
||||
*
|
||||
* @param string $branch
|
||||
* @return $this
|
||||
*/
|
||||
public function setBranch($branch);
|
||||
|
||||
/**
|
||||
* Set authentication credentials.
|
||||
*
|
||||
* @param array|string $credentials
|
||||
* @return $this
|
||||
*/
|
||||
public function setAuthentication($credentials);
|
||||
|
||||
/**
|
||||
* @return Api
|
||||
*/
|
||||
public function getVcsApi();
|
||||
}
|
||||
|
||||
endif;
|
272
lib/plugin-update-checker/Puc/v5p3/Vcs/BitBucketApi.php
Normal file
272
lib/plugin-update-checker/Puc/v5p3/Vcs/BitBucketApi.php
Normal file
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p3\OAuthSignature;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p3\Utils;
|
||||
|
||||
if ( !class_exists(BitBucketApi::class, false) ):
|
||||
|
||||
class BitBucketApi extends Api {
|
||||
/**
|
||||
* @var OAuthSignature
|
||||
*/
|
||||
private $oauth = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $username;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct($repositoryUrl, $credentials = array()) {
|
||||
$path = wp_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 . '"');
|
||||
}
|
||||
|
||||
parent::__construct($repositoryUrl, $credentials);
|
||||
}
|
||||
|
||||
protected function getUpdateDetectionStrategies($configBranch) {
|
||||
$strategies = array(
|
||||
self::STRATEGY_STABLE_TAG => function () use ($configBranch) {
|
||||
return $this->getStableTag($configBranch);
|
||||
},
|
||||
);
|
||||
|
||||
if ( ($configBranch === 'master' || $configBranch === 'main') ) {
|
||||
$strategies[self::STRATEGY_LATEST_TAG] = array($this, 'getLatestTag');
|
||||
}
|
||||
|
||||
$strategies[self::STRATEGY_BRANCH] = function () use ($configBranch) {
|
||||
return $this->getBranch($configBranch);
|
||||
};
|
||||
return $strategies;
|
||||
}
|
||||
|
||||
public function getBranch($branchName) {
|
||||
$branch = $this->api('/refs/branches/' . $branchName);
|
||||
if ( is_wp_error($branch) || empty($branch) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//The "/src/{stuff}/{path}" endpoint doesn't seem to handle branch names that contain slashes.
|
||||
//If we don't encode the slash, we get a 404. If we encode it as "%2F", we get a 401.
|
||||
//To avoid issues, if the branch name is not URL-safe, let's use the commit hash instead.
|
||||
$ref = $branch->name;
|
||||
if ((urlencode($ref) !== $ref) && isset($branch->target->hash)) {
|
||||
$ref = $branch->target->hash;
|
||||
}
|
||||
|
||||
return new Reference(array(
|
||||
'name' => $ref,
|
||||
'updated' => $branch->target->date,
|
||||
'downloadUrl' => $this->getDownloadUrl($branch->name),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return Reference|null
|
||||
*/
|
||||
public function getTag($tagName) {
|
||||
$tag = $this->api('/refs/tags/' . $tagName);
|
||||
if ( is_wp_error($tag) || empty($tag) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'updated' => $tag->target->date,
|
||||
'downloadUrl' => $this->getDownloadUrl($tag->name),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
*
|
||||
* @return Reference|null
|
||||
*/
|
||||
public function getLatestTag() {
|
||||
$tags = $this->api('/refs/tags?sort=-target.date');
|
||||
if ( !isset($tags, $tags->values) || !is_array($tags->values) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Filter and sort the list of tags.
|
||||
$versionTags = $this->sortTagsByVersion($tags->values);
|
||||
|
||||
//Return the first result.
|
||||
if ( !empty($versionTags) ) {
|
||||
$tag = $versionTags[0];
|
||||
return new Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'updated' => $tag->target->date,
|
||||
'downloadUrl' => $this->getDownloadUrl($tag->name),
|
||||
));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag/ref specified by the "Stable tag" header in the readme.txt of a given branch.
|
||||
*
|
||||
* @param string $branch
|
||||
* @return null|Reference
|
||||
*/
|
||||
protected function getStableTag($branch) {
|
||||
$remoteReadme = $this->getRemoteReadme($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 === $branch) || ($tag === 'trunk') ) {
|
||||
return $this->getBranch($branch);
|
||||
}
|
||||
|
||||
return $this->getTag($tag);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ref
|
||||
* @return string
|
||||
*/
|
||||
protected function getDownloadUrl($ref) {
|
||||
return sprintf(
|
||||
'https://bitbucket.org/%s/%s/get/%s.zip',
|
||||
$this->username,
|
||||
$this->repository,
|
||||
$ref
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @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') {
|
||||
$response = $this->api('src/' . $ref . '/' . ltrim($path));
|
||||
if ( is_wp_error($response) || !is_string($response) ) {
|
||||
return null;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a BitBucket API 2.0 request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $version
|
||||
* @return mixed|\WP_Error
|
||||
*/
|
||||
public function api($url, $version = '2.0') {
|
||||
$url = ltrim($url, '/');
|
||||
$isSrcResource = Utils::startsWith($url, 'src/');
|
||||
|
||||
$url = implode('/', array(
|
||||
'https://api.bitbucket.org',
|
||||
$version,
|
||||
'repositories',
|
||||
$this->username,
|
||||
$this->repository,
|
||||
$url
|
||||
));
|
||||
$baseUrl = $url;
|
||||
|
||||
if ( $this->oauth ) {
|
||||
$url = $this->oauth->sign($url,'GET');
|
||||
}
|
||||
|
||||
$options = array('timeout' => wp_doing_cron() ? 10 : 3);
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
$response = wp_remote_get($url, $options);
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
if ( $code === 200 ) {
|
||||
if ( $isSrcResource ) {
|
||||
//Most responses are JSON-encoded, but src resources just
|
||||
//return raw file contents.
|
||||
$document = $body;
|
||||
} else {
|
||||
$document = json_decode($body);
|
||||
}
|
||||
return $document;
|
||||
}
|
||||
|
||||
$error = new \WP_Error(
|
||||
'puc-bitbucket-http-error',
|
||||
sprintf('BitBucket API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
|
||||
);
|
||||
do_action('puc_api_error', $error, $response, $url, $this->slug);
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $credentials
|
||||
*/
|
||||
public function setAuthentication($credentials) {
|
||||
parent::setAuthentication($credentials);
|
||||
|
||||
if ( !empty($credentials) && !empty($credentials['consumer_key']) ) {
|
||||
$this->oauth = new 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;
|
467
lib/plugin-update-checker/Puc/v5p3/Vcs/GitHubApi.php
Normal file
467
lib/plugin-update-checker/Puc/v5p3/Vcs/GitHubApi.php
Normal file
|
@ -0,0 +1,467 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
use Parsedown;
|
||||
|
||||
if ( !class_exists(GitHubApi::class, false) ):
|
||||
|
||||
class GitHubApi extends Api {
|
||||
use ReleaseAssetSupport;
|
||||
use ReleaseFilteringFeature;
|
||||
|
||||
/**
|
||||
* @var string GitHub username.
|
||||
*/
|
||||
protected $userName;
|
||||
/**
|
||||
* @var string GitHub repository name.
|
||||
*/
|
||||
protected $repositoryName;
|
||||
|
||||
/**
|
||||
* @var string Either a fully qualified repository URL, or just "user/repo-name".
|
||||
*/
|
||||
protected $repositoryUrl;
|
||||
|
||||
/**
|
||||
* @var string GitHub authentication token. Optional.
|
||||
*/
|
||||
protected $accessToken;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $downloadFilterAdded = false;
|
||||
|
||||
public function __construct($repositoryUrl, $accessToken = null) {
|
||||
$path = wp_parse_url($repositoryUrl, PHP_URL_PATH);
|
||||
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
|
||||
$this->userName = $matches['username'];
|
||||
$this->repositoryName = $matches['repository'];
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid GitHub repository URL: "' . $repositoryUrl . '"');
|
||||
}
|
||||
|
||||
parent::__construct($repositoryUrl, $accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest release from GitHub.
|
||||
*
|
||||
* @return Reference|null
|
||||
*/
|
||||
public function getLatestRelease() {
|
||||
//The "latest release" endpoint returns one release and always skips pre-releases,
|
||||
//so we can only use it if that's compatible with the current filter settings.
|
||||
if (
|
||||
$this->shouldSkipPreReleases()
|
||||
&& (
|
||||
($this->releaseFilterMaxReleases === 1) || !$this->hasCustomReleaseFilter()
|
||||
)
|
||||
) {
|
||||
//Just get the latest release.
|
||||
$release = $this->api('/repos/:user/:repo/releases/latest');
|
||||
if ( is_wp_error($release) || !is_object($release) || !isset($release->tag_name) ) {
|
||||
return null;
|
||||
}
|
||||
$foundReleases = array($release);
|
||||
} else {
|
||||
//Get a list of the most recent releases.
|
||||
$foundReleases = $this->api(
|
||||
'/repos/:user/:repo/releases',
|
||||
array('per_page' => $this->releaseFilterMaxReleases)
|
||||
);
|
||||
if ( is_wp_error($foundReleases) || !is_array($foundReleases) ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($foundReleases as $release) {
|
||||
//Always skip drafts.
|
||||
if ( isset($release->draft) && !empty($release->draft) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//Skip pre-releases unless specifically included.
|
||||
if (
|
||||
$this->shouldSkipPreReleases()
|
||||
&& isset($release->prerelease)
|
||||
&& !empty($release->prerelease)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$versionNumber = ltrim($release->tag_name, 'v'); //Remove the "v" prefix from "v1.2.3".
|
||||
|
||||
//Custom release filtering.
|
||||
if ( !$this->matchesCustomReleaseFilter($versionNumber, $release) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reference = new Reference(array(
|
||||
'name' => $release->tag_name,
|
||||
'version' => $versionNumber,
|
||||
'downloadUrl' => $release->zipball_url,
|
||||
'updated' => $release->created_at,
|
||||
'apiResponse' => $release,
|
||||
));
|
||||
|
||||
if ( isset($release->assets[0]) ) {
|
||||
$reference->downloadCount = $release->assets[0]->download_count;
|
||||
}
|
||||
|
||||
if ( $this->releaseAssetsEnabled ) {
|
||||
//Use the first release asset that matches the specified regular expression.
|
||||
if ( isset($release->assets, $release->assets[0]) ) {
|
||||
$matchingAssets = array_values(array_filter($release->assets, array($this, 'matchesAssetFilter')));
|
||||
} else {
|
||||
$matchingAssets = array();
|
||||
}
|
||||
|
||||
if ( !empty($matchingAssets) ) {
|
||||
if ( $this->isAuthenticationEnabled() ) {
|
||||
/**
|
||||
* Keep in mind that we'll need to add an "Accept" header to download this asset.
|
||||
*
|
||||
* @see setUpdateDownloadHeaders()
|
||||
*/
|
||||
$reference->downloadUrl = $matchingAssets[0]->url;
|
||||
} else {
|
||||
//It seems that browser_download_url only works for public repositories.
|
||||
//Using an access_token doesn't help. Maybe OAuth would work?
|
||||
$reference->downloadUrl = $matchingAssets[0]->browser_download_url;
|
||||
}
|
||||
|
||||
$reference->downloadCount = $matchingAssets[0]->download_count;
|
||||
} else if ( $this->releaseAssetPreference === Api::REQUIRE_RELEASE_ASSETS ) {
|
||||
//None of the assets match the filter, and we're not allowed
|
||||
//to fall back to the auto-generated source ZIP.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty($release->body) ) {
|
||||
$reference->changelog = Parsedown::instance()->text($release->body);
|
||||
}
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
*
|
||||
* @return Reference|null
|
||||
*/
|
||||
public function getLatestTag() {
|
||||
$tags = $this->api('/repos/:user/:repo/tags');
|
||||
|
||||
if ( is_wp_error($tags) || !is_array($tags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$versionTags = $this->sortTagsByVersion($tags);
|
||||
if ( empty($versionTags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tag = $versionTags[0];
|
||||
return new Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'downloadUrl' => $tag->zipball_url,
|
||||
'apiResponse' => $tag,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a branch by name.
|
||||
*
|
||||
* @param string $branchName
|
||||
* @return null|Reference
|
||||
*/
|
||||
public function getBranch($branchName) {
|
||||
$branch = $this->api('/repos/:user/:repo/branches/' . $branchName);
|
||||
if ( is_wp_error($branch) || empty($branch) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reference = new Reference(array(
|
||||
'name' => $branch->name,
|
||||
'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
|
||||
'apiResponse' => $branch,
|
||||
));
|
||||
|
||||
if ( isset($branch->commit, $branch->commit->commit, $branch->commit->commit->author->date) ) {
|
||||
$reference->updated = $branch->commit->commit->author->date;
|
||||
}
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest commit that changed the specified file.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return \StdClass|null
|
||||
*/
|
||||
public function getLatestCommit($filename, $ref = 'master') {
|
||||
$commits = $this->api(
|
||||
'/repos/:user/:repo/commits',
|
||||
array(
|
||||
'path' => $filename,
|
||||
'sha' => $ref,
|
||||
)
|
||||
);
|
||||
if ( !is_wp_error($commits) && isset($commits[0]) ) {
|
||||
return $commits[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLatestCommitTime($ref) {
|
||||
$commits = $this->api('/repos/:user/:repo/commits', array('sha' => $ref));
|
||||
if ( !is_wp_error($commits) && isset($commits[0]) ) {
|
||||
return $commits[0]->commit->author->date;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a GitHub API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return mixed|\WP_Error
|
||||
*/
|
||||
protected function api($url, $queryParams = array()) {
|
||||
$baseUrl = $url;
|
||||
$url = $this->buildApiUrl($url, $queryParams);
|
||||
|
||||
$options = array('timeout' => wp_doing_cron() ? 10 : 3);
|
||||
if ( $this->isAuthenticationEnabled() ) {
|
||||
$options['headers'] = array('Authorization' => $this->getAuthorizationHeader());
|
||||
}
|
||||
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
$response = wp_remote_get($url, $options);
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
if ( $code === 200 ) {
|
||||
$document = json_decode($body);
|
||||
return $document;
|
||||
}
|
||||
|
||||
$error = new \WP_Error(
|
||||
'puc-github-http-error',
|
||||
sprintf('GitHub API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
|
||||
);
|
||||
do_action('puc_api_error', $error, $response, $url, $this->slug);
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fully qualified URL for an API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return string
|
||||
*/
|
||||
protected function buildApiUrl($url, $queryParams) {
|
||||
$variables = array(
|
||||
'user' => $this->userName,
|
||||
'repo' => $this->repositoryName,
|
||||
);
|
||||
foreach ($variables as $name => $value) {
|
||||
$url = str_replace('/:' . $name, '/' . urlencode($value), $url);
|
||||
}
|
||||
$url = 'https://api.github.com' . $url;
|
||||
|
||||
if ( !empty($queryParams) ) {
|
||||
$url = add_query_arg($queryParams, $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @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') {
|
||||
$apiUrl = '/repos/:user/:repo/contents/' . $path;
|
||||
$response = $this->api($apiUrl, array('ref' => $ref));
|
||||
|
||||
if ( is_wp_error($response) || !isset($response->content) || ($response->encoding !== 'base64') ) {
|
||||
return null;
|
||||
}
|
||||
return base64_decode($response->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL to download a ZIP archive of the specified branch/tag/etc.
|
||||
*
|
||||
* @param string $ref
|
||||
* @return string
|
||||
*/
|
||||
public function buildArchiveDownloadUrl($ref = 'master') {
|
||||
$url = sprintf(
|
||||
'https://api.github.com/repos/%1$s/%2$s/zipball/%3$s',
|
||||
urlencode($this->userName),
|
||||
urlencode($this->repositoryName),
|
||||
urlencode($ref)
|
||||
);
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return void
|
||||
*/
|
||||
public function getTag($tagName) {
|
||||
//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;
|
||||
|
||||
//Optimization: Instead of filtering all HTTP requests, let's do it only when
|
||||
//WordPress is about to download an update.
|
||||
add_filter('upgrader_pre_download', array($this, 'addHttpRequestFilter'), 10, 1); //WP 3.7+
|
||||
}
|
||||
|
||||
protected function getUpdateDetectionStrategies($configBranch) {
|
||||
$strategies = array();
|
||||
|
||||
if ( $configBranch === 'master' || $configBranch === 'main') {
|
||||
//Use the latest release.
|
||||
$strategies[self::STRATEGY_LATEST_RELEASE] = array($this, 'getLatestRelease');
|
||||
//Failing that, use the tag with the highest version number.
|
||||
$strategies[self::STRATEGY_LATEST_TAG] = array($this, 'getLatestTag');
|
||||
}
|
||||
|
||||
//Alternatively, just use the branch itself.
|
||||
$strategies[self::STRATEGY_BRANCH] = function () use ($configBranch) {
|
||||
return $this->getBranch($configBranch);
|
||||
};
|
||||
|
||||
return $strategies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unchanging part of a release asset URL. Used to identify download attempts.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAssetApiBaseUrl() {
|
||||
return sprintf(
|
||||
'//api.github.com/repos/%1$s/%2$s/releases/assets/',
|
||||
$this->userName,
|
||||
$this->repositoryName
|
||||
);
|
||||
}
|
||||
|
||||
protected function getFilterableAssetName($releaseAsset) {
|
||||
if ( isset($releaseAsset->name) ) {
|
||||
return $releaseAsset->name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $result
|
||||
* @return bool
|
||||
* @internal
|
||||
*/
|
||||
public function addHttpRequestFilter($result) {
|
||||
if ( !$this->downloadFilterAdded && $this->isAuthenticationEnabled() ) {
|
||||
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout.
|
||||
add_filter('http_request_args', array($this, 'setUpdateDownloadHeaders'), 10, 2);
|
||||
add_action('requests-requests.before_redirect', array($this, 'removeAuthHeaderFromRedirects'), 10, 4);
|
||||
$this->downloadFilterAdded = true;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP headers that are necessary to download updates from private repositories.
|
||||
*
|
||||
* See GitHub docs:
|
||||
*
|
||||
* @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
|
||||
* @link https://developer.github.com/v3/auth/#basic-authentication
|
||||
*
|
||||
* @internal
|
||||
* @param array $requestArgs
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function setUpdateDownloadHeaders($requestArgs, $url = '') {
|
||||
//Is WordPress trying to download one of our release assets?
|
||||
if ( $this->releaseAssetsEnabled && (strpos($url, $this->getAssetApiBaseUrl()) !== false) ) {
|
||||
$requestArgs['headers']['Accept'] = 'application/octet-stream';
|
||||
}
|
||||
//Use Basic authentication, but only if the download is from our repository.
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
|
||||
if ( $this->isAuthenticationEnabled() && (strpos($url, $repoApiBaseUrl)) === 0 ) {
|
||||
$requestArgs['headers']['Authorization'] = $this->getAuthorizationHeader();
|
||||
}
|
||||
return $requestArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* When following a redirect, the Requests library will automatically forward
|
||||
* the authorization header to other hosts. We don't want that because it breaks
|
||||
* AWS downloads and can leak authorization information.
|
||||
*
|
||||
* @param string $location
|
||||
* @param array $headers
|
||||
* @internal
|
||||
*/
|
||||
public function removeAuthHeaderFromRedirects(&$location, &$headers) {
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
|
||||
if ( strpos($location, $repoApiBaseUrl) === 0 ) {
|
||||
return; //This request is going to GitHub, so it's fine.
|
||||
}
|
||||
//Remove the header.
|
||||
if ( isset($headers['Authorization']) ) {
|
||||
unset($headers['Authorization']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the value of the "Authorization" header.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorizationHeader() {
|
||||
return 'Basic ' . base64_encode($this->userName . ':' . $this->accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
414
lib/plugin-update-checker/Puc/v5p3/Vcs/GitLabApi.php
Normal file
414
lib/plugin-update-checker/Puc/v5p3/Vcs/GitLabApi.php
Normal file
|
@ -0,0 +1,414 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
if ( !class_exists(GitLabApi::class, false) ):
|
||||
|
||||
class GitLabApi extends Api {
|
||||
use ReleaseAssetSupport;
|
||||
use ReleaseFilteringFeature;
|
||||
|
||||
/**
|
||||
* @var string GitLab username.
|
||||
*/
|
||||
protected $userName;
|
||||
|
||||
/**
|
||||
* @var string GitLab server host.
|
||||
*/
|
||||
protected $repositoryHost;
|
||||
|
||||
/**
|
||||
* @var string Protocol used by this GitLab server: "http" or "https".
|
||||
*/
|
||||
protected $repositoryProtocol = 'https';
|
||||
|
||||
/**
|
||||
* @var string GitLab repository name.
|
||||
*/
|
||||
protected $repositoryName;
|
||||
|
||||
/**
|
||||
* @var string GitLab authentication token. Optional.
|
||||
*/
|
||||
protected $accessToken;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @var bool No longer used.
|
||||
*/
|
||||
protected $releasePackageEnabled = false;
|
||||
|
||||
public function __construct($repositoryUrl, $accessToken = null, $subgroup = null) {
|
||||
//Parse the repository host to support custom hosts.
|
||||
$port = wp_parse_url($repositoryUrl, PHP_URL_PORT);
|
||||
if ( !empty($port) ) {
|
||||
$port = ':' . $port;
|
||||
}
|
||||
$this->repositoryHost = wp_parse_url($repositoryUrl, PHP_URL_HOST) . $port;
|
||||
|
||||
if ( $this->repositoryHost !== 'gitlab.com' ) {
|
||||
$this->repositoryProtocol = wp_parse_url($repositoryUrl, PHP_URL_SCHEME);
|
||||
}
|
||||
|
||||
//Find the repository information
|
||||
$path = wp_parse_url($repositoryUrl, PHP_URL_PATH);
|
||||
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
|
||||
$this->userName = $matches['username'];
|
||||
$this->repositoryName = $matches['repository'];
|
||||
} elseif ( ($this->repositoryHost === 'gitlab.com') ) {
|
||||
//This is probably a repository in a subgroup, e.g. "/organization/category/repo".
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
if ( count($parts) < 3 ) {
|
||||
throw new \InvalidArgumentException('Invalid GitLab.com repository URL: "' . $repositoryUrl . '"');
|
||||
}
|
||||
$lastPart = array_pop($parts);
|
||||
$this->userName = implode('/', $parts);
|
||||
$this->repositoryName = $lastPart;
|
||||
} else {
|
||||
//There could be subgroups in the URL: gitlab.domain.com/group/subgroup/subgroup2/repository
|
||||
if ( $subgroup !== null ) {
|
||||
$path = str_replace(trailingslashit($subgroup), '', $path);
|
||||
}
|
||||
|
||||
//This is not a traditional url, it could be gitlab is in a deeper subdirectory.
|
||||
//Get the path segments.
|
||||
$segments = explode('/', untrailingslashit(ltrim($path, '/')));
|
||||
|
||||
//We need at least /user-name/repository-name/
|
||||
if ( count($segments) < 2 ) {
|
||||
throw new \InvalidArgumentException('Invalid GitLab repository URL: "' . $repositoryUrl . '"');
|
||||
}
|
||||
|
||||
//Get the username and repository name.
|
||||
$usernameRepo = array_splice($segments, -2, 2);
|
||||
$this->userName = $usernameRepo[0];
|
||||
$this->repositoryName = $usernameRepo[1];
|
||||
|
||||
//Append the remaining segments to the host if there are segments left.
|
||||
if ( count($segments) > 0 ) {
|
||||
$this->repositoryHost = trailingslashit($this->repositoryHost) . implode('/', $segments);
|
||||
}
|
||||
|
||||
//Add subgroups to username.
|
||||
if ( $subgroup !== null ) {
|
||||
$this->userName = $usernameRepo[0] . '/' . untrailingslashit($subgroup);
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($repositoryUrl, $accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest release from GitLab.
|
||||
*
|
||||
* @return Reference|null
|
||||
*/
|
||||
public function getLatestRelease() {
|
||||
$releases = $this->api('/:id/releases', array('per_page' => $this->releaseFilterMaxReleases));
|
||||
if ( is_wp_error($releases) || empty($releases) || !is_array($releases) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($releases as $release) {
|
||||
if (
|
||||
//Skip invalid/unsupported releases.
|
||||
!is_object($release)
|
||||
|| !isset($release->tag_name)
|
||||
//Skip upcoming releases.
|
||||
|| (
|
||||
!empty($release->upcoming_release)
|
||||
&& $this->shouldSkipPreReleases()
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$versionNumber = ltrim($release->tag_name, 'v'); //Remove the "v" prefix from "v1.2.3".
|
||||
|
||||
//Apply custom filters.
|
||||
if ( !$this->matchesCustomReleaseFilter($versionNumber, $release) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$downloadUrl = $this->findReleaseDownloadUrl($release);
|
||||
if ( empty($downloadUrl) ) {
|
||||
//The latest release doesn't have valid download URL.
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( !empty($this->accessToken) ) {
|
||||
$downloadUrl = add_query_arg('private_token', $this->accessToken, $downloadUrl);
|
||||
}
|
||||
|
||||
return new Reference(array(
|
||||
'name' => $release->tag_name,
|
||||
'version' => $versionNumber,
|
||||
'downloadUrl' => $downloadUrl,
|
||||
'updated' => $release->released_at,
|
||||
'apiResponse' => $release,
|
||||
));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $release
|
||||
* @return string|null
|
||||
*/
|
||||
protected function findReleaseDownloadUrl($release) {
|
||||
if ( $this->releaseAssetsEnabled ) {
|
||||
if ( isset($release->assets, $release->assets->links) ) {
|
||||
//Use the first asset link where the URL matches the filter.
|
||||
foreach ($release->assets->links as $link) {
|
||||
if ( $this->matchesAssetFilter($link) ) {
|
||||
return $link->url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->releaseAssetPreference === Api::REQUIRE_RELEASE_ASSETS ) {
|
||||
//Falling back to source archives is not allowed, so give up.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Use the first source code archive that's in ZIP format.
|
||||
foreach ($release->assets->sources as $source) {
|
||||
if ( isset($source->format) && ($source->format === 'zip') ) {
|
||||
return $source->url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag that looks like the highest version number.
|
||||
*
|
||||
* @return Reference|null
|
||||
*/
|
||||
public function getLatestTag() {
|
||||
$tags = $this->api('/:id/repository/tags');
|
||||
if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$versionTags = $this->sortTagsByVersion($tags);
|
||||
if ( empty($versionTags) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tag = $versionTags[0];
|
||||
return new Reference(array(
|
||||
'name' => $tag->name,
|
||||
'version' => ltrim($tag->name, 'v'),
|
||||
'downloadUrl' => $this->buildArchiveDownloadUrl($tag->name),
|
||||
'apiResponse' => $tag,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a branch by name.
|
||||
*
|
||||
* @param string $branchName
|
||||
* @return null|Reference
|
||||
*/
|
||||
public function getBranch($branchName) {
|
||||
$branch = $this->api('/:id/repository/branches/' . $branchName);
|
||||
if ( is_wp_error($branch) || empty($branch) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reference = new Reference(array(
|
||||
'name' => $branch->name,
|
||||
'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
|
||||
'apiResponse' => $branch,
|
||||
));
|
||||
|
||||
if ( isset($branch->commit, $branch->commit->committed_date) ) {
|
||||
$reference->updated = $branch->commit->committed_date;
|
||||
}
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp of the latest commit that changed the specified branch or tag.
|
||||
*
|
||||
* @param string $ref Reference name (e.g. branch or tag).
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLatestCommitTime($ref) {
|
||||
$commits = $this->api('/:id/repository/commits/', array('ref_name' => $ref));
|
||||
if ( is_wp_error($commits) || !is_array($commits) || !isset($commits[0]) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $commits[0]->committed_date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a GitLab API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return mixed|\WP_Error
|
||||
*/
|
||||
protected function api($url, $queryParams = array()) {
|
||||
$baseUrl = $url;
|
||||
$url = $this->buildApiUrl($url, $queryParams);
|
||||
|
||||
$options = array('timeout' => wp_doing_cron() ? 10 : 3);
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, $options);
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
if ( $code === 200 ) {
|
||||
return json_decode($body);
|
||||
}
|
||||
|
||||
$error = new \WP_Error(
|
||||
'puc-gitlab-http-error',
|
||||
sprintf('GitLab API error. URL: "%s", HTTP status code: %d.', $baseUrl, $code)
|
||||
);
|
||||
do_action('puc_api_error', $error, $response, $url, $this->slug);
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fully qualified URL for an API request.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $queryParams
|
||||
* @return string
|
||||
*/
|
||||
protected function buildApiUrl($url, $queryParams) {
|
||||
$variables = array(
|
||||
'user' => $this->userName,
|
||||
'repo' => $this->repositoryName,
|
||||
'id' => $this->userName . '/' . $this->repositoryName,
|
||||
);
|
||||
|
||||
foreach ($variables as $name => $value) {
|
||||
$url = str_replace("/:{$name}", '/' . urlencode($value), $url);
|
||||
}
|
||||
|
||||
$url = substr($url, 1);
|
||||
$url = sprintf('%1$s://%2$s/api/v4/projects/%3$s', $this->repositoryProtocol, $this->repositoryHost, $url);
|
||||
|
||||
if ( !empty($this->accessToken) ) {
|
||||
$queryParams['private_token'] = $this->accessToken;
|
||||
}
|
||||
|
||||
if ( !empty($queryParams) ) {
|
||||
$url = add_query_arg($queryParams, $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file from a specific branch or tag.
|
||||
*
|
||||
* @param string $path File name.
|
||||
* @param string $ref
|
||||
* @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') {
|
||||
$response = $this->api('/:id/repository/files/' . $path, array('ref' => $ref));
|
||||
if ( is_wp_error($response) || !isset($response->content) || $response->encoding !== 'base64' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64_decode($response->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL to download a ZIP archive of the specified branch/tag/etc.
|
||||
*
|
||||
* @param string $ref
|
||||
* @return string
|
||||
*/
|
||||
public function buildArchiveDownloadUrl($ref = 'master') {
|
||||
$url = sprintf(
|
||||
'%1$s://%2$s/api/v4/projects/%3$s/repository/archive.zip',
|
||||
$this->repositoryProtocol,
|
||||
$this->repositoryHost,
|
||||
urlencode($this->userName . '/' . $this->repositoryName)
|
||||
);
|
||||
$url = add_query_arg('sha', urlencode($ref), $url);
|
||||
|
||||
if ( !empty($this->accessToken) ) {
|
||||
$url = add_query_arg('private_token', $this->accessToken, $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific tag.
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return void
|
||||
*/
|
||||
public function getTag($tagName) {
|
||||
throw new \LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
|
||||
}
|
||||
|
||||
protected function getUpdateDetectionStrategies($configBranch) {
|
||||
$strategies = array();
|
||||
|
||||
if ( ($configBranch === 'main') || ($configBranch === 'master') ) {
|
||||
$strategies[self::STRATEGY_LATEST_RELEASE] = array($this, 'getLatestRelease');
|
||||
$strategies[self::STRATEGY_LATEST_TAG] = array($this, 'getLatestTag');
|
||||
}
|
||||
|
||||
$strategies[self::STRATEGY_BRANCH] = function () use ($configBranch) {
|
||||
return $this->getBranch($configBranch);
|
||||
};
|
||||
|
||||
return $strategies;
|
||||
}
|
||||
|
||||
public function setAuthentication($credentials) {
|
||||
parent::setAuthentication($credentials);
|
||||
$this->accessToken = is_string($credentials) ? $credentials : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use release assets that link to GitLab generic packages (e.g. .zip files)
|
||||
* instead of automatically generated source archives.
|
||||
*
|
||||
* This is included for backwards compatibility with older versions of PUC.
|
||||
*
|
||||
* @return void
|
||||
* @deprecated Use enableReleaseAssets() instead.
|
||||
* @noinspection PhpUnused -- Public API
|
||||
*/
|
||||
public function enableReleasePackages() {
|
||||
$this->enableReleaseAssets(
|
||||
/** @lang RegExp */ '/\.zip($|[?&#])/i',
|
||||
Api::REQUIRE_RELEASE_ASSETS
|
||||
);
|
||||
}
|
||||
|
||||
protected function getFilterableAssetName($releaseAsset) {
|
||||
if ( isset($releaseAsset->url) ) {
|
||||
return $releaseAsset->url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
275
lib/plugin-update-checker/Puc/v5p3/Vcs/PluginUpdateChecker.php
Normal file
275
lib/plugin-update-checker/Puc/v5p3/Vcs/PluginUpdateChecker.php
Normal file
|
@ -0,0 +1,275 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p3\Plugin;
|
||||
|
||||
if ( !class_exists(PluginUpdateChecker::class, false) ):
|
||||
|
||||
class PluginUpdateChecker extends Plugin\UpdateChecker implements BaseChecker {
|
||||
use VcsCheckerMethods;
|
||||
|
||||
/**
|
||||
* PluginUpdateChecker constructor.
|
||||
*
|
||||
* @param Api $api
|
||||
* @param string $pluginFile
|
||||
* @param string $slug
|
||||
* @param int $checkPeriod
|
||||
* @param string $optionName
|
||||
* @param string $muPluginFile
|
||||
*/
|
||||
public function __construct($api, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
|
||||
$this->api = $api;
|
||||
|
||||
parent::__construct($api->getRepositoryUrl(), $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);
|
||||
|
||||
$this->api->setHttpFilterName($this->getUniqueName('request_info_options'));
|
||||
$this->api->setStrategyFilterName($this->getUniqueName('vcs_update_detection_strategies'));
|
||||
$this->api->setSlug($this->slug);
|
||||
}
|
||||
|
||||
public function requestInfo($unusedParameter = null) {
|
||||
//We have to make several remote API requests to gather all the necessary info
|
||||
//which can take a while on slow networks.
|
||||
if ( function_exists('set_time_limit') ) {
|
||||
@set_time_limit(60);
|
||||
}
|
||||
|
||||
$api = $this->api;
|
||||
$api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
|
||||
|
||||
$info = new Plugin\PluginInfo();
|
||||
$info->filename = $this->pluginFile;
|
||||
$info->slug = $this->slug;
|
||||
|
||||
$this->setInfoFromHeader($this->package->getPluginHeader(), $info);
|
||||
$this->setIconsFromLocalAssets($info);
|
||||
$this->setBannersFromLocalAssets($info);
|
||||
|
||||
//Pick a branch or tag.
|
||||
$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.
|
||||
do_action(
|
||||
'puc_api_error',
|
||||
new \WP_Error(
|
||||
'puc-no-update-source',
|
||||
'Could not retrieve version information from the repository. '
|
||||
. 'This usually means that the update checker either can\'t connect '
|
||||
. 'to the repository or it\'s configured incorrectly.'
|
||||
),
|
||||
null, null, $this->slug
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
//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->package->getFileHeader($remotePlugin);
|
||||
$this->setInfoFromHeader($remoteHeader, $info);
|
||||
}
|
||||
|
||||
//Sanity check: Reject updates that don't have a version number.
|
||||
//This can happen when we're using a branch, and we either fail to retrieve the main plugin
|
||||
//file or the file doesn't have a "Version" header.
|
||||
if ( empty($info->version) ) {
|
||||
do_action(
|
||||
'puc_api_error',
|
||||
new \WP_Error(
|
||||
'puc-no-plugin-version',
|
||||
'Could not find the version number in the repository.'
|
||||
),
|
||||
null, null, $this->slug
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
//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, $this->package->getAbsoluteDirectoryPath());
|
||||
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 or branch and use it as the "last_updated" date.
|
||||
$latestCommitTime = $api->getLatestCommitTime($ref);
|
||||
if ( $latestCommitTime !== null ) {
|
||||
$info->last_updated = $latestCommitTime;
|
||||
}
|
||||
}
|
||||
|
||||
$info = apply_filters($this->getUniqueName('request_info_result'), $info, null);
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the currently installed version has a readme.txt file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function readmeTxtExistsLocally() {
|
||||
return $this->package->fileExists($this->api->getLocalReadmeName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy plugin metadata from a file header to a Plugin Info object.
|
||||
*
|
||||
* @param array $fileHeader
|
||||
* @param Plugin\PluginInfo $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',
|
||||
|
||||
'Requires PHP' => 'requires_php',
|
||||
);
|
||||
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 Plugin\PluginInfo $pluginInfo
|
||||
*/
|
||||
protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
|
||||
$readme = $this->api->getRemoteReadme($ref);
|
||||
if ( empty($readme) ) {
|
||||
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 ( !empty($readme['requires_php']) ) {
|
||||
$pluginInfo->requires_php = $readme['requires_php'];
|
||||
}
|
||||
|
||||
if ( isset($readme['upgrade_notice'], $readme['upgrade_notice'][$pluginInfo->version]) ) {
|
||||
$pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add icons from the currently installed version to a Plugin Info object.
|
||||
*
|
||||
* The icons should be in a subdirectory named "assets". Supported image formats
|
||||
* and file names are described here:
|
||||
* @link https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
|
||||
*
|
||||
* @param Plugin\PluginInfo $pluginInfo
|
||||
*/
|
||||
protected function setIconsFromLocalAssets($pluginInfo) {
|
||||
$icons = $this->getLocalAssetUrls(array(
|
||||
'icon.svg' => 'svg',
|
||||
'icon-256x256.png' => '2x',
|
||||
'icon-256x256.jpg' => '2x',
|
||||
'icon-128x128.png' => '1x',
|
||||
'icon-128x128.jpg' => '1x',
|
||||
));
|
||||
|
||||
if ( !empty($icons) ) {
|
||||
//The "default" key seems to be used only as last-resort fallback in WP core (5.8/5.9),
|
||||
//but we'll set it anyway in case some code somewhere needs it.
|
||||
reset($icons);
|
||||
$firstKey = key($icons);
|
||||
$icons['default'] = $icons[$firstKey];
|
||||
|
||||
$pluginInfo->icons = $icons;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add banners from the currently installed version to a Plugin Info object.
|
||||
*
|
||||
* The banners should be in a subdirectory named "assets". Supported image formats
|
||||
* and file names are described here:
|
||||
* @link https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-headers
|
||||
*
|
||||
* @param Plugin\PluginInfo $pluginInfo
|
||||
*/
|
||||
protected function setBannersFromLocalAssets($pluginInfo) {
|
||||
$banners = $this->getLocalAssetUrls(array(
|
||||
'banner-772x250.png' => 'high',
|
||||
'banner-772x250.jpg' => 'high',
|
||||
'banner-1544x500.png' => 'low',
|
||||
'banner-1544x500.jpg' => 'low',
|
||||
));
|
||||
|
||||
if ( !empty($banners) ) {
|
||||
$pluginInfo->banners = $banners;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $filesToKeys
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function getLocalAssetUrls($filesToKeys) {
|
||||
$assetDirectory = $this->package->getAbsoluteDirectoryPath() . DIRECTORY_SEPARATOR . 'assets';
|
||||
if ( !is_dir($assetDirectory) ) {
|
||||
return array();
|
||||
}
|
||||
$assetBaseUrl = trailingslashit(plugins_url('', $assetDirectory . '/imaginary.file'));
|
||||
|
||||
$foundAssets = array();
|
||||
foreach ($filesToKeys as $fileName => $key) {
|
||||
$fullBannerPath = $assetDirectory . DIRECTORY_SEPARATOR . $fileName;
|
||||
if ( !isset($icons[$key]) && is_file($fullBannerPath) ) {
|
||||
$foundAssets[$key] = $assetBaseUrl . $fileName;
|
||||
}
|
||||
}
|
||||
|
||||
return $foundAssets;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
51
lib/plugin-update-checker/Puc/v5p3/Vcs/Reference.php
Normal file
51
lib/plugin-update-checker/Puc/v5p3/Vcs/Reference.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
if ( !class_exists(Reference::class, false) ):
|
||||
|
||||
/**
|
||||
* This class represents a VCS branch or tag. It's intended as a read only, short-lived container
|
||||
* that only exists to provide a limited degree of type checking.
|
||||
*
|
||||
* @property string $name
|
||||
* @property string|null version
|
||||
* @property string $downloadUrl
|
||||
* @property string $updated
|
||||
*
|
||||
* @property string|null $changelog
|
||||
* @property int|null $downloadCount
|
||||
*/
|
||||
class Reference {
|
||||
private $properties = array();
|
||||
|
||||
public function __construct($properties = array()) {
|
||||
$this->properties = $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function __get($name) {
|
||||
return array_key_exists($name, $this->properties) ? $this->properties[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __set($name, $value) {
|
||||
$this->properties[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($name) {
|
||||
return isset($this->properties[$name]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
endif;
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
if ( !trait_exists(ReleaseAssetSupport::class, false) ) :
|
||||
|
||||
trait ReleaseAssetSupport {
|
||||
/**
|
||||
* @var bool Whether to download release assets instead of the auto-generated
|
||||
* source code archives.
|
||||
*/
|
||||
protected $releaseAssetsEnabled = false;
|
||||
|
||||
/**
|
||||
* @var string|null Regular expression that's used to filter release assets
|
||||
* by file name or URL. Optional.
|
||||
*/
|
||||
protected $assetFilterRegex = null;
|
||||
|
||||
/**
|
||||
* How to handle releases that don't have any matching release assets.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $releaseAssetPreference = Api::PREFER_RELEASE_ASSETS;
|
||||
|
||||
/**
|
||||
* Enable updating via release assets.
|
||||
*
|
||||
* If the latest release contains no usable assets, the update checker
|
||||
* will fall back to using the automatically generated ZIP archive.
|
||||
*
|
||||
* @param string|null $nameRegex Optional. Use only those assets where
|
||||
* the file name or URL matches this regex.
|
||||
* @param int $preference Optional. How to handle releases that don't have
|
||||
* any matching release assets.
|
||||
*/
|
||||
public function enableReleaseAssets($nameRegex = null, $preference = Api::PREFER_RELEASE_ASSETS) {
|
||||
$this->releaseAssetsEnabled = true;
|
||||
$this->assetFilterRegex = $nameRegex;
|
||||
$this->releaseAssetPreference = $preference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable release assets.
|
||||
*
|
||||
* @return void
|
||||
* @noinspection PhpUnused -- Public API
|
||||
*/
|
||||
public function disableReleaseAssets() {
|
||||
$this->releaseAssetsEnabled = false;
|
||||
$this->assetFilterRegex = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the specified asset match the name regex?
|
||||
*
|
||||
* @param mixed $releaseAsset Data type and structure depend on the host/API.
|
||||
* @return bool
|
||||
*/
|
||||
protected function matchesAssetFilter($releaseAsset) {
|
||||
if ( $this->assetFilterRegex === null ) {
|
||||
//The default is to accept all assets.
|
||||
return true;
|
||||
}
|
||||
|
||||
$name = $this->getFilterableAssetName($releaseAsset);
|
||||
if ( !is_string($name) ) {
|
||||
return false;
|
||||
}
|
||||
return (bool)preg_match($this->assetFilterRegex, $releaseAsset->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the part of asset data that will be checked against the filter regex.
|
||||
*
|
||||
* @param mixed $releaseAsset
|
||||
* @return string|null
|
||||
*/
|
||||
abstract protected function getFilterableAssetName($releaseAsset);
|
||||
}
|
||||
|
||||
endif;
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
if ( !trait_exists(ReleaseFilteringFeature::class, false) ) :
|
||||
|
||||
trait ReleaseFilteringFeature {
|
||||
/**
|
||||
* @var callable|null
|
||||
*/
|
||||
protected $releaseFilterCallback = null;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $releaseFilterMaxReleases = 1;
|
||||
/**
|
||||
* @var string One of the Api::RELEASE_FILTER_* constants.
|
||||
*/
|
||||
protected $releaseFilterByType = Api::RELEASE_FILTER_SKIP_PRERELEASE;
|
||||
|
||||
/**
|
||||
* Set a custom release filter.
|
||||
*
|
||||
* Setting a new filter will override the old filter, if any.
|
||||
*
|
||||
* @param callable $callback A callback that accepts a version number and a release
|
||||
* object, and returns a boolean.
|
||||
* @param int $releaseTypes One of the Api::RELEASE_FILTER_* constants.
|
||||
* @param int $maxReleases Optional. The maximum number of recent releases to examine
|
||||
* when trying to find a release that matches the filter. 1 to 100.
|
||||
* @return $this
|
||||
*/
|
||||
public function setReleaseFilter(
|
||||
$callback,
|
||||
$releaseTypes = Api::RELEASE_FILTER_SKIP_PRERELEASE,
|
||||
$maxReleases = 20
|
||||
) {
|
||||
if ( $maxReleases > 100 ) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'The max number of releases is too high (%d). It must be 100 or less.',
|
||||
$maxReleases
|
||||
));
|
||||
} else if ( $maxReleases < 1 ) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'The max number of releases is too low (%d). It must be at least 1.',
|
||||
$maxReleases
|
||||
));
|
||||
}
|
||||
|
||||
$this->releaseFilterCallback = $callback;
|
||||
$this->releaseFilterByType = $releaseTypes;
|
||||
$this->releaseFilterMaxReleases = $maxReleases;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter releases by their version number.
|
||||
*
|
||||
* @param string $regex A regular expression. The release version number must match this regex.
|
||||
* @param int $releaseTypes
|
||||
* @param int $maxReleasesToExamine
|
||||
* @return $this
|
||||
* @noinspection PhpUnused -- Public API
|
||||
*/
|
||||
public function setReleaseVersionFilter(
|
||||
$regex,
|
||||
$releaseTypes = Api::RELEASE_FILTER_SKIP_PRERELEASE,
|
||||
$maxReleasesToExamine = 20
|
||||
) {
|
||||
return $this->setReleaseFilter(
|
||||
function ($versionNumber) use ($regex) {
|
||||
return (preg_match($regex, $versionNumber) === 1);
|
||||
},
|
||||
$releaseTypes,
|
||||
$maxReleasesToExamine
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $versionNumber The detected release version number.
|
||||
* @param object $releaseObject Varies depending on the host/API.
|
||||
* @return bool
|
||||
*/
|
||||
protected function matchesCustomReleaseFilter($versionNumber, $releaseObject) {
|
||||
if ( !is_callable($this->releaseFilterCallback) ) {
|
||||
return true; //No custom filter.
|
||||
}
|
||||
return call_user_func($this->releaseFilterCallback, $versionNumber, $releaseObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldSkipPreReleases() {
|
||||
//Maybe this could be a bitfield in the future, if we need to support
|
||||
//more release types.
|
||||
return ($this->releaseFilterByType !== Api::RELEASE_FILTER_ALL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasCustomReleaseFilter() {
|
||||
return isset($this->releaseFilterCallback) && is_callable($this->releaseFilterCallback);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p3\Theme;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p3\Utils;
|
||||
|
||||
if ( !class_exists(ThemeUpdateChecker::class, false) ):
|
||||
|
||||
class ThemeUpdateChecker extends Theme\UpdateChecker implements BaseChecker {
|
||||
use VcsCheckerMethods;
|
||||
|
||||
/**
|
||||
* ThemeUpdateChecker constructor.
|
||||
*
|
||||
* @param Api $api
|
||||
* @param null $stylesheet
|
||||
* @param null $customSlug
|
||||
* @param int $checkPeriod
|
||||
* @param string $optionName
|
||||
*/
|
||||
public function __construct($api, $stylesheet = null, $customSlug = null, $checkPeriod = 12, $optionName = '') {
|
||||
$this->api = $api;
|
||||
|
||||
parent::__construct($api->getRepositoryUrl(), $stylesheet, $customSlug, $checkPeriod, $optionName);
|
||||
|
||||
$this->api->setHttpFilterName($this->getUniqueName('request_update_options'));
|
||||
$this->api->setStrategyFilterName($this->getUniqueName('vcs_update_detection_strategies'));
|
||||
$this->api->setSlug($this->slug);
|
||||
}
|
||||
|
||||
public function requestUpdate() {
|
||||
$api = $this->api;
|
||||
$api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
|
||||
|
||||
$update = new 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);
|
||||
if ( $updateSource ) {
|
||||
$ref = $updateSource->name;
|
||||
$update->download_url = $updateSource->downloadUrl;
|
||||
} else {
|
||||
do_action(
|
||||
'puc_api_error',
|
||||
new \WP_Error(
|
||||
'puc-no-update-source',
|
||||
'Could not retrieve version information from the repository. '
|
||||
. 'This usually means that the update checker either can\'t connect '
|
||||
. 'to the repository or it\'s configured incorrectly.'
|
||||
),
|
||||
null, null, $this->slug
|
||||
);
|
||||
$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.
|
||||
$remoteHeader = $this->package->getFileHeader($api->getRemoteFile('style.css', $ref));
|
||||
$update->version = Utils::findNotEmpty(array(
|
||||
$remoteHeader['Version'],
|
||||
Utils::get($updateSource, 'version'),
|
||||
));
|
||||
|
||||
//The details URL defaults to the Theme URI header or the repository URL.
|
||||
$update->details_url = Utils::findNotEmpty(array(
|
||||
$remoteHeader['ThemeURI'],
|
||||
$this->package->getHeaderValue('ThemeURI'),
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
59
lib/plugin-update-checker/Puc/v5p3/Vcs/VcsCheckerMethods.php
Normal file
59
lib/plugin-update-checker/Puc/v5p3/Vcs/VcsCheckerMethods.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p3\Vcs;
|
||||
|
||||
if ( !trait_exists(VcsCheckerMethods::class, false) ) :
|
||||
|
||||
trait VcsCheckerMethods {
|
||||
/**
|
||||
* @var string The branch where to look for updates. Defaults to "master".
|
||||
*/
|
||||
protected $branch = 'master';
|
||||
|
||||
/**
|
||||
* @var Api Repository API client.
|
||||
*/
|
||||
protected $api = null;
|
||||
|
||||
public function setBranch($branch) {
|
||||
$this->branch = $branch;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication credentials.
|
||||
*
|
||||
* @param array|string $credentials
|
||||
* @return $this
|
||||
*/
|
||||
public function setAuthentication($credentials) {
|
||||
$this->api->setAuthentication($credentials);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Api
|
||||
*/
|
||||
public function getVcsApi() {
|
||||
return $this->api;
|
||||
}
|
||||
|
||||
public function getUpdate() {
|
||||
$update = parent::getUpdate();
|
||||
|
||||
if ( isset($update) && !empty($update->download_url) ) {
|
||||
$update->download_url = $this->api->signDownloadUrl($update->download_url);
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
public function onDisplayConfiguration($panel) {
|
||||
parent::onDisplayConfiguration($panel);
|
||||
$panel->row('Branch', $this->branch);
|
||||
$panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
|
||||
$panel->row('API client', get_class($this->api));
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
Loading…
Add table
Add a link
Reference in a new issue