Show API errors when manually checking for updates.

Introduces a new hook: "puc_api_error". This is an action with up to 4 callback arguments:

1) $error - A WP_Error instance that provides a brief description of the error.
2) $response - The value returned by wp_remote_get(). Can be omitted if the original response is not available at this point.
3) $url - The URL that the update checker requested. Can be omitted.
4) $slug - The plugin or theme slug. Can be omitted. 


See #151
This commit is contained in:
Yahnis Elsts 2017-11-24 17:36:15 +02:00
parent 585f60d91c
commit ec5ee45379
12 changed files with 265 additions and 25 deletions

View File

@ -2,6 +2,8 @@
if ( !class_exists('Puc_v4p3_DebugBar_Extension', false) ):
class Puc_v4p3_DebugBar_Extension {
const RESPONSE_BODY_LENGTH_LIMIT = 4000;
/** @var Puc_v4p3_UpdateChecker */
protected $updateChecker;
protected $panelClass = 'Puc_v4p3_DebugBar_Panel';
@ -39,7 +41,7 @@ if ( !class_exists('Puc_v4p3_DebugBar_Extension', false) ):
'puc-debug-bar-style-v4',
$this->getLibraryUrl("/css/puc-debug-bar.css"),
array('debug-bar'),
'20161217'
'20171124'
);
wp_enqueue_script(
@ -66,6 +68,64 @@ if ( !class_exists('Puc_v4p3_DebugBar_Extension', false) ):
} else {
echo 'No updates found.';
}
$errors = $this->updateChecker->getLastRequestApiErrors();
if ( !empty($errors) ) {
printf('<p>The update checker encountered %d API error%s.</p>', count($errors), (count($errors) > 1) ? 's' : '');
foreach (array_values($errors) as $num => $item) {
$wpError = $item['error'];
/** @var WP_Error $wpError */
printf('<h4>%d) %s</h4>', $num + 1, esc_html($wpError->get_error_message()));
echo '<dl>';
printf('<dt>Error code:</dt><dd><code>%s</code></dd>', esc_html($wpError->get_error_code()));
if ( isset($item['url']) ) {
printf('<dt>Requested URL:</dt><dd><code>%s</code></dd>', esc_html($item['url']));
}
if ( isset($item['httpResponse']) ) {
if ( is_wp_error($item['httpResponse']) ) {
$httpError = $item['httpResponse'];
/** @var WP_Error $httpError */
printf(
'<dt>WordPress HTTP API error:</dt><dd>%s (<code>%s</code>)</dd>',
esc_html($httpError->get_error_message()),
esc_html($httpError->get_error_code())
);
} else {
//Status code.
printf(
'<dt>HTTP status:</dt><dd><code>%d %s</code></dd>',
wp_remote_retrieve_response_code($item['httpResponse']),
wp_remote_retrieve_response_message($item['httpResponse'])
);
//Headers.
echo '<dt>Response headers:</dt><dd><pre>';
foreach (wp_remote_retrieve_headers($item['httpResponse']) as $name => $value) {
printf("%s: %s\n", esc_html($name), esc_html($value));
}
echo '</pre></dd>';
//Body.
$body = wp_remote_retrieve_body($item['httpResponse']);
if ( $body === '' ) {
$body = '(Empty response.)';
} else if ( strlen($body) > self::RESPONSE_BODY_LENGTH_LIMIT ) {
$length = strlen($body);
$body = substr($body, 0, self::RESPONSE_BODY_LENGTH_LIMIT)
. sprintf("\n(Long string truncated. Total length: %d bytes.)", $length);
}
printf('<dt>Response body:</dt><dd><pre>%s</pre></dd>', esc_html($body));
}
}
echo '<dl>';
}
}
exit;
}
@ -79,7 +139,7 @@ if ( !class_exists('Puc_v4p3_DebugBar_Extension', false) ):
check_ajax_referer('puc-ajax');
error_reporting(E_ALL);
@ini_set('display_errors','On');
@ini_set('display_errors', 'On');
}
/**

View File

@ -30,15 +30,15 @@ if ( !class_exists('Puc_v4p3_Metadata', false) ):
/** @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
);
$errorMessage = "Failed to parse update metadata. Try validating your .json file with http://jsonlint.com/";
do_action('puc_api_error', new WP_Error('puc-invalid-json', $errorMessage));
trigger_error($errorMessage, E_USER_NOTICE);
return false;
}
$valid = $target->validateMetadata($apiResponse);
if ( is_wp_error($valid) ){
do_action('puc_api_error', $valid);
trigger_error($valid->get_error_message(), E_USER_NOTICE);
return false;
}

View File

@ -17,6 +17,7 @@ if ( !class_exists('Puc_v4p3_Plugin_UpdateChecker', false) ):
public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
private $cachedInstalledVersion = null;
private $manualCheckErrorTransient = '';
/**
* Class constructor.
@ -61,6 +62,8 @@ if ( !class_exists('Puc_v4p3_Plugin_UpdateChecker', false) ):
//Details: https://github.com/YahnisElsts/plugin-update-checker/issues/138#issuecomment-335590964
add_action('uninstall_' . $this->pluginFile, array($this, 'removeHooks'));
$this->manualCheckErrorTransient = $this->getUniqueName('manual_check_errors');
parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
}
@ -413,7 +416,7 @@ if ( !class_exists('Puc_v4p3_Plugin_UpdateChecker', false) ):
* Returning 'append' places the link after any existing links at the time of the hook.
* Returning 'replace' replaces the "Visit plugin site" link
* Returning anything else disables the link when there is a "Visit plugin site" link.
*
*
* If there is no "Visit plugin site" link 'append' is always used!
*
* @param array $pluginMeta Array of meta links.
@ -490,6 +493,34 @@ if ( !class_exists('Puc_v4p3_Plugin_UpdateChecker', false) ):
if ( $shouldCheck ) {
$update = $this->checkForUpdates();
$status = ($update === null) ? 'no_update' : 'update_available';
if ( ($update === null) && !empty($this->lastRequestApiErrors) ) {
//Some errors are not critical. For example, if PUC tries to retrieve the readme.txt
//file from GitHub and gets a 404, that's an API error, but it doesn't prevent updates
//from working. Maybe the plugin simply doesn't have a readme.
//Let's only show important errors.
$foundCriticalErrors = false;
$questionableErrorCodes = array(
'puc-github-http-error',
'puc-gitlab-http-error',
'puc-bitbucket-http-error',
);
foreach ($this->lastRequestApiErrors as $item) {
$wpError = $item['error'];
/** @var WP_Error $wpError */
if ( !in_array($wpError->get_error_code(), $questionableErrorCodes) ) {
$foundCriticalErrors = true;
break;
}
}
if ( $foundCriticalErrors ) {
$status = 'error';
set_site_transient($this->manualCheckErrorTransient, $this->lastRequestApiErrors, 60);
}
}
wp_redirect(add_query_arg(
array(
'puc_update_check_result' => $status,
@ -508,22 +539,69 @@ if ( !class_exists('Puc_v4p3_Plugin_UpdateChecker', false) ):
*/
public function displayManualCheckResult() {
if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->slug) ) {
$status = strval($_GET['puc_update_check_result']);
$title = $this->getPluginTitle();
$status = strval($_GET['puc_update_check_result']);
$title = $this->getPluginTitle();
$noticeClass = 'updated notice-success';
$details = '';
if ( $status == 'no_update' ) {
$message = sprintf(_x('The %s plugin is up to date.', 'the plugin title', 'plugin-update-checker'), $title);
} else if ( $status == 'update_available' ) {
$message = sprintf(_x('A new version of the %s plugin is available.', 'the plugin title', 'plugin-update-checker'), $title);
} else if ( $status === 'error' ) {
$message = sprintf(_x('Could not determine if updates are available for %s.', 'the plugin title', 'plugin-update-checker'), $title);
$noticeClass = 'error notice-error';
$details = $this->formatManualCheckErrors(get_site_transient($this->manualCheckErrorTransient));
delete_site_transient($this->manualCheckErrorTransient);
} else {
$message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));
$noticeClass = 'error notice-error';
}
printf(
'<div class="updated notice is-dismissible"><p>%s</p></div>',
apply_filters($this->getUniqueName('manual_check_message'), $message, $status)
'<div class="notice %s is-dismissible"><p>%s</p>%s</div>',
$noticeClass,
apply_filters($this->getUniqueName('manual_check_message'), $message, $status),
$details
);
}
}
/**
* Format the list of errors that were thrown during an update check.
*
* @param array $errors
* @return string
*/
protected function formatManualCheckErrors($errors) {
if ( empty($errors) ) {
return '';
}
$output = '';
$showAsList = count($errors) > 1;
if ( $showAsList ) {
$output .= '<ol>';
$formatString = '<li>%1$s <code>%2$s</code></li>';
} else {
$formatString = '<p>%1$s <code>%2$s</code></p>';
}
foreach ($errors as $item) {
$wpError = $item['error'];
/** @var WP_Error $wpError */
$output .= sprintf(
$formatString,
$wpError->get_error_message(),
$wpError->get_error_code()
);
}
if ( $showAsList ) {
$output .= '</ol>';
}
return $output;
}
/**
* Get the translated plugin title.
*

View File

@ -50,6 +50,11 @@ if ( !class_exists('Puc_v4p3_UpdateChecker', false) ):
*/
protected $updateState;
/**
* @var array List of API errors triggered during the last checkForUpdates() call.
*/
protected $lastRequestApiErrors = array();
public function __construct($metadataUrl, $directoryName, $slug = null, $checkPeriod = 12, $optionName = '') {
$this->debugMode = (bool)(constant('WP_DEBUG'));
$this->metadataUrl = $metadataUrl;
@ -214,6 +219,10 @@ if ( !class_exists('Puc_v4p3_UpdateChecker', false) ):
return null;
}
//Start collecting API errors.
$this->lastRequestApiErrors = array();
add_action('puc_api_error', array($this, 'collectApiErrors'), 10, 4);
$state = $this->updateState;
$state->setLastCheckToNow()
->setCheckedVersion($installedVersion)
@ -222,6 +231,9 @@ if ( !class_exists('Puc_v4p3_UpdateChecker', false) ):
$state->setUpdate($this->requestUpdate());
$state->save();
//Stop collecting API errors.
remove_action('puc_api_error', array($this, 'collectApiErrors'), 10);
return $this->getUpdate();
}
@ -340,6 +352,34 @@ if ( !class_exists('Puc_v4p3_UpdateChecker', false) ):
return $name . '-' . $this->slug;
}
/**
* Store API errors that are generated when checking for updates.
*
* @internal
* @param WP_Error $error
* @param array|null $httpResponse
* @param string|null $url
* @param string|null $slug
*/
public function collectApiErrors($error, $httpResponse = null, $url = null, $slug = null) {
if ( isset($slug) && ($slug !== $this->slug) ) {
return;
}
$this->lastRequestApiErrors[] = array(
'error' => $error,
'httpResponse' => $httpResponse,
'url' => $url,
);
}
/**
* @return array
*/
public function getLastRequestApiErrors() {
return $this->lastRequestApiErrors;
}
/* -------------------------------------------------------------------
* PUC filters and filter utilities
* -------------------------------------------------------------------
@ -493,6 +533,7 @@ if ( !class_exists('Puc_v4p3_UpdateChecker', false) ):
if ( !is_wp_error($status) ){
$metadata = call_user_func(array($metaClass, 'fromJson'), $result['body']);
} else {
do_action('puc_api_error', $status, $result, $url, $this->slug);
$this->triggerError(
sprintf('The URL %s does not point to a valid metadata file. ', $url)
. $status->get_error_message(),

View File

@ -3,6 +3,7 @@ if ( !class_exists('Puc_v4p3_Vcs_Api') ):
abstract class Puc_v4p3_Vcs_Api {
protected $tagNameProperty = 'name';
protected $slug = '';
/**
* @var string
@ -289,6 +290,13 @@ if ( !class_exists('Puc_v4p3_Vcs_Api') ):
$this->localDirectory = $directory;
}
}
/**
* @param string $slug
*/
public function setSlug($slug) {
$this->slug = $slug;
}
}
endif;

View File

@ -194,6 +194,7 @@ if ( !class_exists('Puc_v4p3_Vcs_BitBucketApi', false) ):
$this->repository,
ltrim($url, '/')
));
$baseUrl = $url;
if ( $this->oauth ) {
$url = $this->oauth->sign($url,'GET');
@ -205,6 +206,7 @@ if ( !class_exists('Puc_v4p3_Vcs_BitBucketApi', false) ):
}
$response = wp_remote_get($url, $options);
if ( is_wp_error($response) ) {
do_action('puc_api_error', $response, null, $url, $this->slug);
return $response;
}
@ -215,10 +217,13 @@ if ( !class_exists('Puc_v4p3_Vcs_BitBucketApi', false) ):
return $document;
}
return new WP_Error(
$error = new WP_Error(
'puc-bitbucket-http-error',
'BitBucket API error. HTTP status: ' . $code
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;
}
/**

View File

@ -158,6 +158,7 @@ if ( !class_exists('Puc_v4p3_Vcs_GitHubApi', false) ):
* @return mixed|WP_Error
*/
protected function api($url, $queryParams = array()) {
$baseUrl = $url;
$url = $this->buildApiUrl($url, $queryParams);
$options = array('timeout' => 10);
@ -166,6 +167,7 @@ if ( !class_exists('Puc_v4p3_Vcs_GitHubApi', false) ):
}
$response = wp_remote_get($url, $options);
if ( is_wp_error($response) ) {
do_action('puc_api_error', $response, null, $url, $this->slug);
return $response;
}
@ -176,10 +178,13 @@ if ( !class_exists('Puc_v4p3_Vcs_GitHubApi', false) ):
return $document;
}
return new WP_Error(
$error = new WP_Error(
'puc-github-http-error',
'GitHub API error. HTTP status: ' . $code
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;
}
/**

View File

@ -136,6 +136,7 @@ if ( !class_exists('Puc_v4p3_Vcs_GitLabApi', false) ):
* @return mixed|WP_Error
*/
protected function api($url, $queryParams = array()) {
$baseUrl = $url;
$url = $this->buildApiUrl($url, $queryParams);
$options = array('timeout' => 10);
@ -145,6 +146,7 @@ if ( !class_exists('Puc_v4p3_Vcs_GitLabApi', false) ):
$response = wp_remote_get($url, $options);
if ( is_wp_error($response) ) {
do_action('puc_api_error', $response, null, $url, $this->slug);
return $response;
}
@ -154,10 +156,13 @@ if ( !class_exists('Puc_v4p3_Vcs_GitLabApi', false) ):
return json_decode($body);
}
return new WP_Error(
$error = new WP_Error(
'puc-gitlab-http-error',
'GitLab API Error. HTTP status: ' . $code
sprintf('GitLab API error. URL: "%s", HTTP status code: %d.', $baseUrl, $code)
);
do_action('puc_api_error', $error, $response, $url, $this->slug);
return $error;
}
/**

View File

@ -27,6 +27,8 @@ if ( !class_exists('Puc_v4p3_Vcs_PluginUpdateChecker') ):
$this->api->setHttpFilterName($this->getUniqueName('request_info_options'));
parent::__construct($api->getRepositoryUrl(), $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);
$this->api->setSlug($this->slug);
}
public function requestInfo($unusedParameter = null) {
@ -61,6 +63,16 @@ if ( !class_exists('Puc_v4p3_Vcs_PluginUpdateChecker') ):
}
} 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;
}

View File

@ -27,6 +27,8 @@ if ( !class_exists('Puc_v4p3_Vcs_ThemeUpdateChecker', false) ):
$this->api->setHttpFilterName($this->getUniqueName('request_update_options'));
parent::__construct($api->getRepositoryUrl(), $stylesheet, $customSlug, $checkPeriod, $optionName);
$this->api->setSlug($this->slug);
}
public function requestUpdate() {
@ -42,6 +44,16 @@ if ( !class_exists('Puc_v4p3_Vcs_ThemeUpdateChecker', false) ):
$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;
}

View File

@ -59,4 +59,12 @@ table.puc-debug-data td {
.puc-ajax-nonce {
display: none;
}
}
.puc-ajax-response dt {
margin: 0;
}
.puc-ajax-response dd {
margin: 0 0 1em;
}

View File

@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: plugin-update-checker\n"
"POT-Creation-Date: 2017-05-20 10:53+0300\n"
"POT-Creation-Date: 2017-11-24 17:02+0200\n"
"PO-Revision-Date: 2016-01-10 20:59+0100\n"
"Last-Translator: Tamás András Horváth <htomy92@gmail.com>\n"
"Language-Team: \n"
@ -10,34 +10,40 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.2\n"
"X-Generator: Poedit 2.0.4\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
"X-Poedit-SearchPath-0: .\n"
#: Puc/v4p1/Plugin/UpdateChecker.php:358
#: Puc/v4p3/Plugin/UpdateChecker.php:395
msgid "Check for updates"
msgstr ""
#: Puc/v4p1/Plugin/UpdateChecker.php:405
#: Puc/v4p3/Plugin/UpdateChecker.php:548
#, php-format
msgctxt "the plugin title"
msgid "The %s plugin is up to date."
msgstr ""
#: Puc/v4p1/Plugin/UpdateChecker.php:407
#: Puc/v4p3/Plugin/UpdateChecker.php:550
#, php-format
msgctxt "the plugin title"
msgid "A new version of the %s plugin is available."
msgstr ""
#: Puc/v4p1/Plugin/UpdateChecker.php:409
#: Puc/v4p3/Plugin/UpdateChecker.php:552
#, php-format
msgctxt "the plugin title"
msgid "Could not determine if updates are available for %s."
msgstr ""
#: Puc/v4p3/Plugin/UpdateChecker.php:558
#, php-format
msgid "Unknown update checker status \"%s\""
msgstr ""
#: Puc/v4p1/Vcs/PluginUpdateChecker.php:83
#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
msgid "There is no changelog available."
msgstr ""