bsoluteDirectoryPath() {
return $this->package->getAbsoluteDirectoryPath();
}
/**
* Trigger a PHP error, but only when $debugMode is enabled.
*
* @param string $message
* @param int $errorType
*/
public function triggerError($message, $errorType) {
if ( $this->isDebugModeEnabled() ) {
trigger_error($message, $errorType);
}
}
/**
* @return bool
*/
protected function isDebugModeEnabled() {
if ( $this->debugMode === null ) {
$this->debugMode = (bool)(constant('WP_DEBUG'));
}
return $this->debugMode;
}
/**
* Get the full name of an update checker filter, action or DB entry.
*
* This method adds the "puc_" prefix and the "-$slug" suffix to the filter name.
* For example, "pre_inject_update" becomes "puc_pre_inject_update-plugin-slug".
*
* @param string $baseTag
* @return string
*/
public function getUniqueName($baseTag) {
$name = 'puc_' . $baseTag;
if ( $this->filterSuffix !== '' ) {
$name .= '_' . $this->filterSuffix;
}
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
* -------------------------------------------------------------------
*/
/**
* Register a callback for one of the update checker filters.
*
* Identical to add_filter(), except it automatically adds the "puc_" prefix
* and the "-$slug" suffix to the filter name. For example, "request_info_result"
* becomes "puc_request_info_result-your_plugin_slug".
*
* @param string $tag
* @param callable $callback
* @param int $priority
* @param int $acceptedArgs
*/
public function addFilter($tag, $callback, $priority = 10, $acceptedArgs = 1) {
add_filter($this->getUniqueName($tag), $callback, $priority, $acceptedArgs);
}
/* -------------------------------------------------------------------
* Inject updates
* -------------------------------------------------------------------
*/
/**
* Insert the latest update (if any) into the update list maintained by WP.
*
* @param stdClass $updates Update list.
* @return stdClass Modified update list.
*/
public function injectUpdate($updates) {
//Is there an update to insert?
$update = $this->getUpdate();
if ( !$this->shouldShowUpdates() ) {
$update = null;
}
if ( !empty($update) ) {
//Let plugins filter the update info before it's passed on to WordPress.
$update = apply_filters($this->getUniqueName('pre_inject_update'), $update);
$updates = $this->addUpdateToList($updates, $update->toWpFormat());
} else {
//Clean up any stale update info.
$updates = $this->removeUpdateFromList($updates);
}
return $updates;
}
/**
* @param stdClass|null $updates
* @param stdClass|array $updateToAdd
* @return stdClass
*/
protected function addUpdateToList($updates, $updateToAdd) {
if ( !is_object($updates) ) {
$updates = new stdClass();
$updates->response = array();
}
$updates->response[$this->getUpdateListKey()] = $updateToAdd;
return $updates;
}
/**
* @param stdClass|null $updates
* @return stdClass|null
*/
protected function removeUpdateFromList($updates) {
if ( isset($updates, $updates->response) ) {
unset($updates->response[$this->getUpdateListKey()]);
}
return $updates;
}
/**
* Get the key that will be used when adding updates to the update list that's maintained
* by the WordPress core. The list is always an associative array, but the key is different
* for plugins and themes.
*
* @return string
*/
abstract protected function getUpdateListKey();
/**
* Should we show available updates?
*
* Usually the answer is "yes", but there are exceptions. For example, WordPress doesn't
* support automatic updates installation for mu-plugins, so PUC usually won't show update
* notifications in that case. See the plugin-specific subclass for details.
*
* Note: This method only applies to updates that are displayed (or not) in the WordPress
* admin. It doesn't affect APIs like requestUpdate and getUpdate.
*
* @return bool
*/
protected function shouldShowUpdates() {
return true;
}
/* -------------------------------------------------------------------
* JSON-based update API
* -------------------------------------------------------------------
*/
/**
* Retrieve plugin or theme metadata from the JSON document at $this->metadataUrl.
*
* @param string $metaClass Parse the JSON as an instance of this class. It must have a static fromJson method.
* @param string $filterRoot
* @param array $queryArgs Additional query arguments.
* @return array [Puc_v4p9_Metadata|null, array|WP_Error] A metadata instance and the value returned by wp_remote_get().
*/
protected function requestMetadata($metaClass, $filterRoot, $queryArgs = array()) {
//Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).
$queryArgs = array_merge(
array(
'installed_version' => strval($this->getInstalledVersion()),
'php' => phpversion(),
'locale' => get_locale(),
),
$queryArgs
);
$queryArgs = apply_filters($this->getUniqueName($filterRoot . '_query_args'), $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->getUniqueName($filterRoot . '_options'), $options);
//The metadata file should be at 'http://your-api.com/url/here/$slug/info.json'
$url = $this->metadataUrl;
if ( !empty($queryArgs) ){
$url = add_query_arg($queryArgs, $url);
}
$result = wp_remote_get($url, $options);
$result = apply_filters($this->getUniqueName('request_metadata_http_result'), $result, $url, $options);
//Try to parse the response
$status = $this->validateApiResponse($result);
$metadata = null;
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(),
E_USER_WARNING
);
}
return array($metadata, $result);
}
/**
* 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(
'puc_no_response_code',
'wp_remote_get() returned an unexpected result.'
);
}
if ( $result['response']['code'] !== 200 ) {
return new WP_Error(
'puc_unexpected_response_code',
'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'
);
}
if ( empty($result['body']) ) {
return new WP_Error('puc_empty_response', 'The metadata file appears to be empty.');
}
return true;
}
/* -------------------------------------------------------------------
* Language packs / Translation updates
* -------------------------------------------------------------------
*/
/**
* Filter a list of translation updates and return a new list that contains only updates
* that apply to the current site.
*
* @param array $translations
* @return array
*/
protected function filterApplicableTranslations($translations) {
$languages = array_flip(array_values(get_available_languages()));
$installedTranslations = $this->getInstalledTranslations();
$applicableTranslations = array();
foreach ($translations as $translation) {
//Does it match one of the available core languages?
$isApplicable = array_key_exists($translation->language, $languages);
//Is it more recent than an already-installed translation?
if ( isset($installedTranslations[$translation->language]) ) {
$updateTimestamp = strtotime($translation->updated);
$installedTimestamp = strtotime($installedTranslations[$translation->language]['PO-Revision-Date']);
$isApplicable = $updateTimestamp > $installedTimestamp;
}
if ( $isApplicable ) {
$applicableTranslations[] = $translation;
}
}
return $applicableTranslations;
}
/**
* Get a list of installed translations for this plugin or theme.
*
* @return array
*/
protected function getInstalledTranslations() {
if ( !function_exists('wp_get_installed_translations') ) {
return array();
}
$installedTranslations = wp_get_installed_translations($this->translationType . 's');
if ( isset($installedTranslations[$this->directoryName]) ) {
$installedTranslations = $installedTranslations[$this->directoryName];
} else {
$installedTranslations = array();
}
return $installedTranslations;
}
/**
* Insert translation updates into the list maintained by WordPress.
*
* @param stdClass $updates
* @return stdClass
*/
public function injectTranslationUpdates($updates) {
$translationUpdates = $this->getTranslationUpdates();
if ( empty($translationUpdates) ) {
return $updates;
}
//Being defensive.
if ( !is_object($updates) ) {
$updates = new stdClass();
}
if ( !isset($updates->translations) ) {
$updates->translations = array();
}
//In case there's a name collision with a plugin or theme hosted on wordpress.org,
//remove any preexisting updates that match our thing.
$updates->translations = array_values(array_filter(
$updates->translations,
array($this, 'isNotMyTranslation')
));
//Add our updates to the list.
foreach($translationUpdates as $update) {
$convertedUpdate = array_merge(
array(
'type' => $this->translationType,
'slug' => $this->directoryName,
'autoupdate' => 0,
//AFAICT, WordPress doesn't actually use the "version" field for anything.
//But lets make sure it's there, just in case.
'version' => isset($update->version) ? $update->version : ('1.' . strtotime($update->updated)),
),
(array)$update
);
$updates->translations[] = $convertedUpdate;
}
return $updates;
}
/**
* Get a list of available translation updates.
*
* This method will return an empty array if there are no updates.
* Uses cached update data.
*
* @return array
*/
public function getTranslationUpdates() {
return $this->updateState->getTranslations();
}
/**
* Remove all cached translation updates.
*
* @see wp_clean_update_cache
*/
public function clearCachedTranslationUpdates() {
$this->updateState->setTranslations(array());
}
/**
* Filter callback. Keeps only translations that *don't* match this plugin or theme.
*
* @param array $translation
* @return bool
*/
protected function isNotMyTranslation($translation) {
$isMatch = isset($translation['type'], $translation['slug'])
&& ($translation['type'] === $this->translationType)
&& ($translation['slug'] === $this->directoryName);
return !$isMatch;
}
/* -------------------------------------------------------------------
* Fix directory name when installing updates
* -------------------------------------------------------------------
*/
/**
* Rename the update directory to match the existing plugin/theme directory.
*
* When WordPress installs a plugin or theme update, it assumes that the ZIP file will contain
* exactly one directory, and that the directory name will be the same as the directory where
* the plugin or theme is currently installed.
*
* GitHub and other repositories provide ZIP downloads, but they often use directory names like
* "project-branch" or "project-tag-hash". We need to change the name to the actual plugin folder.
*
* This is a hook callback. Don't call it from a plugin.
*
* @access protected
*
* @param string $source The directory to copy to /wp-content/plugins or /wp-content/themes. Usually a subdirectory of $remoteSource.
* @param string $remoteSource WordPress has extracted the update to this directory.
* @param WP_Upgrader $upgrader
* @return string|WP_Error
*/
public function fixDirectoryName($source, $remoteSource, $upgrader) {
global $wp_filesystem;
/** @var WP_Filesystem_Base $wp_filesystem */
//Basic sanity checks.
if ( !isset($source, $remoteSource, $upgrader, $upgrader->skin, $wp_filesystem) ) {
return $source;
}
//If WordPress is upgrading anything other than our plugin/theme, leave the directory name unchanged.
if ( !$this->isBeingUpgraded($upgrader) ) {
return $source;
}
//Rename the source to match the existing directory.
$correctedSource = trailingslashit($remoteSource) . $this->directoryName . '/';
if ( $source !== $correctedSource ) {
//The update archive should contain a single directory that contains the rest of plugin/theme files.
//Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
//We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
//after update.
if ( $this->isBadDirectoryStructure($remoteSource) ) {
return new WP_Error(
'puc-incorrect-directory-structure',
sprintf(
'The directory structure of the update is incorrect. All files should be inside ' .
'a directory named %s, not at the root of the ZIP archive.',
htmlentities($this->slug)
)
);
}
/** @var WP_Upgrader_Skin $upgrader ->skin */
$upgrader->skin->feedback(sprintf(
'Renaming %s to %s…',
'' . basename($source) . '',
'' . $this->directoryName . ''
));
if ( $wp_filesystem->move($source, $correctedSource, true) ) {
$upgrader->skin->feedback('Directory successfully renamed.');
return $correctedSource;
} else {
return new WP_Error(
'puc-rename-failed',
'Unable to rename the update to match the existing directory.'
);
}
}
return $source;
}
/**
* Is there an update being installed right now, for this plugin or theme?
*
* @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
* @return bool
*/
abstract public function isBeingUpgraded($upgrader = null);
/**
* Check for incorrect update directory structure. An update must contain a single directory,
* all other files should be inside that directory.
*
* @param string $remoteSource Directory path.
* @return bool
*/
protected function isBadDirectoryStructure($remoteSource) {
global $wp_filesystem;
/** @var WP_Filesystem_Base $wp_filesystem */
$sourceFiles = $wp_filesystem->dirlist($remoteSource);
if ( is_array($sourceFiles) ) {
$sourceFiles = array_keys($sourceFiles);
$firstFilePath = trailingslashit($remoteSource) . $sourceFiles[0];
return (count($sourceFiles) > 1) || (!$wp_filesystem->is_dir($firstFilePath));
}
//Assume it's fine.
return false;
}
/* -------------------------------------------------------------------
* DebugBar integration
* -------------------------------------------------------------------
*/
/**
* Initialize the update checker Debug Bar plugin/add-on thingy.
*/
public function maybeInitDebugBar() {
if ( class_exists('Debug_Bar', false) && file_exists(dirname(__FILE__) . '/DebugBar') ) {
$this->createDebugBarExtension();
}
}
protected function createDebugBarExtension() {
return new Puc_v4p9_DebugBar_Extension($this);
}
/**
* Display additional configuration details in the Debug Bar panel.
*
* @param Puc_v4p9_DebugBar_Panel $panel
*/
public function onDisplayConfiguration($panel) {
//Do nothing. Subclasses can use this to add additional info to the panel.
}
}
endif;