WIP: Theme updates

This commit is contained in:
Yahnis Elsts 2016-12-12 16:26:41 +02:00
parent 79c2439464
commit 9effd33bfa
10 changed files with 457 additions and 91 deletions

131
Puc/v4/Metadata.php Normal file
View File

@ -0,0 +1,131 @@
<?php
if ( !class_exists('Puc_v4_Metadata', false) ):
/**
* A base container for holding information about updates and plugin metadata.
*
* @author Janis Elsts
* @copyright 2016
* @access public
*/
abstract class Puc_v4_Metadata {
/**
* Create an instance of this class from a JSON document.
*
* @abstract
* @param string $json
* @return self
*/
public static function fromJson(/** @noinspection PhpUnusedParameterInspection */ $json) {
throw new LogicException('The ' . __METHOD__ . ' method must be implemented by subclasses');
}
/**
* @param string $json
* @param self $target
* @return bool
*/
protected static function createFromJson($json, $target) {
/** @var StdClass $apiResponse */
$apiResponse = json_decode($json);
if ( empty($apiResponse) || !is_object($apiResponse) ){
trigger_error(
"Failed to parse update metadata. Try validating your .json file with http://jsonlint.com/",
E_USER_NOTICE
);
return false;
}
$valid = $target->validateMetadata($apiResponse);
if ( is_wp_error($valid) ){
trigger_error($valid->get_error_message(), E_USER_NOTICE);
return false;
}
foreach(get_object_vars($apiResponse) as $key => $value){
$target->$key = $value;
}
return true;
}
/**
* No validation by default! Subclasses should check that the required fields are present.
*
* @param StdClass $apiResponse
* @return bool|WP_Error
*/
protected function validateMetadata(/** @noinspection PhpUnusedParameterInspection */ $apiResponse) {
return true;
}
/**
* Create a new instance by copying the necessary fields from another object.
*
* @abstract
* @param StdClass|self $object The source object.
* @return self The new copy.
*/
public static function fromObject(/** @noinspection PhpUnusedParameterInspection */ $object) {
throw new LogicException('The ' . __METHOD__ . ' method must be implemented by subclasses');
}
/**
* Create an instance of StdClass that can later be converted back to an
* update or info container. Useful for serialization and caching, as it
* avoids the "incomplete object" problem if the cached value is loaded
* before this class.
*
* @return StdClass
*/
public function toStdClass() {
$object = new stdClass();
$this->copyFields($this, $object);
return $object;
}
/**
* Transform the metadata into the format used by WordPress core.
*
* @return object
*/
abstract public function toWpFormat();
/**
* Copy known fields from one object to another.
*
* @param StdClass|self $from
* @param StdClass|self $to
*/
protected function copyFields($from, $to) {
$fields = $this->getFieldNames();
if ( property_exists($from, 'slug') && !empty($from->slug) ) {
//Let plugins add extra fields without having to create subclasses.
$fields = apply_filters($this->getFilterPrefix() . 'retain_fields-' . $from->slug, $fields);
}
foreach ($fields as $field) {
if ( property_exists($from, $field) ) {
$to->$field = $from->$field;
}
}
}
/**
* @return string[]
*/
protected function getFieldNames() {
return array();
}
/**
* @return string
*/
protected function getFilterPrefix() {
return 'puc_';
}
}
endif;

View File

@ -8,7 +8,7 @@ if ( !class_exists('Puc_v4_Plugin_Info', false) ):
* @copyright 2016
* @access public
*/
class Puc_v4_Plugin_Info {
class Puc_v4_Plugin_Info extends Puc_v4_Metadata {
//Most fields map directly to the contents of the plugin's info.json file.
//See the relevant docs for a description of their meaning.
public $name;
@ -38,38 +38,23 @@ if ( !class_exists('Puc_v4_Plugin_Info', false) ):
public $filename; //Plugin filename relative to the plugins directory.
/**
* Create a new instance of PluginInfo from JSON-encoded plugin info
* Create a new instance of Plugin Info from JSON-encoded plugin info
* returned by an external update API.
*
* @param string $json Valid JSON string representing plugin info.
* @return Puc_v4_Plugin_Info|null New instance of PluginInfo, or NULL on error.
* @return self|null New instance of Plugin Info, or NULL on error.
*/
public static function fromJson($json){
/** @var StdClass $apiResponse */
$apiResponse = json_decode($json);
if ( empty($apiResponse) || !is_object($apiResponse) ){
trigger_error(
"Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/",
E_USER_NOTICE
);
return null;
}
$instance = new self();
$valid = self::validateMetadata($apiResponse);
if ( is_wp_error($valid) ){
trigger_error($valid->get_error_message(), E_USER_NOTICE);
if ( !parent::createFromJson($json, $instance) ) {
return null;
}
$info = new self();
foreach(get_object_vars($apiResponse) as $key => $value){
$info->$key = $value;
}
//json_decode decodes assoc. arrays as objects. We want it as an array.
$info->sections = (array)$info->sections;
$instance->sections = (array)$instance->sections;
return $info;
return $instance;
}
/**
@ -78,7 +63,7 @@ if ( !class_exists('Puc_v4_Plugin_Info', false) ):
* @param StdClass $apiResponse
* @return bool|WP_Error
*/
protected static function validateMetadata($apiResponse) {
protected function validateMetadata($apiResponse) {
if (
!isset($apiResponse->name, $apiResponse->version)
|| empty($apiResponse->name)

View File

@ -9,21 +9,16 @@ if ( !class_exists('Puc_v4_Plugin_Update', false) ):
* @version 3.2
* @access public
*/
class Puc_v4_Plugin_Update {
class Puc_v4_Plugin_Update extends Puc_v4_Update {
public $id = 0;
public $slug;
public $version;
public $homepage;
public $download_url;
public $upgrade_notice;
public $tested;
public $translations = array();
public $filename; //Plugin filename relative to the plugins directory.
private static $fields = array(
'id', 'slug', 'version', 'homepage', 'tested',
'download_url', 'upgrade_notice', 'filename',
'translations'
protected static $extraFields = array(
'id', 'homepage', 'tested', 'download_url', 'upgrade_notice',
'filename', 'translations',
);
/**
@ -56,62 +51,34 @@ if ( !class_exists('Puc_v4_Plugin_Update', false) ):
}
/**
* Create a new instance of PluginUpdate by copying the necessary fields from
* another object.
* Create a new instance by copying the necessary fields from another object.
*
* @param StdClass|Puc_v4_Plugin_Info|Puc_v4_Plugin_Update $object The source object.
* @return Puc_v4_Plugin_Update The new copy.
*/
public static function fromObject($object) {
$update = new self();
$fields = self::$fields;
if ( !empty($object->slug) ) {
$fields = apply_filters('puc_retain_fields-' . $object->slug, $fields);
}
foreach($fields as $field){
if (property_exists($object, $field)) {
$update->$field = $object->$field;
}
}
$update->copyFields($object, $update);
return $update;
}
/**
* Create an instance of StdClass that can later be converted back to
* a PluginUpdate. Useful for serialization and caching, as it avoids
* the "incomplete object" problem if the cached value is loaded before
* this class.
*
* @return StdClass
* @return string[]
*/
public function toStdClass() {
$object = new stdClass();
$fields = self::$fields;
if ( !empty($this->slug) ) {
$fields = apply_filters('puc_retain_fields-' . $this->slug, $fields);
}
foreach($fields as $field){
if (property_exists($this, $field)) {
$object->$field = $this->$field;
}
}
return $object;
protected function getFieldNames() {
return array_merge(parent::getFieldNames(), self::$extraFields);
}
/**
* Transform the update into the format used by WordPress native plugin API.
*
* @return object
*/
public function toWpFormat(){
$update = new stdClass;
$update = parent::toWpFormat();
$update->id = $this->id;
$update->slug = $this->slug;
$update->new_version = $this->version;
$update->url = $this->homepage;
$update->package = $this->download_url;
$update->tested = $this->tested;
$update->plugin = $this->filename;

View File

@ -8,15 +8,14 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
* @copyright 2016
* @access public
*/
class Puc_v4_Plugin_UpdateChecker {
class Puc_v4_Plugin_UpdateChecker extends Puc_v4_UpdateChecker {
public $metadataUrl = ''; //The URL of the plugin's metadata file.
public $pluginAbsolutePath = ''; //Full path of the main plugin file.
public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.
public $slug = ''; //Plugin slug.
public $optionName = ''; //Where to store the update info.
public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
public $debugMode = false; //Set to TRUE to enable error reporting. Errors are raised using trigger_error()
//Set to TRUE to enable error reporting. Errors are raised using trigger_error()
//and should be logged to the standard PHP error log.
public $scheduler;
@ -224,7 +223,7 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
* @param array|WP_Error $result
* @return true|WP_Error
*/
private function validateApiResponse($result) {
protected function validateApiResponse($result) {
if ( is_wp_error($result) ) { /** @var WP_Error $result */
return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());
}
@ -254,7 +253,7 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
*
* @return Puc_v4_Plugin_Update An instance of PluginUpdate, or NULL when no updates are available.
*/
public function requestUpdate(){
public function requestUpdate() {
//For the sake of simplicity, this function just calls requestInfo()
//and transforms the result accordingly.
$pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));
@ -563,7 +562,7 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
$convertedUpdate = array_merge(
array(
'type' => $translationType,
'slug' => $this->slug,
'slug' => $this->slug, //FIXME: This should actually be the directory name, not the internal slug.
'autoupdate' => 0,
//AFAICT, WordPress doesn't actually use the "version" field for anything.
//But lets make sure it's there, just in case.
@ -752,7 +751,7 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
$isRelevant = ($pluginFile == $this->pluginFile)
|| (!empty($this->muPluginFile) && $pluginFile == $this->muPluginFile);
if ( $isRelevant && current_user_can('update_plugins') ) {
if ( $isRelevant && $this->userCanInstallUpdates() ) {
$linkUrl = wp_nonce_url(
add_query_arg(
array(
@ -766,6 +765,7 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
$linkText = apply_filters('puc_manual_check_link-' . $this->slug, __('Check for updates', 'plugin-update-checker'));
if ( !empty($linkText) ) {
/** @noinspection HtmlUnknownTarget */
$pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);
}
}
@ -782,7 +782,7 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
$shouldCheck =
isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
&& $_GET['puc_slug'] == $this->slug
&& current_user_can('update_plugins')
&& $this->userCanInstallUpdates()
&& check_admin_referer('puc_check_for_updates');
if ( $shouldCheck ) {
@ -821,6 +821,15 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
}
}
/**
* Check if the current user has the required permissions to install updates.
*
* @return bool
*/
public function userCanInstallUpdates() {
return current_user_can('update_plugins');
}
/**
* Check if the plugin file is inside the mu-plugins directory.
*
@ -949,17 +958,7 @@ if ( !class_exists('Puc_v4_Plugin_UpdateChecker', false) ):
}
}
/**
* Trigger a PHP error, but only when $debugMode is enabled.
*
* @param string $message
* @param int $errorType
*/
protected function triggerError($message, $errorType) {
if ( $this->debugMode ) {
trigger_error($message, $errorType);
}
}
}
endif;

68
Puc/v4/Theme/Update.php Normal file
View File

@ -0,0 +1,68 @@
<?php
if ( class_exists('Puc_v4_Theme_Update', false) ):
class Puc_v4_Theme_Update extends Puc_v4_Update {
public $details_url = '';
protected static $extraFields = array('details_url');
/**
* Transform the metadata into the format used by WordPress core.
*
* @return object
*/
public function toWpFormat() {
$update = parent::toWpFormat();
$update->theme = $this->slug;
$update->new_version = $this->version;
$update->package = $this->download_url;
$update->details_url = $this->details_url;
return $update;
}
/**
* Create a new instance of Theme_Update from its JSON-encoded representation.
*
* @param string $json Valid JSON string representing a theme information object.
* @return self New instance of ThemeUpdate, or NULL on error.
*/
public static function fromJson($json) {
$instance = new self();
if ( !parent::createFromJson($json, $instance) ) {
return null;
}
return $instance;
}
/**
* Basic validation.
*
* @param StdClass $apiResponse
* @return bool|WP_Error
*/
protected function validateMetadata($apiResponse) {
$required = array('version', 'details_url');
foreach($required as $key) {
if ( !isset($apiResponse->$key) || empty($apiResponse->$key) ) {
return new WP_Error(
'tuc-invalid-metadata',
sprintf('The theme metadata is missing the required "%s" key.', $key)
);
}
}
return true;
}
protected function getFieldNames() {
return array_merge(parent::getFieldNames(), self::$extraFields);
}
protected function getFilterPrefix() {
return 'tuc_';
}
}
endif;

View File

@ -0,0 +1,91 @@
<?php
if ( class_exists('Puc_v4_Theme_UpdateChecker', false) ):
class Puc_v4_Theme_UpdateChecker extends Puc_v4_UpdateChecker {
protected $filterPrefix = 'tuc_';
/**
* @var string Theme directory name.
*/
protected $stylesheet;
/**
* @var WP_Theme Theme object.
*/
protected $theme;
public function __construct($metadataUrl, $stylesheet = null, $customSlug = null, $checkPeriod = 12, $optionName = '') {
if ( $stylesheet === null ) {
$stylesheet = get_stylesheet();
}
$this->stylesheet = $stylesheet;
$this->theme = wp_get_theme($this->stylesheet);
parent::__construct($metadataUrl, $customSlug ? $customSlug : $stylesheet);
}
/**
* Retrieve the latest update (if any) from the configured API endpoint.
*
* @return Puc_v4_Update An instance of Update, or NULL when no updates are available.
*/
public function requestUpdate() {
//Query args to append to the URL. Themes can add their own by using a filter callback (see addQueryArgFilter()).
$queryArgs = array();
$installedVersion = $this->getInstalledVersion();
$queryArgs['installed_version'] = ($installedVersion !== null) ? $installedVersion : '';
$queryArgs = apply_filters($this->filterPrefix . 'request_update_query_args-' . $this->slug, $queryArgs);
//Various options for the wp_remote_get() call. Plugins can filter these, too.
$options = array(
'timeout' => 10, //seconds
'headers' => array(
'Accept' => 'application/json'
),
);
$options = apply_filters($this->filterPrefix . 'request_update_options-' . $this->slug, $options);
$url = $this->metadataUrl;
if ( !empty($queryArgs) ){
$url = add_query_arg($queryArgs, $url);
}
$result = wp_remote_get($url, $options);
//Try to parse the response
$status = $this->validateApiResponse($result);
$themeUpdate = null;
if ( !is_wp_error($status) ){
$themeUpdate = Puc_v4_Theme_Update::fromJson($result['body']);
if ( $themeUpdate !== null ) {
$themeUpdate->slug = $this->slug;
}
} else {
$this->triggerError(
sprintf('The URL %s does not point to a valid theme metadata file. ', $url)
. $status->get_error_message(),
E_USER_WARNING
);
}
$themeUpdate = apply_filters(
$this->filterPrefix . 'request_update_result-' . $this->slug,
$themeUpdate,
$result
);
return $themeUpdate;
}
/**
* Get the currently installed version of the plugin or theme.
*
* @return string Version number.
*/
public function getInstalledVersion() {
return $this->theme->get('Version');
}
}
endif;

24
Puc/v4/Update.php Normal file
View File

@ -0,0 +1,24 @@
<?php
if ( !class_exists('Puc_v4_Update', false) ):
/**
* A simple container class for holding information about an available update.
*
* @author Janis Elsts
* @access public
*/
abstract class Puc_v4_Update extends Puc_v4_Metadata {
public $slug;
public $version;
public $download_url;
public $translations = array();
/**
* @return string[]
*/
protected function getFieldNames() {
return array('slug', 'version', 'download_url', 'translations');
}
}
endif;

101
Puc/v4/UpdateChecker.php Normal file
View File

@ -0,0 +1,101 @@
<?php
if ( !class_exists('Puc_v4_UpdateChecker', false) ):
abstract class Puc_v4_UpdateChecker {
protected $filterPrefix = 'puc_';
public $debugMode = false;
/**
* @var string Where to store the update info.
*/
public $optionName = '';
/**
* @var string The URL of the metadata file.
*/
public $metadataUrl = '';
/**
* @var string Plugin slug or theme directory name.
*/
public $slug = '';
public function __construct($metadataUrl, $slug, $checkPeriod = 12, $optionName = '') {
$this->debugMode = (bool)(constant('WP_DEBUG'));
$this->metadataUrl = $metadataUrl;
$this->slug = $slug;
$this->optionName = $optionName;
if ( empty($this->optionName) ) {
//BC: Initially the library only supported plugin updates and didn't use type prefixes
//in the option name. Lets use the same prefix-less name when possible.
if ( $this->filterPrefix === 'puc_' ) {
$this->optionName = 'external_updates-' . $this->slug;
} else {
$this->optionName = $this->filterPrefix . 'external_updates-' . $this->slug;
}
}
}
/**
* Retrieve the latest update (if any) from the configured API endpoint.
*
* @return Puc_v4_Update An instance of Update, or NULL when no updates are available.
*/
abstract public function requestUpdate();
/**
* Check if $result is a successful update API response.
*
* @param array|WP_Error $result
* @return true|WP_Error
*/
protected function validateApiResponse($result) {
if ( is_wp_error($result) ) { /** @var WP_Error $result */
return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());
}
if ( !isset($result['response']['code']) ) {
return new WP_Error(
$this->filterPrefix . 'no_response_code',
'wp_remote_get() returned an unexpected result.'
);
}
if ( $result['response']['code'] !== 200 ) {
return new WP_Error(
$this->filterPrefix . 'unexpected_response_code',
'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'
);
}
if ( empty($result['body']) ) {
return new WP_Error($this->filterPrefix . 'empty_response', 'The metadata file appears to be empty.');
}
return true;
}
/**
* Get the currently installed version of the plugin or theme.
*
* @return string Version number.
*/
abstract public function getInstalledVersion();
/**
* Trigger a PHP error, but only when $debugMode is enabled.
*
* @param string $message
* @param int $errorType
*/
protected function triggerError($message, $errorType) {
if ($this->debugMode) {
trigger_error($message, $errorType);
}
}
}
endif;

View File

@ -23,7 +23,7 @@ class PucDebugBarPlugin_3_2 {
*/
public function addDebugBarPanel($panels) {
require_once dirname(__FILE__) . '/debug-bar-panel.php';
if ( current_user_can('update_plugins') && class_exists('PluginUpdateCheckerPanel_3_2', false) ) {
if ( $this->updateChecker->userCanInstallUpdates() && class_exists('PluginUpdateCheckerPanel_3_2', false) ) {
$panels[] = new PluginUpdateCheckerPanel_3_2($this->updateChecker);
}
return $panels;
@ -89,7 +89,7 @@ class PucDebugBarPlugin_3_2 {
* Check access permissions and enable error display (for debugging).
*/
private function preAjaxReqest() {
if ( !current_user_can('update_plugins') ) {
if ( !$this->updateChecker->userCanInstallUpdates() ) {
die('Access denied');
}
check_ajax_referer('puc-ajax');

View File

@ -101,7 +101,7 @@ class PucFactory {
endif;
//Register classes defined in this file with the factory.
PucFactory::addVersion('PluginUpdateChecker', 'Puc_v4_Plugin_UpdateChecker', '3.2');
PucFactory::addVersion('PluginUpdate', 'Puc_v4_Plugin_Update', '3.2');
PucFactory::addVersion('PluginInfo', 'Puc_v4_Plugin_Info', '3.2');
PucFactory::addVersion('PucGitHubChecker', 'Puc_v4_GitHub_PluginChecker', '3.2');
PucFactory::addVersion('PluginUpdateChecker', 'Puc_v4_Plugin_UpdateChecker', '4.0');
PucFactory::addVersion('PluginUpdate', 'Puc_v4_Plugin_Update', '4.0');
PucFactory::addVersion('PluginInfo', 'Puc_v4_Plugin_Info', '4.0');
PucFactory::addVersion('PucGitHubChecker', 'Puc_v4_GitHub_PluginChecker', '4.0');