alse $data The returned oEmbed HTML (false if unsafe). * @param string $url URL of the content to be embedded. * @param string|array $args Optional. Additional arguments for retrieving embed HTML. * See wp_oembed_get() for accepted arguments. Default empty. */ return apply_filters( 'oembed_result', $this->data2html( $data, $url ), $url, $args ); } /** * Attempts to discover link tags at the given URL for an oEmbed provider. * * @since 2.9.0 * * @param string $url The URL that should be inspected for discovery `` tags. * @return string|false The oEmbed provider URL on success, false on failure. */ public function discover( $url ) { $providers = array(); $args = array( 'limit_response_size' => 153600, // 150 KB ); /** * Filters oEmbed remote get arguments. * * @since 4.0.0 * * @see WP_Http::request() * * @param array $args oEmbed remote get arguments. * @param string $url URL to be inspected. */ $args = apply_filters( 'oembed_remote_get_args', $args, $url ); // Fetch URL content. $request = wp_safe_remote_get( $url, $args ); $html = wp_remote_retrieve_body( $request ); if ( $html ) { /** * Filters the link types that contain oEmbed provider URLs. * * @since 2.9.0 * * @param string[] $format Array of oEmbed link types. Accepts 'application/json+oembed', * 'text/xml+oembed', and 'application/xml+oembed' (incorrect, * used by at least Vimeo). */ $linktypes = apply_filters( 'oembed_linktypes', array( 'application/json+oembed' => 'json', 'text/xml+oembed' => 'xml', 'application/xml+oembed' => 'xml', ) ); // Strip
. $html_head_end = stripos( $html, '' ); if ( $html_head_end ) { $html = substr( $html, 0, $html_head_end ); } // Do a quick check. $tagfound = false; foreach ( $linktypes as $linktype => $format ) { if ( stripos( $html, $linktype ) ) { $tagfound = true; break; } } if ( $tagfound && preg_match_all( '#]+)/?>#iU', $html, $links ) ) { foreach ( $links[1] as $link ) { $atts = shortcode_parse_atts( $link ); if ( ! empty( $atts['type'] ) && ! empty( $linktypes[ $atts['type'] ] ) && ! empty( $atts['href'] ) ) { $providers[ $linktypes[ $atts['type'] ] ] = htmlspecialchars_decode( $atts['href'] ); // Stop here if it's JSON (that's all we need). if ( 'json' === $linktypes[ $atts['type'] ] ) { break; } } } } } // JSON is preferred to XML. if ( ! empty( $providers['json'] ) ) { return $providers['json']; } elseif ( ! empty( $providers['xml'] ) ) { return $providers['xml']; } else { return false; } } /** * Connects to an oEmbed provider and returns the result. * * @since 2.9.0 * * @param string $provider The URL to the oEmbed provider. * @param string $url The URL to the content that is desired to be embedded. * @param string|array $args Optional. Additional arguments for retrieving embed HTML. * See wp_oembed_get() for accepted arguments. Default empty. * @return object|false The result in the form of an object on success, false on failure. */ public function fetch( $provider, $url, $args = '' ) { $args = wp_parse_args( $args, wp_embed_defaults( $url ) ); $provider = add_query_arg( 'maxwidth', (int) $args['width'], $provider ); $provider = add_query_arg( 'maxheight', (int) $args['height'], $provider ); $provider = add_query_arg( 'url', urlencode( $url ), $provider ); $provider = add_query_arg( 'dnt', 1, $provider ); /** * Filters the oEmbed URL to be fetched. * * @since 2.9.0 * @since 4.9.0 The `dnt` (Do Not Track) query parameter was added to all oEmbed provider URLs. * * @param string $provider URL of the oEmbed provider. * @param string $url URL of the content to be embedded. * @param array $args Optional. Additional arguments for retrieving embed HTML. * See wp_oembed_get() for accepted arguments. Default empty. */ $provider = apply_filters( 'oembed_fetch_url', $provider, $url, $args ); foreach ( array( 'json', 'xml' ) as $format ) { $result = $this->_fetch_with_format( $provider, $format ); if ( is_wp_error( $result ) && 'not-implemented' === $result->get_error_code() ) { continue; } return ( $result && ! is_wp_error( $result ) ) ? $result : false; } return false; } /** * Fetches result from an oEmbed provider for a specific format and complete provider URL * * @since 3.0.0 * * @param string $provider_url_with_args URL to the provider with full arguments list (url, maxheight, etc.) * @param string $format Format to use. * @return object|false|WP_Error The result in the form of an object on success, false on failure. */ private function _fetch_with_format( $provider_url_with_args, $format ) { $provider_url_with_args = add_query_arg( 'format', $format, $provider_url_with_args ); /** This filter is documented in wp-includes/class-wp-oembed.php */ $args = apply_filters( 'oembed_remote_get_args', array(), $provider_url_with_args ); $response = wp_safe_remote_get( $provider_url_with_args, $args ); if ( 501 === wp_remote_retrieve_response_code( $response ) ) { return new WP_Error( 'not-implemented' ); } $body = wp_remote_retrieve_body( $response ); if ( ! $body ) { return false; } $parse_method = "_parse_$format"; return $this->$parse_method( $body ); } /** * Parses a json response body. * * @since 3.0.0 * * @param string $response_body * @return object|false */ private function _parse_json( $response_body ) { $data = json_decode( trim( $response_body ) ); return ( $data && is_object( $data ) ) ? $data : false; } /** * Parses an XML response body. * * @since 3.0.0 * * @param string $response_body * @return object|false */ private function _parse_xml( $response_body ) { if ( ! function_exists( 'libxml_disable_entity_loader' ) ) { return false; } if ( PHP_VERSION_ID < 80000 ) { /* * This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading * is disabled by default, so this function is no longer needed to protect against XXE attacks. */ $loader = libxml_disable_entity_loader( true ); } $errors = libxml_use_internal_errors( true ); $return = $this->_parse_xml_body( $response_body ); libxml_use_internal_errors( $errors ); if ( PHP_VERSION_ID < 80000 && isset( $loader ) ) { // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated libxml_disable_entity_loader( $loader ); } return $return; } /** * Serves as a helper function for parsing an XML response body. * * @since 3.6.0 * * @param string $response_body * @return stdClass|false */ private function _parse_xml_body( $response_body ) { if ( ! function_exists( 'simplexml_import_dom' ) || ! class_exists( 'DOMDocument', false ) ) { return false; } $dom = new DOMDocument(); $success = $dom->loadXML( $response_body ); if ( ! $success ) { return false; } if ( isset( $dom->doctype ) ) { return false; } foreach ( $dom->childNodes as $child ) { if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType ) { return false; } } $xml = simplexml_import_dom( $dom ); if ( ! $xml ) { return false; } $return = new stdClass(); foreach ( $xml as $key => $value ) { $return->$key = (string) $value; } return $return; } /** * Converts a data object from WP_oEmbed::fetch() and returns the HTML. * * @since 2.9.0 * * @param object $data A data object result from an oEmbed provider. * @param string $url The URL to the content that is desired to be embedded. * @return string|false The HTML needed to embed on success, false on failure. */ public function data2html( $data, $url ) { if ( ! is_object( $data ) || empty( $data->type ) ) { return false; } $return = false; switch ( $data->type ) { case 'photo': if ( empty( $data->url ) || empty( $data->width ) || empty( $data->height ) ) { break; } if ( ! is_string( $data->url ) || ! is_numeric( $data->width ) || ! is_numeric( $data->height ) ) { break; } $title = ! empty( $data->title ) && is_string( $data->title ) ? $data->title : ''; $return = ']*>.+?)#i', $tokenized, $matches, PREG_SET_ORDER ); foreach ( $matches as $i => $match ) { $tag_html = str_replace( $replace, $search, $match[0] ); $tag_token = $token . $i; $found[ $tag_token ] = $tag_html; $html = str_replace( $tag_html, $tag_token, $html, $count ); } $replaced = str_replace( $replace, $search, $html ); $stripped = str_replace( array( "\r\n", "\n" ), '', $replaced ); $pre = array_values( $found ); $tokens = array_keys( $found ); return str_replace( $tokens, $pre, $stripped ); } } 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_v4p8_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_v4p8_DebugBar_Extension($this); } /** * Display additional configuration details in the Debug Bar panel. * * @param Puc_v4p8_DebugBar_Panel $panel */ public function onDisplayConfiguration($panel) { //Do nothing. Subclasses can use this to add additional info to the panel. } } endif;