diff --git a/README.txt b/README.txt
index 77de572..e7d77b6 100644
--- a/README.txt
+++ b/README.txt
@@ -2,12 +2,12 @@
Contributors: getbowtied, vanesareinerth, adrianlbs, traians
Tags: gutenberg, blocks
Requires at least: 5.0
-Tested up to: 5.6
-Stable tag: 1.7.1
+Tested up to: 5.8
+Stable tag: 1.7.2
Requires PHP: 5.5.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
-~Current Version:1.7.0~
+~Current Version:1.7.2~
Extends the functionality of The Hanger theme by adding theme specific features.
@@ -37,6 +37,9 @@ Companion plugin for the **The Hanger** theme. Extends the functionality by addi
== Changelog ==
+= 1.7.2 =
+- WordPress 5.8 compatibility updates
+
= 1.7.1 =
- Small maintenance updates
diff --git a/core/updater/Puc/v4/Factory.php b/core/updater/Puc/v4/Factory.php
old mode 100755
new mode 100644
index 7311130..ac21a55
--- a/core/updater/Puc/v4/Factory.php
+++ b/core/updater/Puc/v4/Factory.php
@@ -1,6 +1,6 @@
rootDir = dirname(__FILE__) . '/';
+ $nameParts = explode('_', __CLASS__, 3);
+ $this->prefix = $nameParts[0] . '_' . $nameParts[1] . '_';
+
+ $this->libraryDir = $this->rootDir . '../..';
+ if ( !self::isPhar() ) {
+ $this->libraryDir = realpath($this->libraryDir);
+ }
+ $this->libraryDir = $this->libraryDir . '/';
+
+ $this->staticMap = array(
+ 'PucReadmeParser' => 'vendor/PucReadmeParser.php',
+ 'Parsedown' => 'vendor/Parsedown.php',
+ 'Puc_v4_Factory' => 'Puc/v4/Factory.php',
+ );
+
+ spl_autoload_register(array($this, 'autoload'));
+ }
+
+ /**
+ * Determine if this file is running as part of a Phar archive.
+ *
+ * @return bool
+ */
+ private static function isPhar() {
+ //Check if the current file path starts with "phar://".
+ static $pharProtocol = 'phar://';
+ return (substr(__FILE__, 0, strlen($pharProtocol)) === $pharProtocol);
+ }
+
+ public function autoload($className) {
+ if ( isset($this->staticMap[$className]) && file_exists($this->libraryDir . $this->staticMap[$className]) ) {
+ /** @noinspection PhpIncludeInspection */
+ include ($this->libraryDir . $this->staticMap[$className]);
+ return;
+ }
+
+ if (strpos($className, $this->prefix) === 0) {
+ $path = substr($className, strlen($this->prefix));
+ $path = str_replace('_', '/', $path);
+ $path = $this->rootDir . $path . '.php';
+
+ if (file_exists($path)) {
+ /** @noinspection PhpIncludeInspection */
+ include $path;
+ }
+ }
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/DebugBar/Extension.php b/core/updater/Puc/v4p11/DebugBar/Extension.php
new file mode 100644
index 0000000..a39dac0
--- /dev/null
+++ b/core/updater/Puc/v4p11/DebugBar/Extension.php
@@ -0,0 +1,190 @@
+updateChecker = $updateChecker;
+ if ( isset($panelClass) ) {
+ $this->panelClass = $panelClass;
+ }
+
+ if ( version_compare(PHP_VERSION, '5.3', '>=') && (strpos($this->panelClass, '\\') === false) ) {
+ $this->panelClass = __NAMESPACE__ . '\\' . $this->panelClass;
+ }
+
+ add_filter('debug_bar_panels', array($this, 'addDebugBarPanel'));
+ add_action('debug_bar_enqueue_scripts', array($this, 'enqueuePanelDependencies'));
+
+ add_action('wp_ajax_puc_v4_debug_check_now', array($this, 'ajaxCheckNow'));
+ }
+
+ /**
+ * Register the PUC Debug Bar panel.
+ *
+ * @param array $panels
+ * @return array
+ */
+ public function addDebugBarPanel($panels) {
+ if ( $this->updateChecker->userCanInstallUpdates() ) {
+ $panels[] = new $this->panelClass($this->updateChecker);
+ }
+ return $panels;
+ }
+
+ /**
+ * Enqueue our Debug Bar scripts and styles.
+ */
+ public function enqueuePanelDependencies() {
+ wp_enqueue_style(
+ 'puc-debug-bar-style-v4',
+ $this->getLibraryUrl("/css/puc-debug-bar.css"),
+ array('debug-bar'),
+ '20171124'
+ );
+
+ wp_enqueue_script(
+ 'puc-debug-bar-js-v4',
+ $this->getLibraryUrl("/js/debug-bar.js"),
+ array('jquery'),
+ '20201209'
+ );
+ }
+
+ /**
+ * Run an update check and output the result. Useful for making sure that
+ * the update checking process works as expected.
+ */
+ public function ajaxCheckNow() {
+ if ( $_POST['uid'] !== $this->updateChecker->getUniqueName('uid') ) {
+ return;
+ }
+ $this->preAjaxRequest();
+ $update = $this->updateChecker->checkForUpdates();
+ if ( $update !== null ) {
+ echo "An update is available:";
+ echo '
', htmlentities(print_r($update, true)), '
';
+ } else {
+ echo 'No updates found.';
+ }
+
+ $errors = $this->updateChecker->getLastRequestApiErrors();
+ if ( !empty($errors) ) {
+ printf('The update checker encountered %d API error%s.
', count($errors), (count($errors) > 1) ? 's' : '');
+
+ foreach (array_values($errors) as $num => $item) {
+ $wpError = $item['error'];
+ /** @var WP_Error $wpError */
+ printf('%d) %s
', $num + 1, esc_html($wpError->get_error_message()));
+
+ echo '';
+ printf('- Error code:
%s
', esc_html($wpError->get_error_code()));
+
+ if ( isset($item['url']) ) {
+ printf('- Requested URL:
%s
', esc_html($item['url']));
+ }
+
+ if ( isset($item['httpResponse']) ) {
+ if ( is_wp_error($item['httpResponse']) ) {
+ $httpError = $item['httpResponse'];
+ /** @var WP_Error $httpError */
+ printf(
+ '- WordPress HTTP API error:
- %s (
%s
) ',
+ esc_html($httpError->get_error_message()),
+ esc_html($httpError->get_error_code())
+ );
+ } else {
+ //Status code.
+ printf(
+ '- HTTP status:
%d %s
',
+ wp_remote_retrieve_response_code($item['httpResponse']),
+ wp_remote_retrieve_response_message($item['httpResponse'])
+ );
+
+ //Headers.
+ echo '- Response headers:
';
+ foreach (wp_remote_retrieve_headers($item['httpResponse']) as $name => $value) {
+ printf("%s: %s\n", esc_html($name), esc_html($value));
+ }
+ echo '
';
+
+ //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('- Response body:
%s
', esc_html($body));
+ }
+ }
+ echo '';
+ }
+ }
+
+ exit;
+ }
+
+ /**
+ * Check access permissions and enable error display (for debugging).
+ */
+ protected function preAjaxRequest() {
+ if ( !$this->updateChecker->userCanInstallUpdates() ) {
+ die('Access denied');
+ }
+ check_ajax_referer('puc-ajax');
+
+ error_reporting(E_ALL);
+ @ini_set('display_errors', 'On');
+ }
+
+ /**
+ * Remove hooks that were added by this extension.
+ */
+ public function removeHooks() {
+ remove_filter('debug_bar_panels', array($this, 'addDebugBarPanel'));
+ remove_action('debug_bar_enqueue_scripts', array($this, 'enqueuePanelDependencies'));
+ remove_action('wp_ajax_puc_v4_debug_check_now', array($this, 'ajaxCheckNow'));
+ }
+
+ /**
+ * @param string $filePath
+ * @return string
+ */
+ private function getLibraryUrl($filePath) {
+ $absolutePath = realpath(dirname(__FILE__) . '/../../../' . ltrim($filePath, '/'));
+
+ //Where is the library located inside the WordPress directory structure?
+ $absolutePath = Puc_v4p11_Factory::normalizePath($absolutePath);
+
+ $pluginDir = Puc_v4p11_Factory::normalizePath(WP_PLUGIN_DIR);
+ $muPluginDir = Puc_v4p11_Factory::normalizePath(WPMU_PLUGIN_DIR);
+ $themeDir = Puc_v4p11_Factory::normalizePath(get_theme_root());
+
+ if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
+ //It's part of a plugin.
+ return plugins_url(basename($absolutePath), $absolutePath);
+ } else if ( strpos($absolutePath, $themeDir) === 0 ) {
+ //It's part of a theme.
+ $relativePath = substr($absolutePath, strlen($themeDir) + 1);
+ $template = substr($relativePath, 0, strpos($relativePath, '/'));
+ $baseUrl = get_theme_root_uri($template);
+
+ if ( !empty($baseUrl) && $relativePath ) {
+ return $baseUrl . '/' . $relativePath;
+ }
+ }
+
+ return '';
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/DebugBar/Panel.php b/core/updater/Puc/v4p11/DebugBar/Panel.php
new file mode 100644
index 0000000..666cdf4
--- /dev/null
+++ b/core/updater/Puc/v4p11/DebugBar/Panel.php
@@ -0,0 +1,165 @@
+';
+
+ public function __construct($updateChecker) {
+ $this->updateChecker = $updateChecker;
+ $title = sprintf(
+ '',
+ esc_attr($this->updateChecker->getUniqueName('uid')),
+ $this->updateChecker->slug
+ );
+ parent::__construct($title);
+ }
+
+ public function render() {
+ printf(
+ '',
+ esc_attr($this->updateChecker->getUniqueName('debug-bar-panel')),
+ esc_attr($this->updateChecker->slug),
+ esc_attr($this->updateChecker->getUniqueName('uid')),
+ esc_attr(wp_create_nonce('puc-ajax'))
+ );
+
+ $this->displayConfiguration();
+ $this->displayStatus();
+ $this->displayCurrentUpdate();
+
+ echo '
';
+ }
+
+ private function displayConfiguration() {
+ echo 'Configuration
';
+ echo '';
+ $this->displayConfigHeader();
+ $this->row('Slug', htmlentities($this->updateChecker->slug));
+ $this->row('DB option', htmlentities($this->updateChecker->optionName));
+
+ $requestInfoButton = $this->getMetadataButton();
+ $this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox);
+
+ $scheduler = $this->updateChecker->scheduler;
+ if ( $scheduler->checkPeriod > 0 ) {
+ $this->row('Automatic checks', 'Every ' . $scheduler->checkPeriod . ' hours');
+ } else {
+ $this->row('Automatic checks', 'Disabled');
+ }
+
+ if ( isset($scheduler->throttleRedundantChecks) ) {
+ if ( $scheduler->throttleRedundantChecks && ($scheduler->checkPeriod > 0) ) {
+ $this->row(
+ 'Throttling',
+ sprintf(
+ 'Enabled. If an update is already available, check for updates every %1$d hours instead of every %2$d hours.',
+ $scheduler->throttledCheckPeriod,
+ $scheduler->checkPeriod
+ )
+ );
+ } else {
+ $this->row('Throttling', 'Disabled');
+ }
+ }
+
+ $this->updateChecker->onDisplayConfiguration($this);
+
+ echo '
';
+ }
+
+ protected function displayConfigHeader() {
+ //Do nothing. This should be implemented in subclasses.
+ }
+
+ protected function getMetadataButton() {
+ return '';
+ }
+
+ private function displayStatus() {
+ echo 'Status
';
+ echo '';
+ $state = $this->updateChecker->getUpdateState();
+ $checkNowButton = '';
+ if ( function_exists('get_submit_button') ) {
+ $checkNowButton = get_submit_button(
+ 'Check Now',
+ 'secondary',
+ 'puc-check-now-button',
+ false,
+ array('id' => $this->updateChecker->getUniqueName('check-now-button'))
+ );
+ }
+
+ if ( $state->getLastCheck() > 0 ) {
+ $this->row('Last check', $this->formatTimeWithDelta($state->getLastCheck()) . ' ' . $checkNowButton . $this->responseBox);
+ } else {
+ $this->row('Last check', 'Never');
+ }
+
+ $nextCheck = wp_next_scheduled($this->updateChecker->scheduler->getCronHookName());
+ $this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck));
+
+ if ( $state->getCheckedVersion() !== '' ) {
+ $this->row('Checked version', htmlentities($state->getCheckedVersion()));
+ $this->row('Cached update', $state->getUpdate());
+ }
+ $this->row('Update checker class', htmlentities(get_class($this->updateChecker)));
+ echo '
';
+ }
+
+ private function displayCurrentUpdate() {
+ $update = $this->updateChecker->getUpdate();
+ if ( $update !== null ) {
+ echo 'An Update Is Available
';
+ echo '';
+ $fields = $this->getUpdateFields();
+ foreach($fields as $field) {
+ if ( property_exists($update, $field) ) {
+ $this->row(ucwords(str_replace('_', ' ', $field)), htmlentities($update->$field));
+ }
+ }
+ echo '
';
+ } else {
+ echo 'No updates currently available
';
+ }
+ }
+
+ protected function getUpdateFields() {
+ return array('version', 'download_url', 'slug',);
+ }
+
+ private function formatTimeWithDelta($unixTime) {
+ if ( empty($unixTime) ) {
+ return 'Never';
+ }
+
+ $delta = time() - $unixTime;
+ $result = human_time_diff(time(), $unixTime);
+ if ( $delta < 0 ) {
+ $result = 'after ' . $result;
+ } else {
+ $result = $result . ' ago';
+ }
+ $result .= ' (' . $this->formatTimestamp($unixTime) . ')';
+ return $result;
+ }
+
+ private function formatTimestamp($unixTime) {
+ return gmdate('Y-m-d H:i:s', $unixTime + (get_option('gmt_offset') * 3600));
+ }
+
+ public function row($name, $value) {
+ if ( is_object($value) || is_array($value) ) {
+ $value = '' . htmlentities(print_r($value, true)) . '
';
+ } else if ($value === null) {
+ $value = 'null
';
+ }
+ printf('%1$s | %2$s |
', $name, $value);
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/DebugBar/PluginExtension.php b/core/updater/Puc/v4p11/DebugBar/PluginExtension.php
new file mode 100644
index 0000000..edc4a2f
--- /dev/null
+++ b/core/updater/Puc/v4p11/DebugBar/PluginExtension.php
@@ -0,0 +1,33 @@
+updateChecker->getUniqueName('uid') ) {
+ return;
+ }
+ $this->preAjaxRequest();
+ $info = $this->updateChecker->requestInfo();
+ if ( $info !== null ) {
+ echo 'Successfully retrieved plugin info from the metadata URL:';
+ echo '', htmlentities(print_r($info, true)), '
';
+ } else {
+ echo 'Failed to retrieve plugin info from the metadata URL.';
+ }
+ exit;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/DebugBar/PluginPanel.php b/core/updater/Puc/v4p11/DebugBar/PluginPanel.php
new file mode 100644
index 0000000..fa98600
--- /dev/null
+++ b/core/updater/Puc/v4p11/DebugBar/PluginPanel.php
@@ -0,0 +1,38 @@
+row('Plugin file', htmlentities($this->updateChecker->pluginFile));
+ parent::displayConfigHeader();
+ }
+
+ protected function getMetadataButton() {
+ $requestInfoButton = '';
+ if ( function_exists('get_submit_button') ) {
+ $requestInfoButton = get_submit_button(
+ 'Request Info',
+ 'secondary',
+ 'puc-request-info-button',
+ false,
+ array('id' => $this->updateChecker->getUniqueName('request-info-button'))
+ );
+ }
+ return $requestInfoButton;
+ }
+
+ protected function getUpdateFields() {
+ return array_merge(
+ parent::getUpdateFields(),
+ array('homepage', 'upgrade_notice', 'tested',)
+ );
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/DebugBar/ThemePanel.php b/core/updater/Puc/v4p11/DebugBar/ThemePanel.php
new file mode 100644
index 0000000..77f4558
--- /dev/null
+++ b/core/updater/Puc/v4p11/DebugBar/ThemePanel.php
@@ -0,0 +1,21 @@
+row('Theme directory', htmlentities($this->updateChecker->directoryName));
+ parent::displayConfigHeader();
+ }
+
+ protected function getUpdateFields() {
+ return array_merge(parent::getUpdateFields(), array('details_url'));
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Factory.php b/core/updater/Puc/v4p11/Factory.php
new file mode 100644
index 0000000..3541042
--- /dev/null
+++ b/core/updater/Puc/v4p11/Factory.php
@@ -0,0 +1,365 @@
+ '',
+ 'slug' => '',
+ 'checkPeriod' => 12,
+ 'optionName' => '',
+ 'muPluginFile' => '',
+ );
+ $args = array_merge($defaults, array_intersect_key($args, $defaults));
+ extract($args, EXTR_SKIP);
+
+ //Check for the service URI
+ if ( empty($metadataUrl) ) {
+ $metadataUrl = self::getServiceURI($fullPath);
+ }
+
+ /** @noinspection PhpUndefinedVariableInspection These variables are created by extract(), above. */
+ return self::buildUpdateChecker($metadataUrl, $fullPath, $slug, $checkPeriod, $optionName, $muPluginFile);
+ }
+
+ /**
+ * Create a new instance of the update checker.
+ *
+ * This method automatically detects if you're using it for a plugin or a theme and chooses
+ * the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
+ *
+ * @see Puc_v4p11_UpdateChecker::__construct
+ *
+ * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
+ * @param string $fullPath Full path to the main plugin file or to the theme directory.
+ * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
+ * @param int $checkPeriod How often to check for updates (in hours).
+ * @param string $optionName Where to store book-keeping info about update checks.
+ * @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
+ * @return Puc_v4p11_Plugin_UpdateChecker|Puc_v4p11_Theme_UpdateChecker|Puc_v4p11_Vcs_BaseChecker
+ */
+ public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
+ $fullPath = self::normalizePath($fullPath);
+ $id = null;
+
+ //Plugin or theme?
+ $themeDirectory = self::getThemeDirectoryName($fullPath);
+ if ( self::isPluginFile($fullPath) ) {
+ $type = 'Plugin';
+ $id = $fullPath;
+ } else if ( $themeDirectory !== null ) {
+ $type = 'Theme';
+ $id = $themeDirectory;
+ } else {
+ throw new RuntimeException(sprintf(
+ 'The update checker cannot determine if "%s" is a plugin or a theme. ' .
+ 'This is a bug. Please contact the PUC developer.',
+ htmlentities($fullPath)
+ ));
+ }
+
+ //Which hosting service does the URL point to?
+ $service = self::getVcsService($metadataUrl);
+
+ $apiClass = null;
+ if ( empty($service) ) {
+ //The default is to get update information from a remote JSON file.
+ $checkerClass = $type . '_UpdateChecker';
+ } else {
+ //You can also use a VCS repository like GitHub.
+ $checkerClass = 'Vcs_' . $type . 'UpdateChecker';
+ $apiClass = $service . 'Api';
+ }
+
+ $checkerClass = self::getCompatibleClassVersion($checkerClass);
+ if ( $checkerClass === null ) {
+ trigger_error(
+ sprintf(
+ 'PUC %s does not support updates for %ss %s',
+ htmlentities(self::$latestCompatibleVersion),
+ strtolower($type),
+ $service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
+ ),
+ E_USER_ERROR
+ );
+ return null;
+ }
+
+ //Add the current namespace to the class name(s).
+ if ( version_compare(PHP_VERSION, '5.3', '>=') ) {
+ $checkerClass = __NAMESPACE__ . '\\' . $checkerClass;
+ }
+
+ if ( !isset($apiClass) ) {
+ //Plain old update checker.
+ return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
+ } else {
+ //VCS checker + an API client.
+ $apiClass = self::getCompatibleClassVersion($apiClass);
+ if ( $apiClass === null ) {
+ trigger_error(sprintf(
+ 'PUC %s does not support %s',
+ htmlentities(self::$latestCompatibleVersion),
+ htmlentities($service)
+ ), E_USER_ERROR);
+ return null;
+ }
+
+ if ( version_compare(PHP_VERSION, '5.3', '>=') && (strpos($apiClass, '\\') === false) ) {
+ $apiClass = __NAMESPACE__ . '\\' . $apiClass;
+ }
+
+ return new $checkerClass(
+ new $apiClass($metadataUrl),
+ $id,
+ $slug,
+ $checkPeriod,
+ $optionName,
+ $muPluginFile
+ );
+ }
+ }
+
+ /**
+ *
+ * Normalize a filesystem path. Introduced in WP 3.9.
+ * Copying here allows use of the class on earlier versions.
+ * This version adapted from WP 4.8.2 (unchanged since 4.5.0)
+ *
+ * @param string $path Path to normalize.
+ * @return string Normalized path.
+ */
+ public static function normalizePath($path) {
+ if ( function_exists('wp_normalize_path') ) {
+ return wp_normalize_path($path);
+ }
+ $path = str_replace('\\', '/', $path);
+ $path = preg_replace('|(?<=.)/+|', '/', $path);
+ if ( substr($path, 1, 1) === ':' ) {
+ $path = ucfirst($path);
+ }
+ return $path;
+ }
+
+ /**
+ * Check if the path points to a plugin file.
+ *
+ * @param string $absolutePath Normalized path.
+ * @return bool
+ */
+ protected static function isPluginFile($absolutePath) {
+ //Is the file inside the "plugins" or "mu-plugins" directory?
+ $pluginDir = self::normalizePath(WP_PLUGIN_DIR);
+ $muPluginDir = self::normalizePath(WPMU_PLUGIN_DIR);
+ if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
+ return true;
+ }
+
+ //Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
+ if ( !is_file($absolutePath) ) {
+ return false;
+ }
+
+ //Does it have a valid plugin header?
+ //This is a last-ditch check for plugins symlinked from outside the WP root.
+ if ( function_exists('get_file_data') ) {
+ $headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
+ return !empty($headers['Name']);
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the name of the theme's directory from a full path to a file inside that directory.
+ * E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
+ *
+ * Note that subdirectories are currently not supported. For example,
+ * "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
+ *
+ * @param string $absolutePath Normalized path.
+ * @return string|null Directory name, or NULL if the path doesn't point to a theme.
+ */
+ protected static function getThemeDirectoryName($absolutePath) {
+ if ( is_file($absolutePath) ) {
+ $absolutePath = dirname($absolutePath);
+ }
+
+ if ( file_exists($absolutePath . '/style.css') ) {
+ return basename($absolutePath);
+ }
+ return null;
+ }
+
+ /**
+ * Get the service URI from the file header.
+ *
+ * @param string $fullPath
+ * @return string
+ */
+ private static function getServiceURI($fullPath) {
+ //Look for the URI
+ if ( is_readable($fullPath) ) {
+ $seek = array(
+ 'github' => 'GitHub URI',
+ 'gitlab' => 'GitLab URI',
+ 'bucket' => 'BitBucket URI',
+ );
+ $seek = apply_filters('puc_get_source_uri', $seek);
+ $data = get_file_data($fullPath, $seek);
+ foreach ($data as $key => $uri) {
+ if ( $uri ) {
+ return $uri;
+ }
+ }
+ }
+
+ //URI was not found so throw an error.
+ throw new RuntimeException(
+ sprintf('Unable to locate URI in header of "%s"', htmlentities($fullPath))
+ );
+ }
+
+ /**
+ * Get the name of the hosting service that the URL points to.
+ *
+ * @param string $metadataUrl
+ * @return string|null
+ */
+ private static function getVcsService($metadataUrl) {
+ $service = null;
+
+ //Which hosting service does the URL point to?
+ $host = parse_url($metadataUrl, PHP_URL_HOST);
+ $path = parse_url($metadataUrl, PHP_URL_PATH);
+
+ //Check if the path looks like "/user-name/repository".
+ //For GitLab.com it can also be "/user/group1/group2/.../repository".
+ $repoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
+ if ( $host === 'gitlab.com' ) {
+ $repoRegex = '@^/?(?:[^/#?&]++/){1,20}(?:[^/#?&]++)/?$@';
+ }
+ if ( preg_match($repoRegex, $path) ) {
+ $knownServices = array(
+ 'github.com' => 'GitHub',
+ 'bitbucket.org' => 'BitBucket',
+ 'gitlab.com' => 'GitLab',
+ );
+ if ( isset($knownServices[$host]) ) {
+ $service = $knownServices[$host];
+ }
+ }
+
+ return apply_filters('puc_get_vcs_service', $service, $host, $path, $metadataUrl);
+ }
+
+ /**
+ * Get the latest version of the specified class that has the same major version number
+ * as this factory class.
+ *
+ * @param string $class Partial class name.
+ * @return string|null Full class name.
+ */
+ protected static function getCompatibleClassVersion($class) {
+ if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
+ return self::$classVersions[$class][self::$latestCompatibleVersion];
+ }
+ return null;
+ }
+
+ /**
+ * Get the specific class name for the latest available version of a class.
+ *
+ * @param string $class
+ * @return null|string
+ */
+ public static function getLatestClassVersion($class) {
+ if ( !self::$sorted ) {
+ self::sortVersions();
+ }
+
+ if ( isset(self::$classVersions[$class]) ) {
+ return reset(self::$classVersions[$class]);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Sort available class versions in descending order (i.e. newest first).
+ */
+ protected static function sortVersions() {
+ foreach ( self::$classVersions as $class => $versions ) {
+ uksort($versions, array(__CLASS__, 'compareVersions'));
+ self::$classVersions[$class] = $versions;
+ }
+ self::$sorted = true;
+ }
+
+ protected static function compareVersions($a, $b) {
+ return -version_compare($a, $b);
+ }
+
+ /**
+ * Register a version of a class.
+ *
+ * @access private This method is only for internal use by the library.
+ *
+ * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
+ * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
+ * @param string $version Version number, e.g. '1.2'.
+ */
+ public static function addVersion($generalClass, $versionedClass, $version) {
+ if ( empty(self::$myMajorVersion) ) {
+ $nameParts = explode('_', __CLASS__, 3);
+ self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
+ }
+
+ //Store the greatest version number that matches our major version.
+ $components = explode('.', $version);
+ if ( $components[0] === self::$myMajorVersion ) {
+
+ if (
+ empty(self::$latestCompatibleVersion)
+ || version_compare($version, self::$latestCompatibleVersion, '>')
+ ) {
+ self::$latestCompatibleVersion = $version;
+ }
+
+ }
+
+ if ( !isset(self::$classVersions[$generalClass]) ) {
+ self::$classVersions[$generalClass] = array();
+ }
+ self::$classVersions[$generalClass][$version] = $versionedClass;
+ self::$sorted = false;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/InstalledPackage.php b/core/updater/Puc/v4p11/InstalledPackage.php
new file mode 100644
index 0000000..920159b
--- /dev/null
+++ b/core/updater/Puc/v4p11/InstalledPackage.php
@@ -0,0 +1,103 @@
+updateChecker = $updateChecker;
+ }
+
+ /**
+ * Get the currently installed version of the plugin or theme.
+ *
+ * @return string|null Version number.
+ */
+ abstract public function getInstalledVersion();
+
+ /**
+ * Get the full path of the plugin or theme directory (without a trailing slash).
+ *
+ * @return string
+ */
+ abstract public function getAbsoluteDirectoryPath();
+
+ /**
+ * Check whether a regular file exists in the package's directory.
+ *
+ * @param string $relativeFileName File name relative to the package directory.
+ * @return bool
+ */
+ public function fileExists($relativeFileName) {
+ return is_file(
+ $this->getAbsoluteDirectoryPath()
+ . DIRECTORY_SEPARATOR
+ . ltrim($relativeFileName, '/\\')
+ );
+ }
+
+ /* -------------------------------------------------------------------
+ * File header parsing
+ * -------------------------------------------------------------------
+ */
+
+ /**
+ * Parse plugin or theme metadata from the header comment.
+ *
+ * This is basically a simplified version of the get_file_data() function from /wp-includes/functions.php.
+ * It's intended as a utility for subclasses that detect updates by parsing files in a VCS.
+ *
+ * @param string|null $content File contents.
+ * @return string[]
+ */
+ public function getFileHeader($content) {
+ $content = (string)$content;
+
+ //WordPress only looks at the first 8 KiB of the file, so we do the same.
+ $content = substr($content, 0, 8192);
+ //Normalize line endings.
+ $content = str_replace("\r", "\n", $content);
+
+ $headers = $this->getHeaderNames();
+ $results = array();
+ foreach ($headers as $field => $name) {
+ $success = preg_match('/^[ \t\/*#@]*' . preg_quote($name, '/') . ':(.*)$/mi', $content, $matches);
+
+ if ( ($success === 1) && $matches[1] ) {
+ $value = $matches[1];
+ if ( function_exists('_cleanup_header_comment') ) {
+ $value = _cleanup_header_comment($value);
+ }
+ $results[$field] = $value;
+ } else {
+ $results[$field] = '';
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * @return array Format: ['HeaderKey' => 'Header Name']
+ */
+ abstract protected function getHeaderNames();
+
+ /**
+ * Get the value of a specific plugin or theme header.
+ *
+ * @param string $headerName
+ * @return string Either the value of the header, or an empty string if the header doesn't exist.
+ */
+ abstract public function getHeaderValue($headerName);
+
+ }
+endif;
diff --git a/core/updater/Puc/v4p11/Metadata.php b/core/updater/Puc/v4p11/Metadata.php
new file mode 100644
index 0000000..ddba071
--- /dev/null
+++ b/core/updater/Puc/v4p11/Metadata.php
@@ -0,0 +1,132 @@
+validateMetadata($apiResponse);
+ if ( is_wp_error($valid) ){
+ do_action('puc_api_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->getPrefixedFilter('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();
+ }
+
+ /**
+ * @param string $tag
+ * @return string
+ */
+ protected function getPrefixedFilter($tag) {
+ return 'puc_' . $tag;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/OAuthSignature.php b/core/updater/Puc/v4p11/OAuthSignature.php
new file mode 100644
index 0000000..726befa
--- /dev/null
+++ b/core/updater/Puc/v4p11/OAuthSignature.php
@@ -0,0 +1,100 @@
+consumerKey = $consumerKey;
+ $this->consumerSecret = $consumerSecret;
+ }
+
+ /**
+ * Sign a URL using OAuth 1.0.
+ *
+ * @param string $url The URL to be signed. It may contain query parameters.
+ * @param string $method HTTP method such as "GET", "POST" and so on.
+ * @return string The signed URL.
+ */
+ public function sign($url, $method = 'GET') {
+ $parameters = array();
+
+ //Parse query parameters.
+ $query = parse_url($url, PHP_URL_QUERY);
+ if ( !empty($query) ) {
+ parse_str($query, $parsedParams);
+ if ( is_array($parameters) ) {
+ $parameters = $parsedParams;
+ }
+ //Remove the query string from the URL. We'll replace it later.
+ $url = substr($url, 0, strpos($url, '?'));
+ }
+
+ $parameters = array_merge(
+ $parameters,
+ array(
+ 'oauth_consumer_key' => $this->consumerKey,
+ 'oauth_nonce' => $this->nonce(),
+ 'oauth_signature_method' => 'HMAC-SHA1',
+ 'oauth_timestamp' => time(),
+ 'oauth_version' => '1.0',
+ )
+ );
+ unset($parameters['oauth_signature']);
+
+ //Parameters must be sorted alphabetically before signing.
+ ksort($parameters);
+
+ //The most complicated part of the request - generating the signature.
+ //The string to sign contains the HTTP method, the URL path, and all of
+ //our query parameters. Everything is URL encoded. Then we concatenate
+ //them with ampersands into a single string to hash.
+ $encodedVerb = urlencode($method);
+ $encodedUrl = urlencode($url);
+ $encodedParams = urlencode(http_build_query($parameters, '', '&'));
+
+ $stringToSign = $encodedVerb . '&' . $encodedUrl . '&' . $encodedParams;
+
+ //Since we only have one OAuth token (the consumer secret) we only have
+ //to use it as our HMAC key. However, we still have to append an & to it
+ //as if we were using it with additional tokens.
+ $secret = urlencode($this->consumerSecret) . '&';
+
+ //The signature is a hash of the consumer key and the base string. Note
+ //that we have to get the raw output from hash_hmac and base64 encode
+ //the binary data result.
+ $parameters['oauth_signature'] = base64_encode(hash_hmac('sha1', $stringToSign, $secret, true));
+
+ return ($url . '?' . http_build_query($parameters));
+ }
+
+ /**
+ * Generate a random nonce.
+ *
+ * @return string
+ */
+ private function nonce() {
+ $mt = microtime();
+
+ $rand = null;
+ if ( is_callable('random_bytes') ) {
+ try {
+ $rand = random_bytes(16);
+ } catch (Exception $ex) {
+ //Fall back to mt_rand (below).
+ }
+ }
+ if ( $rand === null ) {
+ $rand = mt_rand();
+ }
+
+ return md5($mt . '_' . $rand);
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Plugin/Info.php b/core/updater/Puc/v4p11/Plugin/Info.php
new file mode 100644
index 0000000..f797e0a
--- /dev/null
+++ b/core/updater/Puc/v4p11/Plugin/Info.php
@@ -0,0 +1,132 @@
+sections = (array)$instance->sections;
+ $instance->icons = (array)$instance->icons;
+
+ return $instance;
+ }
+
+ /**
+ * Very, very basic validation.
+ *
+ * @param StdClass $apiResponse
+ * @return bool|WP_Error
+ */
+ protected function validateMetadata($apiResponse) {
+ if (
+ !isset($apiResponse->name, $apiResponse->version)
+ || empty($apiResponse->name)
+ || empty($apiResponse->version)
+ ) {
+ return new WP_Error(
+ 'puc-invalid-metadata',
+ "The plugin metadata file does not contain the required 'name' and/or 'version' keys."
+ );
+ }
+ return true;
+ }
+
+
+ /**
+ * Transform plugin info into the format used by the native WordPress.org API
+ *
+ * @return object
+ */
+ public function toWpFormat(){
+ $info = new stdClass;
+
+ //The custom update API is built so that many fields have the same name and format
+ //as those returned by the native WordPress.org API. These can be assigned directly.
+ $sameFormat = array(
+ 'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',
+ 'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',
+ 'requires_php',
+ );
+ foreach($sameFormat as $field){
+ if ( isset($this->$field) ) {
+ $info->$field = $this->$field;
+ } else {
+ $info->$field = null;
+ }
+ }
+
+ //Other fields need to be renamed and/or transformed.
+ $info->download_link = $this->download_url;
+ $info->author = $this->getFormattedAuthor();
+ $info->sections = array_merge(array('description' => ''), $this->sections);
+
+ if ( !empty($this->banners) ) {
+ //WP expects an array with two keys: "high" and "low". Both are optional.
+ //Docs: https://wordpress.org/plugins/about/faq/#banners
+ $info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners;
+ $info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true));
+ }
+
+ return $info;
+ }
+
+ protected function getFormattedAuthor() {
+ if ( !empty($this->author_homepage) ){
+ /** @noinspection HtmlUnknownTarget */
+ return sprintf('%s', $this->author_homepage, $this->author);
+ }
+ return $this->author;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Plugin/Package.php b/core/updater/Puc/v4p11/Plugin/Package.php
new file mode 100644
index 0000000..23d8ee1
--- /dev/null
+++ b/core/updater/Puc/v4p11/Plugin/Package.php
@@ -0,0 +1,184 @@
+pluginAbsolutePath = $pluginAbsolutePath;
+ $this->pluginFile = plugin_basename($this->pluginAbsolutePath);
+
+ parent::__construct($updateChecker);
+
+ //Clear the version number cache when something - anything - is upgraded or WP clears the update cache.
+ add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
+ add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
+ }
+
+ public function getInstalledVersion() {
+ if ( isset($this->cachedInstalledVersion) ) {
+ return $this->cachedInstalledVersion;
+ }
+
+ $pluginHeader = $this->getPluginHeader();
+ if ( isset($pluginHeader['Version']) ) {
+ $this->cachedInstalledVersion = $pluginHeader['Version'];
+ return $pluginHeader['Version'];
+ } else {
+ //This can happen if the filename points to something that is not a plugin.
+ $this->updateChecker->triggerError(
+ sprintf(
+ "Can't to read the Version header for '%s'. The filename is incorrect or is not a plugin.",
+ $this->updateChecker->pluginFile
+ ),
+ E_USER_WARNING
+ );
+ return null;
+ }
+ }
+
+ /**
+ * Clear the cached plugin version. This method can be set up as a filter (hook) and will
+ * return the filter argument unmodified.
+ *
+ * @param mixed $filterArgument
+ * @return mixed
+ */
+ public function clearCachedVersion($filterArgument = null) {
+ $this->cachedInstalledVersion = null;
+ return $filterArgument;
+ }
+
+ public function getAbsoluteDirectoryPath() {
+ return dirname($this->pluginAbsolutePath);
+ }
+
+ /**
+ * Get the value of a specific plugin or theme header.
+ *
+ * @param string $headerName
+ * @param string $defaultValue
+ * @return string Either the value of the header, or $defaultValue if the header doesn't exist or is empty.
+ */
+ public function getHeaderValue($headerName, $defaultValue = '') {
+ $headers = $this->getPluginHeader();
+ if ( isset($headers[$headerName]) && ($headers[$headerName] !== '') ) {
+ return $headers[$headerName];
+ }
+ return $defaultValue;
+ }
+
+ protected function getHeaderNames() {
+ return array(
+ 'Name' => 'Plugin Name',
+ 'PluginURI' => 'Plugin URI',
+ 'Version' => 'Version',
+ 'Description' => 'Description',
+ 'Author' => 'Author',
+ 'AuthorURI' => 'Author URI',
+ 'TextDomain' => 'Text Domain',
+ 'DomainPath' => 'Domain Path',
+ 'Network' => 'Network',
+
+ //The newest WordPress version that this plugin requires or has been tested with.
+ //We support several different formats for compatibility with other libraries.
+ 'Tested WP' => 'Tested WP',
+ 'Requires WP' => 'Requires WP',
+ 'Tested up to' => 'Tested up to',
+ 'Requires at least' => 'Requires at least',
+ );
+ }
+
+ /**
+ * Get the translated plugin title.
+ *
+ * @return string
+ */
+ public function getPluginTitle() {
+ $title = '';
+ $header = $this->getPluginHeader();
+ if ( $header && !empty($header['Name']) && isset($header['TextDomain']) ) {
+ $title = translate($header['Name'], $header['TextDomain']);
+ }
+ return $title;
+ }
+
+ /**
+ * Get plugin's metadata from its file header.
+ *
+ * @return array
+ */
+ public function getPluginHeader() {
+ if ( !is_file($this->pluginAbsolutePath) ) {
+ //This can happen if the plugin filename is wrong.
+ $this->updateChecker->triggerError(
+ sprintf(
+ "Can't to read the plugin header for '%s'. The file does not exist.",
+ $this->updateChecker->pluginFile
+ ),
+ E_USER_WARNING
+ );
+ return array();
+ }
+
+ if ( !function_exists('get_plugin_data') ) {
+ /** @noinspection PhpIncludeInspection */
+ require_once(ABSPATH . '/wp-admin/includes/plugin.php');
+ }
+ return get_plugin_data($this->pluginAbsolutePath, false, false);
+ }
+
+ public function removeHooks() {
+ remove_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
+ remove_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
+ }
+
+ /**
+ * Check if the plugin file is inside the mu-plugins directory.
+ *
+ * @return bool
+ */
+ public function isMuPlugin() {
+ static $cachedResult = null;
+
+ if ( $cachedResult === null ) {
+ if ( !defined('WPMU_PLUGIN_DIR') || !is_string(WPMU_PLUGIN_DIR) ) {
+ $cachedResult = false;
+ return $cachedResult;
+ }
+
+ //Convert both paths to the canonical form before comparison.
+ $muPluginDir = realpath(WPMU_PLUGIN_DIR);
+ $pluginPath = realpath($this->pluginAbsolutePath);
+ //If realpath() fails, just normalize the syntax instead.
+ if (($muPluginDir === false) || ($pluginPath === false)) {
+ $muPluginDir = Puc_v4p11_Factory::normalizePath(WPMU_PLUGIN_DIR);
+ $pluginPath = Puc_v4p11_Factory::normalizePath($this->pluginAbsolutePath);
+ }
+
+ $cachedResult = (strpos($pluginPath, $muPluginDir) === 0);
+ }
+
+ return $cachedResult;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Plugin/Ui.php b/core/updater/Puc/v4p11/Plugin/Ui.php
new file mode 100644
index 0000000..8aa4b35
--- /dev/null
+++ b/core/updater/Puc/v4p11/Plugin/Ui.php
@@ -0,0 +1,278 @@
+updateChecker = $updateChecker;
+ $this->manualCheckErrorTransient = $this->updateChecker->getUniqueName('manual_check_errors');
+
+ add_action('admin_init', array($this, 'onAdminInit'));
+ }
+
+ public function onAdminInit() {
+ if ( $this->updateChecker->userCanInstallUpdates() ) {
+ $this->handleManualCheck();
+
+ add_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10, 3);
+ add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
+ add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
+ }
+ }
+
+ /**
+ * Add a "View Details" link to the plugin row in the "Plugins" page. By default,
+ * the new link will appear before the "Visit plugin site" link (if present).
+ *
+ * You can change the link text by using the "puc_view_details_link-$slug" filter.
+ * Returning an empty string from the filter will disable the link.
+ *
+ * You can change the position of the link using the
+ * "puc_view_details_link_position-$slug" filter.
+ * Returning 'before' or 'after' will place the link immediately before/after
+ * the "Visit plugin site" link.
+ * 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.
+ * @param string $pluginFile
+ * @param array $pluginData Array of plugin header data.
+ * @return array
+ */
+ public function addViewDetailsLink($pluginMeta, $pluginFile, $pluginData = array()) {
+ if ( $this->isMyPluginFile($pluginFile) && !isset($pluginData['slug']) ) {
+ $linkText = apply_filters($this->updateChecker->getUniqueName('view_details_link'), __('View details'));
+ if ( !empty($linkText) ) {
+ $viewDetailsLinkPosition = 'append';
+
+ //Find the "Visit plugin site" link (if present).
+ $visitPluginSiteLinkIndex = count($pluginMeta) - 1;
+ if ( $pluginData['PluginURI'] ) {
+ $escapedPluginUri = esc_url($pluginData['PluginURI']);
+ foreach ($pluginMeta as $linkIndex => $existingLink) {
+ if ( strpos($existingLink, $escapedPluginUri) !== false ) {
+ $visitPluginSiteLinkIndex = $linkIndex;
+ $viewDetailsLinkPosition = apply_filters(
+ $this->updateChecker->getUniqueName('view_details_link_position'),
+ 'before'
+ );
+ break;
+ }
+ }
+ }
+
+ $viewDetailsLink = sprintf('%s',
+ esc_url(network_admin_url('plugin-install.php?tab=plugin-information&plugin=' . urlencode($this->updateChecker->slug) .
+ '&TB_iframe=true&width=600&height=550')),
+ esc_attr(sprintf(__('More information about %s'), $pluginData['Name'])),
+ esc_attr($pluginData['Name']),
+ $linkText
+ );
+ switch ($viewDetailsLinkPosition) {
+ case 'before':
+ array_splice($pluginMeta, $visitPluginSiteLinkIndex, 0, $viewDetailsLink);
+ break;
+ case 'after':
+ array_splice($pluginMeta, $visitPluginSiteLinkIndex + 1, 0, $viewDetailsLink);
+ break;
+ case 'replace':
+ $pluginMeta[$visitPluginSiteLinkIndex] = $viewDetailsLink;
+ break;
+ case 'append':
+ default:
+ $pluginMeta[] = $viewDetailsLink;
+ break;
+ }
+ }
+ }
+ return $pluginMeta;
+ }
+
+ /**
+ * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
+ * the new link will appear after the "Visit plugin site" link if present, otherwise
+ * after the "View plugin details" link.
+ *
+ * You can change the link text by using the "puc_manual_check_link-$slug" filter.
+ * Returning an empty string from the filter will disable the link.
+ *
+ * @param array $pluginMeta Array of meta links.
+ * @param string $pluginFile
+ * @return array
+ */
+ public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {
+ if ( $this->isMyPluginFile($pluginFile) ) {
+ $linkUrl = wp_nonce_url(
+ add_query_arg(
+ array(
+ 'puc_check_for_updates' => 1,
+ 'puc_slug' => $this->updateChecker->slug,
+ ),
+ self_admin_url('plugins.php')
+ ),
+ 'puc_check_for_updates'
+ );
+
+ $linkText = apply_filters(
+ $this->updateChecker->getUniqueName('manual_check_link'),
+ __('Check for updates', 'plugin-update-checker')
+ );
+ if ( !empty($linkText) ) {
+ /** @noinspection HtmlUnknownTarget */
+ $pluginMeta[] = sprintf('%s', esc_attr($linkUrl), $linkText);
+ }
+ }
+ return $pluginMeta;
+ }
+
+ protected function isMyPluginFile($pluginFile) {
+ return ($pluginFile == $this->updateChecker->pluginFile)
+ || (!empty($this->updateChecker->muPluginFile) && ($pluginFile == $this->updateChecker->muPluginFile));
+ }
+
+ /**
+ * Check for updates when the user clicks the "Check for updates" link.
+ *
+ * @see self::addCheckForUpdatesLink()
+ *
+ * @return void
+ */
+ public function handleManualCheck() {
+ $shouldCheck =
+ isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
+ && $_GET['puc_slug'] == $this->updateChecker->slug
+ && check_admin_referer('puc_check_for_updates');
+
+ if ( $shouldCheck ) {
+ $update = $this->updateChecker->checkForUpdates();
+ $status = ($update === null) ? 'no_update' : 'update_available';
+ $lastRequestApiErrors = $this->updateChecker->getLastRequestApiErrors();
+
+ if ( ($update === null) && !empty($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 ($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, $lastRequestApiErrors, 60);
+ }
+ }
+
+ wp_redirect(add_query_arg(
+ array(
+ 'puc_update_check_result' => $status,
+ 'puc_slug' => $this->updateChecker->slug,
+ ),
+ self_admin_url('plugins.php')
+ ));
+ exit;
+ }
+ }
+
+ /**
+ * Display the results of a manual update check.
+ *
+ * @see self::handleManualCheck()
+ *
+ * You can change the result message by using the "puc_manual_check_message-$slug" filter.
+ */
+ public function displayManualCheckResult() {
+ if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->updateChecker->slug) ) {
+ $status = strval($_GET['puc_update_check_result']);
+ $title = $this->updateChecker->getInstalledPackage()->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(
+ '',
+ $noticeClass,
+ apply_filters($this->updateChecker->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 .= '';
+ $formatString = '- %1$s
%2$s
';
+ } else {
+ $formatString = '%1$s %2$s
';
+ }
+ 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 .= '
';
+ }
+
+ return $output;
+ }
+
+ public function removeHooks() {
+ remove_action('admin_init', array($this, 'onAdminInit'));
+ remove_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10);
+ remove_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10);
+ remove_action('all_admin_notices', array($this, 'displayManualCheckResult'));
+ }
+ }
+endif;
diff --git a/core/updater/Puc/v4p11/Plugin/Update.php b/core/updater/Puc/v4p11/Plugin/Update.php
new file mode 100644
index 0000000..a5ebd4d
--- /dev/null
+++ b/core/updater/Puc/v4p11/Plugin/Update.php
@@ -0,0 +1,112 @@
+copyFields($object, $update);
+ return $update;
+ }
+
+ /**
+ * @return string[]
+ */
+ 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 = parent::toWpFormat();
+
+ $update->id = $this->id;
+ $update->url = $this->homepage;
+ $update->tested = $this->tested;
+ $update->requires_php = $this->requires_php;
+ $update->plugin = $this->filename;
+
+ if ( !empty($this->upgrade_notice) ) {
+ $update->upgrade_notice = $this->upgrade_notice;
+ }
+
+ if ( !empty($this->icons) && is_array($this->icons) ) {
+ //This should be an array with up to 4 keys: 'svg', '1x', '2x' and 'default'.
+ //Docs: https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
+ $icons = array_intersect_key(
+ $this->icons,
+ array('svg' => true, '1x' => true, '2x' => true, 'default' => true)
+ );
+ if ( !empty($icons) ) {
+ $update->icons = $icons;
+
+ //It appears that the 'default' icon isn't used anywhere in WordPress 4.9,
+ //but lets set it just in case a future release needs it.
+ if ( !isset($update->icons['default']) ) {
+ $update->icons['default'] = current($update->icons);
+ }
+ }
+ }
+
+ return $update;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Plugin/UpdateChecker.php b/core/updater/Puc/v4p11/Plugin/UpdateChecker.php
new file mode 100644
index 0000000..b4b39fa
--- /dev/null
+++ b/core/updater/Puc/v4p11/Plugin/UpdateChecker.php
@@ -0,0 +1,414 @@
+pluginAbsolutePath = $pluginFile;
+ $this->pluginFile = plugin_basename($this->pluginAbsolutePath);
+ $this->muPluginFile = $muPluginFile;
+
+ //If no slug is specified, use the name of the main plugin file as the slug.
+ //For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.
+ if ( empty($slug) ){
+ $slug = basename($this->pluginFile, '.php');
+ }
+
+ //Plugin slugs must be unique.
+ $slugCheckFilter = 'puc_is_slug_in_use-' . $slug;
+ $slugUsedBy = apply_filters($slugCheckFilter, false);
+ if ( $slugUsedBy ) {
+ $this->triggerError(sprintf(
+ 'Plugin slug "%s" is already in use by %s. Slugs must be unique.',
+ htmlentities($slug),
+ htmlentities($slugUsedBy)
+ ), E_USER_ERROR);
+ }
+ add_filter($slugCheckFilter, array($this, 'getAbsolutePath'));
+
+ parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
+
+ //Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume
+ //it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).
+ if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {
+ $this->muPluginFile = $this->pluginFile;
+ }
+
+ //To prevent a crash during plugin uninstallation, remove updater hooks when the user removes the plugin.
+ //Details: https://github.com/YahnisElsts/plugin-update-checker/issues/138#issuecomment-335590964
+ add_action('uninstall_' . $this->pluginFile, array($this, 'removeHooks'));
+
+ $this->extraUi = new Puc_v4p11_Plugin_Ui($this);
+ }
+
+ /**
+ * Create an instance of the scheduler.
+ *
+ * @param int $checkPeriod
+ * @return Puc_v4p11_Scheduler
+ */
+ protected function createScheduler($checkPeriod) {
+ $scheduler = new Puc_v4p11_Scheduler($this, $checkPeriod, array('load-plugins.php'));
+ register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
+ return $scheduler;
+ }
+
+ /**
+ * Install the hooks required to run periodic update checks and inject update info
+ * into WP data structures.
+ *
+ * @return void
+ */
+ protected function installHooks(){
+ //Override requests for plugin information
+ add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
+
+ parent::installHooks();
+ }
+
+ /**
+ * Remove update checker hooks.
+ *
+ * The intent is to prevent a fatal error that can happen if the plugin has an uninstall
+ * hook. During uninstallation, WP includes the main plugin file (which creates a PUC instance),
+ * the uninstall hook runs, WP deletes the plugin files and then updates some transients.
+ * If PUC hooks are still around at this time, they could throw an error while trying to
+ * autoload classes from files that no longer exist.
+ *
+ * The "site_transient_{$transient}" filter is the main problem here, but let's also remove
+ * most other PUC hooks to be safe.
+ *
+ * @internal
+ */
+ public function removeHooks() {
+ parent::removeHooks();
+ $this->extraUi->removeHooks();
+ $this->package->removeHooks();
+
+ remove_filter('plugins_api', array($this, 'injectInfo'), 20);
+ }
+
+ /**
+ * Retrieve plugin info from the configured API endpoint.
+ *
+ * @uses wp_remote_get()
+ *
+ * @param array $queryArgs Additional query arguments to append to the request. Optional.
+ * @return Puc_v4p11_Plugin_Info
+ */
+ public function requestInfo($queryArgs = array()) {
+ list($pluginInfo, $result) = $this->requestMetadata('Puc_v4p11_Plugin_Info', 'request_info', $queryArgs);
+
+ if ( $pluginInfo !== null ) {
+ /** @var Puc_v4p11_Plugin_Info $pluginInfo */
+ $pluginInfo->filename = $this->pluginFile;
+ $pluginInfo->slug = $this->slug;
+ }
+
+ $pluginInfo = apply_filters($this->getUniqueName('request_info_result'), $pluginInfo, $result);
+ return $pluginInfo;
+ }
+
+ /**
+ * Retrieve the latest update (if any) from the configured API endpoint.
+ *
+ * @uses PluginUpdateChecker::requestInfo()
+ *
+ * @return Puc_v4p11_Update|null An instance of Plugin_Update, or NULL when no updates are available.
+ */
+ 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'));
+ if ( $pluginInfo === null ){
+ return null;
+ }
+ $update = Puc_v4p11_Plugin_Update::fromPluginInfo($pluginInfo);
+
+ $update = $this->filterUpdateResult($update);
+
+ return $update;
+ }
+
+ /**
+ * Intercept plugins_api() calls that request information about our plugin and
+ * use the configured API endpoint to satisfy them.
+ *
+ * @see plugins_api()
+ *
+ * @param mixed $result
+ * @param string $action
+ * @param array|object $args
+ * @return mixed
+ */
+ public function injectInfo($result, $action = null, $args = null){
+ $relevant = ($action == 'plugin_information') && isset($args->slug) && (
+ ($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))
+ );
+ if ( !$relevant ) {
+ return $result;
+ }
+
+ $pluginInfo = $this->requestInfo();
+ $this->fixSupportedWordpressVersion($pluginInfo);
+
+ $pluginInfo = apply_filters($this->getUniqueName('pre_inject_info'), $pluginInfo);
+ if ( $pluginInfo ) {
+ return $pluginInfo->toWpFormat();
+ }
+
+ return $result;
+ }
+
+ protected function shouldShowUpdates() {
+ //No update notifications for mu-plugins unless explicitly enabled. The MU plugin file
+ //is usually different from the main plugin file so the update wouldn't show up properly anyway.
+ return !$this->isUnknownMuPlugin();
+ }
+
+ /**
+ * @param stdClass|null $updates
+ * @param stdClass $updateToAdd
+ * @return stdClass
+ */
+ protected function addUpdateToList($updates, $updateToAdd) {
+ if ( $this->package->isMuPlugin() ) {
+ //WP does not support automatic update installation for mu-plugins, but we can
+ //still display a notice.
+ $updateToAdd->package = null;
+ }
+ return parent::addUpdateToList($updates, $updateToAdd);
+ }
+
+ /**
+ * @param stdClass|null $updates
+ * @return stdClass|null
+ */
+ protected function removeUpdateFromList($updates) {
+ $updates = parent::removeUpdateFromList($updates);
+ if ( !empty($this->muPluginFile) && isset($updates, $updates->response) ) {
+ unset($updates->response[$this->muPluginFile]);
+ }
+ return $updates;
+ }
+
+ /**
+ * For plugins, the update array is indexed by the plugin filename relative to the "plugins"
+ * directory. Example: "plugin-name/plugin.php".
+ *
+ * @return string
+ */
+ protected function getUpdateListKey() {
+ if ( $this->package->isMuPlugin() ) {
+ return $this->muPluginFile;
+ }
+ return $this->pluginFile;
+ }
+
+ protected function getNoUpdateItemFields() {
+ return array_merge(
+ parent::getNoUpdateItemFields(),
+ array(
+ 'id' => $this->pluginFile,
+ 'slug' => $this->slug,
+ 'plugin' => $this->pluginFile,
+ 'icons' => array(),
+ 'banners' => array(),
+ 'banners_rtl' => array(),
+ 'tested' => '',
+ 'compatibility' => new stdClass(),
+ )
+ );
+ }
+
+ /**
+ * Alias for isBeingUpgraded().
+ *
+ * @deprecated
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
+ * @return bool
+ */
+ public function isPluginBeingUpgraded($upgrader = null) {
+ return $this->isBeingUpgraded($upgrader);
+ }
+
+ /**
+ * Is there an update being installed for this plugin, right now?
+ *
+ * @param WP_Upgrader|null $upgrader
+ * @return bool
+ */
+ public function isBeingUpgraded($upgrader = null) {
+ return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);
+ }
+
+ /**
+ * Get the details of the currently available update, if any.
+ *
+ * If no updates are available, or if the last known update version is below or equal
+ * to the currently installed version, this method will return NULL.
+ *
+ * Uses cached update data. To retrieve update information straight from
+ * the metadata URL, call requestUpdate() instead.
+ *
+ * @return Puc_v4p11_Plugin_Update|null
+ */
+ public function getUpdate() {
+ $update = parent::getUpdate();
+ if ( isset($update) ) {
+ /** @var Puc_v4p11_Plugin_Update $update */
+ $update->filename = $this->pluginFile;
+ }
+ return $update;
+ }
+
+ /**
+ * Get the translated plugin title.
+ *
+ * @deprecated
+ * @return string
+ */
+ public function getPluginTitle() {
+ return $this->package->getPluginTitle();
+ }
+
+ /**
+ * 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.
+ *
+ * @deprecated
+ * @return bool
+ */
+ protected function isMuPlugin() {
+ return $this->package->isMuPlugin();
+ }
+
+ /**
+ * MU plugins are partially supported, but only when we know which file in mu-plugins
+ * corresponds to this plugin.
+ *
+ * @return bool
+ */
+ protected function isUnknownMuPlugin() {
+ return empty($this->muPluginFile) && $this->package->isMuPlugin();
+ }
+
+ /**
+ * Get absolute path to the main plugin file.
+ *
+ * @return string
+ */
+ public function getAbsolutePath() {
+ return $this->pluginAbsolutePath;
+ }
+
+ /**
+ * Register a callback for filtering query arguments.
+ *
+ * The callback function should take one argument - an associative array of query arguments.
+ * It should return a modified array of query arguments.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callable $callback
+ * @return void
+ */
+ public function addQueryArgFilter($callback){
+ $this->addFilter('request_info_query_args', $callback);
+ }
+
+ /**
+ * Register a callback for filtering arguments passed to wp_remote_get().
+ *
+ * The callback function should take one argument - an associative array of arguments -
+ * and return a modified array or arguments. See the WP documentation on wp_remote_get()
+ * for details on what arguments are available and how they work.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callable $callback
+ * @return void
+ */
+ public function addHttpRequestArgFilter($callback) {
+ $this->addFilter('request_info_options', $callback);
+ }
+
+ /**
+ * Register a callback for filtering the plugin info retrieved from the external API.
+ *
+ * The callback function should take two arguments. If the plugin info was retrieved
+ * successfully, the first argument passed will be an instance of PluginInfo. Otherwise,
+ * it will be NULL. The second argument will be the corresponding return value of
+ * wp_remote_get (see WP docs for details).
+ *
+ * The callback function should return a new or modified instance of PluginInfo or NULL.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callable $callback
+ * @return void
+ */
+ public function addResultFilter($callback) {
+ $this->addFilter('request_info_result', $callback, 10, 2);
+ }
+
+ protected function createDebugBarExtension() {
+ return new Puc_v4p11_DebugBar_PluginExtension($this);
+ }
+
+ /**
+ * Create a package instance that represents this plugin or theme.
+ *
+ * @return Puc_v4p11_InstalledPackage
+ */
+ protected function createInstalledPackage() {
+ return new Puc_v4p11_Plugin_Package($this->pluginAbsolutePath, $this);
+ }
+
+ /**
+ * @return Puc_v4p11_Plugin_Package
+ */
+ public function getInstalledPackage() {
+ return $this->package;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Scheduler.php b/core/updater/Puc/v4p11/Scheduler.php
new file mode 100644
index 0000000..dd3c3c3
--- /dev/null
+++ b/core/updater/Puc/v4p11/Scheduler.php
@@ -0,0 +1,266 @@
+updateChecker = $updateChecker;
+ $this->checkPeriod = $checkPeriod;
+
+ //Set up the periodic update checks
+ $this->cronHook = $this->updateChecker->getUniqueName('cron_check_updates');
+ if ( $this->checkPeriod > 0 ){
+
+ //Trigger the check via Cron.
+ //Try to use one of the default schedules if possible as it's less likely to conflict
+ //with other plugins and their custom schedules.
+ $defaultSchedules = array(
+ 1 => 'hourly',
+ 12 => 'twicedaily',
+ 24 => 'daily',
+ );
+ if ( array_key_exists($this->checkPeriod, $defaultSchedules) ) {
+ $scheduleName = $defaultSchedules[$this->checkPeriod];
+ } else {
+ //Use a custom cron schedule.
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';
+ add_filter('cron_schedules', array($this, '_addCustomSchedule'));
+ }
+
+ if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) {
+ //Randomly offset the schedule to help prevent update server traffic spikes. Without this
+ //most checks may happen during times of day when people are most likely to install new plugins.
+ $firstCheckTime = time() - rand(0, max($this->checkPeriod * 3600 - 15 * 60, 1));
+ $firstCheckTime = apply_filters(
+ $this->updateChecker->getUniqueName('first_check_time'),
+ $firstCheckTime
+ );
+ wp_schedule_event($firstCheckTime, $scheduleName, $this->cronHook);
+ }
+ add_action($this->cronHook, array($this, 'maybeCheckForUpdates'));
+
+ //In case Cron is disabled or unreliable, we also manually trigger
+ //the periodic checks while the user is browsing the Dashboard.
+ add_action( 'admin_init', array($this, 'maybeCheckForUpdates') );
+
+ //Like WordPress itself, we check more often on certain pages.
+ /** @see wp_update_plugins */
+ add_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));
+ //"load-update.php" and "load-plugins.php" or "load-themes.php".
+ $this->hourlyCheckHooks = array_merge($this->hourlyCheckHooks, $hourlyHooks);
+ foreach($this->hourlyCheckHooks as $hook) {
+ add_action($hook, array($this, 'maybeCheckForUpdates'));
+ }
+ //This hook fires after a bulk update is complete.
+ add_action('upgrader_process_complete', array($this, 'upgraderProcessComplete'), 11, 2);
+
+ } else {
+ //Periodic checks are disabled.
+ wp_clear_scheduled_hook($this->cronHook);
+ }
+ }
+
+ /**
+ * Runs upon the WP action upgrader_process_complete.
+ *
+ * We look at the parameters to decide whether to call maybeCheckForUpdates() or not.
+ * We also check if the update checker has been removed by the update.
+ *
+ * @param WP_Upgrader $upgrader WP_Upgrader instance
+ * @param array $upgradeInfo extra information about the upgrade
+ */
+ public function upgraderProcessComplete(
+ /** @noinspection PhpUnusedParameterInspection */
+ $upgrader, $upgradeInfo
+ ) {
+ //Cancel all further actions if the current version of PUC has been deleted or overwritten
+ //by a different version during the upgrade. If we try to do anything more in that situation,
+ //we could trigger a fatal error by trying to autoload a deleted class.
+ clearstatcache();
+ if ( !file_exists(__FILE__) ) {
+ $this->removeHooks();
+ $this->updateChecker->removeHooks();
+ return;
+ }
+
+ //Sanity check and limitation to relevant types.
+ if (
+ !is_array($upgradeInfo) || !isset($upgradeInfo['type'], $upgradeInfo['action'])
+ || 'update' !== $upgradeInfo['action'] || !in_array($upgradeInfo['type'], array('plugin', 'theme'))
+ ) {
+ return;
+ }
+
+ //Filter out notifications of upgrades that should have no bearing upon whether or not our
+ //current info is up-to-date.
+ if ( is_a($this->updateChecker, 'Puc_v4p11_Theme_UpdateChecker') ) {
+ if ( 'theme' !== $upgradeInfo['type'] || !isset($upgradeInfo['themes']) ) {
+ return;
+ }
+
+ //Letting too many things going through for checks is not a real problem, so we compare widely.
+ if ( !in_array(
+ strtolower($this->updateChecker->directoryName),
+ array_map('strtolower', $upgradeInfo['themes'])
+ ) ) {
+ return;
+ }
+ }
+
+ if ( is_a($this->updateChecker, 'Puc_v4p11_Plugin_UpdateChecker') ) {
+ if ( 'plugin' !== $upgradeInfo['type'] || !isset($upgradeInfo['plugins']) ) {
+ return;
+ }
+
+ //Themes pass in directory names in the information array, but plugins use the relative plugin path.
+ if ( !in_array(
+ strtolower($this->updateChecker->directoryName),
+ array_map('dirname', array_map('strtolower', $upgradeInfo['plugins']))
+ ) ) {
+ return;
+ }
+ }
+
+ $this->maybeCheckForUpdates();
+ }
+
+ /**
+ * Check for updates if the configured check interval has already elapsed.
+ * Will use a shorter check interval on certain admin pages like "Dashboard -> Updates" or when doing cron.
+ *
+ * You can override the default behaviour by using the "puc_check_now-$slug" filter.
+ * The filter callback will be passed three parameters:
+ * - Current decision. TRUE = check updates now, FALSE = don't check now.
+ * - Last check time as a Unix timestamp.
+ * - Configured check period in hours.
+ * Return TRUE to check for updates immediately, or FALSE to cancel.
+ *
+ * This method is declared public because it's a hook callback. Calling it directly is not recommended.
+ */
+ public function maybeCheckForUpdates() {
+ if ( empty($this->checkPeriod) ){
+ return;
+ }
+
+ $state = $this->updateChecker->getUpdateState();
+ $shouldCheck = ($state->timeSinceLastCheck() >= $this->getEffectiveCheckPeriod());
+
+ //Let plugin authors substitute their own algorithm.
+ $shouldCheck = apply_filters(
+ $this->updateChecker->getUniqueName('check_now'),
+ $shouldCheck,
+ $state->getLastCheck(),
+ $this->checkPeriod
+ );
+
+ if ( $shouldCheck ) {
+ $this->updateChecker->checkForUpdates();
+ }
+ }
+
+ /**
+ * Calculate the actual check period based on the current status and environment.
+ *
+ * @return int Check period in seconds.
+ */
+ protected function getEffectiveCheckPeriod() {
+ $currentFilter = current_filter();
+ if ( in_array($currentFilter, array('load-update-core.php', 'upgrader_process_complete')) ) {
+ //Check more often when the user visits "Dashboard -> Updates" or does a bulk update.
+ $period = 60;
+ } else if ( in_array($currentFilter, $this->hourlyCheckHooks) ) {
+ //Also check more often on /wp-admin/update.php and the "Plugins" or "Themes" page.
+ $period = 3600;
+ } else if ( $this->throttleRedundantChecks && ($this->updateChecker->getUpdate() !== null) ) {
+ //Check less frequently if it's already known that an update is available.
+ $period = $this->throttledCheckPeriod * 3600;
+ } else if ( defined('DOING_CRON') && constant('DOING_CRON') ) {
+ //WordPress cron schedules are not exact, so lets do an update check even
+ //if slightly less than $checkPeriod hours have elapsed since the last check.
+ $cronFuzziness = 20 * 60;
+ $period = $this->checkPeriod * 3600 - $cronFuzziness;
+ } else {
+ $period = $this->checkPeriod * 3600;
+ }
+
+ return $period;
+ }
+
+ /**
+ * Add our custom schedule to the array of Cron schedules used by WP.
+ *
+ * @param array $schedules
+ * @return array
+ */
+ public function _addCustomSchedule($schedules) {
+ if ( $this->checkPeriod && ($this->checkPeriod > 0) ){
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';
+ $schedules[$scheduleName] = array(
+ 'interval' => $this->checkPeriod * 3600,
+ 'display' => sprintf('Every %d hours', $this->checkPeriod),
+ );
+ }
+ return $schedules;
+ }
+
+ /**
+ * Remove the scheduled cron event that the library uses to check for updates.
+ *
+ * @return void
+ */
+ public function removeUpdaterCron() {
+ wp_clear_scheduled_hook($this->cronHook);
+ }
+
+ /**
+ * Get the name of the update checker's WP-cron hook. Mostly useful for debugging.
+ *
+ * @return string
+ */
+ public function getCronHookName() {
+ return $this->cronHook;
+ }
+
+ /**
+ * Remove most hooks added by the scheduler.
+ */
+ public function removeHooks() {
+ remove_filter('cron_schedules', array($this, '_addCustomSchedule'));
+ remove_action('admin_init', array($this, 'maybeCheckForUpdates'));
+ remove_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));
+
+ if ( $this->cronHook !== null ) {
+ remove_action($this->cronHook, array($this, 'maybeCheckForUpdates'));
+ }
+ if ( !empty($this->hourlyCheckHooks) ) {
+ foreach ($this->hourlyCheckHooks as $hook) {
+ remove_action($hook, array($this, 'maybeCheckForUpdates'));
+ }
+ }
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/StateStore.php b/core/updater/Puc/v4p11/StateStore.php
new file mode 100644
index 0000000..01abcc9
--- /dev/null
+++ b/core/updater/Puc/v4p11/StateStore.php
@@ -0,0 +1,207 @@
+optionName = $optionName;
+ }
+
+ /**
+ * Get time elapsed since the last update check.
+ *
+ * If there are no recorded update checks, this method returns a large arbitrary number
+ * (i.e. time since the Unix epoch).
+ *
+ * @return int Elapsed time in seconds.
+ */
+ public function timeSinceLastCheck() {
+ $this->lazyLoad();
+ return time() - $this->lastCheck;
+ }
+
+ /**
+ * @return int
+ */
+ public function getLastCheck() {
+ $this->lazyLoad();
+ return $this->lastCheck;
+ }
+
+ /**
+ * Set the time of the last update check to the current timestamp.
+ *
+ * @return $this
+ */
+ public function setLastCheckToNow() {
+ $this->lazyLoad();
+ $this->lastCheck = time();
+ return $this;
+ }
+
+ /**
+ * @return null|Puc_v4p11_Update
+ */
+ public function getUpdate() {
+ $this->lazyLoad();
+ return $this->update;
+ }
+
+ /**
+ * @param Puc_v4p11_Update|null $update
+ * @return $this
+ */
+ public function setUpdate(Puc_v4p11_Update $update = null) {
+ $this->lazyLoad();
+ $this->update = $update;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCheckedVersion() {
+ $this->lazyLoad();
+ return $this->checkedVersion;
+ }
+
+ /**
+ * @param string $version
+ * @return $this
+ */
+ public function setCheckedVersion($version) {
+ $this->lazyLoad();
+ $this->checkedVersion = strval($version);
+ return $this;
+ }
+
+ /**
+ * Get translation updates.
+ *
+ * @return array
+ */
+ public function getTranslations() {
+ $this->lazyLoad();
+ if ( isset($this->update, $this->update->translations) ) {
+ return $this->update->translations;
+ }
+ return array();
+ }
+
+ /**
+ * Set translation updates.
+ *
+ * @param array $translationUpdates
+ */
+ public function setTranslations($translationUpdates) {
+ $this->lazyLoad();
+ if ( isset($this->update) ) {
+ $this->update->translations = $translationUpdates;
+ $this->save();
+ }
+ }
+
+ public function save() {
+ $state = new stdClass();
+
+ $state->lastCheck = $this->lastCheck;
+ $state->checkedVersion = $this->checkedVersion;
+
+ if ( isset($this->update)) {
+ $state->update = $this->update->toStdClass();
+
+ $updateClass = get_class($this->update);
+ $state->updateClass = $updateClass;
+ $prefix = $this->getLibPrefix();
+ if ( Puc_v4p11_Utils::startsWith($updateClass, $prefix) ) {
+ $state->updateBaseClass = substr($updateClass, strlen($prefix));
+ }
+ }
+
+ update_site_option($this->optionName, $state);
+ $this->isLoaded = true;
+ }
+
+ /**
+ * @return $this
+ */
+ public function lazyLoad() {
+ if ( !$this->isLoaded ) {
+ $this->load();
+ }
+ return $this;
+ }
+
+ protected function load() {
+ $this->isLoaded = true;
+
+ $state = get_site_option($this->optionName, null);
+
+ if ( !is_object($state) ) {
+ $this->lastCheck = 0;
+ $this->checkedVersion = '';
+ $this->update = null;
+ return;
+ }
+
+ $this->lastCheck = intval(Puc_v4p11_Utils::get($state, 'lastCheck', 0));
+ $this->checkedVersion = Puc_v4p11_Utils::get($state, 'checkedVersion', '');
+ $this->update = null;
+
+ if ( isset($state->update) ) {
+ //This mess is due to the fact that the want the update class from this version
+ //of the library, not the version that saved the update.
+
+ $updateClass = null;
+ if ( isset($state->updateBaseClass) ) {
+ $updateClass = $this->getLibPrefix() . $state->updateBaseClass;
+ } else if ( isset($state->updateClass) && class_exists($state->updateClass) ) {
+ $updateClass = $state->updateClass;
+ }
+
+ if ( $updateClass !== null ) {
+ $this->update = call_user_func(array($updateClass, 'fromObject'), $state->update);
+ }
+ }
+ }
+
+ public function delete() {
+ delete_site_option($this->optionName);
+
+ $this->lastCheck = 0;
+ $this->checkedVersion = '';
+ $this->update = null;
+ }
+
+ private function getLibPrefix() {
+ $parts = explode('_', __CLASS__, 3);
+ return $parts[0] . '_' . $parts[1] . '_';
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Theme/Package.php b/core/updater/Puc/v4p11/Theme/Package.php
new file mode 100644
index 0000000..3cfbce9
--- /dev/null
+++ b/core/updater/Puc/v4p11/Theme/Package.php
@@ -0,0 +1,65 @@
+stylesheet = $stylesheet;
+ $this->theme = wp_get_theme($this->stylesheet);
+
+ parent::__construct($updateChecker);
+ }
+
+ public function getInstalledVersion() {
+ return $this->theme->get('Version');
+ }
+
+ public function getAbsoluteDirectoryPath() {
+ if ( method_exists($this->theme, 'get_stylesheet_directory') ) {
+ return $this->theme->get_stylesheet_directory(); //Available since WP 3.4.
+ }
+ return get_theme_root($this->stylesheet) . '/' . $this->stylesheet;
+ }
+
+ /**
+ * Get the value of a specific plugin or theme header.
+ *
+ * @param string $headerName
+ * @param string $defaultValue
+ * @return string Either the value of the header, or $defaultValue if the header doesn't exist or is empty.
+ */
+ public function getHeaderValue($headerName, $defaultValue = '') {
+ $value = $this->theme->get($headerName);
+ if ( ($headerName === false) || ($headerName === '') ) {
+ return $defaultValue;
+ }
+ return $value;
+ }
+
+ protected function getHeaderNames() {
+ return array(
+ 'Name' => 'Theme Name',
+ 'ThemeURI' => 'Theme URI',
+ 'Description' => 'Description',
+ 'Author' => 'Author',
+ 'AuthorURI' => 'Author URI',
+ 'Version' => 'Version',
+ 'Template' => 'Template',
+ 'Status' => 'Status',
+ 'Tags' => 'Tags',
+ 'TextDomain' => 'Text Domain',
+ 'DomainPath' => 'Domain Path',
+ );
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Theme/Update.php b/core/updater/Puc/v4p11/Theme/Update.php
new file mode 100644
index 0000000..9fc46f8
--- /dev/null
+++ b/core/updater/Puc/v4p11/Theme/Update.php
@@ -0,0 +1,84 @@
+ $this->slug,
+ 'new_version' => $this->version,
+ 'url' => $this->details_url,
+ );
+
+ if ( !empty($this->download_url) ) {
+ $update['package'] = $this->download_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;
+ }
+
+ /**
+ * Create a new instance by copying the necessary fields from another object.
+ *
+ * @param StdClass|Puc_v4p11_Theme_Update $object The source object.
+ * @return Puc_v4p11_Theme_Update The new copy.
+ */
+ public static function fromObject($object) {
+ $update = new self();
+ $update->copyFields($object, $update);
+ return $update;
+ }
+
+ /**
+ * 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 getPrefixedFilter($tag) {
+ return parent::getPrefixedFilter($tag) . '_theme';
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Theme/UpdateChecker.php b/core/updater/Puc/v4p11/Theme/UpdateChecker.php
new file mode 100644
index 0000000..091445e
--- /dev/null
+++ b/core/updater/Puc/v4p11/Theme/UpdateChecker.php
@@ -0,0 +1,152 @@
+stylesheet = $stylesheet;
+
+ parent::__construct(
+ $metadataUrl,
+ $stylesheet,
+ $customSlug ? $customSlug : $stylesheet,
+ $checkPeriod,
+ $optionName
+ );
+ }
+
+ /**
+ * For themes, the update array is indexed by theme directory name.
+ *
+ * @return string
+ */
+ protected function getUpdateListKey() {
+ return $this->directoryName;
+ }
+
+ /**
+ * Retrieve the latest update (if any) from the configured API endpoint.
+ *
+ * @return Puc_v4p11_Update|null An instance of Update, or NULL when no updates are available.
+ */
+ public function requestUpdate() {
+ list($themeUpdate, $result) = $this->requestMetadata('Puc_v4p11_Theme_Update', 'request_update');
+
+ if ( $themeUpdate !== null ) {
+ /** @var Puc_v4p11_Theme_Update $themeUpdate */
+ $themeUpdate->slug = $this->slug;
+ }
+
+ $themeUpdate = $this->filterUpdateResult($themeUpdate, $result);
+ return $themeUpdate;
+ }
+
+ protected function getNoUpdateItemFields() {
+ return array_merge(
+ parent::getNoUpdateItemFields(),
+ array(
+ 'theme' => $this->directoryName,
+ 'requires' => '',
+ )
+ );
+ }
+
+ public function userCanInstallUpdates() {
+ return current_user_can('update_themes');
+ }
+
+ /**
+ * Create an instance of the scheduler.
+ *
+ * @param int $checkPeriod
+ * @return Puc_v4p11_Scheduler
+ */
+ protected function createScheduler($checkPeriod) {
+ return new Puc_v4p11_Scheduler($this, $checkPeriod, array('load-themes.php'));
+ }
+
+ /**
+ * Is there an update being installed right now for this theme?
+ *
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
+ * @return bool
+ */
+ public function isBeingUpgraded($upgrader = null) {
+ return $this->upgraderStatus->isThemeBeingUpgraded($this->stylesheet, $upgrader);
+ }
+
+ protected function createDebugBarExtension() {
+ return new Puc_v4p11_DebugBar_Extension($this, 'Puc_v4p11_DebugBar_ThemePanel');
+ }
+
+ /**
+ * Register a callback for filtering query arguments.
+ *
+ * The callback function should take one argument - an associative array of query arguments.
+ * It should return a modified array of query arguments.
+ *
+ * @param callable $callback
+ * @return void
+ */
+ public function addQueryArgFilter($callback){
+ $this->addFilter('request_update_query_args', $callback);
+ }
+
+ /**
+ * Register a callback for filtering arguments passed to wp_remote_get().
+ *
+ * The callback function should take one argument - an associative array of arguments -
+ * and return a modified array or arguments. See the WP documentation on wp_remote_get()
+ * for details on what arguments are available and how they work.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callable $callback
+ * @return void
+ */
+ public function addHttpRequestArgFilter($callback) {
+ $this->addFilter('request_update_options', $callback);
+ }
+
+ /**
+ * Register a callback for filtering theme updates retrieved from the external API.
+ *
+ * The callback function should take two arguments. If the theme update was retrieved
+ * successfully, the first argument passed will be an instance of Theme_Update. Otherwise,
+ * it will be NULL. The second argument will be the corresponding return value of
+ * wp_remote_get (see WP docs for details).
+ *
+ * The callback function should return a new or modified instance of Theme_Update or NULL.
+ *
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
+ *
+ * @param callable $callback
+ * @return void
+ */
+ public function addResultFilter($callback) {
+ $this->addFilter('request_update_result', $callback, 10, 2);
+ }
+
+ /**
+ * Create a package instance that represents this plugin or theme.
+ *
+ * @return Puc_v4p11_InstalledPackage
+ */
+ protected function createInstalledPackage() {
+ return new Puc_v4p11_Theme_Package($this->stylesheet, $this);
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Update.php b/core/updater/Puc/v4p11/Update.php
new file mode 100644
index 0000000..82c048a
--- /dev/null
+++ b/core/updater/Puc/v4p11/Update.php
@@ -0,0 +1,34 @@
+slug = $this->slug;
+ $update->new_version = $this->version;
+ $update->package = $this->download_url;
+
+ return $update;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/UpdateChecker.php b/core/updater/Puc/v4p11/UpdateChecker.php
new file mode 100644
index 0000000..56fe8f0
--- /dev/null
+++ b/core/updater/Puc/v4p11/UpdateChecker.php
@@ -0,0 +1,997 @@
+debugMode = (bool)(constant('WP_DEBUG'));
+ $this->metadataUrl = $metadataUrl;
+ $this->directoryName = $directoryName;
+ $this->slug = !empty($slug) ? $slug : $this->directoryName;
+
+ $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->filterSuffix === '' ) {
+ $this->optionName = 'external_updates-' . $this->slug;
+ } else {
+ $this->optionName = $this->getUniqueName('external_updates');
+ }
+ }
+
+ $this->package = $this->createInstalledPackage();
+ $this->scheduler = $this->createScheduler($checkPeriod);
+ $this->upgraderStatus = new Puc_v4p11_UpgraderStatus();
+ $this->updateState = new Puc_v4p11_StateStore($this->optionName);
+
+ if ( did_action('init') ) {
+ $this->loadTextDomain();
+ } else {
+ add_action('init', array($this, 'loadTextDomain'));
+ }
+
+ $this->installHooks();
+ }
+
+ /**
+ * @internal
+ */
+ public function loadTextDomain() {
+ //We're not using load_plugin_textdomain() or its siblings because figuring out where
+ //the library is located (plugin, mu-plugin, theme, custom wp-content paths) is messy.
+ $domain = 'plugin-update-checker';
+ $locale = apply_filters(
+ 'plugin_locale',
+ (is_admin() && function_exists('get_user_locale')) ? get_user_locale() : get_locale(),
+ $domain
+ );
+
+ $moFile = $domain . '-' . $locale . '.mo';
+ $path = realpath(dirname(__FILE__) . '/../../languages');
+
+ if ($path && file_exists($path)) {
+ load_textdomain($domain, $path . '/' . $moFile);
+ }
+ }
+
+ protected function installHooks() {
+ //Insert our update info into the update array maintained by WP.
+ add_filter('site_transient_' . $this->updateTransient, array($this,'injectUpdate'));
+
+ //Insert translation updates into the update list.
+ add_filter('site_transient_' . $this->updateTransient, array($this, 'injectTranslationUpdates'));
+
+ //Clear translation updates when WP clears the update cache.
+ //This needs to be done directly because the library doesn't actually remove obsolete plugin updates,
+ //it just hides them (see getUpdate()). We can't do that with translations - too much disk I/O.
+ add_action(
+ 'delete_site_transient_' . $this->updateTransient,
+ array($this, 'clearCachedTranslationUpdates')
+ );
+
+ //Rename the update directory to be the same as the existing directory.
+ if ( $this->directoryName !== '.' ) {
+ add_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10, 3);
+ }
+
+ //Allow HTTP requests to the metadata URL even if it's on a local host.
+ add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
+
+ //DebugBar integration.
+ if ( did_action('plugins_loaded') ) {
+ $this->maybeInitDebugBar();
+ } else {
+ add_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
+ }
+ }
+
+ /**
+ * Remove hooks that were added by this update checker instance.
+ */
+ public function removeHooks() {
+ remove_filter('site_transient_' . $this->updateTransient, array($this,'injectUpdate'));
+ remove_filter('site_transient_' . $this->updateTransient, array($this, 'injectTranslationUpdates'));
+ remove_action(
+ 'delete_site_transient_' . $this->updateTransient,
+ array($this, 'clearCachedTranslationUpdates')
+ );
+
+ remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10);
+ remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10);
+ remove_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
+
+ remove_action('init', array($this, 'loadTextDomain'));
+
+ if ( $this->scheduler ) {
+ $this->scheduler->removeHooks();
+ }
+
+ if ( $this->debugBarExtension ) {
+ $this->debugBarExtension->removeHooks();
+ }
+ }
+
+ /**
+ * Check if the current user has the required permissions to install updates.
+ *
+ * @return bool
+ */
+ abstract public function userCanInstallUpdates();
+
+ /**
+ * Explicitly allow HTTP requests to the metadata URL.
+ *
+ * WordPress has a security feature where the HTTP API will reject all requests that are sent to
+ * another site hosted on the same server as the current site (IP match), a local host, or a local
+ * IP, unless the host exactly matches the current site.
+ *
+ * This feature is opt-in (at least in WP 4.4). Apparently some people enable it.
+ *
+ * That can be a problem when you're developing your plugin and you decide to host the update information
+ * on the same server as your test site. Update requests will mysteriously fail.
+ *
+ * We fix that by adding an exception for the metadata host.
+ *
+ * @param bool $allow
+ * @param string $host
+ * @return bool
+ */
+ public function allowMetadataHost($allow, $host) {
+ if ( $this->cachedMetadataHost === 0 ) {
+ $this->cachedMetadataHost = parse_url($this->metadataUrl, PHP_URL_HOST);
+ }
+
+ if ( is_string($this->cachedMetadataHost) && (strtolower($host) === strtolower($this->cachedMetadataHost)) ) {
+ return true;
+ }
+ return $allow;
+ }
+
+ /**
+ * Create a package instance that represents this plugin or theme.
+ *
+ * @return Puc_v4p11_InstalledPackage
+ */
+ abstract protected function createInstalledPackage();
+
+ /**
+ * @return Puc_v4p11_InstalledPackage
+ */
+ public function getInstalledPackage() {
+ return $this->package;
+ }
+
+ /**
+ * Create an instance of the scheduler.
+ *
+ * This is implemented as a method to make it possible for plugins to subclass the update checker
+ * and substitute their own scheduler.
+ *
+ * @param int $checkPeriod
+ * @return Puc_v4p11_Scheduler
+ */
+ abstract protected function createScheduler($checkPeriod);
+
+ /**
+ * Check for updates. The results are stored in the DB option specified in $optionName.
+ *
+ * @return Puc_v4p11_Update|null
+ */
+ public function checkForUpdates() {
+ $installedVersion = $this->getInstalledVersion();
+ //Fail silently if we can't find the plugin/theme or read its header.
+ if ( $installedVersion === null ) {
+ $this->triggerError(
+ sprintf('Skipping update check for %s - installed version unknown.', $this->slug),
+ E_USER_WARNING
+ );
+ 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)
+ ->save(); //Save before checking in case something goes wrong
+
+ $state->setUpdate($this->requestUpdate());
+ $state->save();
+
+ //Stop collecting API errors.
+ remove_action('puc_api_error', array($this, 'collectApiErrors'), 10);
+
+ return $this->getUpdate();
+ }
+
+ /**
+ * Load the update checker state from the DB.
+ *
+ * @return Puc_v4p11_StateStore
+ */
+ public function getUpdateState() {
+ return $this->updateState->lazyLoad();
+ }
+
+ /**
+ * Reset update checker state - i.e. last check time, cached update data and so on.
+ *
+ * Call this when your plugin is being uninstalled, or if you want to
+ * clear the update cache.
+ */
+ public function resetUpdateState() {
+ $this->updateState->delete();
+ }
+
+ /**
+ * Get the details of the currently available update, if any.
+ *
+ * If no updates are available, or if the last known update version is below or equal
+ * to the currently installed version, this method will return NULL.
+ *
+ * Uses cached update data. To retrieve update information straight from
+ * the metadata URL, call requestUpdate() instead.
+ *
+ * @return Puc_v4p11_Update|null
+ */
+ public function getUpdate() {
+ $update = $this->updateState->getUpdate();
+
+ //Is there an update available?
+ if ( isset($update) ) {
+ //Check if the update is actually newer than the currently installed version.
+ $installedVersion = $this->getInstalledVersion();
+ if ( ($installedVersion !== null) && version_compare($update->version, $installedVersion, '>') ){
+ return $update;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the latest update (if any) from the configured API endpoint.
+ *
+ * Subclasses should run the update through filterUpdateResult before returning it.
+ *
+ * @return Puc_v4p11_Update An instance of Update, or NULL when no updates are available.
+ */
+ abstract public function requestUpdate();
+
+ /**
+ * Filter the result of a requestUpdate() call.
+ *
+ * @param Puc_v4p11_Update|null $update
+ * @param array|WP_Error|null $httpResult The value returned by wp_remote_get(), if any.
+ * @return Puc_v4p11_Update
+ */
+ protected function filterUpdateResult($update, $httpResult = null) {
+ //Let plugins/themes modify the update.
+ $update = apply_filters($this->getUniqueName('request_update_result'), $update, $httpResult);
+
+ $this->fixSupportedWordpressVersion($update);
+
+ if ( isset($update, $update->translations) ) {
+ //Keep only those translation updates that apply to this site.
+ $update->translations = $this->filterApplicableTranslations($update->translations);
+ }
+
+ return $update;
+ }
+
+ /**
+ * The "Tested up to" field in the plugin metadata is supposed to be in the form of "major.minor",
+ * while WordPress core's list_plugin_updates() expects the $update->tested field to be an exact
+ * version, e.g. "major.minor.patch", to say it's compatible. In other case it shows
+ * "Compatibility: Unknown".
+ * The function mimics how wordpress.org API crafts the "tested" field out of "Tested up to".
+ *
+ * @param Puc_v4p11_Metadata|null $update
+ */
+ protected function fixSupportedWordpressVersion(Puc_v4p11_Metadata $update = null) {
+ if ( !isset($update->tested) || !preg_match('/^\d++\.\d++$/', $update->tested) ) {
+ return;
+ }
+
+ $actualWpVersions = array();
+
+ $wpVersion = $GLOBALS['wp_version'];
+
+ if ( function_exists('get_core_updates') ) {
+ $coreUpdates = get_core_updates();
+ if ( is_array($coreUpdates) ) {
+ foreach ($coreUpdates as $coreUpdate) {
+ if ( isset($coreUpdate->current) ) {
+ $actualWpVersions[] = $coreUpdate->current;
+ }
+ }
+ }
+ }
+
+ $actualWpVersions[] = $wpVersion;
+
+ $actualWpPatchNumber = null;
+ foreach ($actualWpVersions as $version) {
+ if ( preg_match('/^(?P\d++\.\d++)(?:\.(?P\d++))?/', $version, $versionParts) ) {
+ if ( $versionParts['majorMinor'] === $update->tested ) {
+ $patch = isset($versionParts['patch']) ? intval($versionParts['patch']) : 0;
+ if ( $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;
diff --git a/core/updater/Puc/v4p11/UpgraderStatus.php b/core/updater/Puc/v4p11/UpgraderStatus.php
new file mode 100644
index 0000000..8c0006e
--- /dev/null
+++ b/core/updater/Puc/v4p11/UpgraderStatus.php
@@ -0,0 +1,199 @@
+isBeingUpgraded('plugin', $pluginFile, $upgrader);
+ }
+
+ /**
+ * Is there an update being installed for a specific theme?
+ *
+ * @param string $stylesheet Theme directory name.
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
+ * @return bool
+ */
+ public function isThemeBeingUpgraded($stylesheet, $upgrader = null) {
+ return $this->isBeingUpgraded('theme', $stylesheet, $upgrader);
+ }
+
+ /**
+ * Check if a specific theme or plugin is being upgraded.
+ *
+ * @param string $type
+ * @param string $id
+ * @param Plugin_Upgrader|WP_Upgrader|null $upgrader
+ * @return bool
+ */
+ protected function isBeingUpgraded($type, $id, $upgrader = null) {
+ if ( isset($upgrader) ) {
+ list($currentType, $currentId) = $this->getThingBeingUpgradedBy($upgrader);
+ if ( $currentType !== null ) {
+ $this->currentType = $currentType;
+ $this->currentId = $currentId;
+ }
+ }
+ return ($this->currentType === $type) && ($this->currentId === $id);
+ }
+
+ /**
+ * Figure out which theme or plugin is being upgraded by a WP_Upgrader instance.
+ *
+ * Returns an array with two items. The first item is the type of the thing that's being
+ * upgraded: "plugin" or "theme". The second item is either the plugin basename or
+ * the theme directory name. If we can't determine what the upgrader is doing, both items
+ * will be NULL.
+ *
+ * Examples:
+ * ['plugin', 'plugin-dir-name/plugin.php']
+ * ['theme', 'theme-dir-name']
+ *
+ * @param Plugin_Upgrader|WP_Upgrader $upgrader
+ * @return array
+ */
+ private function getThingBeingUpgradedBy($upgrader) {
+ if ( !isset($upgrader, $upgrader->skin) ) {
+ return array(null, null);
+ }
+
+ //Figure out which plugin or theme is being upgraded.
+ $pluginFile = null;
+ $themeDirectoryName = null;
+
+ $skin = $upgrader->skin;
+ if ( isset($skin->theme_info) && ($skin->theme_info instanceof WP_Theme) ) {
+ $themeDirectoryName = $skin->theme_info->get_stylesheet();
+ } elseif ( $skin instanceof Plugin_Upgrader_Skin ) {
+ if ( isset($skin->plugin) && is_string($skin->plugin) && ($skin->plugin !== '') ) {
+ $pluginFile = $skin->plugin;
+ }
+ } elseif ( $skin instanceof Theme_Upgrader_Skin ) {
+ if ( isset($skin->theme) && is_string($skin->theme) && ($skin->theme !== '') ) {
+ $themeDirectoryName = $skin->theme;
+ }
+ } elseif ( isset($skin->plugin_info) && is_array($skin->plugin_info) ) {
+ //This case is tricky because Bulk_Plugin_Upgrader_Skin (etc) doesn't actually store the plugin
+ //filename anywhere. Instead, it has the plugin headers in $plugin_info. So the best we can
+ //do is compare those headers to the headers of installed plugins.
+ $pluginFile = $this->identifyPluginByHeaders($skin->plugin_info);
+ }
+
+ if ( $pluginFile !== null ) {
+ return array('plugin', $pluginFile);
+ } elseif ( $themeDirectoryName !== null ) {
+ return array('theme', $themeDirectoryName);
+ }
+ return array(null, null);
+ }
+
+ /**
+ * Identify an installed plugin based on its headers.
+ *
+ * @param array $searchHeaders The plugin file header to look for.
+ * @return string|null Plugin basename ("foo/bar.php"), or NULL if we can't identify the plugin.
+ */
+ private function identifyPluginByHeaders($searchHeaders) {
+ if ( !function_exists('get_plugins') ){
+ /** @noinspection PhpIncludeInspection */
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
+ }
+
+ $installedPlugins = get_plugins();
+ $matches = array();
+ foreach($installedPlugins as $pluginBasename => $headers) {
+ $diff1 = array_diff_assoc($headers, $searchHeaders);
+ $diff2 = array_diff_assoc($searchHeaders, $headers);
+ if ( empty($diff1) && empty($diff2) ) {
+ $matches[] = $pluginBasename;
+ }
+ }
+
+ //It's possible (though very unlikely) that there could be two plugins with identical
+ //headers. In that case, we can't unambiguously identify the plugin that's being upgraded.
+ if ( count($matches) !== 1 ) {
+ return null;
+ }
+
+ return reset($matches);
+ }
+
+ /**
+ * @access private
+ *
+ * @param mixed $input
+ * @param array $hookExtra
+ * @return mixed Returns $input unaltered.
+ */
+ public function setUpgradedThing($input, $hookExtra) {
+ if ( !empty($hookExtra['plugin']) && is_string($hookExtra['plugin']) ) {
+ $this->currentId = $hookExtra['plugin'];
+ $this->currentType = 'plugin';
+ } elseif ( !empty($hookExtra['theme']) && is_string($hookExtra['theme']) ) {
+ $this->currentId = $hookExtra['theme'];
+ $this->currentType = 'theme';
+ } else {
+ $this->currentType = null;
+ $this->currentId = null;
+ }
+ return $input;
+ }
+
+ /**
+ * @access private
+ *
+ * @param array $options
+ * @return array
+ */
+ public function setUpgradedPluginFromOptions($options) {
+ if ( isset($options['hook_extra']['plugin']) && is_string($options['hook_extra']['plugin']) ) {
+ $this->currentType = 'plugin';
+ $this->currentId = $options['hook_extra']['plugin'];
+ } else {
+ $this->currentType = null;
+ $this->currentId = null;
+ }
+ return $options;
+ }
+
+ /**
+ * @access private
+ *
+ * @param mixed $input
+ * @return mixed Returns $input unaltered.
+ */
+ public function clearUpgradedThing($input = null) {
+ $this->currentId = null;
+ $this->currentType = null;
+ return $input;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Utils.php b/core/updater/Puc/v4p11/Utils.php
new file mode 100644
index 0000000..3004d31
--- /dev/null
+++ b/core/updater/Puc/v4p11/Utils.php
@@ -0,0 +1,69 @@
+$node) ) {
+ $currentValue = $currentValue->$node;
+ } else {
+ return $default;
+ }
+ }
+
+ return $currentValue;
+ }
+
+ /**
+ * Get the first array element that is not empty.
+ *
+ * @param array $values
+ * @param mixed|null $default Returns this value if there are no non-empty elements.
+ * @return mixed|null
+ */
+ public static function findNotEmpty($values, $default = null) {
+ if ( empty($values) ) {
+ return $default;
+ }
+
+ foreach ($values as $value) {
+ if ( !empty($value) ) {
+ return $value;
+ }
+ }
+
+ return $default;
+ }
+
+ /**
+ * Check if the input string starts with the specified prefix.
+ *
+ * @param string $input
+ * @param string $prefix
+ * @return bool
+ */
+ public static function startsWith($input, $prefix) {
+ $length = strlen($prefix);
+ return (substr($input, 0, $length) === $prefix);
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Vcs/Api.php b/core/updater/Puc/v4p11/Vcs/Api.php
new file mode 100644
index 0000000..fc31619
--- /dev/null
+++ b/core/updater/Puc/v4p11/Vcs/Api.php
@@ -0,0 +1,302 @@
+repositoryUrl = $repositoryUrl;
+ $this->setAuthentication($credentials);
+ }
+
+ /**
+ * @return string
+ */
+ public function getRepositoryUrl() {
+ return $this->repositoryUrl;
+ }
+
+ /**
+ * Figure out which reference (i.e tag or branch) contains the latest version.
+ *
+ * @param string $configBranch Start looking in this branch.
+ * @return null|Puc_v4p11_Vcs_Reference
+ */
+ abstract public function chooseReference($configBranch);
+
+ /**
+ * Get the readme.txt file from the remote repository and parse it
+ * according to the plugin readme standard.
+ *
+ * @param string $ref Tag or branch name.
+ * @return array Parsed readme.
+ */
+ public function getRemoteReadme($ref = 'master') {
+ $fileContents = $this->getRemoteFile($this->getLocalReadmeName(), $ref);
+ if ( empty($fileContents) ) {
+ return array();
+ }
+
+ $parser = new PucReadmeParser();
+ return $parser->parse_readme_contents($fileContents);
+ }
+
+ /**
+ * Get the case-sensitive name of the local readme.txt file.
+ *
+ * In most cases it should just be called "readme.txt", but some plugins call it "README.txt",
+ * "README.TXT", or even "Readme.txt". Most VCS are case-sensitive so we need to know the correct
+ * capitalization.
+ *
+ * Defaults to "readme.txt" (all lowercase).
+ *
+ * @return string
+ */
+ public function getLocalReadmeName() {
+ static $fileName = null;
+ if ( $fileName !== null ) {
+ return $fileName;
+ }
+
+ $fileName = 'readme.txt';
+ if ( isset($this->localDirectory) ) {
+ $files = scandir($this->localDirectory);
+ if ( !empty($files) ) {
+ foreach ($files as $possibleFileName) {
+ if ( strcasecmp($possibleFileName, 'readme.txt') === 0 ) {
+ $fileName = $possibleFileName;
+ break;
+ }
+ }
+ }
+ }
+ return $fileName;
+ }
+
+ /**
+ * Get a branch.
+ *
+ * @param string $branchName
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ abstract public function getBranch($branchName);
+
+ /**
+ * Get a specific tag.
+ *
+ * @param string $tagName
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ abstract public function getTag($tagName);
+
+ /**
+ * Get the tag that looks like the highest version number.
+ * (Implementations should skip pre-release versions if possible.)
+ *
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ abstract public function getLatestTag();
+
+ /**
+ * Check if a tag name string looks like a version number.
+ *
+ * @param string $name
+ * @return bool
+ */
+ protected function looksLikeVersion($name) {
+ //Tag names may be prefixed with "v", e.g. "v1.2.3".
+ $name = ltrim($name, 'v');
+
+ //The version string must start with a number.
+ if ( !is_numeric(substr($name, 0, 1)) ) {
+ return false;
+ }
+
+ //The goal is to accept any SemVer-compatible or "PHP-standardized" version number.
+ return (preg_match('@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name) === 1);
+ }
+
+ /**
+ * Check if a tag appears to be named like a version number.
+ *
+ * @param stdClass $tag
+ * @return bool
+ */
+ protected function isVersionTag($tag) {
+ $property = $this->tagNameProperty;
+ return isset($tag->$property) && $this->looksLikeVersion($tag->$property);
+ }
+
+ /**
+ * Sort a list of tags as if they were version numbers.
+ * Tags that don't look like version number will be removed.
+ *
+ * @param stdClass[] $tags Array of tag objects.
+ * @return stdClass[] Filtered array of tags sorted in descending order.
+ */
+ protected function sortTagsByVersion($tags) {
+ //Keep only those tags that look like version numbers.
+ $versionTags = array_filter($tags, array($this, 'isVersionTag'));
+ //Sort them in descending order.
+ usort($versionTags, array($this, 'compareTagNames'));
+
+ return $versionTags;
+ }
+
+ /**
+ * Compare two tags as if they were version number.
+ *
+ * @param stdClass $tag1 Tag object.
+ * @param stdClass $tag2 Another tag object.
+ * @return int
+ */
+ protected function compareTagNames($tag1, $tag2) {
+ $property = $this->tagNameProperty;
+ if ( !isset($tag1->$property) ) {
+ return 1;
+ }
+ if ( !isset($tag2->$property) ) {
+ return -1;
+ }
+ return -version_compare(ltrim($tag1->$property, 'v'), ltrim($tag2->$property, 'v'));
+ }
+
+ /**
+ * Get the contents of a file from a specific branch or tag.
+ *
+ * @param string $path File name.
+ * @param string $ref
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
+ */
+ abstract public function getRemoteFile($path, $ref = 'master');
+
+ /**
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
+ *
+ * @param string $ref Reference name (e.g. branch or tag).
+ * @return string|null
+ */
+ abstract public function getLatestCommitTime($ref);
+
+ /**
+ * Get the contents of the changelog file from the repository.
+ *
+ * @param string $ref
+ * @param string $localDirectory Full path to the local plugin or theme directory.
+ * @return null|string The HTML contents of the changelog.
+ */
+ public function getRemoteChangelog($ref, $localDirectory) {
+ $filename = $this->findChangelogName($localDirectory);
+ if ( empty($filename) ) {
+ return null;
+ }
+
+ $changelog = $this->getRemoteFile($filename, $ref);
+ if ( $changelog === null ) {
+ return null;
+ }
+
+ /** @noinspection PhpUndefinedClassInspection */
+ return Parsedown::instance()->text($changelog);
+ }
+
+ /**
+ * Guess the name of the changelog file.
+ *
+ * @param string $directory
+ * @return string|null
+ */
+ protected function findChangelogName($directory = null) {
+ if ( !isset($directory) ) {
+ $directory = $this->localDirectory;
+ }
+ if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
+ return null;
+ }
+
+ $possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
+ $files = scandir($directory);
+ $foundNames = array_intersect($possibleNames, $files);
+
+ if ( !empty($foundNames) ) {
+ return reset($foundNames);
+ }
+ return null;
+ }
+
+ /**
+ * Set authentication credentials.
+ *
+ * @param $credentials
+ */
+ public function setAuthentication($credentials) {
+ $this->credentials = $credentials;
+ }
+
+ public function isAuthenticationEnabled() {
+ return !empty($this->credentials);
+ }
+
+ /**
+ * @param string $url
+ * @return string
+ */
+ public function signDownloadUrl($url) {
+ return $url;
+ }
+
+ /**
+ * @param string $filterName
+ */
+ public function setHttpFilterName($filterName) {
+ $this->httpFilterName = $filterName;
+ }
+
+ /**
+ * @param string $directory
+ */
+ public function setLocalDirectory($directory) {
+ if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
+ $this->localDirectory = null;
+ } else {
+ $this->localDirectory = $directory;
+ }
+ }
+
+ /**
+ * @param string $slug
+ */
+ public function setSlug($slug) {
+ $this->slug = $slug;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Vcs/BaseChecker.php b/core/updater/Puc/v4p11/Vcs/BaseChecker.php
new file mode 100644
index 0000000..8ffbae9
--- /dev/null
+++ b/core/updater/Puc/v4p11/Vcs/BaseChecker.php
@@ -0,0 +1,27 @@
+[^/]+?)/(?P[^/#?&]+?)/?$@', $path, $matches) ) {
+ $this->username = $matches['username'];
+ $this->repository = $matches['repository'];
+ } else {
+ throw new InvalidArgumentException('Invalid BitBucket repository URL: "' . $repositoryUrl . '"');
+ }
+
+ parent::__construct($repositoryUrl, $credentials);
+ }
+
+ /**
+ * Figure out which reference (i.e tag or branch) contains the latest version.
+ *
+ * @param string $configBranch Start looking in this branch.
+ * @return null|Puc_v4p11_Vcs_Reference
+ */
+ public function chooseReference($configBranch) {
+ $updateSource = null;
+
+ //Check if there's a "Stable tag: 1.2.3" header that points to a valid tag.
+ $updateSource = $this->getStableTag($configBranch);
+
+ //Look for version-like tags.
+ if ( !$updateSource && ($configBranch === 'master') ) {
+ $updateSource = $this->getLatestTag();
+ }
+ //If all else fails, use the specified branch itself.
+ if ( !$updateSource ) {
+ $updateSource = $this->getBranch($configBranch);
+ }
+
+ return $updateSource;
+ }
+
+ public function getBranch($branchName) {
+ $branch = $this->api('/refs/branches/' . $branchName);
+ if ( is_wp_error($branch) || empty($branch) ) {
+ return null;
+ }
+
+ return new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $branch->name,
+ 'updated' => $branch->target->date,
+ 'downloadUrl' => $this->getDownloadUrl($branch->name),
+ ));
+ }
+
+ /**
+ * Get a specific tag.
+ *
+ * @param string $tagName
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ public function getTag($tagName) {
+ $tag = $this->api('/refs/tags/' . $tagName);
+ if ( is_wp_error($tag) || empty($tag) ) {
+ return null;
+ }
+
+ return new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $tag->name,
+ 'version' => ltrim($tag->name, 'v'),
+ 'updated' => $tag->target->date,
+ 'downloadUrl' => $this->getDownloadUrl($tag->name),
+ ));
+ }
+
+ /**
+ * Get the tag that looks like the highest version number.
+ *
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ public function getLatestTag() {
+ $tags = $this->api('/refs/tags?sort=-target.date');
+ if ( !isset($tags, $tags->values) || !is_array($tags->values) ) {
+ return null;
+ }
+
+ //Filter and sort the list of tags.
+ $versionTags = $this->sortTagsByVersion($tags->values);
+
+ //Return the first result.
+ if ( !empty($versionTags) ) {
+ $tag = $versionTags[0];
+ return new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $tag->name,
+ 'version' => ltrim($tag->name, 'v'),
+ 'updated' => $tag->target->date,
+ 'downloadUrl' => $this->getDownloadUrl($tag->name),
+ ));
+ }
+ return null;
+ }
+
+ /**
+ * Get the tag/ref specified by the "Stable tag" header in the readme.txt of a given branch.
+ *
+ * @param string $branch
+ * @return null|Puc_v4p11_Vcs_Reference
+ */
+ protected function getStableTag($branch) {
+ $remoteReadme = $this->getRemoteReadme($branch);
+ if ( !empty($remoteReadme['stable_tag']) ) {
+ $tag = $remoteReadme['stable_tag'];
+
+ //You can explicitly opt out of using tags by setting "Stable tag" to
+ //"trunk" or the name of the current branch.
+ if ( ($tag === $branch) || ($tag === 'trunk') ) {
+ return $this->getBranch($branch);
+ }
+
+ return $this->getTag($tag);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $ref
+ * @return string
+ */
+ protected function getDownloadUrl($ref) {
+ return sprintf(
+ 'https://bitbucket.org/%s/%s/get/%s.zip',
+ $this->username,
+ $this->repository,
+ $ref
+ );
+ }
+
+ /**
+ * Get the contents of a file from a specific branch or tag.
+ *
+ * @param string $path File name.
+ * @param string $ref
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
+ */
+ public function getRemoteFile($path, $ref = 'master') {
+ $response = $this->api('src/' . $ref . '/' . ltrim($path));
+ if ( is_wp_error($response) || !is_string($response) ) {
+ return null;
+ }
+ return $response;
+ }
+
+ /**
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
+ *
+ * @param string $ref Reference name (e.g. branch or tag).
+ * @return string|null
+ */
+ public function getLatestCommitTime($ref) {
+ $response = $this->api('commits/' . $ref);
+ if ( isset($response->values, $response->values[0], $response->values[0]->date) ) {
+ return $response->values[0]->date;
+ }
+ return null;
+ }
+
+ /**
+ * Perform a BitBucket API 2.0 request.
+ *
+ * @param string $url
+ * @param string $version
+ * @return mixed|WP_Error
+ */
+ public function api($url, $version = '2.0') {
+ $url = ltrim($url, '/');
+ $isSrcResource = Puc_v4p11_Utils::startsWith($url, 'src/');
+
+ $url = implode('/', array(
+ 'https://api.bitbucket.org',
+ $version,
+ 'repositories',
+ $this->username,
+ $this->repository,
+ $url
+ ));
+ $baseUrl = $url;
+
+ if ( $this->oauth ) {
+ $url = $this->oauth->sign($url,'GET');
+ }
+
+ $options = array('timeout' => 10);
+ if ( !empty($this->httpFilterName) ) {
+ $options = apply_filters($this->httpFilterName, $options);
+ }
+ $response = wp_remote_get($url, $options);
+ if ( is_wp_error($response) ) {
+ do_action('puc_api_error', $response, null, $url, $this->slug);
+ return $response;
+ }
+
+ $code = wp_remote_retrieve_response_code($response);
+ $body = wp_remote_retrieve_body($response);
+ if ( $code === 200 ) {
+ if ( $isSrcResource ) {
+ //Most responses are JSON-encoded, but src resources just
+ //return raw file contents.
+ $document = $body;
+ } else {
+ $document = json_decode($body);
+ }
+ return $document;
+ }
+
+ $error = new WP_Error(
+ 'puc-bitbucket-http-error',
+ 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;
+ }
+
+ /**
+ * @param array $credentials
+ */
+ public function setAuthentication($credentials) {
+ parent::setAuthentication($credentials);
+
+ if ( !empty($credentials) && !empty($credentials['consumer_key']) ) {
+ $this->oauth = new Puc_v4p11_OAuthSignature(
+ $credentials['consumer_key'],
+ $credentials['consumer_secret']
+ );
+ } else {
+ $this->oauth = null;
+ }
+ }
+
+ public function signDownloadUrl($url) {
+ //Add authentication data to download URLs. Since OAuth signatures incorporate
+ //timestamps, we have to do this immediately before inserting the update. Otherwise
+ //authentication could fail due to a stale timestamp.
+ if ( $this->oauth ) {
+ $url = $this->oauth->sign($url);
+ }
+ return $url;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Vcs/GitHubApi.php b/core/updater/Puc/v4p11/Vcs/GitHubApi.php
new file mode 100644
index 0000000..79f3c56
--- /dev/null
+++ b/core/updater/Puc/v4p11/Vcs/GitHubApi.php
@@ -0,0 +1,441 @@
+[^/]+?)/(?P[^/#?&]+?)/?$@', $path, $matches) ) {
+ $this->userName = $matches['username'];
+ $this->repositoryName = $matches['repository'];
+ } else {
+ throw new InvalidArgumentException('Invalid GitHub repository URL: "' . $repositoryUrl . '"');
+ }
+
+ parent::__construct($repositoryUrl, $accessToken);
+ }
+
+ /**
+ * Get the latest release from GitHub.
+ *
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ public function getLatestRelease() {
+ $release = $this->api('/repos/:user/:repo/releases/latest');
+ if ( is_wp_error($release) || !is_object($release) || !isset($release->tag_name) ) {
+ return null;
+ }
+
+ $reference = new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $release->tag_name,
+ 'version' => ltrim($release->tag_name, 'v'), //Remove the "v" prefix from "v1.2.3".
+ 'downloadUrl' => $release->zipball_url,
+ 'updated' => $release->created_at,
+ 'apiResponse' => $release,
+ ));
+
+ if ( isset($release->assets[0]) ) {
+ $reference->downloadCount = $release->assets[0]->download_count;
+ }
+
+ if ( $this->releaseAssetsEnabled && isset($release->assets, $release->assets[0]) ) {
+ //Use the first release asset that matches the specified regular expression.
+ $matchingAssets = array_filter($release->assets, array($this, 'matchesAssetFilter'));
+ if ( !empty($matchingAssets) ) {
+ if ( $this->isAuthenticationEnabled() ) {
+ /**
+ * Keep in mind that we'll need to add an "Accept" header to download this asset.
+ *
+ * @see setUpdateDownloadHeaders()
+ */
+ $reference->downloadUrl = $matchingAssets[0]->url;
+ } else {
+ //It seems that browser_download_url only works for public repositories.
+ //Using an access_token doesn't help. Maybe OAuth would work?
+ $reference->downloadUrl = $matchingAssets[0]->browser_download_url;
+ }
+
+ $reference->downloadCount = $matchingAssets[0]->download_count;
+ }
+ }
+
+ if ( !empty($release->body) ) {
+ /** @noinspection PhpUndefinedClassInspection */
+ $reference->changelog = Parsedown::instance()->text($release->body);
+ }
+
+ return $reference;
+ }
+
+ /**
+ * Get the tag that looks like the highest version number.
+ *
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ public function getLatestTag() {
+ $tags = $this->api('/repos/:user/:repo/tags');
+
+ if ( is_wp_error($tags) || !is_array($tags) ) {
+ return null;
+ }
+
+ $versionTags = $this->sortTagsByVersion($tags);
+ if ( empty($versionTags) ) {
+ return null;
+ }
+
+ $tag = $versionTags[0];
+ return new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $tag->name,
+ 'version' => ltrim($tag->name, 'v'),
+ 'downloadUrl' => $tag->zipball_url,
+ 'apiResponse' => $tag,
+ ));
+ }
+
+ /**
+ * Get a branch by name.
+ *
+ * @param string $branchName
+ * @return null|Puc_v4p11_Vcs_Reference
+ */
+ public function getBranch($branchName) {
+ $branch = $this->api('/repos/:user/:repo/branches/' . $branchName);
+ if ( is_wp_error($branch) || empty($branch) ) {
+ return null;
+ }
+
+ $reference = new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $branch->name,
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
+ 'apiResponse' => $branch,
+ ));
+
+ if ( isset($branch->commit, $branch->commit->commit, $branch->commit->commit->author->date) ) {
+ $reference->updated = $branch->commit->commit->author->date;
+ }
+
+ return $reference;
+ }
+
+ /**
+ * Get the latest commit that changed the specified file.
+ *
+ * @param string $filename
+ * @param string $ref Reference name (e.g. branch or tag).
+ * @return StdClass|null
+ */
+ public function getLatestCommit($filename, $ref = 'master') {
+ $commits = $this->api(
+ '/repos/:user/:repo/commits',
+ array(
+ 'path' => $filename,
+ 'sha' => $ref,
+ )
+ );
+ if ( !is_wp_error($commits) && isset($commits[0]) ) {
+ return $commits[0];
+ }
+ return null;
+ }
+
+ /**
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
+ *
+ * @param string $ref Reference name (e.g. branch or tag).
+ * @return string|null
+ */
+ public function getLatestCommitTime($ref) {
+ $commits = $this->api('/repos/:user/:repo/commits', array('sha' => $ref));
+ if ( !is_wp_error($commits) && isset($commits[0]) ) {
+ return $commits[0]->commit->author->date;
+ }
+ return null;
+ }
+
+ /**
+ * Perform a GitHub API request.
+ *
+ * @param string $url
+ * @param array $queryParams
+ * @return mixed|WP_Error
+ */
+ protected function api($url, $queryParams = array()) {
+ $baseUrl = $url;
+ $url = $this->buildApiUrl($url, $queryParams);
+
+ $options = array('timeout' => 10);
+ if ( $this->isAuthenticationEnabled() ) {
+ $options['headers'] = array('Authorization' => $this->getAuthorizationHeader());
+ }
+
+ if ( !empty($this->httpFilterName) ) {
+ $options = apply_filters($this->httpFilterName, $options);
+ }
+ $response = wp_remote_get($url, $options);
+ if ( is_wp_error($response) ) {
+ do_action('puc_api_error', $response, null, $url, $this->slug);
+ return $response;
+ }
+
+ $code = wp_remote_retrieve_response_code($response);
+ $body = wp_remote_retrieve_body($response);
+ if ( $code === 200 ) {
+ $document = json_decode($body);
+ return $document;
+ }
+
+ $error = new WP_Error(
+ 'puc-github-http-error',
+ 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;
+ }
+
+ /**
+ * Build a fully qualified URL for an API request.
+ *
+ * @param string $url
+ * @param array $queryParams
+ * @return string
+ */
+ protected function buildApiUrl($url, $queryParams) {
+ $variables = array(
+ 'user' => $this->userName,
+ 'repo' => $this->repositoryName,
+ );
+ foreach ($variables as $name => $value) {
+ $url = str_replace('/:' . $name, '/' . urlencode($value), $url);
+ }
+ $url = 'https://api.github.com' . $url;
+
+ if ( !empty($queryParams) ) {
+ $url = add_query_arg($queryParams, $url);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Get the contents of a file from a specific branch or tag.
+ *
+ * @param string $path File name.
+ * @param string $ref
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
+ */
+ public function getRemoteFile($path, $ref = 'master') {
+ $apiUrl = '/repos/:user/:repo/contents/' . $path;
+ $response = $this->api($apiUrl, array('ref' => $ref));
+
+ if ( is_wp_error($response) || !isset($response->content) || ($response->encoding !== 'base64') ) {
+ return null;
+ }
+ return base64_decode($response->content);
+ }
+
+ /**
+ * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
+ *
+ * @param string $ref
+ * @return string
+ */
+ public function buildArchiveDownloadUrl($ref = 'master') {
+ $url = sprintf(
+ 'https://api.github.com/repos/%1$s/%2$s/zipball/%3$s',
+ urlencode($this->userName),
+ urlencode($this->repositoryName),
+ urlencode($ref)
+ );
+ return $url;
+ }
+
+ /**
+ * Get a specific tag.
+ *
+ * @param string $tagName
+ * @return void
+ */
+ public function getTag($tagName) {
+ //The current GitHub update checker doesn't use getTag, so I didn't bother to implement it.
+ throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
+ }
+
+ public function setAuthentication($credentials) {
+ parent::setAuthentication($credentials);
+ $this->accessToken = is_string($credentials) ? $credentials : null;
+
+ //Optimization: Instead of filtering all HTTP requests, let's do it only when
+ //WordPress is about to download an update.
+ add_filter('upgrader_pre_download', array($this, 'addHttpRequestFilter'), 10, 1); //WP 3.7+
+ }
+
+ /**
+ * Figure out which reference (i.e tag or branch) contains the latest version.
+ *
+ * @param string $configBranch Start looking in this branch.
+ * @return null|Puc_v4p11_Vcs_Reference
+ */
+ public function chooseReference($configBranch) {
+ $updateSource = null;
+
+ if ( $configBranch === 'master' ) {
+ //Use the latest release.
+ $updateSource = $this->getLatestRelease();
+ if ( $updateSource === null ) {
+ //Failing that, use the tag with the highest version number.
+ $updateSource = $this->getLatestTag();
+ }
+ }
+ //Alternatively, just use the branch itself.
+ if ( empty($updateSource) ) {
+ $updateSource = $this->getBranch($configBranch);
+ }
+
+ return $updateSource;
+ }
+
+ /**
+ * Enable updating via release assets.
+ *
+ * If the latest release contains no usable assets, the update checker
+ * will fall back to using the automatically generated ZIP archive.
+ *
+ * Private repositories will only work with WordPress 3.7 or later.
+ *
+ * @param string|null $fileNameRegex Optional. Use only those assets where the file name matches this regex.
+ */
+ public function enableReleaseAssets($fileNameRegex = null) {
+ $this->releaseAssetsEnabled = true;
+ $this->assetFilterRegex = $fileNameRegex;
+ $this->assetApiBaseUrl = sprintf(
+ '//api.github.com/repos/%1$s/%2$s/releases/assets/',
+ $this->userName,
+ $this->repositoryName
+ );
+ }
+
+ /**
+ * Does this asset match the file name regex?
+ *
+ * @param stdClass $releaseAsset
+ * @return bool
+ */
+ protected function matchesAssetFilter($releaseAsset) {
+ if ( $this->assetFilterRegex === null ) {
+ //The default is to accept all assets.
+ return true;
+ }
+ return isset($releaseAsset->name) && preg_match($this->assetFilterRegex, $releaseAsset->name);
+ }
+
+ /**
+ * @internal
+ * @param bool $result
+ * @return bool
+ */
+ public function addHttpRequestFilter($result) {
+ if ( !$this->downloadFilterAdded && $this->isAuthenticationEnabled() ) {
+ add_filter('http_request_args', array($this, 'setUpdateDownloadHeaders'), 10, 2);
+ add_action('requests-requests.before_redirect', array($this, 'removeAuthHeaderFromRedirects'), 10, 4);
+ $this->downloadFilterAdded = true;
+ }
+ return $result;
+ }
+
+ /**
+ * Set the HTTP headers that are necessary to download updates from private repositories.
+ *
+ * See GitHub docs:
+ * @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
+ * @link https://developer.github.com/v3/auth/#basic-authentication
+ *
+ * @internal
+ * @param array $requestArgs
+ * @param string $url
+ * @return array
+ */
+ public function setUpdateDownloadHeaders($requestArgs, $url = '') {
+ //Is WordPress trying to download one of our release assets?
+ if ( $this->releaseAssetsEnabled && (strpos($url, $this->assetApiBaseUrl) !== false) ) {
+ $requestArgs['headers']['Accept'] = 'application/octet-stream';
+ }
+ //Use Basic authentication, but only if the download is from our repository.
+ $repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
+ if ( $this->isAuthenticationEnabled() && (strpos($url, $repoApiBaseUrl)) === 0 ) {
+ $requestArgs['headers']['Authorization'] = $this->getAuthorizationHeader();
+ }
+ return $requestArgs;
+ }
+
+ /**
+ * When following a redirect, the Requests library will automatically forward
+ * the authorization header to other hosts. We don't want that because it breaks
+ * AWS downloads and can leak authorization information.
+ *
+ * @internal
+ * @param string $location
+ * @param array $headers
+ */
+ public function removeAuthHeaderFromRedirects(&$location, &$headers) {
+ $repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
+ if ( strpos($location, $repoApiBaseUrl) === 0 ) {
+ return; //This request is going to GitHub, so it's fine.
+ }
+ //Remove the header.
+ if ( isset($headers['Authorization']) ) {
+ unset($headers['Authorization']);
+ }
+ }
+
+ /**
+ * Generate the value of the "Authorization" header.
+ *
+ * @return string
+ */
+ protected function getAuthorizationHeader() {
+ return 'Basic ' . base64_encode($this->userName . ':' . $this->accessToken);
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Vcs/GitLabApi.php b/core/updater/Puc/v4p11/Vcs/GitLabApi.php
new file mode 100644
index 0000000..8fd3f45
--- /dev/null
+++ b/core/updater/Puc/v4p11/Vcs/GitLabApi.php
@@ -0,0 +1,309 @@
+repositoryHost = parse_url($repositoryUrl, PHP_URL_HOST) . $port;
+
+ if ( $this->repositoryHost !== 'gitlab.com' ) {
+ $this->repositoryProtocol = parse_url($repositoryUrl, PHP_URL_SCHEME);
+ }
+
+ //Find the repository information
+ $path = parse_url($repositoryUrl, PHP_URL_PATH);
+ if ( preg_match('@^/?(?P[^/]+?)/(?P[^/#?&]+?)/?$@', $path, $matches) ) {
+ $this->userName = $matches['username'];
+ $this->repositoryName = $matches['repository'];
+ } elseif ( ($this->repositoryHost === 'gitlab.com') ) {
+ //This is probably a repository in a subgroup, e.g. "/organization/category/repo".
+ $parts = explode('/', trim($path, '/'));
+ if ( count($parts) < 3 ) {
+ throw new InvalidArgumentException('Invalid GitLab.com repository URL: "' . $repositoryUrl . '"');
+ }
+ $lastPart = array_pop($parts);
+ $this->userName = implode('/', $parts);
+ $this->repositoryName = $lastPart;
+ } else {
+ //There could be subgroups in the URL: gitlab.domain.com/group/subgroup/subgroup2/repository
+ if ( $subgroup !== null ) {
+ $path = str_replace(trailingslashit($subgroup), '', $path);
+ }
+
+ //This is not a traditional url, it could be gitlab is in a deeper subdirectory.
+ //Get the path segments.
+ $segments = explode('/', untrailingslashit(ltrim($path, '/')));
+
+ //We need at least /user-name/repository-name/
+ if ( count($segments) < 2 ) {
+ throw new InvalidArgumentException('Invalid GitLab repository URL: "' . $repositoryUrl . '"');
+ }
+
+ //Get the username and repository name.
+ $usernameRepo = array_splice($segments, -2, 2);
+ $this->userName = $usernameRepo[0];
+ $this->repositoryName = $usernameRepo[1];
+
+ //Append the remaining segments to the host if there are segments left.
+ if ( count($segments) > 0 ) {
+ $this->repositoryHost = trailingslashit($this->repositoryHost) . implode('/', $segments);
+ }
+
+ //Add subgroups to username.
+ if ( $subgroup !== null ) {
+ $this->userName = $usernameRepo[0] . '/' . untrailingslashit($subgroup);
+ }
+ }
+
+ parent::__construct($repositoryUrl, $accessToken);
+ }
+
+ /**
+ * Get the latest release from GitLab.
+ *
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ public function getLatestRelease() {
+ return $this->getLatestTag();
+ }
+
+ /**
+ * Get the tag that looks like the highest version number.
+ *
+ * @return Puc_v4p11_Vcs_Reference|null
+ */
+ public function getLatestTag() {
+ $tags = $this->api('/:id/repository/tags');
+ if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
+ return null;
+ }
+
+ $versionTags = $this->sortTagsByVersion($tags);
+ if ( empty($versionTags) ) {
+ return null;
+ }
+
+ $tag = $versionTags[0];
+ return new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $tag->name,
+ 'version' => ltrim($tag->name, 'v'),
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($tag->name),
+ 'apiResponse' => $tag,
+ ));
+ }
+
+ /**
+ * Get a branch by name.
+ *
+ * @param string $branchName
+ * @return null|Puc_v4p11_Vcs_Reference
+ */
+ public function getBranch($branchName) {
+ $branch = $this->api('/:id/repository/branches/' . $branchName);
+ if ( is_wp_error($branch) || empty($branch) ) {
+ return null;
+ }
+
+ $reference = new Puc_v4p11_Vcs_Reference(array(
+ 'name' => $branch->name,
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
+ 'apiResponse' => $branch,
+ ));
+
+ if ( isset($branch->commit, $branch->commit->committed_date) ) {
+ $reference->updated = $branch->commit->committed_date;
+ }
+
+ return $reference;
+ }
+
+ /**
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
+ *
+ * @param string $ref Reference name (e.g. branch or tag).
+ * @return string|null
+ */
+ public function getLatestCommitTime($ref) {
+ $commits = $this->api('/:id/repository/commits/', array('ref_name' => $ref));
+ if ( is_wp_error($commits) || !is_array($commits) || !isset($commits[0]) ) {
+ return null;
+ }
+
+ return $commits[0]->committed_date;
+ }
+
+ /**
+ * Perform a GitLab API request.
+ *
+ * @param string $url
+ * @param array $queryParams
+ * @return mixed|WP_Error
+ */
+ protected function api($url, $queryParams = array()) {
+ $baseUrl = $url;
+ $url = $this->buildApiUrl($url, $queryParams);
+
+ $options = array('timeout' => 10);
+ if ( !empty($this->httpFilterName) ) {
+ $options = apply_filters($this->httpFilterName, $options);
+ }
+
+ $response = wp_remote_get($url, $options);
+ if ( is_wp_error($response) ) {
+ do_action('puc_api_error', $response, null, $url, $this->slug);
+ return $response;
+ }
+
+ $code = wp_remote_retrieve_response_code($response);
+ $body = wp_remote_retrieve_body($response);
+ if ( $code === 200 ) {
+ return json_decode($body);
+ }
+
+ $error = new WP_Error(
+ 'puc-gitlab-http-error',
+ sprintf('GitLab API error. URL: "%s", HTTP status code: %d.', $baseUrl, $code)
+ );
+ do_action('puc_api_error', $error, $response, $url, $this->slug);
+
+ return $error;
+ }
+
+ /**
+ * Build a fully qualified URL for an API request.
+ *
+ * @param string $url
+ * @param array $queryParams
+ * @return string
+ */
+ protected function buildApiUrl($url, $queryParams) {
+ $variables = array(
+ 'user' => $this->userName,
+ 'repo' => $this->repositoryName,
+ 'id' => $this->userName . '/' . $this->repositoryName,
+ );
+
+ foreach ($variables as $name => $value) {
+ $url = str_replace("/:{$name}", '/' . urlencode($value), $url);
+ }
+
+ $url = substr($url, 1);
+ $url = sprintf('%1$s://%2$s/api/v4/projects/%3$s', $this->repositoryProtocol, $this->repositoryHost, $url);
+
+ if ( !empty($this->accessToken) ) {
+ $queryParams['private_token'] = $this->accessToken;
+ }
+
+ if ( !empty($queryParams) ) {
+ $url = add_query_arg($queryParams, $url);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Get the contents of a file from a specific branch or tag.
+ *
+ * @param string $path File name.
+ * @param string $ref
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
+ */
+ public function getRemoteFile($path, $ref = 'master') {
+ $response = $this->api('/:id/repository/files/' . $path, array('ref' => $ref));
+ if ( is_wp_error($response) || !isset($response->content) || $response->encoding !== 'base64' ) {
+ return null;
+ }
+
+ return base64_decode($response->content);
+ }
+
+ /**
+ * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
+ *
+ * @param string $ref
+ * @return string
+ */
+ public function buildArchiveDownloadUrl($ref = 'master') {
+ $url = sprintf(
+ '%1$s://%2$s/api/v4/projects/%3$s/repository/archive.zip',
+ $this->repositoryProtocol,
+ $this->repositoryHost,
+ urlencode($this->userName . '/' . $this->repositoryName)
+ );
+ $url = add_query_arg('sha', urlencode($ref), $url);
+
+ if ( !empty($this->accessToken) ) {
+ $url = add_query_arg('private_token', $this->accessToken, $url);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Get a specific tag.
+ *
+ * @param string $tagName
+ * @return void
+ */
+ public function getTag($tagName) {
+ throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
+ }
+
+ /**
+ * Figure out which reference (i.e tag or branch) contains the latest version.
+ *
+ * @param string $configBranch Start looking in this branch.
+ * @return null|Puc_v4p11_Vcs_Reference
+ */
+ public function chooseReference($configBranch) {
+ $updateSource = null;
+
+ // GitLab doesn't handle releases the same as GitHub so just use the latest tag
+ if ( $configBranch === 'master' ) {
+ $updateSource = $this->getLatestTag();
+ }
+
+ if ( empty($updateSource) ) {
+ $updateSource = $this->getBranch($configBranch);
+ }
+
+ return $updateSource;
+ }
+
+ public function setAuthentication($credentials) {
+ parent::setAuthentication($credentials);
+ $this->accessToken = is_string($credentials) ? $credentials : null;
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Vcs/PluginUpdateChecker.php b/core/updater/Puc/v4p11/Vcs/PluginUpdateChecker.php
new file mode 100644
index 0000000..bc6d94a
--- /dev/null
+++ b/core/updater/Puc/v4p11/Vcs/PluginUpdateChecker.php
@@ -0,0 +1,218 @@
+api = $api;
+ $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) {
+ //We have to make several remote API requests to gather all the necessary info
+ //which can take a while on slow networks.
+ if ( function_exists('set_time_limit') ) {
+ @set_time_limit(60);
+ }
+
+ $api = $this->api;
+ $api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
+
+ $info = new Puc_v4p11_Plugin_Info();
+ $info->filename = $this->pluginFile;
+ $info->slug = $this->slug;
+
+ $this->setInfoFromHeader($this->package->getPluginHeader(), $info);
+
+ //Pick a branch or tag.
+ $updateSource = $api->chooseReference($this->branch);
+ if ( $updateSource ) {
+ $ref = $updateSource->name;
+ $info->version = $updateSource->version;
+ $info->last_updated = $updateSource->updated;
+ $info->download_url = $updateSource->downloadUrl;
+
+ if ( !empty($updateSource->changelog) ) {
+ $info->sections['changelog'] = $updateSource->changelog;
+ }
+ if ( isset($updateSource->downloadCount) ) {
+ $info->downloaded = $updateSource->downloadCount;
+ }
+ } 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;
+ }
+
+ //Get headers from the main plugin file in this branch/tag. Its "Version" header and other metadata
+ //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
+ $mainPluginFile = basename($this->pluginFile);
+ $remotePlugin = $api->getRemoteFile($mainPluginFile, $ref);
+ if ( !empty($remotePlugin) ) {
+ $remoteHeader = $this->package->getFileHeader($remotePlugin);
+ $this->setInfoFromHeader($remoteHeader, $info);
+ }
+
+ //Try parsing readme.txt. If it's formatted according to WordPress.org standards, it will contain
+ //a lot of useful information like the required/tested WP version, changelog, and so on.
+ if ( $this->readmeTxtExistsLocally() ) {
+ $this->setInfoFromRemoteReadme($ref, $info);
+ }
+
+ //The changelog might be in a separate file.
+ if ( empty($info->sections['changelog']) ) {
+ $info->sections['changelog'] = $api->getRemoteChangelog($ref, $this->package->getAbsoluteDirectoryPath());
+ if ( empty($info->sections['changelog']) ) {
+ $info->sections['changelog'] = __('There is no changelog available.', 'plugin-update-checker');
+ }
+ }
+
+ if ( empty($info->last_updated) ) {
+ //Fetch the latest commit that changed the tag or branch and use it as the "last_updated" date.
+ $latestCommitTime = $api->getLatestCommitTime($ref);
+ if ( $latestCommitTime !== null ) {
+ $info->last_updated = $latestCommitTime;
+ }
+ }
+
+ $info = apply_filters($this->getUniqueName('request_info_result'), $info, null);
+ return $info;
+ }
+
+ /**
+ * Check if the currently installed version has a readme.txt file.
+ *
+ * @return bool
+ */
+ protected function readmeTxtExistsLocally() {
+ return $this->package->fileExists($this->api->getLocalReadmeName());
+ }
+
+ /**
+ * Copy plugin metadata from a file header to a Plugin Info object.
+ *
+ * @param array $fileHeader
+ * @param Puc_v4p11_Plugin_Info $pluginInfo
+ */
+ protected function setInfoFromHeader($fileHeader, $pluginInfo) {
+ $headerToPropertyMap = array(
+ 'Version' => 'version',
+ 'Name' => 'name',
+ 'PluginURI' => 'homepage',
+ 'Author' => 'author',
+ 'AuthorName' => 'author',
+ 'AuthorURI' => 'author_homepage',
+
+ 'Requires WP' => 'requires',
+ 'Tested WP' => 'tested',
+ 'Requires at least' => 'requires',
+ 'Tested up to' => 'tested',
+
+ 'Requires PHP' => 'requires_php',
+ );
+ foreach ($headerToPropertyMap as $headerName => $property) {
+ if ( isset($fileHeader[$headerName]) && !empty($fileHeader[$headerName]) ) {
+ $pluginInfo->$property = $fileHeader[$headerName];
+ }
+ }
+
+ if ( !empty($fileHeader['Description']) ) {
+ $pluginInfo->sections['description'] = $fileHeader['Description'];
+ }
+ }
+
+ /**
+ * Copy plugin metadata from the remote readme.txt file.
+ *
+ * @param string $ref GitHub tag or branch where to look for the readme.
+ * @param Puc_v4p11_Plugin_Info $pluginInfo
+ */
+ protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
+ $readme = $this->api->getRemoteReadme($ref);
+ if ( empty($readme) ) {
+ return;
+ }
+
+ if ( isset($readme['sections']) ) {
+ $pluginInfo->sections = array_merge($pluginInfo->sections, $readme['sections']);
+ }
+ if ( !empty($readme['tested_up_to']) ) {
+ $pluginInfo->tested = $readme['tested_up_to'];
+ }
+ if ( !empty($readme['requires_at_least']) ) {
+ $pluginInfo->requires = $readme['requires_at_least'];
+ }
+ if ( !empty($readme['requires_php']) ) {
+ $pluginInfo->requires_php = $readme['requires_php'];
+ }
+
+ if ( isset($readme['upgrade_notice'], $readme['upgrade_notice'][$pluginInfo->version]) ) {
+ $pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version];
+ }
+ }
+
+ public function setBranch($branch) {
+ $this->branch = $branch;
+ return $this;
+ }
+
+ public function setAuthentication($credentials) {
+ $this->api->setAuthentication($credentials);
+ return $this;
+ }
+
+ public function getVcsApi() {
+ return $this->api;
+ }
+
+ public function getUpdate() {
+ $update = parent::getUpdate();
+
+ if ( isset($update) && !empty($update->download_url) ) {
+ $update->download_url = $this->api->signDownloadUrl($update->download_url);
+ }
+
+ return $update;
+ }
+
+ public function onDisplayConfiguration($panel) {
+ parent::onDisplayConfiguration($panel);
+ $panel->row('Branch', $this->branch);
+ $panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
+ $panel->row('API client', get_class($this->api));
+ }
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Vcs/Reference.php b/core/updater/Puc/v4p11/Vcs/Reference.php
new file mode 100644
index 0000000..0bf8c69
--- /dev/null
+++ b/core/updater/Puc/v4p11/Vcs/Reference.php
@@ -0,0 +1,49 @@
+properties = $properties;
+ }
+
+ /**
+ * @param string $name
+ * @return mixed|null
+ */
+ public function __get($name) {
+ return array_key_exists($name, $this->properties) ? $this->properties[$name] : null;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ */
+ public function __set($name, $value) {
+ $this->properties[$name] = $value;
+ }
+
+ /**
+ * @param string $name
+ * @return bool
+ */
+ public function __isset($name) {
+ return isset($this->properties[$name]);
+ }
+
+ }
+
+endif;
diff --git a/core/updater/Puc/v4p11/Vcs/ThemeUpdateChecker.php b/core/updater/Puc/v4p11/Vcs/ThemeUpdateChecker.php
new file mode 100644
index 0000000..1850af6
--- /dev/null
+++ b/core/updater/Puc/v4p11/Vcs/ThemeUpdateChecker.php
@@ -0,0 +1,118 @@
+api = $api;
+ $this->api->setHttpFilterName($this->getUniqueName('request_update_options'));
+
+ parent::__construct($api->getRepositoryUrl(), $stylesheet, $customSlug, $checkPeriod, $optionName);
+
+ $this->api->setSlug($this->slug);
+ }
+
+ public function requestUpdate() {
+ $api = $this->api;
+ $api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
+
+ $update = new Puc_v4p11_Theme_Update();
+ $update->slug = $this->slug;
+
+ //Figure out which reference (tag or branch) we'll use to get the latest version of the theme.
+ $updateSource = $api->chooseReference($this->branch);
+ if ( $updateSource ) {
+ $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;
+ }
+
+ //Get headers from the main stylesheet in this branch/tag. Its "Version" header and other metadata
+ //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
+ $remoteHeader = $this->package->getFileHeader($api->getRemoteFile('style.css', $ref));
+ $update->version = Puc_v4p11_Utils::findNotEmpty(array(
+ $remoteHeader['Version'],
+ Puc_v4p11_Utils::get($updateSource, 'version'),
+ ));
+
+ //The details URL defaults to the Theme URI header or the repository URL.
+ $update->details_url = Puc_v4p11_Utils::findNotEmpty(array(
+ $remoteHeader['ThemeURI'],
+ $this->package->getHeaderValue('ThemeURI'),
+ $this->metadataUrl,
+ ));
+
+ if ( empty($update->version) ) {
+ //It looks like we didn't find a valid update after all.
+ $update = null;
+ }
+
+ $update = $this->filterUpdateResult($update);
+ return $update;
+ }
+
+ //FIXME: This is duplicated code. Both theme and plugin subclasses that use VCS share these methods.
+
+ public function setBranch($branch) {
+ $this->branch = $branch;
+ return $this;
+ }
+
+ public function setAuthentication($credentials) {
+ $this->api->setAuthentication($credentials);
+ return $this;
+ }
+
+ public function getVcsApi() {
+ return $this->api;
+ }
+
+ public function getUpdate() {
+ $update = parent::getUpdate();
+
+ if ( isset($update) && !empty($update->download_url) ) {
+ $update->download_url = $this->api->signDownloadUrl($update->download_url);
+ }
+
+ return $update;
+ }
+
+ public function onDisplayConfiguration($panel) {
+ parent::onDisplayConfiguration($panel);
+ $panel->row('Branch', $this->branch);
+ $panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
+ $panel->row('API client', get_class($this->api));
+ }
+ }
+
+endif;
diff --git a/core/updater/README.md b/core/updater/README.md
old mode 100755
new mode 100644
index 73aadf4..d946c84
--- a/core/updater/README.md
+++ b/core/updater/README.md
@@ -28,6 +28,8 @@ From the users' perspective, it works just like with plugins and themes hosted o
Getting Started
---------------
+*Note:* In each of the below examples, part of the instructions are to create an instance of the update checker class. It's recommended to do this either during the `plugins_loaded` action or outside of any hooks. If you do it only during an `admin_*` action, then updates will not be visible to a wide variety of WordPress maanagement tools; they will only be visible to logged-in users on dashboard pages.
+
### Self-hosted Plugins and Themes
1. Download [the latest release](https://github.com/YahnisElsts/plugin-update-checker/releases/latest) and copy the `plugin-update-checker` directory to your plugin or theme.
@@ -45,7 +47,7 @@ Getting Started
}
```
- This is a minimal example that leaves out optional fields. See [this table](https://docs.google.com/spreadsheets/d/1eOBbW7Go2qEQXReOOCdidMTf_tDYRq4JfegcO1CBPIs/edit?usp=sharing&authkey=CK7h9toK&output=html) for a full list of supported fields and their descriptions.
+ This is a minimal example that leaves out optional fields. See [this table](https://docs.google.com/spreadsheets/d/1eOBbW7Go2qEQXReOOCdidMTf_tDYRq4JfegcO1CBPIs/edit?usp=sharing) for a full list of supported fields and their descriptions.
- Theme example:
```json
@@ -106,7 +108,7 @@ By default, the library will check the specified URL for changes every 12 hours.
//Optional: Set the branch that contains the stable release.
$myUpdateChecker->setBranch('stable-branch-name');
```
-3. Plugins only: Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/about/readme.txt) to your repository. The contents of this file will be shown when the user clicks the "View version 1.2.3 details" link.
+3. Plugins only: Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/readme.txt) to your repository. The contents of this file will be shown when the user clicks the "View version 1.2.3 details" link.
#### How to Release an Update
@@ -192,7 +194,7 @@ The library will pull update details from the following parts of a release/tag/b
//Optional: Set the branch that contains the stable release.
$myUpdateChecker->setBranch('stable-branch-name');
```
-3. Optional: Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/about/readme.txt) to your repository. For plugins, the contents of this file will be shown when the user clicks the "View version 1.2.3 details" link.
+3. Optional: Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/readme.txt) to your repository. For plugins, the contents of this file will be shown when the user clicks the "View version 1.2.3 details" link.
#### How to Release an Update
@@ -200,7 +202,7 @@ BitBucket doesn't have an equivalent to GitHub's releases, so the process is sli
- **`Stable tag` header**
- This is the recommended approach if you're using tags to mark each version. Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/about/readme.txt) to your repository. Set the "stable tag" header to the tag that represents the latest release. Example:
+ This is the recommended approach if you're using tags to mark each version. Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/readme.txt) to your repository. Set the "stable tag" header to the tag that represents the latest release. Example:
```text
Stable tag: v1.2.3
```
@@ -244,15 +246,24 @@ BitBucket doesn't have an equivalent to GitHub's releases, so the process is sli
Alternatively, if you're using a self-hosted GitLab instance, initialize the update checker like this:
```php
- $myUpdateChecker = new Puc_v4p6_Vcs_PluginUpdateChecker(
- new Puc_v4p6_Vcs_GitLabApi('https://myserver.com/user-name/repo-name/'),
+ $myUpdateChecker = new Puc_v4p11_Vcs_PluginUpdateChecker(
+ new Puc_v4p11_Vcs_GitLabApi('https://myserver.com/user-name/repo-name/'),
__FILE__,
'unique-plugin-or-theme-slug'
);
//Optional: Add setAuthentication(...) and setBranch(...) as shown above.
```
-
-3. Plugins only: Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/about/readme.txt) to your repository. The contents of this file will be shown when the user clicks the "View version 1.2.3 details" link.
+ If you're using a self-hosted GitLab instance and [subgroups or nested groups](https://docs.gitlab.com/ce/user/group/subgroups/index.html), you have to tell the update checker which parts of the URL are subgroups:
+ ```php
+ $myUpdateChecker = new Puc_v4p11_Vcs_PluginUpdateChecker(
+ new Puc_v4p11_Vcs_GitLabApi('https://myserver.com/group-name/subgroup-level1/subgroup-level2/subgroup-level3/repo-name/', null, 'subgroup-level1/subgroup-level2/subgroup-level3'),
+ __FILE__,
+ 'unique-plugin-or-theme-slug'
+ );
+
+ ```
+
+3. Plugins only: Add a `readme.txt` file formatted according to the [WordPress.org plugin readme standard](https://wordpress.org/plugins/readme.txt) to your repository. The contents of this file will be shown when the user clicks the "View version 1.2.3 details" link.
#### How to Release an Update
@@ -284,6 +295,7 @@ Resources
- [This blog post](http://w-shadow.com/blog/2010/09/02/automatic-updates-for-any-plugin/) has more information about the update checker API. *Slightly out of date.*
- [Debug Bar](https://wordpress.org/plugins/debug-bar/) - useful for testing and debugging the update checker.
+- [Update format reference](https://docs.google.com/spreadsheets/d/1eOBbW7Go2qEQXReOOCdidMTf_tDYRq4JfegcO1CBPIs/edit?usp=sharing) - describes all fields supported by the JSON-based update information format used by the update checker. Only covers plugins. Themes use a similar but more limited format.
- [Securing download links](http://w-shadow.com/blog/2013/03/19/plugin-updates-securing-download-links/) - a general overview.
- [A GUI for entering download credentials](http://open-tools.net/documentation/tutorial-automatic-updates.html#wordpress)
- [Theme Update Checker](http://w-shadow.com/blog/2011/06/02/automatic-updates-for-commercial-themes/) - an older, theme-only variant of this update checker.
diff --git a/core/updater/assets/plugin.json b/core/updater/assets/plugin.json
index 340a52d..7cb5c11 100755
--- a/core/updater/assets/plugin.json
+++ b/core/updater/assets/plugin.json
@@ -1,19 +1,19 @@
{
"name": "The Hanger Extender",
- "version": "1.7.1",
+ "version": "1.7.2",
"download_url": "https://github.com/getbowtied/the-hanger-extender/zipball/master",
"homepage": "https://themeforest.net/item/the-hanger-modern-classic-woocommerce-theme/21753302",
"requires": "5.0",
- "tested": "5.6",
- "last_updated": "2021-01-25 15:00:00",
+ "tested": "5.8",
+ "last_updated": "2021-09-06 14:00:00",
"author": "GetBowtied",
"author_homepage": "https://getbowtied.com/",
"sections": {
"description": "Companion plugin for the The Hanger theme. Extends the functionality by adding theme specific features.
Gutenberg Blocks:
- Image Slider
- Blog Posts
- Social Media Profiles
WPBakery Page Builder Elements:
- Slider
- Blog Posts
- Custom Button
Widgets:
- eCommerce Info
- Product Categories with Icon
Features:
- Adds 'Social Media' section in Customizer for easy management of links to social media profiles
- Adds social sharing options for the product page and blog post
- Display recently purchased products in My Account / Dashboard
- Adds header image and icon options for WooCommerce product categories
",
- "changelog": "1.7.1
- Small maintenance updates
1.7.0
- Small maintenance updates
1.6.9
- Fixed: Product category icon picker issue
- Maintenance: Fixed a few jQuery deprecated features warnings
1.6.8
- Maintenance: Updated Swiper library to its latest version
1.6.7
- WordPress 5.5 compatibility updates
1.6.6
- New: Option to show/hide product's social sharing buttons
- New: Social Media Customizer Control that allows for custom Icons and Social Profile links
- Fixed: eCommerce Info Widget customizer warnings
1.6.5
- Small maintenance updates
1.6.4
- Added 'alt' attribute to Product Category Header's image
1.6.3
- Fixed: Product categories with icon widget - error when icons are disabled
- WordPress 5.3.1 compatibility updates
1.6.2
- WordPress 5.3 compatibility updates
1.6.1
- Fixed: Social Sharing buttons display issue on smaller screens
1.6.0
- WordPress 5.3 compatibility updates
- Slider block style adjustments
1.5.9
- WPBakery Page Builder: Added Slider element font size options
- Blocks: Added Slider block font size options
1.5.8
- Small maintenance updates
1.5.7
- Adds: the 'Social Media' section in Customizer allowing for easy management of links to social media profiles. The icons can be turned on/off in the Top Bar or displayed in pages using the Gutenberg Block (functionality transferred from the theme)
- Adds: Upload header images and icons for WooCommerce product categories (functionality transferred from the theme)
1.5.6
- Adds: Theme-specific custom widgets: eCommerce Info and Product Categories with Icon
1.5.5
- Fixed: PHP 7.3 compatibility updates
1.5.4
- Fixed: Localization issues
1.5.3
- WordPress 5.1 compatibility improvements
1.5.2
1.5.1
- Fixed: Columns Block displaying issue
- Fixed: Yoast SEO compatibiity issue
1.5
- WordPress 5+ compatibility improvements
- Improved styles for theme neutrality
- Localization issues with the Posts Grid Block
1.4
- WordPress 5+ compatibility improvements
1.3
- Gutenberg Slider Block Fixes
1.2
- Gutenberg Compatibility
- Gutenberg Custom Blocks: Slider, Latest Posts Grid, Social Media Profiles
1.1
1.0
"
+ "changelog": "1.7.2
- WordPress 5.8 compatibility updates
1.7.1
- Small maintenance updates
1.7.0
- Small maintenance updates
1.6.9
- Fixed: Product category icon picker issue
- Maintenance: Fixed a few jQuery deprecated features warnings
1.6.8
- Maintenance: Updated Swiper library to its latest version
1.6.7
- WordPress 5.5 compatibility updates
1.6.6
- New: Option to show/hide product's social sharing buttons
- New: Social Media Customizer Control that allows for custom Icons and Social Profile links
- Fixed: eCommerce Info Widget customizer warnings
1.6.5
- Small maintenance updates
1.6.4
- Added 'alt' attribute to Product Category Header's image
1.6.3
- Fixed: Product categories with icon widget - error when icons are disabled
- WordPress 5.3.1 compatibility updates
1.6.2
- WordPress 5.3 compatibility updates
1.6.1
- Fixed: Social Sharing buttons display issue on smaller screens
1.6.0
- WordPress 5.3 compatibility updates
- Slider block style adjustments
1.5.9
- WPBakery Page Builder: Added Slider element font size options
- Blocks: Added Slider block font size options
1.5.8
- Small maintenance updates
1.5.7
- Adds: the 'Social Media' section in Customizer allowing for easy management of links to social media profiles. The icons can be turned on/off in the Top Bar or displayed in pages using the Gutenberg Block (functionality transferred from the theme)
- Adds: Upload header images and icons for WooCommerce product categories (functionality transferred from the theme)
1.5.6
- Adds: Theme-specific custom widgets: eCommerce Info and Product Categories with Icon
1.5.5
- Fixed: PHP 7.3 compatibility updates
1.5.4
- Fixed: Localization issues
1.5.3
- WordPress 5.1 compatibility improvements
1.5.2
1.5.1
- Fixed: Columns Block displaying issue
- Fixed: Yoast SEO compatibiity issue
1.5
- WordPress 5+ compatibility improvements
- Improved styles for theme neutrality
- Localization issues with the Posts Grid Block
1.4
- WordPress 5+ compatibility improvements
1.3
- Gutenberg Slider Block Fixes
1.2
- Gutenberg Compatibility
- Gutenberg Custom Blocks: Slider, Latest Posts Grid, Social Media Profiles
1.1
1.0
"
},
"icons" : {
diff --git a/core/updater/composer.json b/core/updater/composer.json
new file mode 100644
index 0000000..6064bd4
--- /dev/null
+++ b/core/updater/composer.json
@@ -0,0 +1,23 @@
+{
+ "name": "yahnis-elsts/plugin-update-checker",
+ "type": "library",
+ "description": "A custom update checker for WordPress plugins and themes. Useful if you can't host your plugin in the official WP repository but still want it to support automatic updates.",
+ "keywords": ["wordpress", "plugin updates", "automatic updates", "theme updates"],
+ "homepage": "https://github.com/YahnisElsts/plugin-update-checker/",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Yahnis Elsts",
+ "email": "whiteshadow@w-shadow.com",
+ "homepage": "http://w-shadow.com/",
+ "role": "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=5.2.0",
+ "ext-json": "*"
+ },
+ "autoload": {
+ "files": ["load-v4p11.php"]
+ }
+}
diff --git a/core/updater/css/puc-debug-bar.css b/core/updater/css/puc-debug-bar.css
old mode 100755
new mode 100644
diff --git a/core/updater/js/debug-bar.js b/core/updater/js/debug-bar.js
old mode 100755
new mode 100644
index b8435db..2452c02
--- a/core/updater/js/debug-bar.js
+++ b/core/updater/js/debug-bar.js
@@ -20,12 +20,12 @@ jQuery(function($) {
);
}
- $('.puc-debug-bar-panel-v4 input[name="puc-check-now-button"]').click(function() {
+ $('.puc-debug-bar-panel-v4 input[name="puc-check-now-button"]').on('click', function() {
runAjaxAction(this, 'puc_v4_debug_check_now');
return false;
});
- $('.puc-debug-bar-panel-v4 input[name="puc-request-info-button"]').click(function() {
+ $('.puc-debug-bar-panel-v4 input[name="puc-request-info-button"]').on('click', function() {
runAjaxAction(this, 'puc_v4_debug_request_info');
return false;
});
diff --git a/core/updater/languages/plugin-update-checker-ca.mo b/core/updater/languages/plugin-update-checker-ca.mo
new file mode 100644
index 0000000..59645fa
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-ca.mo differ
diff --git a/core/updater/languages/plugin-update-checker-ca.po b/core/updater/languages/plugin-update-checker-ca.po
new file mode 100644
index 0000000..36f3ad7
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-ca.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2019-09-25 18:15+0200\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.2.3\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"
+"Last-Translator: \n"
+"Language: ca\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprova si hi ha actualitzacions"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "L’extensió %s està actualitzada."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nova versió de l’extensió %s està disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No s’ha pogut determinar si hi ha actualitzacions per a %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estat del comprovador d’actualitzacions desconegut \"%s\""
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hi ha cap registre de canvis disponible."
diff --git a/core/updater/languages/plugin-update-checker-cs_CZ.mo b/core/updater/languages/plugin-update-checker-cs_CZ.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-cs_CZ.po b/core/updater/languages/plugin-update-checker-cs_CZ.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-da_DK.mo b/core/updater/languages/plugin-update-checker-da_DK.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-da_DK.po b/core/updater/languages/plugin-update-checker-da_DK.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-de_DE.mo b/core/updater/languages/plugin-update-checker-de_DE.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-de_DE.po b/core/updater/languages/plugin-update-checker-de_DE.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-es_AR.mo b/core/updater/languages/plugin-update-checker-es_AR.mo
new file mode 100644
index 0000000..85afecd
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_AR.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_AR.po b/core/updater/languages/plugin-update-checker-es_AR.po
new file mode 100644
index 0000000..80b1c1a
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_AR.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:13-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_CL.mo b/core/updater/languages/plugin-update-checker-es_CL.mo
new file mode 100644
index 0000000..de9c752
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_CL.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_CL.po b/core/updater/languages/plugin-update-checker-es_CL.po
new file mode 100644
index 0000000..1ab41b1
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_CL.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:14-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_CO.mo b/core/updater/languages/plugin-update-checker-es_CO.mo
new file mode 100644
index 0000000..de9c752
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_CO.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_CO.po b/core/updater/languages/plugin-update-checker-es_CO.po
new file mode 100644
index 0000000..1ab41b1
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_CO.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:14-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_CR.mo b/core/updater/languages/plugin-update-checker-es_CR.mo
new file mode 100644
index 0000000..de9c752
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_CR.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_CR.po b/core/updater/languages/plugin-update-checker-es_CR.po
new file mode 100644
index 0000000..1ab41b1
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_CR.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:14-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_DO.mo b/core/updater/languages/plugin-update-checker-es_DO.mo
new file mode 100644
index 0000000..de9c752
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_DO.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_DO.po b/core/updater/languages/plugin-update-checker-es_DO.po
new file mode 100644
index 0000000..1ab41b1
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_DO.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:14-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_ES.mo b/core/updater/languages/plugin-update-checker-es_ES.mo
new file mode 100644
index 0000000..1cf71f4
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_ES.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_ES.po b/core/updater/languages/plugin-update-checker-es_ES.po
new file mode 100644
index 0000000..c404f91
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_ES.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 14:56-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_GT.mo b/core/updater/languages/plugin-update-checker-es_GT.mo
new file mode 100644
index 0000000..de9c752
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_GT.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_GT.po b/core/updater/languages/plugin-update-checker-es_GT.po
new file mode 100644
index 0000000..1ab41b1
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_GT.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:14-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_HN.mo b/core/updater/languages/plugin-update-checker-es_HN.mo
new file mode 100644
index 0000000..de9c752
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_HN.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_HN.po b/core/updater/languages/plugin-update-checker-es_HN.po
new file mode 100644
index 0000000..1ab41b1
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_HN.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:14-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_MX.mo b/core/updater/languages/plugin-update-checker-es_MX.mo
new file mode 100644
index 0000000..9ce83f6
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_MX.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_MX.po b/core/updater/languages/plugin-update-checker-es_MX.po
new file mode 100644
index 0000000..0e29c45
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_MX.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 14:57-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_PE.mo b/core/updater/languages/plugin-update-checker-es_PE.mo
new file mode 100644
index 0000000..92c4f34
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_PE.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_PE.po b/core/updater/languages/plugin-update-checker-es_PE.po
new file mode 100644
index 0000000..c0db8e3
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_PE.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:15-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_PR.mo b/core/updater/languages/plugin-update-checker-es_PR.mo
new file mode 100644
index 0000000..92c4f34
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_PR.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_PR.po b/core/updater/languages/plugin-update-checker-es_PR.po
new file mode 100644
index 0000000..c0db8e3
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_PR.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:15-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_UY.mo b/core/updater/languages/plugin-update-checker-es_UY.mo
new file mode 100644
index 0000000..92c4f34
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_UY.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_UY.po b/core/updater/languages/plugin-update-checker-es_UY.po
new file mode 100644
index 0000000..c0db8e3
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_UY.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 15:15-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-es_VE.mo b/core/updater/languages/plugin-update-checker-es_VE.mo
new file mode 100644
index 0000000..9ce83f6
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-es_VE.mo differ
diff --git a/core/updater/languages/plugin-update-checker-es_VE.po b/core/updater/languages/plugin-update-checker-es_VE.po
new file mode 100644
index 0000000..0e29c45
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-es_VE.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-03-21 14:57-0400\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.3\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"
+"Last-Translator: \n"
+"Language: es_ES\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "El plugin %s está actualizado."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Una nueva versión del %s plugin está disponible."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Estado del comprobador de actualización desconocido «%s»"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "No hay un registro de cambios disponible."
diff --git a/core/updater/languages/plugin-update-checker-fa_IR.mo b/core/updater/languages/plugin-update-checker-fa_IR.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-fa_IR.po b/core/updater/languages/plugin-update-checker-fa_IR.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-fr_CA.mo b/core/updater/languages/plugin-update-checker-fr_CA.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-fr_CA.po b/core/updater/languages/plugin-update-checker-fr_CA.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-fr_FR.mo b/core/updater/languages/plugin-update-checker-fr_FR.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-fr_FR.po b/core/updater/languages/plugin-update-checker-fr_FR.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-hu_HU.mo b/core/updater/languages/plugin-update-checker-hu_HU.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-hu_HU.po b/core/updater/languages/plugin-update-checker-hu_HU.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-it_IT.mo b/core/updater/languages/plugin-update-checker-it_IT.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-it_IT.po b/core/updater/languages/plugin-update-checker-it_IT.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-ja.mo b/core/updater/languages/plugin-update-checker-ja.mo
old mode 100755
new mode 100644
index 376102a..941b6ba
Binary files a/core/updater/languages/plugin-update-checker-ja.mo and b/core/updater/languages/plugin-update-checker-ja.mo differ
diff --git a/core/updater/languages/plugin-update-checker-ja.po b/core/updater/languages/plugin-update-checker-ja.po
old mode 100755
new mode 100644
index 673c311..5a5c5ff
--- a/core/updater/languages/plugin-update-checker-ja.po
+++ b/core/updater/languages/plugin-update-checker-ja.po
@@ -1,42 +1,57 @@
msgid ""
msgstr ""
-"Project-Id-Version: plugin-update-checker\n"
-"POT-Creation-Date: 2017-06-02 18:31+0900\n"
-"PO-Revision-Date: 2017-06-02 18:32+0900\n"
+"Project-Id-Version: \n"
+"POT-Creation-Date: 2019-07-15 17:07+0900\n"
+"PO-Revision-Date: 2019-07-15 17:12+0900\n"
"Last-Translator: tak \n"
"Language-Team: \n"
"Language: ja_JP\n"
"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-Poedit-Basepath: ..\n"
+"X-Generator: Poedit 2.2.3\n"
+"X-Poedit-Basepath: ../../../../../../Applications/XAMPP/xamppfiles/htdocs/"
+"kisagai/wordpress/wp-content/plugins/simple-stripe-gateway/Puc\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Poedit-SourceCharset: UTF-8\n"
-"X-Poedit-KeywordsList: __;_e;_x;_x:1,2c\n"
+"X-Poedit-KeywordsList: __;_x:1,2c\n"
"X-Poedit-SearchPath-0: .\n"
-#: Puc/v4p1/Plugin/UpdateChecker.php:362
+#: v4p7/Plugin/Ui.php:54
+msgid "View details"
+msgstr "詳細を表示"
+
+#: v4p7/Plugin/Ui.php:77
+#, php-format
+msgid "More information about %s"
+msgstr "%sについての詳細"
+
+#: v4p7/Plugin/Ui.php:128
msgid "Check for updates"
msgstr "アップデートを確認"
-#: Puc/v4p1/Plugin/UpdateChecker.php:409
+#: v4p7/Plugin/Ui.php:213
#, php-format
msgctxt "the plugin title"
msgid "The %s plugin is up to date."
msgstr "%s プラグインは、最新バージョンです。"
-#: Puc/v4p1/Plugin/UpdateChecker.php:411
+#: v4p7/Plugin/Ui.php:215
#, php-format
msgctxt "the plugin title"
msgid "A new version of the %s plugin is available."
msgstr "%s プラグインの最新バージョンがあります。"
-#: Puc/v4p1/Plugin/UpdateChecker.php:413
+#: v4p7/Plugin/Ui.php:217
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "%s のアップデートがあるかどうかを判断できませんでした。"
+
+#: v4p7/Plugin/Ui.php:223
#, php-format
msgid "Unknown update checker status \"%s\""
-msgstr "バージョンアップの確認で想定外の状態になりました。ステータス:\"%s\""
+msgstr "バージョンアップの確認で想定外の状態になりました。ステータス:”%s”"
-#: Puc/v4p1/Vcs/PluginUpdateChecker.php:83
+#: v4p7/Vcs/PluginUpdateChecker.php:98
msgid "There is no changelog available."
msgstr "更新履歴はありません。"
diff --git a/core/updater/languages/plugin-update-checker-nl_BE.mo b/core/updater/languages/plugin-update-checker-nl_BE.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-nl_BE.po b/core/updater/languages/plugin-update-checker-nl_BE.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-nl_NL.mo b/core/updater/languages/plugin-update-checker-nl_NL.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-nl_NL.po b/core/updater/languages/plugin-update-checker-nl_NL.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-pt_BR.mo b/core/updater/languages/plugin-update-checker-pt_BR.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-pt_BR.po b/core/updater/languages/plugin-update-checker-pt_BR.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-sl_SI.mo b/core/updater/languages/plugin-update-checker-sl_SI.mo
new file mode 100644
index 0000000..df47ca7
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-sl_SI.mo differ
diff --git a/core/updater/languages/plugin-update-checker-sl_SI.po b/core/updater/languages/plugin-update-checker-sl_SI.po
new file mode 100644
index 0000000..385d801
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-sl_SI.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2018-10-27 20:36+0200\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.2\n"
+"X-Poedit-Basepath: ..\n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
+"Last-Translator: Igor Funa\n"
+"Language: sl_SI\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "Preveri posodobitve"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "Vtičnik %s je že posodobljen."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "Nova različica vtičnika %s je na razpolago."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "Ne morem ugotoviti če se za vtičnik %s na razpolago posodobitve."
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "Neznan status preverjanja posodobitev za \"%s\""
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "Dnevnik sprememb ni na razpolago."
diff --git a/core/updater/languages/plugin-update-checker-sv_SE.mo b/core/updater/languages/plugin-update-checker-sv_SE.mo
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-sv_SE.po b/core/updater/languages/plugin-update-checker-sv_SE.po
old mode 100755
new mode 100644
diff --git a/core/updater/languages/plugin-update-checker-zh_CN.mo b/core/updater/languages/plugin-update-checker-zh_CN.mo
new file mode 100644
index 0000000..c0fc405
Binary files /dev/null and b/core/updater/languages/plugin-update-checker-zh_CN.mo differ
diff --git a/core/updater/languages/plugin-update-checker-zh_CN.po b/core/updater/languages/plugin-update-checker-zh_CN.po
new file mode 100644
index 0000000..005b4d0
--- /dev/null
+++ b/core/updater/languages/plugin-update-checker-zh_CN.po
@@ -0,0 +1,48 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: plugin-update-checker\n"
+"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"PO-Revision-Date: 2020-08-04 08:10+0800\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.4\n"
+"X-Poedit-Basepath: ..\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
+"Last-Translator: Seaton Jiang \n"
+"Language: zh_CN\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:395
+msgid "Check for updates"
+msgstr "检查更新"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#, php-format
+msgctxt "the plugin title"
+msgid "The %s plugin is up to date."
+msgstr "%s 目前是最新版本。"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#, php-format
+msgctxt "the plugin title"
+msgid "A new version of the %s plugin is available."
+msgstr "%s 当前有可用的更新。"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#, php-format
+msgctxt "the plugin title"
+msgid "Could not determine if updates are available for %s."
+msgstr "%s 无法确定是否有可用的更新。"
+
+#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#, php-format
+msgid "Unknown update checker status \"%s\""
+msgstr "未知的更新检查状态:%s"
+
+#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+msgid "There is no changelog available."
+msgstr "没有可用的更新日志。"
diff --git a/core/updater/languages/plugin-update-checker.pot b/core/updater/languages/plugin-update-checker.pot
old mode 100755
new mode 100644
index 67f4f00..99cc24c
--- a/core/updater/languages/plugin-update-checker.pot
+++ b/core/updater/languages/plugin-update-checker.pot
@@ -2,7 +2,7 @@
msgid ""
msgstr ""
"Project-Id-Version: plugin-update-checker\n"
-"POT-Creation-Date: 2017-11-24 17:02+0200\n"
+"POT-Creation-Date: 2020-08-08 14:36+0300\n"
"PO-Revision-Date: 2016-01-10 20:59+0100\n"
"Last-Translator: Tamás András Horváth \n"
"Language-Team: \n"
@@ -10,40 +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.4\n"
+"X-Generator: Poedit 2.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/v4p3/Plugin/UpdateChecker.php:395
+#: Puc/v4p11/Plugin/Ui.php:128
msgid "Check for updates"
msgstr ""
-#: Puc/v4p3/Plugin/UpdateChecker.php:548
+#: Puc/v4p11/Plugin/Ui.php:213
#, php-format
msgctxt "the plugin title"
msgid "The %s plugin is up to date."
msgstr ""
-#: Puc/v4p3/Plugin/UpdateChecker.php:550
+#: Puc/v4p11/Plugin/Ui.php:215
#, php-format
msgctxt "the plugin title"
msgid "A new version of the %s plugin is available."
msgstr ""
-#: Puc/v4p3/Plugin/UpdateChecker.php:552
+#: Puc/v4p11/Plugin/Ui.php:217
#, php-format
msgctxt "the plugin title"
msgid "Could not determine if updates are available for %s."
msgstr ""
-#: Puc/v4p3/Plugin/UpdateChecker.php:558
+#: Puc/v4p11/Plugin/Ui.php:223
#, php-format
msgid "Unknown update checker status \"%s\""
msgstr ""
-#: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
+#: Puc/v4p11/Vcs/PluginUpdateChecker.php:98
msgid "There is no changelog available."
msgstr ""
diff --git a/core/updater/license.txt b/core/updater/license.txt
old mode 100755
new mode 100644
diff --git a/core/updater/load-v4p11.php b/core/updater/load-v4p11.php
new file mode 100644
index 0000000..76916cc
--- /dev/null
+++ b/core/updater/load-v4p11.php
@@ -0,0 +1,28 @@
+ 'Puc_v4p11_Plugin_UpdateChecker',
+ 'Theme_UpdateChecker' => 'Puc_v4p11_Theme_UpdateChecker',
+
+ 'Vcs_PluginUpdateChecker' => 'Puc_v4p11_Vcs_PluginUpdateChecker',
+ 'Vcs_ThemeUpdateChecker' => 'Puc_v4p11_Vcs_ThemeUpdateChecker',
+
+ 'GitHubApi' => 'Puc_v4p11_Vcs_GitHubApi',
+ 'BitBucketApi' => 'Puc_v4p11_Vcs_BitBucketApi',
+ 'GitLabApi' => 'Puc_v4p11_Vcs_GitLabApi',
+ )
+ as $pucGeneralClass => $pucVersionedClass
+) {
+ Puc_v4_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.11');
+ //Also add it to the minor-version factory in case the major-version factory
+ //was already defined by another, older version of the update checker.
+ Puc_v4p11_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.11');
+}
+
diff --git a/core/updater/plugin-update-checker.php b/core/updater/plugin-update-checker.php
old mode 100755
new mode 100644
index 01ec425..f6ae63f
--- a/core/updater/plugin-update-checker.php
+++ b/core/updater/plugin-update-checker.php
@@ -1,34 +1,10 @@
'Puc_v4p6_Plugin_UpdateChecker',
- 'Theme_UpdateChecker' => 'Puc_v4p6_Theme_UpdateChecker',
-
- 'Vcs_PluginUpdateChecker' => 'Puc_v4p6_Vcs_PluginUpdateChecker',
- 'Vcs_ThemeUpdateChecker' => 'Puc_v4p6_Vcs_ThemeUpdateChecker',
-
- 'GitHubApi' => 'Puc_v4p6_Vcs_GitHubApi',
- 'BitBucketApi' => 'Puc_v4p6_Vcs_BitBucketApi',
- 'GitLabApi' => 'Puc_v4p6_Vcs_GitLabApi',
- )
- as $pucGeneralClass => $pucVersionedClass
-) {
- Puc_v4_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.6');
- //Also add it to the minor-version factory in case the major-version factory
- //was already defined by another, older version of the update checker.
- Puc_v4p6_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.6');
-}
\ No newline at end of file
+require dirname(__FILE__) . '/load-v4p11.php';
\ No newline at end of file
diff --git a/core/updater/vendor/Parsedown.php b/core/updater/vendor/Parsedown.php
old mode 100755
new mode 100644
index 5d96071..6a07497
--- a/core/updater/vendor/Parsedown.php
+++ b/core/updater/vendor/Parsedown.php
@@ -1,1538 +1,9 @@
DefinitionData = array();
-
- # standardize line breaks
- $text = str_replace(array("\r\n", "\r"), "\n", $text);
-
- # remove surrounding line breaks
- $text = trim($text, "\n");
-
- # split text into lines
- $lines = explode("\n", $text);
-
- # iterate through lines to identify blocks
- $markup = $this->lines($lines);
-
- # trim line breaks
- $markup = trim($markup, "\n");
-
- return $markup;
- }
-
- #
- # Setters
- #
-
- function setBreaksEnabled($breaksEnabled)
- {
- $this->breaksEnabled = $breaksEnabled;
-
- return $this;
- }
-
- protected $breaksEnabled;
-
- function setMarkupEscaped($markupEscaped)
- {
- $this->markupEscaped = $markupEscaped;
-
- return $this;
- }
-
- protected $markupEscaped;
-
- function setUrlsLinked($urlsLinked)
- {
- $this->urlsLinked = $urlsLinked;
-
- return $this;
- }
-
- protected $urlsLinked = true;
-
- #
- # Lines
- #
-
- protected $BlockTypes = array(
- '#' => array('Header'),
- '*' => array('Rule', 'List'),
- '+' => array('List'),
- '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
- '0' => array('List'),
- '1' => array('List'),
- '2' => array('List'),
- '3' => array('List'),
- '4' => array('List'),
- '5' => array('List'),
- '6' => array('List'),
- '7' => array('List'),
- '8' => array('List'),
- '9' => array('List'),
- ':' => array('Table'),
- '<' => array('Comment', 'Markup'),
- '=' => array('SetextHeader'),
- '>' => array('Quote'),
- '[' => array('Reference'),
- '_' => array('Rule'),
- '`' => array('FencedCode'),
- '|' => array('Table'),
- '~' => array('FencedCode'),
- );
-
- # ~
-
- protected $unmarkedBlockTypes = array(
- 'Code',
- );
-
- #
- # Blocks
- #
-
- protected function lines(array $lines)
- {
- $CurrentBlock = null;
-
- foreach ($lines as $line)
- {
- if (chop($line) === '')
- {
- if (isset($CurrentBlock))
- {
- $CurrentBlock['interrupted'] = true;
- }
-
- continue;
- }
-
- if (strpos($line, "\t") !== false)
- {
- $parts = explode("\t", $line);
-
- $line = $parts[0];
-
- unset($parts[0]);
-
- foreach ($parts as $part)
- {
- $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
-
- $line .= str_repeat(' ', $shortage);
- $line .= $part;
- }
- }
-
- $indent = 0;
-
- while (isset($line[$indent]) and $line[$indent] === ' ')
- {
- $indent ++;
- }
-
- $text = $indent > 0 ? substr($line, $indent) : $line;
-
- # ~
-
- $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
-
- # ~
-
- if (isset($CurrentBlock['continuable']))
- {
- $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
-
- if (isset($Block))
- {
- $CurrentBlock = $Block;
-
- continue;
- }
- else
- {
- if ($this->isBlockCompletable($CurrentBlock['type']))
- {
- $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
- }
- }
- }
-
- # ~
-
- $marker = $text[0];
-
- # ~
-
- $blockTypes = $this->unmarkedBlockTypes;
-
- if (isset($this->BlockTypes[$marker]))
- {
- foreach ($this->BlockTypes[$marker] as $blockType)
- {
- $blockTypes []= $blockType;
- }
- }
-
- #
- # ~
-
- foreach ($blockTypes as $blockType)
- {
- $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
-
- if (isset($Block))
- {
- $Block['type'] = $blockType;
-
- if ( ! isset($Block['identified']))
- {
- $Blocks []= $CurrentBlock;
-
- $Block['identified'] = true;
- }
-
- if ($this->isBlockContinuable($blockType))
- {
- $Block['continuable'] = true;
- }
-
- $CurrentBlock = $Block;
-
- continue 2;
- }
- }
-
- # ~
-
- if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
- {
- $CurrentBlock['element']['text'] .= "\n".$text;
- }
- else
- {
- $Blocks []= $CurrentBlock;
-
- $CurrentBlock = $this->paragraph($Line);
-
- $CurrentBlock['identified'] = true;
- }
- }
-
- # ~
-
- if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
- {
- $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
- }
-
- # ~
-
- $Blocks []= $CurrentBlock;
-
- unset($Blocks[0]);
-
- # ~
-
- $markup = '';
-
- foreach ($Blocks as $Block)
- {
- if (isset($Block['hidden']))
- {
- continue;
- }
-
- $markup .= "\n";
- $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
- }
-
- $markup .= "\n";
-
- # ~
-
- return $markup;
- }
-
- protected function isBlockContinuable($Type)
- {
- return method_exists($this, 'block'.$Type.'Continue');
- }
-
- protected function isBlockCompletable($Type)
- {
- return method_exists($this, 'block'.$Type.'Complete');
- }
-
- #
- # Code
-
- protected function blockCode($Line, $Block = null)
- {
- if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
- {
- return;
- }
-
- if ($Line['indent'] >= 4)
- {
- $text = substr($Line['body'], 4);
-
- $Block = array(
- 'element' => array(
- 'name' => 'pre',
- 'handler' => 'element',
- 'text' => array(
- 'name' => 'code',
- 'text' => $text,
- ),
- ),
- );
-
- return $Block;
- }
- }
-
- protected function blockCodeContinue($Line, $Block)
- {
- if ($Line['indent'] >= 4)
- {
- if (isset($Block['interrupted']))
- {
- $Block['element']['text']['text'] .= "\n";
-
- unset($Block['interrupted']);
- }
-
- $Block['element']['text']['text'] .= "\n";
-
- $text = substr($Line['body'], 4);
-
- $Block['element']['text']['text'] .= $text;
-
- return $Block;
- }
- }
-
- protected function blockCodeComplete($Block)
- {
- $text = $Block['element']['text']['text'];
-
- $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
-
- $Block['element']['text']['text'] = $text;
-
- return $Block;
- }
-
- #
- # Comment
-
- protected function blockComment($Line)
- {
- if ($this->markupEscaped)
- {
- return;
- }
-
- if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
- {
- $Block = array(
- 'markup' => $Line['body'],
- );
-
- if (preg_match('/-->$/', $Line['text']))
- {
- $Block['closed'] = true;
- }
-
- return $Block;
- }
- }
-
- protected function blockCommentContinue($Line, array $Block)
- {
- if (isset($Block['closed']))
- {
- return;
- }
-
- $Block['markup'] .= "\n" . $Line['body'];
-
- if (preg_match('/-->$/', $Line['text']))
- {
- $Block['closed'] = true;
- }
-
- return $Block;
- }
-
- #
- # Fenced Code
-
- protected function blockFencedCode($Line)
- {
- if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
- {
- $Element = array(
- 'name' => 'code',
- 'text' => '',
- );
-
- if (isset($matches[1]))
- {
- $class = 'language-'.$matches[1];
-
- $Element['attributes'] = array(
- 'class' => $class,
- );
- }
-
- $Block = array(
- 'char' => $Line['text'][0],
- 'element' => array(
- 'name' => 'pre',
- 'handler' => 'element',
- 'text' => $Element,
- ),
- );
-
- return $Block;
- }
- }
-
- protected function blockFencedCodeContinue($Line, $Block)
- {
- if (isset($Block['complete']))
- {
- return;
- }
-
- if (isset($Block['interrupted']))
- {
- $Block['element']['text']['text'] .= "\n";
-
- unset($Block['interrupted']);
- }
-
- if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
- {
- $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
-
- $Block['complete'] = true;
-
- return $Block;
- }
-
- $Block['element']['text']['text'] .= "\n".$Line['body'];;
-
- return $Block;
- }
-
- protected function blockFencedCodeComplete($Block)
- {
- $text = $Block['element']['text']['text'];
-
- $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
-
- $Block['element']['text']['text'] = $text;
-
- return $Block;
- }
-
- #
- # Header
-
- protected function blockHeader($Line)
- {
- if (isset($Line['text'][1]))
- {
- $level = 1;
-
- while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
- {
- $level ++;
- }
-
- if ($level > 6)
- {
- return;
- }
-
- $text = trim($Line['text'], '# ');
-
- $Block = array(
- 'element' => array(
- 'name' => 'h' . min(6, $level),
- 'text' => $text,
- 'handler' => 'line',
- ),
- );
-
- return $Block;
- }
- }
-
- #
- # List
-
- protected function blockList($Line)
- {
- list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
-
- if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
- {
- $Block = array(
- 'indent' => $Line['indent'],
- 'pattern' => $pattern,
- 'element' => array(
- 'name' => $name,
- 'handler' => 'elements',
- ),
- );
-
- $Block['li'] = array(
- 'name' => 'li',
- 'handler' => 'li',
- 'text' => array(
- $matches[2],
- ),
- );
-
- $Block['element']['text'] []= & $Block['li'];
-
- return $Block;
- }
- }
-
- protected function blockListContinue($Line, array $Block)
- {
- if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
- {
- if (isset($Block['interrupted']))
- {
- $Block['li']['text'] []= '';
-
- unset($Block['interrupted']);
- }
-
- unset($Block['li']);
-
- $text = isset($matches[1]) ? $matches[1] : '';
-
- $Block['li'] = array(
- 'name' => 'li',
- 'handler' => 'li',
- 'text' => array(
- $text,
- ),
- );
-
- $Block['element']['text'] []= & $Block['li'];
-
- return $Block;
- }
-
- if ($Line['text'][0] === '[' and $this->blockReference($Line))
- {
- return $Block;
- }
-
- if ( ! isset($Block['interrupted']))
- {
- $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
-
- $Block['li']['text'] []= $text;
-
- return $Block;
- }
-
- if ($Line['indent'] > 0)
- {
- $Block['li']['text'] []= '';
-
- $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
-
- $Block['li']['text'] []= $text;
-
- unset($Block['interrupted']);
-
- return $Block;
- }
- }
-
- #
- # Quote
-
- protected function blockQuote($Line)
- {
- if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
- {
- $Block = array(
- 'element' => array(
- 'name' => 'blockquote',
- 'handler' => 'lines',
- 'text' => (array) $matches[1],
- ),
- );
-
- return $Block;
- }
- }
-
- protected function blockQuoteContinue($Line, array $Block)
- {
- if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
- {
- if (isset($Block['interrupted']))
- {
- $Block['element']['text'] []= '';
-
- unset($Block['interrupted']);
- }
-
- $Block['element']['text'] []= $matches[1];
-
- return $Block;
- }
-
- if ( ! isset($Block['interrupted']))
- {
- $Block['element']['text'] []= $Line['text'];
-
- return $Block;
- }
- }
-
- #
- # Rule
-
- protected function blockRule($Line)
- {
- if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
- {
- $Block = array(
- 'element' => array(
- 'name' => 'hr'
- ),
- );
-
- return $Block;
- }
- }
-
- #
- # Setext
-
- protected function blockSetextHeader($Line, array $Block = null)
- {
- if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
- {
- return;
- }
-
- if (chop($Line['text'], $Line['text'][0]) === '')
- {
- $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
-
- return $Block;
- }
- }
-
- #
- # Markup
-
- protected function blockMarkup($Line)
- {
- if ($this->markupEscaped)
- {
- return;
- }
-
- if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
- {
- $element = strtolower($matches[1]);
-
- if (in_array($element, $this->textLevelElements))
- {
- return;
- }
-
- $Block = array(
- 'name' => $matches[1],
- 'depth' => 0,
- 'markup' => $Line['text'],
- );
-
- $length = strlen($matches[0]);
-
- $remainder = substr($Line['text'], $length);
-
- if (trim($remainder) === '')
- {
- if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
- {
- $Block['closed'] = true;
-
- $Block['void'] = true;
- }
- }
- else
- {
- if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
- {
- return;
- }
-
- if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
- {
- $Block['closed'] = true;
- }
- }
-
- return $Block;
- }
- }
-
- protected function blockMarkupContinue($Line, array $Block)
- {
- if (isset($Block['closed']))
- {
- return;
- }
-
- if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
- {
- $Block['depth'] ++;
- }
-
- if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
- {
- if ($Block['depth'] > 0)
- {
- $Block['depth'] --;
- }
- else
- {
- $Block['closed'] = true;
- }
- }
-
- if (isset($Block['interrupted']))
- {
- $Block['markup'] .= "\n";
-
- unset($Block['interrupted']);
- }
-
- $Block['markup'] .= "\n".$Line['body'];
-
- return $Block;
- }
-
- #
- # Reference
-
- protected function blockReference($Line)
- {
- if (preg_match('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
- {
- $id = strtolower($matches[1]);
-
- $Data = array(
- 'url' => $matches[2],
- 'title' => null,
- );
-
- if (isset($matches[3]))
- {
- $Data['title'] = $matches[3];
- }
-
- $this->DefinitionData['Reference'][$id] = $Data;
-
- $Block = array(
- 'hidden' => true,
- );
-
- return $Block;
- }
- }
-
- #
- # Table
-
- protected function blockTable($Line, array $Block = null)
- {
- if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
- {
- return;
- }
-
- if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
- {
- $alignments = array();
-
- $divider = $Line['text'];
-
- $divider = trim($divider);
- $divider = trim($divider, '|');
-
- $dividerCells = explode('|', $divider);
-
- foreach ($dividerCells as $dividerCell)
- {
- $dividerCell = trim($dividerCell);
-
- if ($dividerCell === '')
- {
- continue;
- }
-
- $alignment = null;
-
- if ($dividerCell[0] === ':')
- {
- $alignment = 'left';
- }
-
- if (substr($dividerCell, - 1) === ':')
- {
- $alignment = $alignment === 'left' ? 'center' : 'right';
- }
-
- $alignments []= $alignment;
- }
-
- # ~
-
- $HeaderElements = array();
-
- $header = $Block['element']['text'];
-
- $header = trim($header);
- $header = trim($header, '|');
-
- $headerCells = explode('|', $header);
-
- foreach ($headerCells as $index => $headerCell)
- {
- $headerCell = trim($headerCell);
-
- $HeaderElement = array(
- 'name' => 'th',
- 'text' => $headerCell,
- 'handler' => 'line',
- );
-
- if (isset($alignments[$index]))
- {
- $alignment = $alignments[$index];
-
- $HeaderElement['attributes'] = array(
- 'style' => 'text-align: '.$alignment.';',
- );
- }
-
- $HeaderElements []= $HeaderElement;
- }
-
- # ~
-
- $Block = array(
- 'alignments' => $alignments,
- 'identified' => true,
- 'element' => array(
- 'name' => 'table',
- 'handler' => 'elements',
- ),
- );
-
- $Block['element']['text'] []= array(
- 'name' => 'thead',
- 'handler' => 'elements',
- );
-
- $Block['element']['text'] []= array(
- 'name' => 'tbody',
- 'handler' => 'elements',
- 'text' => array(),
- );
-
- $Block['element']['text'][0]['text'] []= array(
- 'name' => 'tr',
- 'handler' => 'elements',
- 'text' => $HeaderElements,
- );
-
- return $Block;
- }
- }
-
- protected function blockTableContinue($Line, array $Block)
- {
- if (isset($Block['interrupted']))
- {
- return;
- }
-
- if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
- {
- $Elements = array();
-
- $row = $Line['text'];
-
- $row = trim($row);
- $row = trim($row, '|');
-
- preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
-
- foreach ($matches[0] as $index => $cell)
- {
- $cell = trim($cell);
-
- $Element = array(
- 'name' => 'td',
- 'handler' => 'line',
- 'text' => $cell,
- );
-
- if (isset($Block['alignments'][$index]))
- {
- $Element['attributes'] = array(
- 'style' => 'text-align: '.$Block['alignments'][$index].';',
- );
- }
-
- $Elements []= $Element;
- }
-
- $Element = array(
- 'name' => 'tr',
- 'handler' => 'elements',
- 'text' => $Elements,
- );
-
- $Block['element']['text'][1]['text'] []= $Element;
-
- return $Block;
- }
- }
-
- #
- # ~
- #
-
- protected function paragraph($Line)
- {
- $Block = array(
- 'element' => array(
- 'name' => 'p',
- 'text' => $Line['text'],
- 'handler' => 'line',
- ),
- );
-
- return $Block;
- }
-
- #
- # Inline Elements
- #
-
- protected $InlineTypes = array(
- '"' => array('SpecialCharacter'),
- '!' => array('Image'),
- '&' => array('SpecialCharacter'),
- '*' => array('Emphasis'),
- ':' => array('Url'),
- '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
- '>' => array('SpecialCharacter'),
- '[' => array('Link'),
- '_' => array('Emphasis'),
- '`' => array('Code'),
- '~' => array('Strikethrough'),
- '\\' => array('EscapeSequence'),
- );
-
- # ~
-
- protected $inlineMarkerList = '!"*_&[:<>`~\\';
-
- #
- # ~
- #
-
- public function line($text)
- {
- $markup = '';
-
- # $excerpt is based on the first occurrence of a marker
-
- while ($excerpt = strpbrk($text, $this->inlineMarkerList))
- {
- $marker = $excerpt[0];
-
- $markerPosition = strpos($text, $marker);
-
- $Excerpt = array('text' => $excerpt, 'context' => $text);
-
- foreach ($this->InlineTypes[$marker] as $inlineType)
- {
- $Inline = $this->{'inline'.$inlineType}($Excerpt);
-
- if ( ! isset($Inline))
- {
- continue;
- }
-
- # makes sure that the inline belongs to "our" marker
-
- if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
- {
- continue;
- }
-
- # sets a default inline position
-
- if ( ! isset($Inline['position']))
- {
- $Inline['position'] = $markerPosition;
- }
-
- # the text that comes before the inline
- $unmarkedText = substr($text, 0, $Inline['position']);
-
- # compile the unmarked text
- $markup .= $this->unmarkedText($unmarkedText);
-
- # compile the inline
- $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
-
- # remove the examined text
- $text = substr($text, $Inline['position'] + $Inline['extent']);
-
- continue 2;
- }
-
- # the marker does not belong to an inline
-
- $unmarkedText = substr($text, 0, $markerPosition + 1);
-
- $markup .= $this->unmarkedText($unmarkedText);
-
- $text = substr($text, $markerPosition + 1);
- }
-
- $markup .= $this->unmarkedText($text);
-
- return $markup;
- }
-
- #
- # ~
- #
-
- protected function inlineCode($Excerpt)
- {
- $marker = $Excerpt['text'][0];
-
- if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]),
- 'element' => array(
- 'name' => 'code',
- 'text' => $text,
- ),
- );
- }
- }
-
- protected function inlineEmailTag($Excerpt)
- {
- if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
- {
- $url = $matches[1];
-
- if ( ! isset($matches[2]))
- {
- $url = 'mailto:' . $url;
- }
-
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => 'a',
- 'text' => $matches[1],
- 'attributes' => array(
- 'href' => $url,
- ),
- ),
- );
- }
- }
-
- protected function inlineEmphasis($Excerpt)
- {
- if ( ! isset($Excerpt['text'][1]))
- {
- return;
- }
-
- $marker = $Excerpt['text'][0];
-
- if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
- {
- $emphasis = 'strong';
- }
- elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
- {
- $emphasis = 'em';
- }
- else
- {
- return;
- }
-
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => $emphasis,
- 'handler' => 'line',
- 'text' => $matches[1],
- ),
- );
- }
-
- protected function inlineEscapeSequence($Excerpt)
- {
- if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
- {
- return array(
- 'markup' => $Excerpt['text'][1],
- 'extent' => 2,
- );
- }
- }
-
- protected function inlineImage($Excerpt)
- {
- if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
- {
- return;
- }
-
- $Excerpt['text']= substr($Excerpt['text'], 1);
-
- $Link = $this->inlineLink($Excerpt);
-
- if ($Link === null)
- {
- return;
- }
-
- $Inline = array(
- 'extent' => $Link['extent'] + 1,
- 'element' => array(
- 'name' => 'img',
- 'attributes' => array(
- 'src' => $Link['element']['attributes']['href'],
- 'alt' => $Link['element']['text'],
- ),
- ),
- );
-
- $Inline['element']['attributes'] += $Link['element']['attributes'];
-
- unset($Inline['element']['attributes']['href']);
-
- return $Inline;
- }
-
- protected function inlineLink($Excerpt)
- {
- $Element = array(
- 'name' => 'a',
- 'handler' => 'line',
- 'text' => null,
- 'attributes' => array(
- 'href' => null,
- 'title' => null,
- ),
- );
-
- $extent = 0;
-
- $remainder = $Excerpt['text'];
-
- if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
- {
- $Element['text'] = $matches[1];
-
- $extent += strlen($matches[0]);
-
- $remainder = substr($remainder, $extent);
- }
- else
- {
- return;
- }
-
- if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
- {
- $Element['attributes']['href'] = $matches[1];
-
- if (isset($matches[2]))
- {
- $Element['attributes']['title'] = substr($matches[2], 1, - 1);
- }
-
- $extent += strlen($matches[0]);
- }
- else
- {
- if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
- {
- $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
- $definition = strtolower($definition);
-
- $extent += strlen($matches[0]);
- }
- else
- {
- $definition = strtolower($Element['text']);
- }
-
- if ( ! isset($this->DefinitionData['Reference'][$definition]))
- {
- return;
- }
-
- $Definition = $this->DefinitionData['Reference'][$definition];
-
- $Element['attributes']['href'] = $Definition['url'];
- $Element['attributes']['title'] = $Definition['title'];
- }
-
- $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']);
-
- return array(
- 'extent' => $extent,
- 'element' => $Element,
- );
- }
-
- protected function inlineMarkup($Excerpt)
- {
- if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
- {
- return;
- }
-
- if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
- {
- return array(
- 'markup' => $matches[0],
- 'extent' => strlen($matches[0]),
- );
- }
-
- if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches))
- {
- return array(
- 'markup' => $matches[0],
- 'extent' => strlen($matches[0]),
- );
- }
-
- if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
- {
- return array(
- 'markup' => $matches[0],
- 'extent' => strlen($matches[0]),
- );
- }
- }
-
- protected function inlineSpecialCharacter($Excerpt)
- {
- if ($Excerpt['text'][0] === '&' and ! preg_match('/^?\w+;/', $Excerpt['text']))
- {
- return array(
- 'markup' => '&',
- 'extent' => 1,
- );
- }
-
- $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
-
- if (isset($SpecialCharacter[$Excerpt['text'][0]]))
- {
- return array(
- 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
- 'extent' => 1,
- );
- }
- }
-
- protected function inlineStrikethrough($Excerpt)
- {
- if ( ! isset($Excerpt['text'][1]))
- {
- return;
- }
-
- if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
- {
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => 'del',
- 'text' => $matches[1],
- 'handler' => 'line',
- ),
- );
- }
- }
-
- protected function inlineUrl($Excerpt)
- {
- if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
- {
- return;
- }
-
- if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
- {
- $Inline = array(
- 'extent' => strlen($matches[0][0]),
- 'position' => $matches[0][1],
- 'element' => array(
- 'name' => 'a',
- 'text' => $matches[0][0],
- 'attributes' => array(
- 'href' => $matches[0][0],
- ),
- ),
- );
-
- return $Inline;
- }
- }
-
- protected function inlineUrlTag($Excerpt)
- {
- if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
- {
- $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]);
-
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => 'a',
- 'text' => $url,
- 'attributes' => array(
- 'href' => $url,
- ),
- ),
- );
- }
- }
-
- # ~
-
- protected function unmarkedText($text)
- {
- if ($this->breaksEnabled)
- {
- $text = preg_replace('/[ ]*\n/', "
\n", $text);
- }
- else
- {
- $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text);
- $text = str_replace(" \n", "\n", $text);
- }
-
- return $text;
- }
-
- #
- # Handlers
- #
-
- protected function element(array $Element)
- {
- $markup = '<'.$Element['name'];
-
- if (isset($Element['attributes']))
- {
- foreach ($Element['attributes'] as $name => $value)
- {
- if ($value === null)
- {
- continue;
- }
-
- $markup .= ' '.$name.'="'.$value.'"';
- }
- }
-
- if (isset($Element['text']))
- {
- $markup .= '>';
-
- if (isset($Element['handler']))
- {
- $markup .= $this->{$Element['handler']}($Element['text']);
- }
- else
- {
- $markup .= $Element['text'];
- }
-
- $markup .= ''.$Element['name'].'>';
- }
- else
- {
- $markup .= ' />';
- }
-
- return $markup;
- }
-
- protected function elements(array $Elements)
- {
- $markup = '';
-
- foreach ($Elements as $Element)
- {
- $markup .= "\n" . $this->element($Element);
- }
-
- $markup .= "\n";
-
- return $markup;
- }
-
- # ~
-
- protected function li($lines)
- {
- $markup = $this->lines($lines);
-
- $trimmedMarkup = trim($markup);
-
- if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '')
- {
- $markup = $trimmedMarkup;
- $markup = substr($markup, 3);
-
- $position = strpos($markup, "
");
-
- $markup = substr_replace($markup, '', $position, 4);
- }
-
- return $markup;
- }
-
- #
- # Deprecated Methods
- #
-
- function parse($text)
- {
- $markup = $this->text($text);
-
- return $markup;
- }
-
- #
- # Static Methods
- #
-
- static function instance($name = 'default')
- {
- if (isset(self::$instances[$name]))
- {
- return self::$instances[$name];
- }
-
- $instance = new static();
-
- self::$instances[$name] = $instance;
-
- return $instance;
- }
-
- private static $instances = array();
-
- #
- # Fields
- #
-
- protected $DefinitionData;
-
- #
- # Read-Only
-
- protected $specialCharacters = array(
- '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
- );
-
- protected $StrongRegex = array(
- '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
- '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
- );
-
- protected $EmRegex = array(
- '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
- '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
- );
-
- protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
-
- protected $voidElements = array(
- 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
- );
-
- protected $textLevelElements = array(
- 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
- 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
- 'i', 'rp', 'del', 'code', 'strike', 'marquee',
- 'q', 'rt', 'ins', 'font', 'strong',
- 's', 'tt', 'sub', 'mark',
- 'u', 'xm', 'sup', 'nobr',
- 'var', 'ruby',
- 'wbr', 'span',
- 'time',
- );
-}
\ No newline at end of file
+if ( !class_exists('Parsedown', false) ) {
+ //Load the Parsedown version that's compatible with the current PHP version.
+ if ( version_compare(PHP_VERSION, '5.3.0', '>=') ) {
+ require __DIR__ . '/ParsedownModern.php';
+ } else {
+ require __DIR__ . '/ParsedownLegacy.php';
+ }
+}
diff --git a/core/updater/vendor/ParsedownLegacy.php b/core/updater/vendor/ParsedownLegacy.php
old mode 100755
new mode 100644
diff --git a/core/updater/vendor/ParsedownModern.php b/core/updater/vendor/ParsedownModern.php
new file mode 100644
index 0000000..5d96071
--- /dev/null
+++ b/core/updater/vendor/ParsedownModern.php
@@ -0,0 +1,1538 @@
+DefinitionData = array();
+
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ # remove surrounding line breaks
+ $text = trim($text, "\n");
+
+ # split text into lines
+ $lines = explode("\n", $text);
+
+ # iterate through lines to identify blocks
+ $markup = $this->lines($lines);
+
+ # trim line breaks
+ $markup = trim($markup, "\n");
+
+ return $markup;
+ }
+
+ #
+ # Setters
+ #
+
+ function setBreaksEnabled($breaksEnabled)
+ {
+ $this->breaksEnabled = $breaksEnabled;
+
+ return $this;
+ }
+
+ protected $breaksEnabled;
+
+ function setMarkupEscaped($markupEscaped)
+ {
+ $this->markupEscaped = $markupEscaped;
+
+ return $this;
+ }
+
+ protected $markupEscaped;
+
+ function setUrlsLinked($urlsLinked)
+ {
+ $this->urlsLinked = $urlsLinked;
+
+ return $this;
+ }
+
+ protected $urlsLinked = true;
+
+ #
+ # Lines
+ #
+
+ protected $BlockTypes = array(
+ '#' => array('Header'),
+ '*' => array('Rule', 'List'),
+ '+' => array('List'),
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
+ '0' => array('List'),
+ '1' => array('List'),
+ '2' => array('List'),
+ '3' => array('List'),
+ '4' => array('List'),
+ '5' => array('List'),
+ '6' => array('List'),
+ '7' => array('List'),
+ '8' => array('List'),
+ '9' => array('List'),
+ ':' => array('Table'),
+ '<' => array('Comment', 'Markup'),
+ '=' => array('SetextHeader'),
+ '>' => array('Quote'),
+ '[' => array('Reference'),
+ '_' => array('Rule'),
+ '`' => array('FencedCode'),
+ '|' => array('Table'),
+ '~' => array('FencedCode'),
+ );
+
+ # ~
+
+ protected $unmarkedBlockTypes = array(
+ 'Code',
+ );
+
+ #
+ # Blocks
+ #
+
+ protected function lines(array $lines)
+ {
+ $CurrentBlock = null;
+
+ foreach ($lines as $line)
+ {
+ if (chop($line) === '')
+ {
+ if (isset($CurrentBlock))
+ {
+ $CurrentBlock['interrupted'] = true;
+ }
+
+ continue;
+ }
+
+ if (strpos($line, "\t") !== false)
+ {
+ $parts = explode("\t", $line);
+
+ $line = $parts[0];
+
+ unset($parts[0]);
+
+ foreach ($parts as $part)
+ {
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
+
+ $line .= str_repeat(' ', $shortage);
+ $line .= $part;
+ }
+ }
+
+ $indent = 0;
+
+ while (isset($line[$indent]) and $line[$indent] === ' ')
+ {
+ $indent ++;
+ }
+
+ $text = $indent > 0 ? substr($line, $indent) : $line;
+
+ # ~
+
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']))
+ {
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $CurrentBlock = $Block;
+
+ continue;
+ }
+ else
+ {
+ if ($this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+ }
+ }
+ }
+
+ # ~
+
+ $marker = $text[0];
+
+ # ~
+
+ $blockTypes = $this->unmarkedBlockTypes;
+
+ if (isset($this->BlockTypes[$marker]))
+ {
+ foreach ($this->BlockTypes[$marker] as $blockType)
+ {
+ $blockTypes []= $blockType;
+ }
+ }
+
+ #
+ # ~
+
+ foreach ($blockTypes as $blockType)
+ {
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $Block['type'] = $blockType;
+
+ if ( ! isset($Block['identified']))
+ {
+ $Blocks []= $CurrentBlock;
+
+ $Block['identified'] = true;
+ }
+
+ if ($this->isBlockContinuable($blockType))
+ {
+ $Block['continuable'] = true;
+ }
+
+ $CurrentBlock = $Block;
+
+ continue 2;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
+ {
+ $CurrentBlock['element']['text'] .= "\n".$text;
+ }
+ else
+ {
+ $Blocks []= $CurrentBlock;
+
+ $CurrentBlock = $this->paragraph($Line);
+
+ $CurrentBlock['identified'] = true;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+ }
+
+ # ~
+
+ $Blocks []= $CurrentBlock;
+
+ unset($Blocks[0]);
+
+ # ~
+
+ $markup = '';
+
+ foreach ($Blocks as $Block)
+ {
+ if (isset($Block['hidden']))
+ {
+ continue;
+ }
+
+ $markup .= "\n";
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
+ }
+
+ $markup .= "\n";
+
+ # ~
+
+ return $markup;
+ }
+
+ protected function isBlockContinuable($Type)
+ {
+ return method_exists($this, 'block'.$Type.'Continue');
+ }
+
+ protected function isBlockCompletable($Type)
+ {
+ return method_exists($this, 'block'.$Type.'Complete');
+ }
+
+ #
+ # Code
+
+ protected function blockCode($Line, $Block = null)
+ {
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['indent'] >= 4)
+ {
+ $text = substr($Line['body'], 4);
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'pre',
+ 'handler' => 'element',
+ 'text' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeContinue($Line, $Block)
+ {
+ if ($Line['indent'] >= 4)
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text']['text'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['text']['text'] .= "\n";
+
+ $text = substr($Line['body'], 4);
+
+ $Block['element']['text']['text'] .= $text;
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeComplete($Block)
+ {
+ $text = $Block['element']['text']['text'];
+
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+
+ $Block['element']['text']['text'] = $text;
+
+ return $Block;
+ }
+
+ #
+ # Comment
+
+ protected function blockComment($Line)
+ {
+ if ($this->markupEscaped)
+ {
+ return;
+ }
+
+ if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
+ {
+ $Block = array(
+ 'markup' => $Line['body'],
+ );
+
+ if (preg_match('/-->$/', $Line['text']))
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockCommentContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ $Block['markup'] .= "\n" . $Line['body'];
+
+ if (preg_match('/-->$/', $Line['text']))
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+
+ #
+ # Fenced Code
+
+ protected function blockFencedCode($Line)
+ {
+ if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
+ {
+ $Element = array(
+ 'name' => 'code',
+ 'text' => '',
+ );
+
+ if (isset($matches[1]))
+ {
+ $class = 'language-'.$matches[1];
+
+ $Element['attributes'] = array(
+ 'class' => $class,
+ );
+ }
+
+ $Block = array(
+ 'char' => $Line['text'][0],
+ 'element' => array(
+ 'name' => 'pre',
+ 'handler' => 'element',
+ 'text' => $Element,
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockFencedCodeContinue($Line, $Block)
+ {
+ if (isset($Block['complete']))
+ {
+ return;
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text']['text'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
+ {
+ $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
+
+ $Block['complete'] = true;
+
+ return $Block;
+ }
+
+ $Block['element']['text']['text'] .= "\n".$Line['body'];;
+
+ return $Block;
+ }
+
+ protected function blockFencedCodeComplete($Block)
+ {
+ $text = $Block['element']['text']['text'];
+
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+
+ $Block['element']['text']['text'] = $text;
+
+ return $Block;
+ }
+
+ #
+ # Header
+
+ protected function blockHeader($Line)
+ {
+ if (isset($Line['text'][1]))
+ {
+ $level = 1;
+
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
+ {
+ $level ++;
+ }
+
+ if ($level > 6)
+ {
+ return;
+ }
+
+ $text = trim($Line['text'], '# ');
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'h' . min(6, $level),
+ 'text' => $text,
+ 'handler' => 'line',
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # List
+
+ protected function blockList($Line)
+ {
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
+
+ if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'indent' => $Line['indent'],
+ 'pattern' => $pattern,
+ 'element' => array(
+ 'name' => $name,
+ 'handler' => 'elements',
+ ),
+ );
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => 'li',
+ 'text' => array(
+ $matches[2],
+ ),
+ );
+
+ $Block['element']['text'] []= & $Block['li'];
+
+ return $Block;
+ }
+ }
+
+ protected function blockListContinue($Line, array $Block)
+ {
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['li']['text'] []= '';
+
+ unset($Block['interrupted']);
+ }
+
+ unset($Block['li']);
+
+ $text = isset($matches[1]) ? $matches[1] : '';
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => 'li',
+ 'text' => array(
+ $text,
+ ),
+ );
+
+ $Block['element']['text'] []= & $Block['li'];
+
+ return $Block;
+ }
+
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
+ {
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+ $Block['li']['text'] []= $text;
+
+ return $Block;
+ }
+
+ if ($Line['indent'] > 0)
+ {
+ $Block['li']['text'] []= '';
+
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+ $Block['li']['text'] []= $text;
+
+ unset($Block['interrupted']);
+
+ return $Block;
+ }
+ }
+
+ #
+ # Quote
+
+ protected function blockQuote($Line)
+ {
+ if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'blockquote',
+ 'handler' => 'lines',
+ 'text' => (array) $matches[1],
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockQuoteContinue($Line, array $Block)
+ {
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text'] []= '';
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['text'] []= $matches[1];
+
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $Block['element']['text'] []= $Line['text'];
+
+ return $Block;
+ }
+ }
+
+ #
+ # Rule
+
+ protected function blockRule($Line)
+ {
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'hr'
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Setext
+
+ protected function blockSetextHeader($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (chop($Line['text'], $Line['text'][0]) === '')
+ {
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+ return $Block;
+ }
+ }
+
+ #
+ # Markup
+
+ protected function blockMarkup($Line)
+ {
+ if ($this->markupEscaped)
+ {
+ return;
+ }
+
+ if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
+ {
+ $element = strtolower($matches[1]);
+
+ if (in_array($element, $this->textLevelElements))
+ {
+ return;
+ }
+
+ $Block = array(
+ 'name' => $matches[1],
+ 'depth' => 0,
+ 'markup' => $Line['text'],
+ );
+
+ $length = strlen($matches[0]);
+
+ $remainder = substr($Line['text'], $length);
+
+ if (trim($remainder) === '')
+ {
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+ {
+ $Block['closed'] = true;
+
+ $Block['void'] = true;
+ }
+ }
+ else
+ {
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+ {
+ return;
+ }
+
+ if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
+ {
+ $Block['closed'] = true;
+ }
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockMarkupContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
+ {
+ $Block['depth'] ++;
+ }
+
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
+ {
+ if ($Block['depth'] > 0)
+ {
+ $Block['depth'] --;
+ }
+ else
+ {
+ $Block['closed'] = true;
+ }
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['markup'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['markup'] .= "\n".$Line['body'];
+
+ return $Block;
+ }
+
+ #
+ # Reference
+
+ protected function blockReference($Line)
+ {
+ if (preg_match('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
+ {
+ $id = strtolower($matches[1]);
+
+ $Data = array(
+ 'url' => $matches[2],
+ 'title' => null,
+ );
+
+ if (isset($matches[3]))
+ {
+ $Data['title'] = $matches[3];
+ }
+
+ $this->DefinitionData['Reference'][$id] = $Data;
+
+ $Block = array(
+ 'hidden' => true,
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Table
+
+ protected function blockTable($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
+ {
+ $alignments = array();
+
+ $divider = $Line['text'];
+
+ $divider = trim($divider);
+ $divider = trim($divider, '|');
+
+ $dividerCells = explode('|', $divider);
+
+ foreach ($dividerCells as $dividerCell)
+ {
+ $dividerCell = trim($dividerCell);
+
+ if ($dividerCell === '')
+ {
+ continue;
+ }
+
+ $alignment = null;
+
+ if ($dividerCell[0] === ':')
+ {
+ $alignment = 'left';
+ }
+
+ if (substr($dividerCell, - 1) === ':')
+ {
+ $alignment = $alignment === 'left' ? 'center' : 'right';
+ }
+
+ $alignments []= $alignment;
+ }
+
+ # ~
+
+ $HeaderElements = array();
+
+ $header = $Block['element']['text'];
+
+ $header = trim($header);
+ $header = trim($header, '|');
+
+ $headerCells = explode('|', $header);
+
+ foreach ($headerCells as $index => $headerCell)
+ {
+ $headerCell = trim($headerCell);
+
+ $HeaderElement = array(
+ 'name' => 'th',
+ 'text' => $headerCell,
+ 'handler' => 'line',
+ );
+
+ if (isset($alignments[$index]))
+ {
+ $alignment = $alignments[$index];
+
+ $HeaderElement['attributes'] = array(
+ 'style' => 'text-align: '.$alignment.';',
+ );
+ }
+
+ $HeaderElements []= $HeaderElement;
+ }
+
+ # ~
+
+ $Block = array(
+ 'alignments' => $alignments,
+ 'identified' => true,
+ 'element' => array(
+ 'name' => 'table',
+ 'handler' => 'elements',
+ ),
+ );
+
+ $Block['element']['text'] []= array(
+ 'name' => 'thead',
+ 'handler' => 'elements',
+ );
+
+ $Block['element']['text'] []= array(
+ 'name' => 'tbody',
+ 'handler' => 'elements',
+ 'text' => array(),
+ );
+
+ $Block['element']['text'][0]['text'] []= array(
+ 'name' => 'tr',
+ 'handler' => 'elements',
+ 'text' => $HeaderElements,
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockTableContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
+ {
+ $Elements = array();
+
+ $row = $Line['text'];
+
+ $row = trim($row);
+ $row = trim($row, '|');
+
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
+
+ foreach ($matches[0] as $index => $cell)
+ {
+ $cell = trim($cell);
+
+ $Element = array(
+ 'name' => 'td',
+ 'handler' => 'line',
+ 'text' => $cell,
+ );
+
+ if (isset($Block['alignments'][$index]))
+ {
+ $Element['attributes'] = array(
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
+ );
+ }
+
+ $Elements []= $Element;
+ }
+
+ $Element = array(
+ 'name' => 'tr',
+ 'handler' => 'elements',
+ 'text' => $Elements,
+ );
+
+ $Block['element']['text'][1]['text'] []= $Element;
+
+ return $Block;
+ }
+ }
+
+ #
+ # ~
+ #
+
+ protected function paragraph($Line)
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'p',
+ 'text' => $Line['text'],
+ 'handler' => 'line',
+ ),
+ );
+
+ return $Block;
+ }
+
+ #
+ # Inline Elements
+ #
+
+ protected $InlineTypes = array(
+ '"' => array('SpecialCharacter'),
+ '!' => array('Image'),
+ '&' => array('SpecialCharacter'),
+ '*' => array('Emphasis'),
+ ':' => array('Url'),
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
+ '>' => array('SpecialCharacter'),
+ '[' => array('Link'),
+ '_' => array('Emphasis'),
+ '`' => array('Code'),
+ '~' => array('Strikethrough'),
+ '\\' => array('EscapeSequence'),
+ );
+
+ # ~
+
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
+
+ #
+ # ~
+ #
+
+ public function line($text)
+ {
+ $markup = '';
+
+ # $excerpt is based on the first occurrence of a marker
+
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
+ {
+ $marker = $excerpt[0];
+
+ $markerPosition = strpos($text, $marker);
+
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
+
+ foreach ($this->InlineTypes[$marker] as $inlineType)
+ {
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
+
+ if ( ! isset($Inline))
+ {
+ continue;
+ }
+
+ # makes sure that the inline belongs to "our" marker
+
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
+ {
+ continue;
+ }
+
+ # sets a default inline position
+
+ if ( ! isset($Inline['position']))
+ {
+ $Inline['position'] = $markerPosition;
+ }
+
+ # the text that comes before the inline
+ $unmarkedText = substr($text, 0, $Inline['position']);
+
+ # compile the unmarked text
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ # compile the inline
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
+
+ # remove the examined text
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+ continue 2;
+ }
+
+ # the marker does not belong to an inline
+
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ $text = substr($text, $markerPosition + 1);
+ }
+
+ $markup .= $this->unmarkedText($text);
+
+ return $markup;
+ }
+
+ #
+ # ~
+ #
+
+ protected function inlineCode($Excerpt)
+ {
+ $marker = $Excerpt['text'][0];
+
+ if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmailTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ if ( ! isset($matches[2]))
+ {
+ $url = 'mailto:' . $url;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[1],
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmphasis($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ $marker = $Excerpt['text'][0];
+
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'strong';
+ }
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'em';
+ }
+ else
+ {
+ return;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => $emphasis,
+ 'handler' => 'line',
+ 'text' => $matches[1],
+ ),
+ );
+ }
+
+ protected function inlineEscapeSequence($Excerpt)
+ {
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+ {
+ return array(
+ 'markup' => $Excerpt['text'][1],
+ 'extent' => 2,
+ );
+ }
+ }
+
+ protected function inlineImage($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+ {
+ return;
+ }
+
+ $Excerpt['text']= substr($Excerpt['text'], 1);
+
+ $Link = $this->inlineLink($Excerpt);
+
+ if ($Link === null)
+ {
+ return;
+ }
+
+ $Inline = array(
+ 'extent' => $Link['extent'] + 1,
+ 'element' => array(
+ 'name' => 'img',
+ 'attributes' => array(
+ 'src' => $Link['element']['attributes']['href'],
+ 'alt' => $Link['element']['text'],
+ ),
+ ),
+ );
+
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+ unset($Inline['element']['attributes']['href']);
+
+ return $Inline;
+ }
+
+ protected function inlineLink($Excerpt)
+ {
+ $Element = array(
+ 'name' => 'a',
+ 'handler' => 'line',
+ 'text' => null,
+ 'attributes' => array(
+ 'href' => null,
+ 'title' => null,
+ ),
+ );
+
+ $extent = 0;
+
+ $remainder = $Excerpt['text'];
+
+ if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
+ {
+ $Element['text'] = $matches[1];
+
+ $extent += strlen($matches[0]);
+
+ $remainder = substr($remainder, $extent);
+ }
+ else
+ {
+ return;
+ }
+
+ if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
+ {
+ $Element['attributes']['href'] = $matches[1];
+
+ if (isset($matches[2]))
+ {
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+ }
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+ {
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
+ $definition = strtolower($definition);
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ $definition = strtolower($Element['text']);
+ }
+
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
+ {
+ return;
+ }
+
+ $Definition = $this->DefinitionData['Reference'][$definition];
+
+ $Element['attributes']['href'] = $Definition['url'];
+ $Element['attributes']['title'] = $Definition['title'];
+ }
+
+ $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']);
+
+ return array(
+ 'extent' => $extent,
+ 'element' => $Element,
+ );
+ }
+
+ protected function inlineMarkup($Excerpt)
+ {
+ if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+ }
+
+ protected function inlineSpecialCharacter($Excerpt)
+ {
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^?\w+;/', $Excerpt['text']))
+ {
+ return array(
+ 'markup' => '&',
+ 'extent' => 1,
+ );
+ }
+
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
+
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
+ {
+ return array(
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
+ 'extent' => 1,
+ );
+ }
+ }
+
+ protected function inlineStrikethrough($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'del',
+ 'text' => $matches[1],
+ 'handler' => 'line',
+ ),
+ );
+ }
+ }
+
+ protected function inlineUrl($Excerpt)
+ {
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+ {
+ return;
+ }
+
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
+ {
+ $Inline = array(
+ 'extent' => strlen($matches[0][0]),
+ 'position' => $matches[0][1],
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[0][0],
+ 'attributes' => array(
+ 'href' => $matches[0][0],
+ ),
+ ),
+ );
+
+ return $Inline;
+ }
+ }
+
+ protected function inlineUrlTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
+ {
+ $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]);
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ # ~
+
+ protected function unmarkedText($text)
+ {
+ if ($this->breaksEnabled)
+ {
+ $text = preg_replace('/[ ]*\n/', "
\n", $text);
+ }
+ else
+ {
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text);
+ $text = str_replace(" \n", "\n", $text);
+ }
+
+ return $text;
+ }
+
+ #
+ # Handlers
+ #
+
+ protected function element(array $Element)
+ {
+ $markup = '<'.$Element['name'];
+
+ if (isset($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $name => $value)
+ {
+ if ($value === null)
+ {
+ continue;
+ }
+
+ $markup .= ' '.$name.'="'.$value.'"';
+ }
+ }
+
+ if (isset($Element['text']))
+ {
+ $markup .= '>';
+
+ if (isset($Element['handler']))
+ {
+ $markup .= $this->{$Element['handler']}($Element['text']);
+ }
+ else
+ {
+ $markup .= $Element['text'];
+ }
+
+ $markup .= ''.$Element['name'].'>';
+ }
+ else
+ {
+ $markup .= ' />';
+ }
+
+ return $markup;
+ }
+
+ protected function elements(array $Elements)
+ {
+ $markup = '';
+
+ foreach ($Elements as $Element)
+ {
+ $markup .= "\n" . $this->element($Element);
+ }
+
+ $markup .= "\n";
+
+ return $markup;
+ }
+
+ # ~
+
+ protected function li($lines)
+ {
+ $markup = $this->lines($lines);
+
+ $trimmedMarkup = trim($markup);
+
+ if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '')
+ {
+ $markup = $trimmedMarkup;
+ $markup = substr($markup, 3);
+
+ $position = strpos($markup, "
");
+
+ $markup = substr_replace($markup, '', $position, 4);
+ }
+
+ return $markup;
+ }
+
+ #
+ # Deprecated Methods
+ #
+
+ function parse($text)
+ {
+ $markup = $this->text($text);
+
+ return $markup;
+ }
+
+ #
+ # Static Methods
+ #
+
+ static function instance($name = 'default')
+ {
+ if (isset(self::$instances[$name]))
+ {
+ return self::$instances[$name];
+ }
+
+ $instance = new static();
+
+ self::$instances[$name] = $instance;
+
+ return $instance;
+ }
+
+ private static $instances = array();
+
+ #
+ # Fields
+ #
+
+ protected $DefinitionData;
+
+ #
+ # Read-Only
+
+ protected $specialCharacters = array(
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
+ );
+
+ protected $StrongRegex = array(
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
+ );
+
+ protected $EmRegex = array(
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+ );
+
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
+
+ protected $voidElements = array(
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
+ );
+
+ protected $textLevelElements = array(
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
+ 'q', 'rt', 'ins', 'font', 'strong',
+ 's', 'tt', 'sub', 'mark',
+ 'u', 'xm', 'sup', 'nobr',
+ 'var', 'ruby',
+ 'wbr', 'span',
+ 'time',
+ );
+}
\ No newline at end of file
diff --git a/core/updater/vendor/PucReadmeParser.php b/core/updater/vendor/PucReadmeParser.php
new file mode 100644
index 0000000..1f5cec9
--- /dev/null
+++ b/core/updater/vendor/PucReadmeParser.php
@@ -0,0 +1,348 @@
+parse_readme_contents( $file_contents );
+ }
+
+ function parse_readme_contents( $file_contents ) {
+ $file_contents = str_replace(array("\r\n", "\r"), "\n", $file_contents);
+ $file_contents = trim($file_contents);
+ if ( 0 === strpos( $file_contents, "\xEF\xBB\xBF" ) )
+ $file_contents = substr( $file_contents, 3 );
+
+ // Markdown transformations
+ $file_contents = preg_replace( "|^###([^#]+)#*?\s*?\n|im", '=$1='."\n", $file_contents );
+ $file_contents = preg_replace( "|^##([^#]+)#*?\s*?\n|im", '==$1=='."\n", $file_contents );
+ $file_contents = preg_replace( "|^#([^#]+)#*?\s*?\n|im", '===$1==='."\n", $file_contents );
+
+ // === Plugin Name ===
+ // Must be the very first thing.
+ if ( !preg_match('|^===(.*)===|', $file_contents, $_name) )
+ return array(); // require a name
+ $name = trim($_name[1], '=');
+ $name = $this->sanitize_text( $name );
+
+ $file_contents = $this->chop_string( $file_contents, $_name[0] );
+
+
+ // Requires at least: 1.5
+ if ( preg_match('|Requires at least:(.*)|i', $file_contents, $_requires_at_least) )
+ $requires_at_least = $this->sanitize_text($_requires_at_least[1]);
+ else
+ $requires_at_least = NULL;
+
+
+ // Tested up to: 2.1
+ if ( preg_match('|Tested up to:(.*)|i', $file_contents, $_tested_up_to) )
+ $tested_up_to = $this->sanitize_text( $_tested_up_to[1] );
+ else
+ $tested_up_to = NULL;
+
+ // Requires PHP: 5.2.4
+ if ( preg_match('|Requires PHP:(.*)|i', $file_contents, $_requires_php) ) {
+ $requires_php = $this->sanitize_text( $_requires_php[1] );
+ } else {
+ $requires_php = null;
+ }
+
+ // Stable tag: 10.4-ride-the-fire-eagle-danger-day
+ if ( preg_match('|Stable tag:(.*)|i', $file_contents, $_stable_tag) )
+ $stable_tag = $this->sanitize_text( $_stable_tag[1] );
+ else
+ $stable_tag = NULL; // we assume trunk, but don't set it here to tell the difference between specified trunk and default trunk
+
+
+ // Tags: some tag, another tag, we like tags
+ if ( preg_match('|Tags:(.*)|i', $file_contents, $_tags) ) {
+ $tags = preg_split('|,[\s]*?|', trim($_tags[1]));
+ foreach ( array_keys($tags) as $t )
+ $tags[$t] = $this->sanitize_text( $tags[$t] );
+ } else {
+ $tags = array();
+ }
+
+
+ // Contributors: markjaquith, mdawaffe, zefrank
+ $contributors = array();
+ if ( preg_match('|Contributors:(.*)|i', $file_contents, $_contributors) ) {
+ $temp_contributors = preg_split('|,[\s]*|', trim($_contributors[1]));
+ foreach ( array_keys($temp_contributors) as $c ) {
+ $tmp_sanitized = $this->user_sanitize( $temp_contributors[$c] );
+ if ( strlen(trim($tmp_sanitized)) > 0 )
+ $contributors[$c] = $tmp_sanitized;
+ unset($tmp_sanitized);
+ }
+ }
+
+
+ // Donate Link: URL
+ if ( preg_match('|Donate link:(.*)|i', $file_contents, $_donate_link) )
+ $donate_link = esc_url( $_donate_link[1] );
+ else
+ $donate_link = NULL;
+
+
+ // togs, conts, etc are optional and order shouldn't matter. So we chop them only after we've grabbed their values.
+ foreach ( array('tags', 'contributors', 'requires_at_least', 'tested_up_to', 'stable_tag', 'donate_link') as $chop ) {
+ if ( $$chop ) {
+ $_chop = '_' . $chop;
+ $file_contents = $this->chop_string( $file_contents, ${$_chop}[0] );
+ }
+ }
+
+ $file_contents = trim($file_contents);
+
+
+ // short-description fu
+ if ( !preg_match('/(^(.*?))^[\s]*=+?[\s]*.+?[\s]*=+?/ms', $file_contents, $_short_description) )
+ $_short_description = array( 1 => &$file_contents, 2 => &$file_contents );
+ $short_desc_filtered = $this->sanitize_text( $_short_description[2] );
+ $short_desc_length = strlen($short_desc_filtered);
+ $short_description = substr($short_desc_filtered, 0, 150);
+ if ( $short_desc_length > strlen($short_description) )
+ $truncated = true;
+ else
+ $truncated = false;
+ if ( $_short_description[1] )
+ $file_contents = $this->chop_string( $file_contents, $_short_description[1] ); // yes, the [1] is intentional
+
+ // == Section ==
+ // Break into sections
+ // $_sections[0] will be the title of the first section, $_sections[1] will be the content of the first section
+ // the array alternates from there: title2, content2, title3, content3... and so forth
+ $_sections = preg_split('/^[\s]*==[\s]*(.+?)[\s]*==/m', $file_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
+
+ $sections = array();
+ for ( $i=0; $i < count($_sections); $i +=2 ) {
+ $title = $this->sanitize_text( $_sections[$i] );
+ if ( isset($_sections[$i+1]) ) {
+ $content = preg_replace('/(^[\s]*)=[\s]+(.+?)[\s]+=/m', '$1$2
', $_sections[$i+1]);
+ $content = $this->filter_text( $content, true );
+ } else {
+ $content = '';
+ }
+ $sections[str_replace(' ', '_', strtolower($title))] = array('title' => $title, 'content' => $content);
+ }
+
+
+ // Special sections
+ // This is where we nab our special sections, so we can enforce their order and treat them differently, if needed
+ // upgrade_notice is not a section, but parse it like it is for now
+ $final_sections = array();
+ foreach ( array('description', 'installation', 'frequently_asked_questions', 'screenshots', 'changelog', 'change_log', 'upgrade_notice') as $special_section ) {
+ if ( isset($sections[$special_section]) ) {
+ $final_sections[$special_section] = $sections[$special_section]['content'];
+ unset($sections[$special_section]);
+ }
+ }
+ if ( isset($final_sections['change_log']) && empty($final_sections['changelog']) )
+ $final_sections['changelog'] = $final_sections['change_log'];
+
+
+ $final_screenshots = array();
+ if ( isset($final_sections['screenshots']) ) {
+ preg_match_all('|- (.*?)
|s', $final_sections['screenshots'], $screenshots, PREG_SET_ORDER);
+ if ( $screenshots ) {
+ foreach ( (array) $screenshots as $ss )
+ $final_screenshots[] = $ss[1];
+ }
+ }
+
+ // Parse the upgrade_notice section specially:
+ // 1.0 => blah, 1.1 => fnord
+ $upgrade_notice = array();
+ if ( isset($final_sections['upgrade_notice']) ) {
+ $split = preg_split( '#(.*?)
#', $final_sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
+ if ( count($split) >= 2 ) {
+ for ( $i = 0; $i < count( $split ); $i += 2 ) {
+ $upgrade_notice[$this->sanitize_text( $split[$i] )] = substr( $this->sanitize_text( $split[$i + 1] ), 0, 300 );
+ }
+ }
+ unset( $final_sections['upgrade_notice'] );
+ }
+
+ // No description?
+ // No problem... we'll just fall back to the old style of description
+ // We'll even let you use markup this time!
+ $excerpt = false;
+ if ( !isset($final_sections['description']) ) {
+ $final_sections = array_merge(array('description' => $this->filter_text( $_short_description[2], true )), $final_sections);
+ $excerpt = true;
+ }
+
+
+ // dump the non-special sections into $remaining_content
+ // their order will be determined by their original order in the readme.txt
+ $remaining_content = '';
+ foreach ( $sections as $s_name => $s_data ) {
+ $remaining_content .= "\n{$s_data['title']}
\n{$s_data['content']}";
+ }
+ $remaining_content = trim($remaining_content);
+
+
+ // All done!
+ // $r['tags'] and $r['contributors'] are simple arrays
+ // $r['sections'] is an array with named elements
+ $r = array(
+ 'name' => $name,
+ 'tags' => $tags,
+ 'requires_at_least' => $requires_at_least,
+ 'tested_up_to' => $tested_up_to,
+ 'requires_php' => $requires_php,
+ 'stable_tag' => $stable_tag,
+ 'contributors' => $contributors,
+ 'donate_link' => $donate_link,
+ 'short_description' => $short_description,
+ 'screenshots' => $final_screenshots,
+ 'is_excerpt' => $excerpt,
+ 'is_truncated' => $truncated,
+ 'sections' => $final_sections,
+ 'remaining_content' => $remaining_content,
+ 'upgrade_notice' => $upgrade_notice
+ );
+
+ return $r;
+ }
+
+ function chop_string( $string, $chop ) { // chop a "prefix" from a string: Agressive! uses strstr not 0 === strpos
+ if ( $_string = strstr($string, $chop) ) {
+ $_string = substr($_string, strlen($chop));
+ return trim($_string);
+ } else {
+ return trim($string);
+ }
+ }
+
+ function user_sanitize( $text, $strict = false ) { // whitelisted chars
+ if ( function_exists('user_sanitize') ) // bbPress native
+ return user_sanitize( $text, $strict );
+
+ if ( $strict ) {
+ $text = preg_replace('/[^a-z0-9-]/i', '', $text);
+ $text = preg_replace('|-+|', '-', $text);
+ } else {
+ $text = preg_replace('/[^a-z0-9_-]/i', '', $text);
+ }
+ return $text;
+ }
+
+ function sanitize_text( $text ) { // not fancy
+ $text = strip_tags($text);
+ $text = esc_html($text);
+ $text = trim($text);
+ return $text;
+ }
+
+ function filter_text( $text, $markdown = false ) { // fancy, Markdown
+ $text = trim($text);
+
+ $text = call_user_func( array( __CLASS__, 'code_trick' ), $text, $markdown ); // A better parser than Markdown's for: backticks -> CODE
+
+ if ( $markdown ) { // Parse markdown.
+ if ( !class_exists('Parsedown', false) ) {
+ /** @noinspection PhpIncludeInspection */
+ require_once(dirname(__FILE__) . '/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php');
+ }
+ $instance = Parsedown::instance();
+ $text = $instance->text($text);
+ }
+
+ $allowed = array(
+ 'a' => array(
+ 'href' => array(),
+ 'title' => array(),
+ 'rel' => array()),
+ 'blockquote' => array('cite' => array()),
+ 'br' => array(),
+ 'p' => array(),
+ 'code' => array(),
+ 'pre' => array(),
+ 'em' => array(),
+ 'strong' => array(),
+ 'ul' => array(),
+ 'ol' => array(),
+ 'li' => array(),
+ 'h3' => array(),
+ 'h4' => array()
+ );
+
+ $text = balanceTags($text);
+
+ $text = wp_kses( $text, $allowed );
+ $text = trim($text);
+ return $text;
+ }
+
+ function code_trick( $text, $markdown ) { // Don't use bbPress native function - it's incompatible with Markdown
+ // If doing markdown, first take any user formatted code blocks and turn them into backticks so that
+ // markdown will preserve things like underscores in code blocks
+ if ( $markdown )
+ $text = preg_replace_callback("!(|)(.*?)(
|)!s", array( __CLASS__,'decodeit'), $text);
+
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+ if ( !$markdown ) {
+ // This gets the "inline" code blocks, but can't be used with Markdown.
+ $text = preg_replace_callback("|(`)(.*?)`|", array( __CLASS__, 'encodeit'), $text);
+ // This gets the "block level" code blocks and converts them to PRE CODE
+ $text = preg_replace_callback("!(^|\n)`(.*?)`!s", array( __CLASS__, 'encodeit'), $text);
+ } else {
+ // Markdown can do inline code, we convert bbPress style block level code to Markdown style
+ $text = preg_replace_callback("!(^|\n)([ \t]*?)`(.*?)`!s", array( __CLASS__, 'indent'), $text);
+ }
+ return $text;
+ }
+
+ function indent( $matches ) {
+ $text = $matches[3];
+ $text = preg_replace('|^|m', $matches[2] . ' ', $text);
+ return $matches[1] . $text;
+ }
+
+ function encodeit( $matches ) {
+ if ( function_exists('encodeit') ) // bbPress native
+ return encodeit( $matches );
+
+ $text = trim($matches[2]);
+ $text = htmlspecialchars($text, ENT_QUOTES);
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+ $text = preg_replace("|\n\n\n+|", "\n\n", $text);
+ $text = str_replace('<', '<', $text);
+ $text = str_replace('>', '>', $text);
+ $text = "$text
";
+ if ( "`" != $matches[1] )
+ $text = "$text
";
+ return $text;
+ }
+
+ function decodeit( $matches ) {
+ if ( function_exists('decodeit') ) // bbPress native
+ return decodeit( $matches );
+
+ $text = $matches[2];
+ $trans_table = array_flip(get_html_translation_table(HTML_ENTITIES));
+ $text = strtr($text, $trans_table);
+ $text = str_replace('
', '', $text);
+ $text = str_replace('&', '&', $text);
+ $text = str_replace(''', "'", $text);
+ if ( '' == $matches[1] )
+ $text = "\n$text\n";
+ return "`$text`";
+ }
+
+} // end class
+
+endif;
diff --git a/the-hanger-extender.php b/the-hanger-extender.php
index b85f5f7..a5f41ec 100755
--- a/the-hanger-extender.php
+++ b/the-hanger-extender.php
@@ -3,11 +3,11 @@
* Plugin Name: The Hanger Extender
* Plugin URI: https://thehanger.wp-theme.design/
* Description: Extends the functionality of The Hanger with theme specific shortcodes and page builder elements.
- * Version: 1.7.1
+ * Version: 1.7.2
* Author: GetBowtied
* Author URI: https://getbowtied.com
* Requires at least: 5.0
- * Tested up to: 5.6
+ * Tested up to: 5.8
*
* @package The Hanger Extender
* @author GetBowtied