Update bundled plugin-update-checker to v5.7 and remove broken setBranch() call
The bundled PUC was missing the setBranch() method on the main UpdateChecker class (it's only on Vcs\PluginUpdateChecker via the VcsCheckerMethods trait). When called with a non-VCS URL like the Gitea update URL, this caused a fatal error at plugin activation. Updating to PUC v5.7 (May 2026) which is the latest upstream release. The setBranch() call is removed because PUC doesn't recognize Gitea as a VCS host; the plugin falls back to PUC's plain JSON metadata mode, which is fine for a plugin hosted on a self-managed repo.
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\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|null The value of the "Authorization" header for API requests.
|
||||
*/
|
||||
private $authorizationHeader = null;
|
||||
|
||||
/**
|
||||
* @var string|null If set, add the "Authorization" header to update downloads that start with this prefix.
|
||||
*/
|
||||
private $downloadUrlPrefixRequiringAuth = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getApiRequestHttpOptions() {
|
||||
$options = ['timeout' => wp_doing_cron() ? 10 : 3];
|
||||
if ( $this->isAuthenticationEnabled() && !empty($this->authorizationHeader) ) {
|
||||
$options['headers'] = ['Authorization' => $this->authorizationHeader];
|
||||
}
|
||||
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication credentials.
|
||||
*
|
||||
* @param $credentials
|
||||
*/
|
||||
public function setAuthentication($credentials) {
|
||||
$this->credentials = $credentials;
|
||||
}
|
||||
|
||||
public function isAuthenticationEnabled() {
|
||||
return !empty($this->credentials) || !empty($this->authorizationHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the "Authorization" header for API requests, if any.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorizationHeader() {
|
||||
return $this->authorizationHeader ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable basic access authentication with the specified username and password.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string|null $downloadUrlPrefix Optionally, add the same Authorization header to update
|
||||
* downloads where the download URL starts with this prefix.
|
||||
*/
|
||||
protected function enableBasicAuth($username, $password, $downloadUrlPrefix = null) {
|
||||
$this->authorizationHeader = 'Basic ' . base64_encode($username . ':' . $password);
|
||||
|
||||
$this->downloadUrlPrefixRequiringAuth = $downloadUrlPrefix;
|
||||
if ( !empty($this->downloadUrlPrefixRequiringAuth) ) {
|
||||
$this->enableDownloadRequestFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool Whether the hook that registers download request filter(s) has already been added.
|
||||
*/
|
||||
private $preDownloadHookAdded = false;
|
||||
|
||||
/**
|
||||
* Enable the hooks that let you filter HTTP requests for update downloads.
|
||||
*/
|
||||
protected function enableDownloadRequestFilter() {
|
||||
if ( $this->preDownloadHookAdded ) {
|
||||
return;
|
||||
}
|
||||
$this->preDownloadHookAdded = true;
|
||||
|
||||
//Optimization: Instead of filtering all HTTP requests, let's do it only when
|
||||
//WordPress is about to download an update. So this is a two-step process;
|
||||
//the actual request arg filter is added in the following hook.
|
||||
add_filter('upgrader_pre_download', [$this, 'addDownloadRequestFilters']); //WP 3.7+
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $downloadFiltersAdded = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param bool $result Pass-through value for the "upgrader_pre_download" filter. Ignored by this callback.
|
||||
* @return bool
|
||||
*/
|
||||
public function addDownloadRequestFilters($result) {
|
||||
if ( !$this->downloadFiltersAdded ) {
|
||||
$this->downloadFiltersAdded = true;
|
||||
|
||||
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout.
|
||||
add_filter('http_request_args', [$this, 'filterUpdateDownloadRequestArgs'], 10, 2);
|
||||
|
||||
$authorizationHeader = $this->getAuthorizationHeader();
|
||||
if ( $this->isAuthenticationEnabled() && !empty($authorizationHeader) ) {
|
||||
add_action('requests-requests.before_redirect', [$this, 'removeAuthHeaderFromRedirects'], 10, 2);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter request arguments/options for update downloads.
|
||||
*
|
||||
* Note that this callback can potentially be called for *any* update download. You still
|
||||
* need to verify that the URL is one of yours.
|
||||
*
|
||||
* @internal
|
||||
* @param array $requestArgs
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') {
|
||||
//Add an authorization header to our downloads if needed.
|
||||
$authHeader = $this->getAuthorizationHeader();
|
||||
if (
|
||||
$this->isAuthenticationEnabled()
|
||||
&& !empty($authHeader)
|
||||
&& !empty($this->downloadUrlPrefixRequiringAuth)
|
||||
&& ((strpos($url, $this->downloadUrlPrefixRequiringAuth)) === 0)
|
||||
) {
|
||||
$requestArgs['headers']['Authorization'] = $authHeader;
|
||||
}
|
||||
return $requestArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* At least in older WP versions, 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) {
|
||||
if (
|
||||
//If there's no download URL prefix configured, we would not have added an auth header,
|
||||
//and there's also no way to check if this URL needs auth or not.
|
||||
empty($this->downloadUrlPrefixRequiringAuth)
|
||||
//If this request goes to our download URL, we can leave the header.
|
||||
|| ((strpos($location, $this->downloadUrlPrefixRequiringAuth)) === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Remove the header.
|
||||
$authorizationHeader = $this->getAuthorizationHeader();
|
||||
if ( isset($headers['Authorization']) && ($headers['Authorization'] === $authorizationHeader) ) {
|
||||
unset($headers['Authorization']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\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;
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Utils;
|
||||
|
||||
if ( !class_exists(BitBucketApi::class, false) ):
|
||||
|
||||
class BitBucketApi extends Api {
|
||||
/**
|
||||
* @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 $this->getDownloadBaseUrl() . $ref . '.zip';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for ZIP downloads from our repo. Includes the trailing slash.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDownloadBaseUrl() {
|
||||
return sprintf(
|
||||
'https://bitbucket.org/%s/%s/get/',
|
||||
$this->username,
|
||||
$this->repository
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
$response = wp_remote_get($url, $this->getApiRequestHttpOptions());
|
||||
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 (
|
||||
is_array($credentials)
|
||||
&& !empty($credentials['username'])
|
||||
&& !empty($credentials['api_token'])
|
||||
) {
|
||||
$this->enableBasicAuth(
|
||||
$credentials['username'],
|
||||
$credentials['api_token'],
|
||||
$this->getDownloadBaseUrl()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,401 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\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;
|
||||
|
||||
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);
|
||||
|
||||
$response = wp_remote_get($url, $this->getApiRequestHttpOptions());
|
||||
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 property is no longer used internally, but is kept for backwards compatibility.
|
||||
$this->accessToken = is_string($credentials) ? $credentials : null;
|
||||
|
||||
if ( is_string($credentials) && !empty($credentials) ) {
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', []);
|
||||
$this->enableBasicAuth($this->userName, $credentials, $repoApiBaseUrl);
|
||||
}
|
||||
|
||||
//Assets sometimes need an Accept header, so we add a request filter even if basic
|
||||
//authentication isn't enabled.
|
||||
$this->enableDownloadRequestFilter();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') {
|
||||
//Release assets need an "Accept" header. See GitHub Docs:
|
||||
//https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
|
||||
//Is WordPress trying to download one of our release assets?
|
||||
if ( $this->releaseAssetsEnabled && (strpos($url, $this->getAssetApiBaseUrl()) !== false) ) {
|
||||
$requestArgs['headers']['Accept'] = 'application/octet-stream';
|
||||
}
|
||||
|
||||
return parent::filterUpdateDownloadRequestArgs($requestArgs, $url);
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,409 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\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);
|
||||
|
||||
$response = wp_remote_get($url, $this->getApiRequestHttpOptions());
|
||||
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;
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\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;
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\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\v5p7\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\v5p7\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\v5p7\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Theme;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\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;
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\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;
|
||||
Reference in New Issue
Block a user