* for every applied update, they are limited and require
* a name. They should not be created with programmatically-made
* names, such as "li_{$index}" with some loop. As a general
* rule they should only be created with string-literal names
* like "start-of-section" or "last-paragraph".
*
* Bookmarks are a powerful tool to enable complicated behavior.
* Consider double-checking that you need this tool if you are
* reaching for it, as inappropriate use could lead to broken
* HTML structure or unwanted processing overhead.
*
* @since 6.4.0
*
* @param string $bookmark_name Identifies this particular bookmark.
* @return bool Whether the bookmark was successfully created.
*/
public function set_bookmark( $bookmark_name ) {
return parent::set_bookmark( "_{$bookmark_name}" );
}
/*
* HTML Parsing Algorithms
*/
/**
* Closes a P element.
*
* @since 6.4.0
*
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#close-a-p-element
*/
private function close_a_p_element() {
$this->generate_implied_end_tags( 'P' );
$this->state->stack_of_open_elements->pop_until( 'P' );
}
/**
* Closes elements that have implied end tags.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#generate-implied-end-tags
*
* @param string|null $except_for_this_element Perform as if this element doesn't exist in the stack of open elements.
*/
private function generate_implied_end_tags( $except_for_this_element = null ) {
$elements_with_implied_end_tags = array(
'P',
);
$current_node = $this->state->stack_of_open_elements->current_node();
while (
$current_node && $current_node->node_name !== $except_for_this_element &&
in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true )
) {
$this->state->stack_of_open_elements->pop();
}
}
/**
* Closes elements that have implied end tags, thoroughly.
*
* See the HTML specification for an explanation why this is
* different from generating end tags in the normal sense.
*
* @since 6.4.0
*
* @see WP_HTML_Processor::generate_implied_end_tags
* @see https://html.spec.whatwg.org/#generate-implied-end-tags
*/
private function generate_implied_end_tags_thoroughly() {
$elements_with_implied_end_tags = array(
'P',
);
while ( in_array( $this->state->stack_of_open_elements->current_node(), $elements_with_implied_end_tags, true ) ) {
$this->state->stack_of_open_elements->pop();
}
}
/**
* Reconstructs the active formatting elements.
*
* > This has the effect of reopening all the formatting elements that were opened
* > in the current body, cell, or caption (whichever is youngest) that haven't
* > been explicitly closed.
*
* @since 6.4.0
*
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#reconstruct-the-active-formatting-elements
*
* @return bool Whether any formatting elements needed to be reconstructed.
*/
private function reconstruct_active_formatting_elements() {
/*
* > If there are no entries in the list of active formatting elements, then there is nothing
* > to reconstruct; stop this algorithm.
*/
if ( 0 === $this->state->active_formatting_elements->count() ) {
return false;
}
$last_entry = $this->state->active_formatting_elements->current_node();
if (
/*
* > If the last (most recently added) entry in the list of active formatting elements is a marker;
* > stop this algorithm.
*/
'marker' === $last_entry->node_name ||
/*
* > If the last (most recently added) entry in the list of active formatting elements is an
* > element that is in the stack of open elements, then there is nothing to reconstruct;
* > stop this algorithm.
*/
$this->state->stack_of_open_elements->contains_node( $last_entry )
) {
return false;
}
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot reconstruct active formatting elements when advancing and rewinding is required.' );
}
/**
* Runs the adoption agency algorithm.
*
* @since 6.4.0
*
* @throws WP_HTML_Unsupported_Exception When encountering unsupported HTML input.
*
* @see https://html.spec.whatwg.org/#adoption-agency-algorithm
*/
private function run_adoption_agency_algorithm() {
$budget = 1000;
$subject = $this->get_tag();
$current_node = $this->state->stack_of_open_elements->current_node();
if (
// > If the current node is an HTML element whose tag name is subject
$current_node && $subject === $current_node->node_name &&
// > the current node is not in the list of active formatting elements
! $this->state->active_formatting_elements->contains_node( $current_node )
) {
$this->state->stack_of_open_elements->pop();
return;
}
$outer_loop_counter = 0;
while ( $budget-- > 0 ) {
if ( $outer_loop_counter++ >= 8 ) {
return;
}
/*
* > Let formatting element be the last element in the list of active formatting elements that:
* > - is between the end of the list and the last marker in the list,
* > if any, or the start of the list otherwise,
* > - and has the tag name subject.
*/
$formatting_element = null;
foreach ( $this->state->active_formatting_elements->walk_up() as $item ) {
if ( 'marker' === $item->node_name ) {
break;
}
if ( $subject === $item->node_name ) {
$formatting_element = $item;
break;
}
}
// > If there is no such element, then return and instead act as described in the "any other end tag" entry above.
if ( null === $formatting_element ) {
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot run adoption agency when "any other end tag" is required.' );
}
// > If formatting element is not in the stack of open elements, then this is a parse error; remove the element from the list, and return.
if ( ! $this->state->stack_of_open_elements->contains_node( $formatting_element ) ) {
$this->state->active_formatting_elements->remove_node( $formatting_element->bookmark_name );
return;
}
// > If formatting element is in the stack of open elements, but the element is not in scope, then this is a parse error; return.
if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $formatting_element->node_name ) ) {
return;
}
/*
* > Let furthest block be the topmost node in the stack of open elements that is lower in the stack
* > than formatting element, and is an element in the special category. There might not be one.
*/
$is_above_formatting_element = true;
$furthest_block = null;
foreach ( $this->state->stack_of_open_elements->walk_down() as $item ) {
if ( $is_above_formatting_element && $formatting_element->bookmark_name !== $item->bookmark_name ) {
continue;
}
if ( $is_above_formatting_element ) {
$is_above_formatting_element = false;
continue;
}
if ( self::is_special( $item->node_name ) ) {
$furthest_block = $item;
break;
}
}
/*
* > If there is no furthest block, then the UA must first pop all the nodes from the bottom of the
* > stack of open elements, from the current node up to and including formatting element, then
* > remove formatting element from the list of active formatting elements, and finally return.
*/
if ( null === $furthest_block ) {
foreach ( $this->state->stack_of_open_elements->walk_up() as $item ) {
$this->state->stack_of_open_elements->pop();
if ( $formatting_element->bookmark_name === $item->bookmark_name ) {
$this->state->active_formatting_elements->remove_node( $formatting_element );
return;
}
}
}
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot extract common ancestor in adoption agency algorithm.' );
}
$this->last_error = self::ERROR_UNSUPPORTED;
throw new WP_HTML_Unsupported_Exception( 'Cannot run adoption agency when looping required.' );
}
/**
* Inserts an HTML element on the stack of open elements.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#insert-a-foreign-element
*
* @param WP_HTML_Token $token Name of bookmark pointing to element in original input HTML.
*/
private function insert_html_element( $token ) {
$this->state->stack_of_open_elements->push( $token );
}
/*
* HTML Specification Helpers
*/
/**
* Returns whether an element of a given name is in the HTML special category.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#special
*
* @param string $tag_name Name of element to check.
* @return bool Whether the element of the given name is in the special category.
*/
public static function is_special( $tag_name ) {
$tag_name = strtoupper( $tag_name );
return (
'ADDRESS' === $tag_name ||
'APPLET' === $tag_name ||
'AREA' === $tag_name ||
'ARTICLE' === $tag_name ||
'ASIDE' === $tag_name ||
'BASE' === $tag_name ||
'BASEFONT' === $tag_name ||
'BGSOUND' === $tag_name ||
'BLOCKQUOTE' === $tag_name ||
'BODY' === $tag_name ||
'BR' === $tag_name ||
'BUTTON' === $tag_name ||
'CAPTION' === $tag_name ||
'CENTER' === $tag_name ||
'COL' === $tag_name ||
'COLGROUP' === $tag_name ||
'DD' === $tag_name ||
'DETAILS' === $tag_name ||
'DIR' === $tag_name ||
'DIV' === $tag_name ||
'DL' === $tag_name ||
'DT' === $tag_name ||
'EMBED' === $tag_name ||
'FIELDSET' === $tag_name ||
'FIGCAPTION' === $tag_name ||
'FIGURE' === $tag_name ||
'FOOTER' === $tag_name ||
'FORM' === $tag_name ||
'FRAME' === $tag_name ||
'FRAMESET' === $tag_name ||
'H1' === $tag_name ||
'H2' === $tag_name ||
'H3' === $tag_name ||
'H4' === $tag_name ||
'H5' === $tag_name ||
'H6' === $tag_name ||
'HEAD' === $tag_name ||
'HEADER' === $tag_name ||
'HGROUP' === $tag_name ||
'HR' === $tag_name ||
'HTML' === $tag_name ||
'IFRAME' === $tag_name ||
'IMG' === $tag_name ||
'INPUT' === $tag_name ||
'KEYGEN' === $tag_name ||
'LI' === $tag_name ||
'LINK' === $tag_name ||
'LISTING' === $tag_name ||
'MAIN' === $tag_name ||
'MARQUEE' === $tag_name ||
'MENU' === $tag_name ||
'META' === $tag_name ||
'NAV' === $tag_name ||
'NOEMBED' === $tag_name ||
'NOFRAMES' === $tag_name ||
'NOSCRIPT' === $tag_name ||
'OBJECT' === $tag_name ||
'OL' === $tag_name ||
'P' === $tag_name ||
'PARAM' === $tag_name ||
'PLAINTEXT' === $tag_name ||
'PRE' === $tag_name ||
'SCRIPT' === $tag_name ||
'SEARCH' === $tag_name ||
'SECTION' === $tag_name ||
'SELECT' === $tag_name ||
'SOURCE' === $tag_name ||
'STYLE' === $tag_name ||
'SUMMARY' === $tag_name ||
'TABLE' === $tag_name ||
'TBODY' === $tag_name ||
'TD' === $tag_name ||
'TEMPLATE' === $tag_name ||
'TEXTAREA' === $tag_name ||
'TFOOT' === $tag_name ||
'TH' === $tag_name ||
'THEAD' === $tag_name ||
'TITLE' === $tag_name ||
'TR' === $tag_name ||
'TRACK' === $tag_name ||
'UL' === $tag_name ||
'WBR' === $tag_name ||
'XMP' === $tag_name ||
// MathML.
'MI' === $tag_name ||
'MO' === $tag_name ||
'MN' === $tag_name ||
'MS' === $tag_name ||
'MTEXT' === $tag_name ||
'ANNOTATION-XML' === $tag_name ||
// SVG.
'FOREIGNOBJECT' === $tag_name ||
'DESC' === $tag_name ||
'TITLE' === $tag_name
);
}
/**
* Returns whether a given element is an HTML Void Element
*
* > area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#void-elements
*
* @param string $tag_name Name of HTML tag to check.
* @return bool Whether the given tag is an HTML Void Element.
*/
public static function is_void( $tag_name ) {
$tag_name = strtoupper( $tag_name );
return (
'AREA' === $tag_name ||
'BASE' === $tag_name ||
'BR' === $tag_name ||
'COL' === $tag_name ||
'EMBED' === $tag_name ||
'HR' === $tag_name ||
'IMG' === $tag_name ||
'INPUT' === $tag_name ||
'LINK' === $tag_name ||
'META' === $tag_name ||
'SOURCE' === $tag_name ||
'TRACK' === $tag_name ||
'WBR' === $tag_name
);
}
/*
* Constants that would pollute the top of the class if they were found there.
*/
/**
* Indicates that the next HTML token should be parsed and processed.
*
* @since 6.4.0
*
* @var string
*/
const PROCESS_NEXT_NODE = 'process-next-node';
/**
* Indicates that the current HTML token should be reprocessed in the newly-selected insertion mode.
*
* @since 6.4.0
*
* @var string
*/
const REPROCESS_CURRENT_NODE = 'reprocess-current-node';
/**
* Indicates that the parser encountered unsupported markup and has bailed.
*
* @since 6.4.0
*
* @var string
*/
const ERROR_UNSUPPORTED = 'unsupported';
/**
* Indicates that the parser encountered more HTML tokens than it
* was able to process and has bailed.
*
* @since 6.4.0
*
* @var string
*/
const ERROR_EXCEEDED_MAX_BOOKMARKS = 'exceeded-max-bookmarks';
/**
* Unlock code that must be passed into the constructor to create this class.
*
* This class extends the WP_HTML_Tag_Processor, which has a public class
* constructor. Therefore, it's not possible to have a private constructor here.
*
* This unlock code is used to ensure that anyone calling the constructor is
* doing so with a full understanding that it's intended to be a private API.
*
* @access private
*/
const CONSTRUCTOR_UNLOCK_CODE = 'Use WP_HTML_Processor::create_fragment() instead of calling the class constructor directly.';
}
f ( $actualWpPatchNumber === null ) {
$actualWpPatchNumber = $patch;
} else {
$actualWpPatchNumber = max($actualWpPatchNumber, $patch);
}
}
}
}
if ( $actualWpPatchNumber === null ) {
$actualWpPatchNumber = 999;
}
if ( $actualWpPatchNumber > 0 ) {
$update->tested .= '.' . $actualWpPatchNumber;
}
}
/**
* Get the currently installed version of the plugin or theme.
*
* @return string|null Version number.
*/
public function getInstalledVersion() {
return $this->package->getInstalledVersion();
}
/**
* Get the full path of the plugin or theme directory.
*
* @return string
*/
public function getAbsoluteDirectoryPath() {
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);
//Add a placeholder item to the "no_update" list to enable auto-update support.
//If we don't do this, the option to enable automatic updates will only show up
//when an update is available.
$updates = $this->addNoUpdateItem($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;
}
/**
* See this post for more information:
* @link https://make.wordpress.org/core/2020/07/30/recommended-usage-of-the-updates-api-to-support-the-auto-updates-ui-for-plugins-and-themes-in-wordpress-5-5/
*
* @param stdClass|null $updates
* @return stdClass
*/
protected function addNoUpdateItem($updates) {
if ( !is_object($updates) ) {
$updates = new stdClass();
$updates->response = array();
$updates->no_update = array();
} else if ( !isset($updates->no_update) ) {
$updates->no_update = array();
}
$updates->no_update[$this->getUpdateListKey()] = (object) $this->getNoUpdateItemFields();
return $updates;
}
/**
* Subclasses should override this method to add fields that are specific to plugins or themes.
* @return array
*/
protected function getNoUpdateItemFields() {
return array(
'new_version' => $this->getInstalledVersion(),
'url' => '',
'package' => '',
'requires_php' => '',
);
}
/**
* 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_v4p11_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) ){
if ( version_compare(PHP_VERSION, '5.3', '>=') && (strpos($metaClass, '\\') === false) ) {
$metaClass = __NAMESPACE__ . '\\' . $metaClass;
}
$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->debugBarExtension = $this->createDebugBarExtension();
}
}
protected function createDebugBarExtension() {
return new Puc_v4p11_DebugBar_Extension($this);
}
/**
* Display additional configuration details in the Debug Bar panel.
*
* @param Puc_v4p11_DebugBar_Panel $panel
*/
public function onDisplayConfiguration($panel) {
//Do nothing. Subclasses can use this to add additional info to the panel.
}
}
endif;