From 2bbd2dc7c8bf89a1a3c43972b93d3f11228bc28e Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Tue, 11 Aug 2026 16:08:20 -0500 Subject: [PATCH] Update bundled plugin-update-checker to v5.7 and remove broken setBranch() call The bundled PUC was missing the setBranch() method on the main UpdateChecker class (it's only on Vcs\PluginUpdateChecker via the VcsCheckerMethods trait). When called with a non-VCS URL like the Gitea update URL, this caused a fatal error at plugin activation. Updating to PUC v5.7 (May 2026) which is the latest upstream release. The setBranch() call is removed because PUC doesn't recognize Gitea as a VCS host; the plugin falls back to PUC's plain JSON metadata mode, which is fine for a plugin hosted on a self-managed repo. --- .../Puc/v5/PucFactory.php | 2 +- .../Puc/{v5p4 => v5p7}/Autoloader.php | 2 +- .../Puc/{v5p4 => v5p7}/DebugBar/Extension.php | 6 +- .../Puc/{v5p4 => v5p7}/DebugBar/Panel.php | 30 +- .../DebugBar/PluginExtension.php | 4 +- .../{v5p4 => v5p7}/DebugBar/PluginPanel.php | 16 +- .../{v5p4 => v5p7}/DebugBar/ThemePanel.php | 6 +- .../Puc/{v5p4 => v5p7}/InstalledPackage.php | 2 +- .../Puc/{v5p4 => v5p7}/Metadata.php | 2 +- .../Puc/{v5p4 => v5p7}/OAuthSignature.php | 2 +- .../Puc/{v5p4 => v5p7}/Plugin/Package.php | 6 +- .../Puc/{v5p4 => v5p7}/Plugin/PluginInfo.php | 5 +- .../Puc/{v5p4 => v5p7}/Plugin/Ui.php | 4 +- .../Puc/{v5p4 => v5p7}/Plugin/Update.php | 10 +- .../{v5p4 => v5p7}/Plugin/UpdateChecker.php | 10 +- .../Puc/{v5p4 => v5p7}/PucFactory.php | 14 +- .../Puc/{v5p4 => v5p7}/Scheduler.php | 2 +- .../Puc/{v5p4 => v5p7}/StateStore.php | 4 +- .../Puc/{v5p4 => v5p7}/Theme/Package.php | 4 +- .../Puc/{v5p4 => v5p7}/Theme/Update.php | 4 +- .../{v5p4 => v5p7}/Theme/UpdateChecker.php | 10 +- .../Puc/{v5p4 => v5p7}/Update.php | 2 +- .../Puc/{v5p4 => v5p7}/UpdateChecker.php | 192 +- .../Puc/{v5p4 => v5p7}/UpgraderStatus.php | 2 +- .../Puc/{v5p4 => v5p7}/Utils.php | 2 +- .../Puc/{v5p4 => v5p7}/Vcs/Api.php | 153 +- .../Puc/{v5p4 => v5p7}/Vcs/BaseChecker.php | 2 +- .../Puc/{v5p4 => v5p7}/Vcs/BitBucketApi.php | 59 +- .../Puc/{v5p4 => v5p7}/Vcs/GitHubApi.php | 96 +- .../Puc/{v5p4 => v5p7}/Vcs/GitLabApi.php | 9 +- .../Vcs/PluginUpdateChecker.php | 4 +- .../Puc/{v5p4 => v5p7}/Vcs/Reference.php | 2 +- .../Vcs/ReleaseAssetSupport.php | 2 +- .../Vcs/ReleaseFilteringFeature.php | 2 +- .../{v5p4 => v5p7}/Vcs/ThemeUpdateChecker.php | 6 +- .../{v5p4 => v5p7}/Vcs/VcsCheckerMethods.php | 2 +- .../Puc/{v5p4 => v5p7}/WpCliCheckTrigger.php | 2 +- includes/plugin-update-checker/README.md | 43 +- includes/plugin-update-checker/composer.json | 2 +- .../languages/plugin-update-checker-el.mo | Bin 0 -> 1455 bytes .../languages/plugin-update-checker-el.po | 52 + .../languages/plugin-update-checker-fa_IR.mo | Bin 1128 -> 956 bytes .../languages/plugin-update-checker-fa_IR.po | 54 +- .../languages/plugin-update-checker-sr_RS.mo | Bin 0 -> 1414 bytes .../languages/plugin-update-checker-sr_RS.po | 57 + .../languages/plugin-update-checker-zh_CN.mo | Bin 1174 -> 1172 bytes .../languages/plugin-update-checker-zh_CN.po | 24 +- .../languages/plugin-update-checker-zh_TW.mo | Bin 0 -> 1592 bytes .../languages/plugin-update-checker-zh_TW.po | 66 + .../languages/plugin-update-checker.pot | 16 +- .../{load-v5p4.php => load-v5p7.php} | 12 +- .../plugin-update-checker.php | 6 +- .../vendor/Parsedown.php | 4 - .../vendor/ParsedownModern.php | 1538 ----------------- .../vendor/PucReadmeParser.php | 352 ---- projects-portfolio.php | 6 +- 56 files changed, 722 insertions(+), 2192 deletions(-) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Autoloader.php (98%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/DebugBar/Extension.php (97%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/DebugBar/Panel.php (80%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/DebugBar/PluginExtension.php (90%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/DebugBar/PluginPanel.php (56%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/DebugBar/ThemePanel.php (64%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/InstalledPackage.php (98%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Metadata.php (98%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/OAuthSignature.php (98%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Plugin/Package.php (96%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Plugin/PluginInfo.php (96%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Plugin/Ui.php (99%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Plugin/Update.php (91%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Plugin/UpdateChecker.php (97%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/PucFactory.php (96%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Scheduler.php (99%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/StateStore.php (97%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Theme/Package.php (93%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Theme/Update.php (94%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Theme/UpdateChecker.php (94%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Update.php (93%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/UpdateChecker.php (82%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/UpgraderStatus.php (99%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Utils.php (97%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/Api.php (65%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/BaseChecker.php (90%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/BitBucketApi.php (84%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/GitHubApi.php (78%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/GitLabApi.php (97%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/PluginUpdateChecker.php (98%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/Reference.php (95%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/ReleaseAssetSupport.php (97%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/ReleaseFilteringFeature.php (98%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/ThemeUpdateChecker.php (94%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/Vcs/VcsCheckerMethods.php (95%) rename includes/plugin-update-checker/Puc/{v5p4 => v5p7}/WpCliCheckTrigger.php (98%) create mode 100644 includes/plugin-update-checker/languages/plugin-update-checker-el.mo create mode 100644 includes/plugin-update-checker/languages/plugin-update-checker-el.po create mode 100644 includes/plugin-update-checker/languages/plugin-update-checker-sr_RS.mo create mode 100644 includes/plugin-update-checker/languages/plugin-update-checker-sr_RS.po create mode 100644 includes/plugin-update-checker/languages/plugin-update-checker-zh_TW.mo create mode 100644 includes/plugin-update-checker/languages/plugin-update-checker-zh_TW.po rename includes/plugin-update-checker/{load-v5p4.php => load-v5p7.php} (80%) delete mode 100644 includes/plugin-update-checker/vendor/Parsedown.php delete mode 100644 includes/plugin-update-checker/vendor/ParsedownModern.php delete mode 100644 includes/plugin-update-checker/vendor/PucReadmeParser.php diff --git a/includes/plugin-update-checker/Puc/v5/PucFactory.php b/includes/plugin-update-checker/Puc/v5/PucFactory.php index 0bc62ce..920248d 100644 --- a/includes/plugin-update-checker/Puc/v5/PucFactory.php +++ b/includes/plugin-update-checker/Puc/v5/PucFactory.php @@ -4,7 +4,7 @@ namespace YahnisElsts\PluginUpdateChecker\v5; if ( !class_exists(PucFactory::class, false) ): - class PucFactory extends \YahnisElsts\PluginUpdateChecker\v5p4\PucFactory { + class PucFactory extends \YahnisElsts\PluginUpdateChecker\v5p7\PucFactory { } endif; diff --git a/includes/plugin-update-checker/Puc/v5p4/Autoloader.php b/includes/plugin-update-checker/Puc/v5p7/Autoloader.php similarity index 98% rename from includes/plugin-update-checker/Puc/v5p4/Autoloader.php rename to includes/plugin-update-checker/Puc/v5p7/Autoloader.php index 5a98133..9f036c1 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Autoloader.php +++ b/includes/plugin-update-checker/Puc/v5p7/Autoloader.php @@ -1,6 +1,6 @@ Configuration'; echo ''; $this->displayConfigHeader(); - $this->row('Slug', htmlentities($this->updateChecker->slug)); - $this->row('DB option', htmlentities($this->updateChecker->optionName)); + $this->row('Slug', esc_html($this->updateChecker->slug)); + $this->row('DB option', esc_html($this->updateChecker->optionName)); $requestInfoButton = $this->getMetadataButton(); - $this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox); + $this->row('Metadata URL', esc_html($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox); $scheduler = $this->updateChecker->scheduler; if ( $scheduler->checkPeriod > 0 ) { @@ -86,14 +86,22 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false echo '

Status

'; echo '
'; $state = $this->updateChecker->getUpdateState(); - $checkNowButton = ''; + $checkButtonId = $this->updateChecker->getUniqueName('check-now-button'); 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')) + array('id' => $checkButtonId) + ); + } else { + //get_submit_button() is not available in the frontend. Make a button directly. + //It won't look the same without admin styles, but it should still work. + $checkNowButton = sprintf( + '', + esc_attr($checkButtonId), + esc_attr('Check Now') ); } @@ -107,10 +115,10 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false $this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck)); if ( $state->getCheckedVersion() !== '' ) { - $this->row('Checked version', htmlentities($state->getCheckedVersion())); + $this->row('Checked version', esc_html($state->getCheckedVersion())); $this->row('Cached update', $state->getUpdate()); } - $this->row('Update checker class', htmlentities(get_class($this->updateChecker))); + $this->row('Update checker class', esc_html(get_class($this->updateChecker))); echo '
'; } @@ -124,7 +132,7 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false if ( property_exists($update, $field) ) { $this->row( ucwords(str_replace('_', ' ', $field)), - isset($update->$field) ? htmlentities($update->$field) : null + isset($update->$field) ? esc_html($update->$field) : null ); } } @@ -162,7 +170,7 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false if ( is_object($value) || is_array($value) ) { //This is specifically for debugging, so print_r() is fine. //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r - $value = '
' . htmlentities(print_r($value, true)) . '
'; + $value = '
' . esc_html(print_r($value, true)) . '
'; } else if ($value === null) { $value = 'null'; } diff --git a/includes/plugin-update-checker/Puc/v5p4/DebugBar/PluginExtension.php b/includes/plugin-update-checker/Puc/v5p7/DebugBar/PluginExtension.php similarity index 90% rename from includes/plugin-update-checker/Puc/v5p4/DebugBar/PluginExtension.php rename to includes/plugin-update-checker/Puc/v5p7/DebugBar/PluginExtension.php index b30f3ee..17770d0 100644 --- a/includes/plugin-update-checker/Puc/v5p4/DebugBar/PluginExtension.php +++ b/includes/plugin-update-checker/Puc/v5p7/DebugBar/PluginExtension.php @@ -1,8 +1,8 @@ row('Plugin file', htmlentities($this->updateChecker->pluginFile)); + $this->row('Plugin file', esc_html($this->updateChecker->pluginFile)); parent::displayConfigHeader(); } protected function getMetadataButton() { - $requestInfoButton = ''; + $buttonId = $this->updateChecker->getUniqueName('request-info-button'); 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')) + array('id' => $buttonId) + ); + } else { + $requestInfoButton = sprintf( + '', + esc_attr($buttonId), + esc_attr('Request Info') ); } return $requestInfoButton; diff --git a/includes/plugin-update-checker/Puc/v5p4/DebugBar/ThemePanel.php b/includes/plugin-update-checker/Puc/v5p7/DebugBar/ThemePanel.php similarity index 64% rename from includes/plugin-update-checker/Puc/v5p4/DebugBar/ThemePanel.php rename to includes/plugin-update-checker/Puc/v5p7/DebugBar/ThemePanel.php index 7b9d99a..dd43d25 100644 --- a/includes/plugin-update-checker/Puc/v5p4/DebugBar/ThemePanel.php +++ b/includes/plugin-update-checker/Puc/v5p7/DebugBar/ThemePanel.php @@ -1,8 +1,8 @@ row('Theme directory', htmlentities($this->updateChecker->directoryName)); + $this->row('Theme directory', esc_html($this->updateChecker->directoryName)); parent::displayConfigHeader(); } diff --git a/includes/plugin-update-checker/Puc/v5p4/InstalledPackage.php b/includes/plugin-update-checker/Puc/v5p7/InstalledPackage.php similarity index 98% rename from includes/plugin-update-checker/Puc/v5p4/InstalledPackage.php rename to includes/plugin-update-checker/Puc/v5p7/InstalledPackage.php index 341e7a3..894e316 100644 --- a/includes/plugin-update-checker/Puc/v5p4/InstalledPackage.php +++ b/includes/plugin-update-checker/Puc/v5p7/InstalledPackage.php @@ -1,5 +1,5 @@ $status, 'puc_slug' => $this->updateChecker->slug, diff --git a/includes/plugin-update-checker/Puc/v5p4/Plugin/Update.php b/includes/plugin-update-checker/Puc/v5p7/Plugin/Update.php similarity index 91% rename from includes/plugin-update-checker/Puc/v5p4/Plugin/Update.php rename to includes/plugin-update-checker/Puc/v5p7/Plugin/Update.php index 0fb3137..280bf68 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Plugin/Update.php +++ b/includes/plugin-update-checker/Puc/v5p7/Plugin/Update.php @@ -1,7 +1,7 @@ tested = $this->tested; $update->requires_php = $this->requires_php; $update->plugin = $this->filename; + $update->autoupdate = $this->autoupdate; if ( !empty($this->upgrade_notice) ) { $update->upgrade_notice = $this->upgrade_notice; @@ -102,7 +104,7 @@ if ( !class_exists(Update::class, false) ): $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. + //but let's set it just in case a future release needs it. if ( !isset($update->icons['default']) ) { $update->icons['default'] = current($update->icons); } diff --git a/includes/plugin-update-checker/Puc/v5p4/Plugin/UpdateChecker.php b/includes/plugin-update-checker/Puc/v5p7/Plugin/UpdateChecker.php similarity index 97% rename from includes/plugin-update-checker/Puc/v5p4/Plugin/UpdateChecker.php rename to includes/plugin-update-checker/Puc/v5p7/Plugin/UpdateChecker.php index 6d1aae8..20cf839 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Plugin/UpdateChecker.php +++ b/includes/plugin-update-checker/Puc/v5p7/Plugin/UpdateChecker.php @@ -1,10 +1,10 @@ lazyLoad(); $this->update = $update; return $this; diff --git a/includes/plugin-update-checker/Puc/v5p4/Theme/Package.php b/includes/plugin-update-checker/Puc/v5p7/Theme/Package.php similarity index 93% rename from includes/plugin-update-checker/Puc/v5p4/Theme/Package.php rename to includes/plugin-update-checker/Puc/v5p7/Theme/Package.php index 0b20702..90528bb 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Theme/Package.php +++ b/includes/plugin-update-checker/Puc/v5p7/Theme/Package.php @@ -1,7 +1,7 @@ maybeInitDebugBar(); @@ -192,6 +196,7 @@ if ( !class_exists(UpdateChecker::class, false) ): remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10); remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10); + remove_filter('http_request_args', array($this, 'excludeEntityFromWordPressAPI')); remove_action('plugins_loaded', array($this, 'maybeInitDebugBar')); remove_action('init', array($this, 'loadTextDomain')); @@ -266,6 +271,76 @@ if ( !class_exists(UpdateChecker::class, false) ): */ abstract protected function createScheduler($checkPeriod); + /** + * Remove information about this plugin or theme from the requests that WordPress core sends + * to api.wordpress.org when checking for updates. + * + * @param array $args + * @param string $url + * @return array + */ + public function excludeEntityFromWordPressAPI($args, $url) { + //Is this an api.wordpress.org update check request? + $parsedUrl = wp_parse_url($url); + if ( !isset($parsedUrl['host']) || (strtolower($parsedUrl['host']) !== 'api.wordpress.org') ) { + return $args; + } + + $typePluralised = $this->componentType . 's'; + $expectedPathPrefix = '/' . $typePluralised . '/update-check/1.'; //e.g. "/plugins/update-check/1.1/" + if ( !isset($parsedUrl['path']) || !Utils::startsWith($parsedUrl['path'], $expectedPathPrefix) ) { + return $args; + } + + //Plugins and themes can disable this feature by using the filter below. + if ( !apply_filters( + $this->getUniqueName('remove_from_default_update_checks'), + true, $this, $args, $url + ) ) { + return $args; + } + + if ( empty($args['body'][$typePluralised]) ) { + return $args; + } + + $reportingItems = json_decode($args['body'][$typePluralised], true); + if ( $reportingItems === null ) { + return $args; + } + + //The list of installed items uses different key formats for plugins and themes. + //Luckily, we can reuse the getUpdateListKey() method here. + $updateListKey = $this->getUpdateListKey(); + if ( isset($reportingItems[$typePluralised][$updateListKey]) ) { + unset($reportingItems[$typePluralised][$updateListKey]); + } + + if ( !empty($reportingItems['active']) ) { + if ( is_array($reportingItems['active']) ) { + foreach ($reportingItems['active'] as $index => $relativePath) { + if ( $relativePath === $updateListKey ) { + unset($reportingItems['active'][$index]); + } + } + //Re-index the array. + $reportingItems['active'] = array_values($reportingItems['active']); + } else if ( $reportingItems['active'] === $updateListKey ) { + //For themes, the "active" field is a string that contains the theme's directory name. + //Pretend that the default theme is active so that we don't reveal the actual theme. + if ( defined('WP_DEFAULT_THEME') ) { + $reportingItems['active'] = WP_DEFAULT_THEME; + } + + //Unfortunately, it doesn't seem to be documented if we can safely remove the "active" + //key. So when we don't know the default theme, we'll just leave it as is. + } + } + + $args['body'][$typePluralised] = wp_json_encode($reportingItems); + return $args; + } + /** * Check for updates. The results are stored in the DB option specified in $optionName. * @@ -384,7 +459,7 @@ if ( !class_exists(UpdateChecker::class, false) ): * * @param Metadata|null $update */ - protected function fixSupportedWordpressVersion(Metadata $update = null) { + protected function fixSupportedWordpressVersion($update = null) { if ( !isset($update->tested) || !preg_match('/^\d++\.\d++$/', $update->tested) ) { return; } @@ -557,7 +632,15 @@ if ( !class_exists(UpdateChecker::class, false) ): 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()); + //Convert the update into the format used by WordPress core. + $wpUpdate = $update->toWpFormat(); + //Disable the "autoupdate" flag unless explicitly allowed. This is a safety precaution; + //untrusted or compromised update sources could otherwise set this flag to true even + //if the plugin/theme developer didn't intend to allow automatic update installation. + if ( isset($wpUpdate->autoupdate) && !$this->isAutoupdateFieldAllowed($update) ) { + $wpUpdate->autoupdate = false; + } + $updates = $this->addUpdateToList($updates, $wpUpdate); } else { //Clean up any stale update info. $updates = $this->removeUpdateFromList($updates); @@ -655,6 +738,38 @@ if ( !class_exists(UpdateChecker::class, false) ): return true; } + /** + * @var bool + */ + protected $autoupdateFieldAllowed = false; + + /** + * Allow the "autoupdate" field in incoming plugin updates to be set to `true`. + * + * By default, the update checker will parse the field (so it will be available in the update + * object), but will set it to `false` before passing the update to WordPress. + * + * @return $this + */ + public function allowAutoupdateField() { + $this->autoupdateFieldAllowed = true; + return $this; + } + + /** + * Is the "autoupdate" field for injected updates allowed to be set to true? + * + * @param object $incomingUpdate + * @return bool + */ + protected function isAutoupdateFieldAllowed($incomingUpdate) { + return apply_filters( + $this->getUniqueName('autoupdate_field_allowed'), + $this->autoupdateFieldAllowed, + $incomingUpdate + ); + } + /* ------------------------------------------------------------------- * JSON-based update API * ------------------------------------------------------------------- @@ -698,7 +813,7 @@ if ( !class_exists(UpdateChecker::class, false) ): $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; @@ -924,25 +1039,62 @@ if ( !class_exists(UpdateChecker::class, false) ): return $source; } + //Fix the remote source structure if necessary. + //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) ) { + //Create a new directory using the plugin slug. + $newDirectory = trailingslashit($remoteSource) . $this->slug . '/'; + + if ( !$wp_filesystem->is_dir($newDirectory) ) { + $wp_filesystem->mkdir($newDirectory); + + //Move all files to the newly created directory. + $sourceFiles = $wp_filesystem->dirlist($remoteSource); + if ( is_array($sourceFiles) ) { + $sourceFiles = array_keys($sourceFiles); + $allMoved = true; + foreach ($sourceFiles as $filename) { + //Skip our newly created folder. + if ( $filename === $this->slug ) { + continue; + } + + $previousSource = trailingslashit($remoteSource) . $filename; + $newSource = trailingslashit($newDirectory) . $filename; + + if ( !$wp_filesystem->move($previousSource, $newSource, true) ) { + $allMoved = false; + break; + } + } + + if ( $allMoved ) { + //Rename source. + $source = $newDirectory; + } else { + //Delete our newly created folder including all files in it. + $wp_filesystem->rmdir($newDirectory, true); + + //And return a relevant error. + return new WP_Error( + 'puc-incorrect-directory-structure', + sprintf( + 'The directory structure of the update was incorrect. All files should be inside ' . + 'a directory named %s, not at the root of the ZIP archive. Plugin Update Checker tried to fix the directory structure, but failed.', + esc_html($this->slug) + ) + ); + } + } + } + } + //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) . '', diff --git a/includes/plugin-update-checker/Puc/v5p4/UpgraderStatus.php b/includes/plugin-update-checker/Puc/v5p7/UpgraderStatus.php similarity index 99% rename from includes/plugin-update-checker/Puc/v5p4/UpgraderStatus.php rename to includes/plugin-update-checker/Puc/v5p7/UpgraderStatus.php index e8340fd..9cc364c 100644 --- a/includes/plugin-update-checker/Puc/v5p4/UpgraderStatus.php +++ b/includes/plugin-update-checker/Puc/v5p7/UpgraderStatus.php @@ -1,5 +1,5 @@ wp_doing_cron() ? 10 : 3]; + if ( $this->isAuthenticationEnabled() && !empty($this->authorizationHeader) ) { + $options['headers'] = ['Authorization' => $this->authorizationHeader]; + } + + if ( !empty($this->httpFilterName) ) { + $options = apply_filters($this->httpFilterName, $options); + } + + return $options; + } + /** * Set authentication credentials. * @@ -332,7 +358,130 @@ if ( !class_exists(Api::class, false) ): } public function isAuthenticationEnabled() { - return !empty($this->credentials); + return !empty($this->credentials) || !empty($this->authorizationHeader); + } + + /** + * Get the value of the "Authorization" header for API requests, if any. + * + * @return string + */ + protected function getAuthorizationHeader() { + return $this->authorizationHeader ?: ''; + } + + /** + * Enable basic access authentication with the specified username and password. + * + * @param string $username + * @param string $password + * @param string|null $downloadUrlPrefix Optionally, add the same Authorization header to update + * downloads where the download URL starts with this prefix. + */ + protected function enableBasicAuth($username, $password, $downloadUrlPrefix = null) { + $this->authorizationHeader = 'Basic ' . base64_encode($username . ':' . $password); + + $this->downloadUrlPrefixRequiringAuth = $downloadUrlPrefix; + if ( !empty($this->downloadUrlPrefixRequiringAuth) ) { + $this->enableDownloadRequestFilter(); + } + } + + /** + * @var bool Whether the hook that registers download request filter(s) has already been added. + */ + private $preDownloadHookAdded = false; + + /** + * Enable the hooks that let you filter HTTP requests for update downloads. + */ + protected function enableDownloadRequestFilter() { + if ( $this->preDownloadHookAdded ) { + return; + } + $this->preDownloadHookAdded = true; + + //Optimization: Instead of filtering all HTTP requests, let's do it only when + //WordPress is about to download an update. So this is a two-step process; + //the actual request arg filter is added in the following hook. + add_filter('upgrader_pre_download', [$this, 'addDownloadRequestFilters']); //WP 3.7+ + } + + /** + * @var bool + */ + private $downloadFiltersAdded = false; + + /** + * @internal + * @param bool $result Pass-through value for the "upgrader_pre_download" filter. Ignored by this callback. + * @return bool + */ + public function addDownloadRequestFilters($result) { + if ( !$this->downloadFiltersAdded ) { + $this->downloadFiltersAdded = true; + + //phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout. + add_filter('http_request_args', [$this, 'filterUpdateDownloadRequestArgs'], 10, 2); + + $authorizationHeader = $this->getAuthorizationHeader(); + if ( $this->isAuthenticationEnabled() && !empty($authorizationHeader) ) { + add_action('requests-requests.before_redirect', [$this, 'removeAuthHeaderFromRedirects'], 10, 2); + } + } + return $result; + } + + /** + * Filter request arguments/options for update downloads. + * + * Note that this callback can potentially be called for *any* update download. You still + * need to verify that the URL is one of yours. + * + * @internal + * @param array $requestArgs + * @param string $url + * @return array + */ + public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') { + //Add an authorization header to our downloads if needed. + $authHeader = $this->getAuthorizationHeader(); + if ( + $this->isAuthenticationEnabled() + && !empty($authHeader) + && !empty($this->downloadUrlPrefixRequiringAuth) + && ((strpos($url, $this->downloadUrlPrefixRequiringAuth)) === 0) + ) { + $requestArgs['headers']['Authorization'] = $authHeader; + } + return $requestArgs; + } + + /** + * At least in older WP versions, 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. + * + * @param string $location + * @param array $headers + * @internal + */ + public function removeAuthHeaderFromRedirects(&$location, &$headers) { + if ( + //If there's no download URL prefix configured, we would not have added an auth header, + //and there's also no way to check if this URL needs auth or not. + empty($this->downloadUrlPrefixRequiringAuth) + //If this request goes to our download URL, we can leave the header. + || ((strpos($location, $this->downloadUrlPrefixRequiringAuth)) === 0) + ) { + return; + } + + //Remove the header. + $authorizationHeader = $this->getAuthorizationHeader(); + if ( isset($headers['Authorization']) && ($headers['Authorization'] === $authorizationHeader) ) { + unset($headers['Authorization']); + } } /** diff --git a/includes/plugin-update-checker/Puc/v5p4/Vcs/BaseChecker.php b/includes/plugin-update-checker/Puc/v5p7/Vcs/BaseChecker.php similarity index 90% rename from includes/plugin-update-checker/Puc/v5p4/Vcs/BaseChecker.php rename to includes/plugin-update-checker/Puc/v5p7/Vcs/BaseChecker.php index 78e0dde..c416370 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Vcs/BaseChecker.php +++ b/includes/plugin-update-checker/Puc/v5p7/Vcs/BaseChecker.php @@ -1,5 +1,5 @@ getDownloadBaseUrl() . $ref . '.zip'; + } + + /** + * Get the base URL for ZIP downloads from our repo. Includes the trailing slash. + * + * @return string + */ + protected function getDownloadBaseUrl() { return sprintf( - 'https://bitbucket.org/%s/%s/get/%s.zip', + 'https://bitbucket.org/%s/%s/get/', $this->username, - $this->repository, - $ref + $this->repository ); } @@ -206,15 +208,7 @@ if ( !class_exists(BitBucketApi::class, false) ): )); $baseUrl = $url; - if ( $this->oauth ) { - $url = $this->oauth->sign($url,'GET'); - } - - $options = array('timeout' => wp_doing_cron() ? 10 : 3); - if ( !empty($this->httpFilterName) ) { - $options = apply_filters($this->httpFilterName, $options); - } - $response = wp_remote_get($url, $options); + $response = wp_remote_get($url, $this->getApiRequestHttpOptions()); if ( is_wp_error($response) ) { do_action('puc_api_error', $response, null, $url, $this->slug); return $response; @@ -248,25 +242,18 @@ if ( !class_exists(BitBucketApi::class, false) ): public function setAuthentication($credentials) { parent::setAuthentication($credentials); - if ( !empty($credentials) && !empty($credentials['consumer_key']) ) { - $this->oauth = new OAuthSignature( - $credentials['consumer_key'], - $credentials['consumer_secret'] + if ( + is_array($credentials) + && !empty($credentials['username']) + && !empty($credentials['api_token']) + ) { + $this->enableBasicAuth( + $credentials['username'], + $credentials['api_token'], + $this->getDownloadBaseUrl() ); - } 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/includes/plugin-update-checker/Puc/v5p4/Vcs/GitHubApi.php b/includes/plugin-update-checker/Puc/v5p7/Vcs/GitHubApi.php similarity index 78% rename from includes/plugin-update-checker/Puc/v5p4/Vcs/GitHubApi.php rename to includes/plugin-update-checker/Puc/v5p7/Vcs/GitHubApi.php index 610d932..782bfc2 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Vcs/GitHubApi.php +++ b/includes/plugin-update-checker/Puc/v5p7/Vcs/GitHubApi.php @@ -1,6 +1,6 @@ [^/]+?)/(?P[^/#?&]+?)/?$@', $path, $matches) ) { @@ -248,15 +243,7 @@ if ( !class_exists(GitHubApi::class, false) ): $baseUrl = $url; $url = $this->buildApiUrl($url, $queryParams); - $options = array('timeout' => wp_doing_cron() ? 10 : 3); - 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); + $response = wp_remote_get($url, $this->getApiRequestHttpOptions()); if ( is_wp_error($response) ) { do_action('puc_api_error', $response, null, $url, $this->slug); return $response; @@ -348,11 +335,17 @@ if ( !class_exists(GitHubApi::class, false) ): public function setAuthentication($credentials) { parent::setAuthentication($credentials); + //This property is no longer used internally, but is kept for backwards compatibility. $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+ + if ( is_string($credentials) && !empty($credentials) ) { + $repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', []); + $this->enableBasicAuth($this->userName, $credentials, $repoApiBaseUrl); + } + + //Assets sometimes need an Accept header, so we add a request filter even if basic + //authentication isn't enabled. + $this->enableDownloadRequestFilter(); } protected function getUpdateDetectionStrategies($configBranch) { @@ -393,74 +386,15 @@ if ( !class_exists(GitHubApi::class, false) ): return null; } - /** - * @param bool $result - * @return bool - * @internal - */ - public function addHttpRequestFilter($result) { - if ( !$this->downloadFilterAdded && $this->isAuthenticationEnabled() ) { - //phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout. - 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 = '') { + public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') { + //Release assets need an "Accept" header. See GitHub Docs: + //https://developer.github.com/v3/repos/releases/#get-a-single-release-asset //Is WordPress trying to download one of our release assets? if ( $this->releaseAssetsEnabled && (strpos($url, $this->getAssetApiBaseUrl()) !== 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. - * - * @param string $location - * @param array $headers - * @internal - */ - 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); + return parent::filterUpdateDownloadRequestArgs($requestArgs, $url); } } diff --git a/includes/plugin-update-checker/Puc/v5p4/Vcs/GitLabApi.php b/includes/plugin-update-checker/Puc/v5p7/Vcs/GitLabApi.php similarity index 97% rename from includes/plugin-update-checker/Puc/v5p4/Vcs/GitLabApi.php rename to includes/plugin-update-checker/Puc/v5p7/Vcs/GitLabApi.php index 2cbd6eb..8b5dcc5 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Vcs/GitLabApi.php +++ b/includes/plugin-update-checker/Puc/v5p7/Vcs/GitLabApi.php @@ -1,6 +1,6 @@ buildApiUrl($url, $queryParams); - $options = array('timeout' => wp_doing_cron() ? 10 : 3); - if ( !empty($this->httpFilterName) ) { - $options = apply_filters($this->httpFilterName, $options); - } - - $response = wp_remote_get($url, $options); + $response = wp_remote_get($url, $this->getApiRequestHttpOptions()); if ( is_wp_error($response) ) { do_action('puc_api_error', $response, null, $url, $this->slug); return $response; diff --git a/includes/plugin-update-checker/Puc/v5p4/Vcs/PluginUpdateChecker.php b/includes/plugin-update-checker/Puc/v5p7/Vcs/PluginUpdateChecker.php similarity index 98% rename from includes/plugin-update-checker/Puc/v5p4/Vcs/PluginUpdateChecker.php rename to includes/plugin-update-checker/Puc/v5p7/Vcs/PluginUpdateChecker.php index f00097f..3c4f6b7 100644 --- a/includes/plugin-update-checker/Puc/v5p4/Vcs/PluginUpdateChecker.php +++ b/includes/plugin-update-checker/Puc/v5p7/Vcs/PluginUpdateChecker.php @@ -1,8 +1,8 @@ getVcsApi()->enableReleaseAssets(); ``` + By default, PUC will simply use the first available asset. You can pass a regular expression to `enableReleaseAssets()` to filter assets by name. For example: + ```php + $myUpdateChecker->getVcsApi()->enableReleaseAssets('/\.zip($|[?&#])/i'); + ``` + - **Tags** To release version 1.2.3, create a new Git tag named `v1.2.3` or `1.2.3`. That's it. @@ -188,14 +193,12 @@ The library will pull update details from the following parts of a release/tag/b 'unique-plugin-or-theme-slug' ); - //Optional: If you're using a private repository, create an OAuth consumer - //and set the authentication credentials like this: - //Note: For now you need to check "This is a private consumer" when - //creating the consumer to work around #134: - // https://github.com/YahnisElsts/plugin-update-checker/issues/134 + //Optional: If you're using a private repository, create an API token + //with the "read:repository:bitbucket" scope and set the authentication + //credentials like this: $myUpdateChecker->setAuthentication(array( - 'consumer_key' => '...', - 'consumer_secret' => '...', + 'username' => 'example@example.com', //Your BitBucket email address. + 'api_token' => '...', )); //Optional: Set the branch that contains the stable release. @@ -252,8 +255,8 @@ 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 - use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker; - use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi; + use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker; + use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi; $myUpdateChecker = new PluginUpdateChecker( new GitLabApi('https://myserver.com/user-name/repo-name/'), @@ -264,8 +267,8 @@ BitBucket doesn't have an equivalent to GitHub's releases, so the process is sli ``` 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 - use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker; - use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi; + use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker; + use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi; $myUpdateChecker = new PluginUpdateChecker( new GitLabApi( @@ -347,14 +350,14 @@ Other classes have also been renamed, usually by simply removing the `Puc_vXpY_` | Old class name | New class name | |-------------------------------------|----------------------------------------------------------------| | `Puc_v4_Factory` | `YahnisElsts\PluginUpdateChecker\v5\PucFactory` | -| `Puc_v4p13_Factory` | `YahnisElsts\PluginUpdateChecker\v5p4\PucFactory` | -| `Puc_v4p13_Plugin_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Plugin\UpdateChecker` | -| `Puc_v4p13_Theme_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Theme\UpdateChecker` | -| `Puc_v4p13_Vcs_PluginUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker` | -| `Puc_v4p13_Vcs_ThemeUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\ThemeUpdateChecker` | -| `Puc_v4p13_Vcs_GitHubApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitHubApi` | -| `Puc_v4p13_Vcs_GitLabApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi` | -| `Puc_v4p13_Vcs_BitBucketApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\BitBucketApi` | +| `Puc_v4p13_Factory` | `YahnisElsts\PluginUpdateChecker\v5p7\PucFactory` | +| `Puc_v4p13_Plugin_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Plugin\UpdateChecker` | +| `Puc_v4p13_Theme_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Theme\UpdateChecker` | +| `Puc_v4p13_Vcs_PluginUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker` | +| `Puc_v4p13_Vcs_ThemeUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\ThemeUpdateChecker` | +| `Puc_v4p13_Vcs_GitHubApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitHubApi` | +| `Puc_v4p13_Vcs_GitLabApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi` | +| `Puc_v4p13_Vcs_BitBucketApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\BitBucketApi` | License Management ------------------ diff --git a/includes/plugin-update-checker/composer.json b/includes/plugin-update-checker/composer.json index f7af7eb..85b1f35 100644 --- a/includes/plugin-update-checker/composer.json +++ b/includes/plugin-update-checker/composer.json @@ -18,6 +18,6 @@ "ext-json": "*" }, "autoload": { - "files": ["load-v5p4.php"] + "files": ["load-v5p7.php"] } } diff --git a/includes/plugin-update-checker/languages/plugin-update-checker-el.mo b/includes/plugin-update-checker/languages/plugin-update-checker-el.mo new file mode 100644 index 0000000000000000000000000000000000000000..08306de69e2d1258c0ab69400fac88e54439e837 GIT binary patch literal 1455 zcma)5&u<$=6kZ?*$v1lD@ER$nb~c+$6PmSrXibVL64ewJ!6};96MNgujI}eHw4B-` z{ShftNDzqw5*LnjqtI9-P%B8>Xa55Z@DK1O@Xgwd6E%mf_560;``&xsdvEsdb7Q+6 zo(~`&LF(>+XJ?=a{UP4Jg%IRt$T;LrNComQWFB(<9nWLXEvMi0yj|#vIREwD!2cig z=Qz)fdEVF1PoTep{s+1Yt>4FAwJjQVXho%zb(-7=?bX{N6%=cdiW+S$TOv^{;%huk z_;MnA@0PqH)w&!80D}x7C2hFLG>uQ_anG~`b&{+VOER%Z#Ml**Vx87Rs$(Tdt&l@Z z=+_`rJ5k_`=&H(+Cioa?3M10hSc2fnYsSRE_c*-hWkUOJazJCJOJ*H1id@&eS4-7x z(J<_KlYKd084by?!C=_1&r_#sVHkzsg#b@!fosF+DVI9oMx_ziAMKGn$hY$c_IbWR_Ivxg-OIP~ zO?#AY04`e@ZwXcxd==nb`v-n6ZQmXO_w3jokw{95*B9qU4Ndt$sj8$A5(V#dz}$$t zX0ns;SeD(gQ!Py8R{a{gJi<9*DY(;c=IX0zQlBGOh*HpTyBa+xQj=B<- z-PrJ2k#xR7Q{~cE>{B5{I%sV{H59dt>F8*3GEohl#9H{v@i&ve)s$$)hF#@abhv3p zx=NdRA=W0MrKR$cC@Q*xAV*xETAJ?E-j+pd+&TL+@U3DE7LK zB2R7K_v|B7>Elvt=38**_krE9zZR>1h2qYdKRnH08^-4a6u!-()>gg^F2zS`f3kay zb>DWK>f-hs6u6fz%O33eXYqu0(C!HyMe<%@aG&0=W8B^?a@<+~FNpuNW0h tD1$K;9ibq(uDfRr)=})aL7zF{w;=S(@yJe5J=N&R+}l`47fg?Q?|%dqbwmIF literal 0 HcmV?d00001 diff --git a/includes/plugin-update-checker/languages/plugin-update-checker-el.po b/includes/plugin-update-checker/languages/plugin-update-checker-el.po new file mode 100644 index 0000000..6b1ba21 --- /dev/null +++ b/includes/plugin-update-checker/languages/plugin-update-checker-el.po @@ -0,0 +1,52 @@ +msgid "" +msgstr "" +"Project-Id-Version: plugin-update-checker\n" +"POT-Creation-Date: 2022-07-29 15:34+0300\n" +"PO-Revision-Date: 2024-05-09 22:22+0000\n" +"Last-Translator: theogk\n" +"Language-Team: Ελληνικά\n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Loco https://localise.biz/\n" +"X-Poedit-Basepath: ..\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n" +"X-Poedit-SearchPath-0: .\n" +"Report-Msgid-Bugs-To: \n" +"X-Loco-Version: 2.6.9; wp-6.5.3" + +#: Puc/v5p4/Plugin/Ui.php:128 +msgid "Check for updates" +msgstr "Έλεγχος για ενημερώσεις" + +#: Puc/v5p4/Plugin/Ui.php:214 +#, php-format +msgctxt "the plugin title" +msgid "The %s plugin is up to date." +msgstr "Το πρόσθετο %s είναι ενημερωμένο." + +#: Puc/v5p4/Plugin/Ui.php:216 +#, php-format +msgctxt "the plugin title" +msgid "A new version of the %s plugin is available." +msgstr "Μία νέα έκδοση είναι διαθέσιμη για το πρόσθετο %s." + +#: Puc/v5p4/Plugin/Ui.php:218 +#, php-format +msgctxt "the plugin title" +msgid "Could not determine if updates are available for %s." +msgstr "" +"Δεν ήταν εφικτό να εκτελεστεί ο έλεγχος για νέες ενημερώσεις για το πρόσθετο " +"%s." + +#: Puc/v5p4/Plugin/Ui.php:224 +#, php-format +msgid "Unknown update checker status \"%s\"" +msgstr "Άγνωστο πρόβλημα του ενημερωτή προσθέτων \"%s\"" + +#: Puc/v5p4/Vcs/PluginUpdateChecker.php:100 +msgid "There is no changelog available." +msgstr "Δεν υπάρχει διαθέσιμο αρχείο αλλαγών." diff --git a/includes/plugin-update-checker/languages/plugin-update-checker-fa_IR.mo b/includes/plugin-update-checker/languages/plugin-update-checker-fa_IR.mo index a68f1004811da6f8438dcb46da94dd438ba24843..4a9e94899fa58578389dfd1054a512fe661507f0 100644 GIT binary patch delta 464 zcmXYrJxjw-7=>?Y{TN&nRO)o8RuGNER7H!MMJGW7-J`u?P#bAe9YxUU&_T%$5IPtw z(vbRvA2>Pq2ZZ<1#lb;vclO-s4NuN}PtJSDSKD*zN&j94W9$;dz!Tu$9mK&8Xa_Nk zu~#sl!2BIyY!KSn#Mm%&8afHJp{l10MYM4cWl_-lH?o$0MMNFrPiEG|>IPqPa=fse zv0Ra7^G;51I}2;ea&0Hetre%>^2F(9pBLBq+Lvvy%QM2Y?9IHF*7SaTQS8`xB=s~3 zQrtAmaow2EM@?=fQ)5#@#;D=FXm8$KWVmZiWJS($om`49I3i=aJUKGqoke=QkI2#< zT~LVUx?bYyDvznAM)GA*SyvR)OB68j>0Tb-{QxS6!o8sig`5K9FW`pqh$r%l%2cJW zUXn$Q?~nBolA&5c9j?CI$G$9LI~ZZI`YeYJxj6Q+|C+@vm=bTJFd zY+QHY*g;#;6hZL^xVMY@oSh4C=fXeWd1*F%a5(3kd+)jT+w`A{Z)bbAiD(~~0Zsr0 zegoHm=>ky+Tmt_E9s*1F&rcEE0>1{k;63nD@Co=nxN?pNt>Xua@TCi zs0D7RP4232!w*^&jo3v=ZaYcW(_gluaGBMq)P?p@2NTJAk|?=-4z#c>@cSuR|@1mAV6 zYTfuXr{dIVNw@I*+B55sYN^nRg0R7l1GVZ$?Amp^p7e@Wk_W}P\n" -"Language-Team: Pro Style \n" +"POT-Creation-Date: 2025-06-12 23:40+0100\n" +"PO-Revision-Date: 2025-06-12 23:49+0100\n" +"Last-Translator: Pro Style \n" +"Language-Team: Alex Javadi \n" "Language: fa_IR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.8.8\n" -"X-Poedit-Basepath: ..\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.6\n" +"X-Poedit-Basepath: ..\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: __;_e\n" "X-Poedit-SearchPath-0: .\n" -#: github-checker.php:120 -msgid "There is no changelog available." -msgstr "شرحی برای تغییرات یافت نشد" +#: Puc/v5p6/Plugin/Ui.php:56 +msgid "View details" +msgstr "مشاهده جزئیات" -#: plugin-update-checker.php:637 +#: Puc/v5p6/Plugin/Ui.php:79 +#, php-format +msgid "More information about %s" +msgstr "اطلاعات بیشتر درباره %s" + +# It had some “potential” grammar issues and also didn’t sound native. +# P.S. I know the current translation is literally “Checking for new updates”, however, I thought it might sound more natural and known to others. +#: Puc/v5p6/Plugin/Ui.php:130 msgid "Check for updates" -msgstr "بررسی برای بروزرسانی " +msgstr "بررسی بروزرسانی جدید" -#: plugin-update-checker.php:681 -msgid "This plugin is up to date." -msgstr "شما از آخرین نسخه استفاده میکنید . به‌روز باشید" - -#: plugin-update-checker.php:683 -msgid "A new version of this plugin is available." -msgstr "نسخه جدیدی برای افزونه ارائه شده است ." - -#: plugin-update-checker.php:685 +# The word “ناشناخته” is seems to be translated directly from the word (Un-known), rather than checking for the context. +# I think “نامشخص” (unknown) might be a suitable version in this scenario. +#: Puc/v5p6/Plugin/Ui.php:227 #, php-format msgid "Unknown update checker status \"%s\"" -msgstr "وضعیت ناشناخته برای بروزرسانی \"%s\"" +msgstr "وضعیت نامشخص برای بروزرسانی \"%s\"" + +# The previous translation was okay, however, it didn’t sound native to me. +#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113 +msgid "There is no changelog available." +msgstr "آخرین تغییراتی یافت نشد." + +#~ msgid "This plugin is up to date." +#~ msgstr "شما از آخرین نسخه استفاده میکنید . به‌روز باشید" + +#~ msgid "A new version of this plugin is available." +#~ msgstr "نسخه جدیدی برای افزونه ارائه شده است ." diff --git a/includes/plugin-update-checker/languages/plugin-update-checker-sr_RS.mo b/includes/plugin-update-checker/languages/plugin-update-checker-sr_RS.mo new file mode 100644 index 0000000000000000000000000000000000000000..af67f9e9a0569d0f56a3b5e7db2ff39acb55421f GIT binary patch literal 1414 zcmZuw&u<$=6doY_vZw?|96%giBUu8$F1E>`tZPI`Qxwq%(y76KlKg6 zdK&gJtOGj%`x)lfU$Ez3f5Tpc-G>qE#3PJN14H05zz=~9;77nY;19q6_z&<`*t3r^ zb`s}59%HNw{1@osUz%dgB0{z#`%>n2P}VX<>e0?nq`etdwSzv_hiZ)gc5~amBtdOn5Bo{(EX8Y-4Hh zTyW|r&g)tcI+&Af)rn;{qEpMUEj5!=^CUQjf5b#2@eOe_jauCg1KY+cl1?_L`f9y& zIe1S%c$XV7`{L@F(j9X? zHa4PGtKJgz)|IIGdaVt7fJJcK?lr+4RKQ`S> zxvy}z=0@ap++OyX8{=jdcb~(3-KUKGV{l)(k-H0)YwnJZ9sBTY1diMuNRdF;>}2cN zCb`=nS`\n" +"Language-Team: \n" +"Language: sr_RS\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.4.3\n" +"X-Poedit-Basepath: ..\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n" +"X-Poedit-SearchPath-0: .\n" + +#: Puc/v5p5/Plugin/Ui.php:56 +msgid "View details" +msgstr "Види детаље" + +#: Puc/v5p5/Plugin/Ui.php:79 +#, php-format +msgid "More information about %s" +msgstr "Више информација о %s" + +#: Puc/v5p5/Plugin/Ui.php:130 +msgid "Check for updates" +msgstr "Провера ажурирања" + +#: Puc/v5p5/Plugin/Ui.php:217 +#, php-format +msgctxt "the plugin title" +msgid "The %s plugin is up to date." +msgstr "Додатак %s је у најновијем издању." + +#: Puc/v5p5/Plugin/Ui.php:219 +#, php-format +msgctxt "the plugin title" +msgid "A new version of the %s plugin is available." +msgstr "Доступно је ново издање за %s." + +#: Puc/v5p5/Plugin/Ui.php:221 +#, php-format +msgctxt "the plugin title" +msgid "Could not determine if updates are available for %s." +msgstr "Није могуће утврдити да ли су доступне исправке за %s." + +#: Puc/v5p5/Plugin/Ui.php:227 +#, php-format +msgid "Unknown update checker status \"%s\"" +msgstr "Непознат статус провере ажурирања \"%s\"" + +#: Puc/v5p5/Vcs/PluginUpdateChecker.php:113 +msgid "There is no changelog available." +msgstr "Белешке о изменама нису доступне." diff --git a/includes/plugin-update-checker/languages/plugin-update-checker-zh_CN.mo b/includes/plugin-update-checker/languages/plugin-update-checker-zh_CN.mo index 86d114472907c99814f248ed3e063f8bd0ce5819..4e8ce4a1eeb55be777dc83ac448cb8dfacbb0931 100644 GIT binary patch delta 150 zcmbQnIfZjVit7eO1_l8p1_mAm28M&o3=Bd*`W%p!1JVzHv;vT3V1dXh0%?%A9+1`o z(!M}i8c0`cT)B*q$5hwQP}j&%!O+0UWO6jqmdW\n" "Language-Team: \n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.4.3\n" -"X-Poedit-Basepath: ..\n" "Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Poedit 3.8\n" +"X-Poedit-Basepath: ..\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n" "X-Poedit-SearchPath-0: .\n" -#: Puc/v4p11/Plugin/Ui.php:54 +#: Puc/v5p6/Plugin/Ui.php:56 msgid "View details" msgstr "查看详情" -#: Puc/v4p11/Plugin/Ui.php:77 +#: Puc/v5p6/Plugin/Ui.php:79 #, php-format msgid "More information about %s" msgstr "%s 的更多信息" -#: Puc/v4p11/Plugin/Ui.php:128 +#: Puc/v5p6/Plugin/Ui.php:130 msgid "Check for updates" msgstr "检查更新" -#: Puc/v4p11/Plugin/Ui.php:214 +#: Puc/v5p6/Plugin/Ui.php:217 #, php-format msgctxt "the plugin title" msgid "The %s plugin is up to date." msgstr "%s 目前是最新版本。" -#: Puc/v4p11/Plugin/Ui.php:216 +#: Puc/v5p6/Plugin/Ui.php:219 #, php-format msgctxt "the plugin title" msgid "A new version of the %s plugin is available." msgstr "%s 当前有可用的更新。" -#: Puc/v4p11/Plugin/Ui.php:218 +#: Puc/v5p6/Plugin/Ui.php:221 #, php-format msgctxt "the plugin title" msgid "Could not determine if updates are available for %s." msgstr "%s 无法确定是否有可用的更新。" -#: Puc/v4p11/Plugin/Ui.php:224 +#: Puc/v5p6/Plugin/Ui.php:227 #, php-format msgid "Unknown update checker status \"%s\"" msgstr "未知的更新检查状态:%s" -#: Puc/v4p11/Vcs/PluginUpdateChecker.php:100 +#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113 msgid "There is no changelog available." msgstr "没有可用的更新日志。" diff --git a/includes/plugin-update-checker/languages/plugin-update-checker-zh_TW.mo b/includes/plugin-update-checker/languages/plugin-update-checker-zh_TW.mo new file mode 100644 index 0000000000000000000000000000000000000000..a7d251127da6a6bdd2cce3acd909d893a322ed5e GIT binary patch literal 1592 zcmb7ETW=dh6kZ_QEGj|YDj_tAWFbLziJ^jOO)A=uLPa7>KwRQo=Yc2w0N;3B+bAzE((yMl=gjw=b7t1R z9vr%4U_1_c8s@?dz;43y_zm_H?0489*xxV$JMe&E3wr{$jC;(PCvzeq)zGDiNn=VxjQAW|6{>u}37r*HMY7T;dQ7_{;et3Gv zV}!%Kz#^%I#T?NQS$RJQ(gSMyPLGRf;37Gt1z1$^IkKI*Ul^fCzIVJ+a#mXVb3m2< zmaK+E38I^08HGr^#~fvzbj{OgmNqG*70h({M>-89l}EAnTDgLbSdOS1%@p1?Pq77Gx4?WIsy4|?QmM>hM7a#8648I- znZylMX_=WtMl1jCh)v%2O2sposgqL^{X%orXl7h+#W<2#4w0B*OX`I%pgzy*7ZFLO z#%78q%p?6co%9@w%n9xY*XL!M9GUf122G1-V4lD$lQ!Xa1z5_lTwdQbremY|%vrM_ znCmO^OyNyNU52{h#A`9rw`z7Dm=$^3r>mjvzD<^yJZrweIE&K0rGzAB zy=)!s^G}OvC@loG#h9kr$o5tJIlN zrJ`{g{eAxc(8~s@&S5}h7y}d7+?d^}+8|RIrvDsSvB;NWwZmza2-roJW z0fxQ$4~T1To(E5Rc?^OW>>G1s}@LBzK*vqCD(RZn^$zM@#WjC a&#tG5C0}gxly&SNXL4>QZq|CV+Wil8yEqpB literal 0 HcmV?d00001 diff --git a/includes/plugin-update-checker/languages/plugin-update-checker-zh_TW.po b/includes/plugin-update-checker/languages/plugin-update-checker-zh_TW.po new file mode 100644 index 0000000..e3ab5ee --- /dev/null +++ b/includes/plugin-update-checker/languages/plugin-update-checker-zh_TW.po @@ -0,0 +1,66 @@ +# Blank Plugin POT Template +# Copyright 2025 ... +# This file is distributed under the GNU General Public License v3 or later. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Plugin Update Checker\n" +"Report-Msgid-Bugs-To: Alex Lion \n" +"POT-Creation-Date: 2025-09-19 14:05-0700\n" +"PO-Revision-Date: \n" +"Last-Translator: Alex Lion \n" +"Language-Team: Alex Lion \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Poedit-WPHeader: plugin-update-checker.php\n" +"X-Textdomain-Support: yesX-Generator: Poedit 1.6.4\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: " +"__;_e;esc_html_e;esc_html_x:1,2c;esc_html__;esc_attr_e;esc_attr_x:1,2c;esc_attr__;_ex:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;_x:1,2c;_n:1,2;_n_noop:1,2;__ngettext:1,2;__ngettext_noop:1,2;_c,_nc:4c,1,2\n" +"X-Poedit-Basepath: ..\n" +"X-Poedit-Bookmarks: \n" +"X-Generator: Poedit 3.7\n" +"X-Poedit-SearchPath-0: .\n" + +#: Puc/v5p6/Plugin/Ui.php:56 +msgid "View details" +msgstr "檢視詳細資料" + +#: Puc/v5p6/Plugin/Ui.php:79 +#, php-format +msgid "More information about %s" +msgstr "進一步了解 %s 的相關資訊" + +#: Puc/v5p6/Plugin/Ui.php:130 +msgid "Check for updates" +msgstr "檢查更新" + +#: Puc/v5p6/Plugin/Ui.php:217 +#, php-format +msgctxt "the plugin title" +msgid "The %s plugin is up to date." +msgstr "%s 外掛已為最新版本。" + +#: Puc/v5p6/Plugin/Ui.php:219 +#, php-format +msgctxt "the plugin title" +msgid "A new version of the %s plugin is available." +msgstr "%s 外掛已有新版本可供更新。" + +#: Puc/v5p6/Plugin/Ui.php:221 +#, php-format +msgctxt "the plugin title" +msgid "Could not determine if updates are available for %s." +msgstr "無法確定 %s 是否有可用的更新。" + +#: Puc/v5p6/Plugin/Ui.php:227 +#, php-format +msgid "Unknown update checker status \"%s\"" +msgstr "未知的更新檢查程式狀態: %s" + +#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113 +msgid "There is no changelog available." +msgstr "目前沒有可供檢閱的變更記錄。" diff --git a/includes/plugin-update-checker/languages/plugin-update-checker.pot b/includes/plugin-update-checker/languages/plugin-update-checker.pot index 5b6319c..abe04a4 100644 --- a/includes/plugin-update-checker/languages/plugin-update-checker.pot +++ b/includes/plugin-update-checker/languages/plugin-update-checker.pot @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: plugin-update-checker\n" -"POT-Creation-Date: 2022-07-29 15:34+0300\n" +"POT-Creation-Date: 2025-05-20 15:27+0300\n" "PO-Revision-Date: 2016-01-10 20:59+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -11,39 +11,39 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.1.1\n" +"X-Generator: Poedit 3.6\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n" "X-Poedit-SearchPath-0: .\n" -#: Puc/v5p4/Plugin/Ui.php:128 +#: Puc/v5p7/Plugin/Ui.php:130 msgid "Check for updates" msgstr "" -#: Puc/v5p4/Plugin/Ui.php:214 +#: Puc/v5p7/Plugin/Ui.php:217 #, php-format msgctxt "the plugin title" msgid "The %s plugin is up to date." msgstr "" -#: Puc/v5p4/Plugin/Ui.php:216 +#: Puc/v5p7/Plugin/Ui.php:219 #, php-format msgctxt "the plugin title" msgid "A new version of the %s plugin is available." msgstr "" -#: Puc/v5p4/Plugin/Ui.php:218 +#: Puc/v5p7/Plugin/Ui.php:221 #, php-format msgctxt "the plugin title" msgid "Could not determine if updates are available for %s." msgstr "" -#: Puc/v5p4/Plugin/Ui.php:224 +#: Puc/v5p7/Plugin/Ui.php:227 #, php-format msgid "Unknown update checker status \"%s\"" msgstr "" -#: Puc/v5p4/Vcs/PluginUpdateChecker.php:100 +#: Puc/v5p7/Vcs/PluginUpdateChecker.php:113 msgid "There is no changelog available." msgstr "" diff --git a/includes/plugin-update-checker/load-v5p4.php b/includes/plugin-update-checker/load-v5p7.php similarity index 80% rename from includes/plugin-update-checker/load-v5p4.php rename to includes/plugin-update-checker/load-v5p7.php index 2cd9580..be2b1b6 100644 --- a/includes/plugin-update-checker/load-v5p4.php +++ b/includes/plugin-update-checker/load-v5p7.php @@ -1,14 +1,14 @@ $pucVersionedClass ) { - MajorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.4'); + MajorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.7'); //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. - MinorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.4'); + MinorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.7'); } diff --git a/includes/plugin-update-checker/plugin-update-checker.php b/includes/plugin-update-checker/plugin-update-checker.php index da4cb1e..e5b3753 100644 --- a/includes/plugin-update-checker/plugin-update-checker.php +++ b/includes/plugin-update-checker/plugin-update-checker.php @@ -1,10 +1,10 @@ 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('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $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 .= ''; - } - 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/includes/plugin-update-checker/vendor/PucReadmeParser.php b/includes/plugin-update-checker/vendor/PucReadmeParser.php deleted file mode 100644 index a794c49..0000000 --- a/includes/plugin-update-checker/vendor/PucReadmeParser.php +++ /dev/null @@ -1,352 +0,0 @@ -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 = function_exists('wp_strip_all_tags') - ? wp_strip_all_tags($text) - //phpcs:ignore WordPressVIPMinimum.Functions.StripTags.StripTagsOneParameter -- Using wp_strip_all_tags() if available - : 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('&lt;', '<', $text); - $text = str_replace('&gt;', '>', $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/projects-portfolio.php b/projects-portfolio.php
    index d9dc47e..eb43cbe 100644
    --- a/projects-portfolio.php
    +++ b/projects-portfolio.php
    @@ -34,9 +34,9 @@ $myUpdateChecker = PucFactory::buildUpdateChecker(
     	__FILE__,
     	'projects-portfolio'
     );
    -
    -// Set the branch that contains the stable release.
    -$myUpdateChecker->setBranch( 'main' );
    +// Note: PUC's setBranch() is only available when the host is recognized as a
    +// VCS provider (GitHub, GitLab, Bitbucket). The Gitea URL here falls back to
    +// PUC's plain JSON metadata mode, so we skip the call rather than throw.
     
     // Current plugin version.
     define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.1' );