Quick Page/Post Redirect Plugin - Version 5.2.1

Version Description

Download this release

Release Info

Developer anadnet
Plugin Icon 128x128 Quick Page/Post Redirect Plugin
Version 5.2.1
Comparing to
See all releases

Code changes from version 5.2.0 to 5.2.1

Files changed (102) hide show
  1. page_post_redirect_plugin.php +11 -2
  2. readme.txt +2 -2
  3. updater/Puc/v4/Factory.php +6 -0
  4. updater/Puc/v4p10/Autoloader.php +63 -0
  5. updater/Puc/v4p10/DebugBar/Extension.php +186 -0
  6. updater/Puc/v4p10/DebugBar/Panel.php +165 -0
  7. updater/Puc/v4p10/DebugBar/PluginExtension.php +33 -0
  8. updater/Puc/v4p10/DebugBar/PluginPanel.php +38 -0
  9. updater/Puc/v4p10/DebugBar/ThemePanel.php +21 -0
  10. updater/Puc/v4p10/Factory.php +356 -0
  11. updater/Puc/v4p10/InstalledPackage.php +103 -0
  12. updater/Puc/v4p10/Metadata.php +132 -0
  13. updater/Puc/v4p10/OAuthSignature.php +100 -0
  14. updater/Puc/v4p10/Plugin/Info.php +132 -0
  15. updater/Puc/v4p10/Plugin/Package.php +184 -0
  16. updater/Puc/v4p10/Plugin/Ui.php +277 -0
  17. updater/Puc/v4p10/Plugin/Update.php +112 -0
  18. updater/Puc/v4p10/Plugin/UpdateChecker.php +414 -0
  19. updater/Puc/v4p10/Scheduler.php +266 -0
  20. updater/Puc/v4p10/StateStore.php +207 -0
  21. updater/Puc/v4p10/Theme/Package.php +65 -0
  22. updater/Puc/v4p10/Theme/Update.php +84 -0
  23. updater/Puc/v4p10/Theme/UpdateChecker.php +152 -0
  24. updater/Puc/v4p10/Update.php +34 -0
  25. updater/Puc/v4p10/UpdateChecker.php +994 -0
  26. updater/Puc/v4p10/UpgraderStatus.php +199 -0
  27. updater/Puc/v4p10/Utils.php +69 -0
  28. updater/Puc/v4p10/Vcs/Api.php +302 -0
  29. updater/Puc/v4p10/Vcs/BaseChecker.php +27 -0
  30. updater/Puc/v4p10/Vcs/BitBucketApi.php +265 -0
  31. updater/Puc/v4p10/Vcs/GitHubApi.php +441 -0
  32. updater/Puc/v4p10/Vcs/GitLabApi.php +309 -0
  33. updater/Puc/v4p10/Vcs/PluginUpdateChecker.php +218 -0
  34. updater/Puc/v4p10/Vcs/Reference.php +49 -0
  35. updater/Puc/v4p10/Vcs/ThemeUpdateChecker.php +118 -0
  36. updater/css/puc-debug-bar.css +70 -0
  37. updater/js/debug-bar.js +52 -0
  38. updater/languages/plugin-update-checker-ca.mo +0 -0
  39. updater/languages/plugin-update-checker-ca.po +48 -0
  40. updater/languages/plugin-update-checker-cs_CZ.mo +0 -0
  41. updater/languages/plugin-update-checker-cs_CZ.po +45 -0
  42. updater/languages/plugin-update-checker-da_DK.mo +0 -0
  43. updater/languages/plugin-update-checker-da_DK.po +42 -0
  44. updater/languages/plugin-update-checker-de_DE.mo +0 -0
  45. updater/languages/plugin-update-checker-de_DE.po +38 -0
  46. updater/languages/plugin-update-checker-es_AR.mo +0 -0
  47. updater/languages/plugin-update-checker-es_AR.po +48 -0
  48. updater/languages/plugin-update-checker-es_CL.mo +0 -0
  49. updater/languages/plugin-update-checker-es_CL.po +48 -0
  50. updater/languages/plugin-update-checker-es_CO.mo +0 -0
  51. updater/languages/plugin-update-checker-es_CO.po +48 -0
  52. updater/languages/plugin-update-checker-es_CR.mo +0 -0
  53. updater/languages/plugin-update-checker-es_CR.po +48 -0
  54. updater/languages/plugin-update-checker-es_DO.mo +0 -0
  55. updater/languages/plugin-update-checker-es_DO.po +48 -0
  56. updater/languages/plugin-update-checker-es_ES.mo +0 -0
  57. updater/languages/plugin-update-checker-es_ES.po +48 -0
  58. updater/languages/plugin-update-checker-es_GT.mo +0 -0
  59. updater/languages/plugin-update-checker-es_GT.po +48 -0
  60. updater/languages/plugin-update-checker-es_HN.mo +0 -0
  61. updater/languages/plugin-update-checker-es_HN.po +48 -0
  62. updater/languages/plugin-update-checker-es_MX.mo +0 -0
  63. updater/languages/plugin-update-checker-es_MX.po +48 -0
  64. updater/languages/plugin-update-checker-es_PE.mo +0 -0
  65. updater/languages/plugin-update-checker-es_PE.po +48 -0
  66. updater/languages/plugin-update-checker-es_PR.mo +0 -0
  67. updater/languages/plugin-update-checker-es_PR.po +48 -0
  68. updater/languages/plugin-update-checker-es_UY.mo +0 -0
  69. updater/languages/plugin-update-checker-es_UY.po +48 -0
  70. updater/languages/plugin-update-checker-es_VE.mo +0 -0
  71. updater/languages/plugin-update-checker-es_VE.po +48 -0
  72. updater/languages/plugin-update-checker-fa_IR.mo +0 -0
  73. updater/languages/plugin-update-checker-fa_IR.po +38 -0
  74. updater/languages/plugin-update-checker-fr_CA.mo +0 -0
  75. updater/languages/plugin-update-checker-fr_CA.po +48 -0
  76. updater/languages/plugin-update-checker-fr_FR.mo +0 -0
  77. updater/languages/plugin-update-checker-fr_FR.po +42 -0
  78. updater/languages/plugin-update-checker-hu_HU.mo +0 -0
  79. updater/languages/plugin-update-checker-hu_HU.po +41 -0
  80. updater/languages/plugin-update-checker-it_IT.mo +0 -0
  81. updater/languages/plugin-update-checker-it_IT.po +38 -0
  82. updater/languages/plugin-update-checker-ja.mo +0 -0
  83. updater/languages/plugin-update-checker-ja.po +57 -0
  84. updater/languages/plugin-update-checker-nl_BE.mo +0 -0
  85. updater/languages/plugin-update-checker-nl_BE.po +48 -0
  86. updater/languages/plugin-update-checker-nl_NL.mo +0 -0
  87. updater/languages/plugin-update-checker-nl_NL.po +48 -0
  88. updater/languages/plugin-update-checker-pt_BR.mo +0 -0
  89. updater/languages/plugin-update-checker-pt_BR.po +48 -0
  90. updater/languages/plugin-update-checker-sl_SI.mo +0 -0
  91. updater/languages/plugin-update-checker-sl_SI.po +48 -0
  92. updater/languages/plugin-update-checker-sv_SE.mo +0 -0
  93. updater/languages/plugin-update-checker-sv_SE.po +42 -0
  94. updater/languages/plugin-update-checker-zh_CN.mo +0 -0
  95. updater/languages/plugin-update-checker-zh_CN.po +48 -0
  96. updater/languages/plugin-update-checker.pot +49 -0
  97. updater/load-v4p10.php +28 -0
  98. updater/plugin-update-checker.php +10 -0
  99. updater/vendor/Parsedown.php +9 -0
  100. updater/vendor/ParsedownLegacy.php +1535 -0
  101. updater/vendor/ParsedownModern.php +1538 -0
  102. updater/vendor/PucReadmeParser.php +348 -0
page_post_redirect_plugin.php CHANGED
@@ -6,7 +6,7 @@ Description: Redirect Pages, Posts or Custom Post Types to another location quic
6
  Author: anadnet
7
  Author URI: http://www.anadnet.com/
8
  Donate link:
9
- Version: 5.2.0
10
  Text Domain: quick-pagepost-redirect-plugin
11
  Domain Path: /lang
12
  License: GPLv2 or later
@@ -33,6 +33,15 @@ License URI: http://www.gnu.org/licenses/gpl-2.0.html
33
  ===================
34
  */
35
 
 
 
 
 
 
 
 
 
 
36
  global $newqppr, $redirect_plugin, $qppr_setting_links;
37
  $qppr_setting_links = false;
38
  start_ppr_class();
@@ -71,7 +80,7 @@ class quick_page_post_reds {
71
  public $pprptypes_ok;
72
 
73
  function __construct() {
74
- $this->ppr_curr_version = '5.2.0';
75
  $this->ppr_nofollow = array();
76
  $this->ppr_newindow = array();
77
  $this->ppr_url = array();
6
  Author: anadnet
7
  Author URI: http://www.anadnet.com/
8
  Donate link:
9
+ Version: 5.2.1
10
  Text Domain: quick-pagepost-redirect-plugin
11
  Domain Path: /lang
12
  License: GPLv2 or later
33
  ===================
34
  */
35
 
36
+ // update functionality
37
+ require dirname(__FILE__).'/updater/plugin-update-checker.php';
38
+ $myUpdateChecker = Puc_v4_Factory::buildUpdateChecker(
39
+ //'https://anadnet.com/updates/plugin.json',
40
+ 'https://anadnet.com/updates/?action=get_metadata&slug=quick-pagepost-redirect-plugin',
41
+ __FILE__, //Full path to the main plugin file or functions.php.
42
+ 'quick-pagepost-redirect-plugin'
43
+ );
44
+
45
  global $newqppr, $redirect_plugin, $qppr_setting_links;
46
  $qppr_setting_links = false;
47
  start_ppr_class();
80
  public $pprptypes_ok;
81
 
82
  function __construct() {
83
+ $this->ppr_curr_version = '5.2.1';
84
  $this->ppr_nofollow = array();
85
  $this->ppr_newindow = array();
86
  $this->ppr_url = array();
readme.txt CHANGED
@@ -6,12 +6,12 @@ Requires at least: 4.0
6
  License: GPLv2 or later
7
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
8
  Tested up to: 5.5.1
9
- Stable tag: 5.2.0
10
 
11
  Easily redirect pages/posts or custom post types to another page/post or external URL by specifying the redirect URL and type (301, 302, 307, meta).
12
 
13
  == Description ==
14
- **Current Version 5.2.0**
15
 
16
  This plugin has two redirect functionalities - **"Quick Redirects"** and **"Individual Redirects"**:
17
 
6
  License: GPLv2 or later
7
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
8
  Tested up to: 5.5.1
9
+ Stable tag: 5.2.1
10
 
11
  Easily redirect pages/posts or custom post types to another page/post or external URL by specifying the redirect URL and type (301, 302, 307, meta).
12
 
13
  == Description ==
14
+ **Current Version 5.2.1**
15
 
16
  This plugin has two redirect functionalities - **"Quick Redirects"** and **"Individual Redirects"**:
17
 
updater/Puc/v4/Factory.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4_Factory', false) ):
3
+
4
+ class Puc_v4_Factory extends Puc_v4p10_Factory { }
5
+
6
+ endif;
updater/Puc/v4p10/Autoloader.php ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_Autoloader', false) ):
4
+
5
+ class Puc_v4p10_Autoloader {
6
+ private $prefix = '';
7
+ private $rootDir = '';
8
+ private $libraryDir = '';
9
+
10
+ private $staticMap;
11
+
12
+ public function __construct() {
13
+ $this->rootDir = dirname(__FILE__) . '/';
14
+ $nameParts = explode('_', __CLASS__, 3);
15
+ $this->prefix = $nameParts[0] . '_' . $nameParts[1] . '_';
16
+
17
+ $this->libraryDir = $this->rootDir . '../..';
18
+ if ( !self::isPhar() ) {
19
+ $this->libraryDir = realpath($this->libraryDir);
20
+ }
21
+ $this->libraryDir = $this->libraryDir . '/';
22
+
23
+ $this->staticMap = array(
24
+ 'PucReadmeParser' => 'vendor/PucReadmeParser.php',
25
+ 'Parsedown' => 'vendor/Parsedown.php',
26
+ 'Puc_v4_Factory' => 'Puc/v4/Factory.php',
27
+ );
28
+
29
+ spl_autoload_register(array($this, 'autoload'));
30
+ }
31
+
32
+ /**
33
+ * Determine if this file is running as part of a Phar archive.
34
+ *
35
+ * @return bool
36
+ */
37
+ private static function isPhar() {
38
+ //Check if the current file path starts with "phar://".
39
+ static $pharProtocol = 'phar://';
40
+ return (substr(__FILE__, 0, strlen($pharProtocol)) === $pharProtocol);
41
+ }
42
+
43
+ public function autoload($className) {
44
+ if ( isset($this->staticMap[$className]) && file_exists($this->libraryDir . $this->staticMap[$className]) ) {
45
+ /** @noinspection PhpIncludeInspection */
46
+ include ($this->libraryDir . $this->staticMap[$className]);
47
+ return;
48
+ }
49
+
50
+ if (strpos($className, $this->prefix) === 0) {
51
+ $path = substr($className, strlen($this->prefix));
52
+ $path = str_replace('_', '/', $path);
53
+ $path = $this->rootDir . $path . '.php';
54
+
55
+ if (file_exists($path)) {
56
+ /** @noinspection PhpIncludeInspection */
57
+ include $path;
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ endif;
updater/Puc/v4p10/DebugBar/Extension.php ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_DebugBar_Extension', false) ):
3
+
4
+ class Puc_v4p10_DebugBar_Extension {
5
+ const RESPONSE_BODY_LENGTH_LIMIT = 4000;
6
+
7
+ /** @var Puc_v4p10_UpdateChecker */
8
+ protected $updateChecker;
9
+ protected $panelClass = 'Puc_v4p10_DebugBar_Panel';
10
+
11
+ public function __construct($updateChecker, $panelClass = null) {
12
+ $this->updateChecker = $updateChecker;
13
+ if ( isset($panelClass) ) {
14
+ $this->panelClass = $panelClass;
15
+ }
16
+
17
+ add_filter('debug_bar_panels', array($this, 'addDebugBarPanel'));
18
+ add_action('debug_bar_enqueue_scripts', array($this, 'enqueuePanelDependencies'));
19
+
20
+ add_action('wp_ajax_puc_v4_debug_check_now', array($this, 'ajaxCheckNow'));
21
+ }
22
+
23
+ /**
24
+ * Register the PUC Debug Bar panel.
25
+ *
26
+ * @param array $panels
27
+ * @return array
28
+ */
29
+ public function addDebugBarPanel($panels) {
30
+ if ( $this->updateChecker->userCanInstallUpdates() ) {
31
+ $panels[] = new $this->panelClass($this->updateChecker);
32
+ }
33
+ return $panels;
34
+ }
35
+
36
+ /**
37
+ * Enqueue our Debug Bar scripts and styles.
38
+ */
39
+ public function enqueuePanelDependencies() {
40
+ wp_enqueue_style(
41
+ 'puc-debug-bar-style-v4',
42
+ $this->getLibraryUrl("/css/puc-debug-bar.css"),
43
+ array('debug-bar'),
44
+ '20171124'
45
+ );
46
+
47
+ wp_enqueue_script(
48
+ 'puc-debug-bar-js-v4',
49
+ $this->getLibraryUrl("/js/debug-bar.js"),
50
+ array('jquery'),
51
+ '20170516'
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Run an update check and output the result. Useful for making sure that
57
+ * the update checking process works as expected.
58
+ */
59
+ public function ajaxCheckNow() {
60
+ if ( $_POST['uid'] !== $this->updateChecker->getUniqueName('uid') ) {
61
+ return;
62
+ }
63
+ $this->preAjaxRequest();
64
+ $update = $this->updateChecker->checkForUpdates();
65
+ if ( $update !== null ) {
66
+ echo "An update is available:";
67
+ echo '<pre>', htmlentities(print_r($update, true)), '</pre>';
68
+ } else {
69
+ echo 'No updates found.';
70
+ }
71
+
72
+ $errors = $this->updateChecker->getLastRequestApiErrors();
73
+ if ( !empty($errors) ) {
74
+ printf('<p>The update checker encountered %d API error%s.</p>', count($errors), (count($errors) > 1) ? 's' : '');
75
+
76
+ foreach (array_values($errors) as $num => $item) {
77
+ $wpError = $item['error'];
78
+ /** @var WP_Error $wpError */
79
+ printf('<h4>%d) %s</h4>', $num + 1, esc_html($wpError->get_error_message()));
80
+
81
+ echo '<dl>';
82
+ printf('<dt>Error code:</dt><dd><code>%s</code></dd>', esc_html($wpError->get_error_code()));
83
+
84
+ if ( isset($item['url']) ) {
85
+ printf('<dt>Requested URL:</dt><dd><code>%s</code></dd>', esc_html($item['url']));
86
+ }
87
+
88
+ if ( isset($item['httpResponse']) ) {
89
+ if ( is_wp_error($item['httpResponse']) ) {
90
+ $httpError = $item['httpResponse'];
91
+ /** @var WP_Error $httpError */
92
+ printf(
93
+ '<dt>WordPress HTTP API error:</dt><dd>%s (<code>%s</code>)</dd>',
94
+ esc_html($httpError->get_error_message()),
95
+ esc_html($httpError->get_error_code())
96
+ );
97
+ } else {
98
+ //Status code.
99
+ printf(
100
+ '<dt>HTTP status:</dt><dd><code>%d %s</code></dd>',
101
+ wp_remote_retrieve_response_code($item['httpResponse']),
102
+ wp_remote_retrieve_response_message($item['httpResponse'])
103
+ );
104
+
105
+ //Headers.
106
+ echo '<dt>Response headers:</dt><dd><pre>';
107
+ foreach (wp_remote_retrieve_headers($item['httpResponse']) as $name => $value) {
108
+ printf("%s: %s\n", esc_html($name), esc_html($value));
109
+ }
110
+ echo '</pre></dd>';
111
+
112
+ //Body.
113
+ $body = wp_remote_retrieve_body($item['httpResponse']);
114
+ if ( $body === '' ) {
115
+ $body = '(Empty response.)';
116
+ } else if ( strlen($body) > self::RESPONSE_BODY_LENGTH_LIMIT ) {
117
+ $length = strlen($body);
118
+ $body = substr($body, 0, self::RESPONSE_BODY_LENGTH_LIMIT)
119
+ . sprintf("\n(Long string truncated. Total length: %d bytes.)", $length);
120
+ }
121
+
122
+ printf('<dt>Response body:</dt><dd><pre>%s</pre></dd>', esc_html($body));
123
+ }
124
+ }
125
+ echo '<dl>';
126
+ }
127
+ }
128
+
129
+ exit;
130
+ }
131
+
132
+ /**
133
+ * Check access permissions and enable error display (for debugging).
134
+ */
135
+ protected function preAjaxRequest() {
136
+ if ( !$this->updateChecker->userCanInstallUpdates() ) {
137
+ die('Access denied');
138
+ }
139
+ check_ajax_referer('puc-ajax');
140
+
141
+ error_reporting(E_ALL);
142
+ @ini_set('display_errors', 'On');
143
+ }
144
+
145
+ /**
146
+ * Remove hooks that were added by this extension.
147
+ */
148
+ public function removeHooks() {
149
+ remove_filter('debug_bar_panels', array($this, 'addDebugBarPanel'));
150
+ remove_action('debug_bar_enqueue_scripts', array($this, 'enqueuePanelDependencies'));
151
+ remove_action('wp_ajax_puc_v4_debug_check_now', array($this, 'ajaxCheckNow'));
152
+ }
153
+
154
+ /**
155
+ * @param string $filePath
156
+ * @return string
157
+ */
158
+ private function getLibraryUrl($filePath) {
159
+ $absolutePath = realpath(dirname(__FILE__) . '/../../../' . ltrim($filePath, '/'));
160
+
161
+ //Where is the library located inside the WordPress directory structure?
162
+ $absolutePath = Puc_v4p10_Factory::normalizePath($absolutePath);
163
+
164
+ $pluginDir = Puc_v4p10_Factory::normalizePath(WP_PLUGIN_DIR);
165
+ $muPluginDir = Puc_v4p10_Factory::normalizePath(WPMU_PLUGIN_DIR);
166
+ $themeDir = Puc_v4p10_Factory::normalizePath(get_theme_root());
167
+
168
+ if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
169
+ //It's part of a plugin.
170
+ return plugins_url(basename($absolutePath), $absolutePath);
171
+ } else if ( strpos($absolutePath, $themeDir) === 0 ) {
172
+ //It's part of a theme.
173
+ $relativePath = substr($absolutePath, strlen($themeDir) + 1);
174
+ $template = substr($relativePath, 0, strpos($relativePath, '/'));
175
+ $baseUrl = get_theme_root_uri($template);
176
+
177
+ if ( !empty($baseUrl) && $relativePath ) {
178
+ return $baseUrl . '/' . $relativePath;
179
+ }
180
+ }
181
+
182
+ return '';
183
+ }
184
+ }
185
+
186
+ endif;
updater/Puc/v4p10/DebugBar/Panel.php ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_DebugBar_Panel', false) && class_exists('Debug_Bar_Panel', false) ):
4
+
5
+ class Puc_v4p10_DebugBar_Panel extends Debug_Bar_Panel {
6
+ /** @var Puc_v4p10_UpdateChecker */
7
+ protected $updateChecker;
8
+
9
+ private $responseBox = '<div class="puc-ajax-response" style="display: none;"></div>';
10
+
11
+ public function __construct($updateChecker) {
12
+ $this->updateChecker = $updateChecker;
13
+ $title = sprintf(
14
+ '<span class="puc-debug-menu-link-%s">PUC (%s)</span>',
15
+ esc_attr($this->updateChecker->getUniqueName('uid')),
16
+ $this->updateChecker->slug
17
+ );
18
+ parent::__construct($title);
19
+ }
20
+
21
+ public function render() {
22
+ printf(
23
+ '<div class="puc-debug-bar-panel-v4" id="%1$s" data-slug="%2$s" data-uid="%3$s" data-nonce="%4$s">',
24
+ esc_attr($this->updateChecker->getUniqueName('debug-bar-panel')),
25
+ esc_attr($this->updateChecker->slug),
26
+ esc_attr($this->updateChecker->getUniqueName('uid')),
27
+ esc_attr(wp_create_nonce('puc-ajax'))
28
+ );
29
+
30
+ $this->displayConfiguration();
31
+ $this->displayStatus();
32
+ $this->displayCurrentUpdate();
33
+
34
+ echo '</div>';
35
+ }
36
+
37
+ private function displayConfiguration() {
38
+ echo '<h3>Configuration</h3>';
39
+ echo '<table class="puc-debug-data">';
40
+ $this->displayConfigHeader();
41
+ $this->row('Slug', htmlentities($this->updateChecker->slug));
42
+ $this->row('DB option', htmlentities($this->updateChecker->optionName));
43
+
44
+ $requestInfoButton = $this->getMetadataButton();
45
+ $this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox);
46
+
47
+ $scheduler = $this->updateChecker->scheduler;
48
+ if ( $scheduler->checkPeriod > 0 ) {
49
+ $this->row('Automatic checks', 'Every ' . $scheduler->checkPeriod . ' hours');
50
+ } else {
51
+ $this->row('Automatic checks', 'Disabled');
52
+ }
53
+
54
+ if ( isset($scheduler->throttleRedundantChecks) ) {
55
+ if ( $scheduler->throttleRedundantChecks && ($scheduler->checkPeriod > 0) ) {
56
+ $this->row(
57
+ 'Throttling',
58
+ sprintf(
59
+ 'Enabled. If an update is already available, check for updates every %1$d hours instead of every %2$d hours.',
60
+ $scheduler->throttledCheckPeriod,
61
+ $scheduler->checkPeriod
62
+ )
63
+ );
64
+ } else {
65
+ $this->row('Throttling', 'Disabled');
66
+ }
67
+ }
68
+
69
+ $this->updateChecker->onDisplayConfiguration($this);
70
+
71
+ echo '</table>';
72
+ }
73
+
74
+ protected function displayConfigHeader() {
75
+ //Do nothing. This should be implemented in subclasses.
76
+ }
77
+
78
+ protected function getMetadataButton() {
79
+ return '';
80
+ }
81
+
82
+ private function displayStatus() {
83
+ echo '<h3>Status</h3>';
84
+ echo '<table class="puc-debug-data">';
85
+ $state = $this->updateChecker->getUpdateState();
86
+ $checkNowButton = '';
87
+ if ( function_exists('get_submit_button') ) {
88
+ $checkNowButton = get_submit_button(
89
+ 'Check Now',
90
+ 'secondary',
91
+ 'puc-check-now-button',
92
+ false,
93
+ array('id' => $this->updateChecker->getUniqueName('check-now-button'))
94
+ );
95
+ }
96
+
97
+ if ( $state->getLastCheck() > 0 ) {
98
+ $this->row('Last check', $this->formatTimeWithDelta($state->getLastCheck()) . ' ' . $checkNowButton . $this->responseBox);
99
+ } else {
100
+ $this->row('Last check', 'Never');
101
+ }
102
+
103
+ $nextCheck = wp_next_scheduled($this->updateChecker->scheduler->getCronHookName());
104
+ $this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck));
105
+
106
+ if ( $state->getCheckedVersion() !== '' ) {
107
+ $this->row('Checked version', htmlentities($state->getCheckedVersion()));
108
+ $this->row('Cached update', $state->getUpdate());
109
+ }
110
+ $this->row('Update checker class', htmlentities(get_class($this->updateChecker)));
111
+ echo '</table>';
112
+ }
113
+
114
+ private function displayCurrentUpdate() {
115
+ $update = $this->updateChecker->getUpdate();
116
+ if ( $update !== null ) {
117
+ echo '<h3>An Update Is Available</h3>';
118
+ echo '<table class="puc-debug-data">';
119
+ $fields = $this->getUpdateFields();
120
+ foreach($fields as $field) {
121
+ if ( property_exists($update, $field) ) {
122
+ $this->row(ucwords(str_replace('_', ' ', $field)), htmlentities($update->$field));
123
+ }
124
+ }
125
+ echo '</table>';
126
+ } else {
127
+ echo '<h3>No updates currently available</h3>';
128
+ }
129
+ }
130
+
131
+ protected function getUpdateFields() {
132
+ return array('version', 'download_url', 'slug',);
133
+ }
134
+
135
+ private function formatTimeWithDelta($unixTime) {
136
+ if ( empty($unixTime) ) {
137
+ return 'Never';
138
+ }
139
+
140
+ $delta = time() - $unixTime;
141
+ $result = human_time_diff(time(), $unixTime);
142
+ if ( $delta < 0 ) {
143
+ $result = 'after ' . $result;
144
+ } else {
145
+ $result = $result . ' ago';
146
+ }
147
+ $result .= ' (' . $this->formatTimestamp($unixTime) . ')';
148
+ return $result;
149
+ }
150
+
151
+ private function formatTimestamp($unixTime) {
152
+ return gmdate('Y-m-d H:i:s', $unixTime + (get_option('gmt_offset') * 3600));
153
+ }
154
+
155
+ public function row($name, $value) {
156
+ if ( is_object($value) || is_array($value) ) {
157
+ $value = '<pre>' . htmlentities(print_r($value, true)) . '</pre>';
158
+ } else if ($value === null) {
159
+ $value = '<code>null</code>';
160
+ }
161
+ printf('<tr><th scope="row">%1$s</th> <td>%2$s</td></tr>', $name, $value);
162
+ }
163
+ }
164
+
165
+ endif;
updater/Puc/v4p10/DebugBar/PluginExtension.php ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_DebugBar_PluginExtension', false) ):
3
+
4
+ class Puc_v4p10_DebugBar_PluginExtension extends Puc_v4p10_DebugBar_Extension {
5
+ /** @var Puc_v4p10_Plugin_UpdateChecker */
6
+ protected $updateChecker;
7
+
8
+ public function __construct($updateChecker) {
9
+ parent::__construct($updateChecker, 'Puc_v4p10_DebugBar_PluginPanel');
10
+
11
+ add_action('wp_ajax_puc_v4_debug_request_info', array($this, 'ajaxRequestInfo'));
12
+ }
13
+
14
+ /**
15
+ * Request plugin info and output it.
16
+ */
17
+ public function ajaxRequestInfo() {
18
+ if ( $_POST['uid'] !== $this->updateChecker->getUniqueName('uid') ) {
19
+ return;
20
+ }
21
+ $this->preAjaxRequest();
22
+ $info = $this->updateChecker->requestInfo();
23
+ if ( $info !== null ) {
24
+ echo 'Successfully retrieved plugin info from the metadata URL:';
25
+ echo '<pre>', htmlentities(print_r($info, true)), '</pre>';
26
+ } else {
27
+ echo 'Failed to retrieve plugin info from the metadata URL.';
28
+ }
29
+ exit;
30
+ }
31
+ }
32
+
33
+ endif;
updater/Puc/v4p10/DebugBar/PluginPanel.php ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_DebugBar_PluginPanel', false) ):
4
+
5
+ class Puc_v4p10_DebugBar_PluginPanel extends Puc_v4p10_DebugBar_Panel {
6
+ /**
7
+ * @var Puc_v4p10_Plugin_UpdateChecker
8
+ */
9
+ protected $updateChecker;
10
+
11
+ protected function displayConfigHeader() {
12
+ $this->row('Plugin file', htmlentities($this->updateChecker->pluginFile));
13
+ parent::displayConfigHeader();
14
+ }
15
+
16
+ protected function getMetadataButton() {
17
+ $requestInfoButton = '';
18
+ if ( function_exists('get_submit_button') ) {
19
+ $requestInfoButton = get_submit_button(
20
+ 'Request Info',
21
+ 'secondary',
22
+ 'puc-request-info-button',
23
+ false,
24
+ array('id' => $this->updateChecker->getUniqueName('request-info-button'))
25
+ );
26
+ }
27
+ return $requestInfoButton;
28
+ }
29
+
30
+ protected function getUpdateFields() {
31
+ return array_merge(
32
+ parent::getUpdateFields(),
33
+ array('homepage', 'upgrade_notice', 'tested',)
34
+ );
35
+ }
36
+ }
37
+
38
+ endif;
updater/Puc/v4p10/DebugBar/ThemePanel.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_DebugBar_ThemePanel', false) ):
4
+
5
+ class Puc_v4p10_DebugBar_ThemePanel extends Puc_v4p10_DebugBar_Panel {
6
+ /**
7
+ * @var Puc_v4p10_Theme_UpdateChecker
8
+ */
9
+ protected $updateChecker;
10
+
11
+ protected function displayConfigHeader() {
12
+ $this->row('Theme directory', htmlentities($this->updateChecker->directoryName));
13
+ parent::displayConfigHeader();
14
+ }
15
+
16
+ protected function getUpdateFields() {
17
+ return array_merge(parent::getUpdateFields(), array('details_url'));
18
+ }
19
+ }
20
+
21
+ endif;
updater/Puc/v4p10/Factory.php ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Factory', false) ):
3
+
4
+ /**
5
+ * A factory that builds update checker instances.
6
+ *
7
+ * When multiple versions of the same class have been loaded (e.g. PluginUpdateChecker 4.0
8
+ * and 4.1), this factory will always use the latest available minor version. Register class
9
+ * versions by calling {@link PucFactory::addVersion()}.
10
+ *
11
+ * At the moment it can only build instances of the UpdateChecker class. Other classes are
12
+ * intended mainly for internal use and refer directly to specific implementations.
13
+ */
14
+ class Puc_v4p10_Factory {
15
+ protected static $classVersions = array();
16
+ protected static $sorted = false;
17
+
18
+ protected static $myMajorVersion = '';
19
+ protected static $latestCompatibleVersion = '';
20
+
21
+ /**
22
+ * A wrapper method for buildUpdateChecker() that reads the metadata URL from the plugin or theme header.
23
+ *
24
+ * @param string $fullPath Full path to the main plugin file or the theme's style.css.
25
+ * @param array $args Optional arguments. Keys should match the argument names of the buildUpdateChecker() method.
26
+ * @return Puc_v4p10_Plugin_UpdateChecker|Puc_v4p10_Theme_UpdateChecker|Puc_v4p10_Vcs_BaseChecker
27
+ */
28
+ public static function buildFromHeader($fullPath, $args = array()) {
29
+ $fullPath = self::normalizePath($fullPath);
30
+
31
+ //Set up defaults.
32
+ $defaults = array(
33
+ 'metadataUrl' => '',
34
+ 'slug' => '',
35
+ 'checkPeriod' => 12,
36
+ 'optionName' => '',
37
+ 'muPluginFile' => '',
38
+ );
39
+ $args = array_merge($defaults, array_intersect_key($args, $defaults));
40
+ extract($args, EXTR_SKIP);
41
+
42
+ //Check for the service URI
43
+ if ( empty($metadataUrl) ) {
44
+ $metadataUrl = self::getServiceURI($fullPath);
45
+ }
46
+
47
+ /** @noinspection PhpUndefinedVariableInspection These variables are created by extract(), above. */
48
+ return self::buildUpdateChecker($metadataUrl, $fullPath, $slug, $checkPeriod, $optionName, $muPluginFile);
49
+ }
50
+
51
+ /**
52
+ * Create a new instance of the update checker.
53
+ *
54
+ * This method automatically detects if you're using it for a plugin or a theme and chooses
55
+ * the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
56
+ *
57
+ * @see Puc_v4p10_UpdateChecker::__construct
58
+ *
59
+ * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
60
+ * @param string $fullPath Full path to the main plugin file or to the theme directory.
61
+ * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
62
+ * @param int $checkPeriod How often to check for updates (in hours).
63
+ * @param string $optionName Where to store book-keeping info about update checks.
64
+ * @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
65
+ * @return Puc_v4p10_Plugin_UpdateChecker|Puc_v4p10_Theme_UpdateChecker|Puc_v4p10_Vcs_BaseChecker
66
+ */
67
+ public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
68
+ $fullPath = self::normalizePath($fullPath);
69
+ $id = null;
70
+
71
+ //Plugin or theme?
72
+ $themeDirectory = self::getThemeDirectoryName($fullPath);
73
+ if ( self::isPluginFile($fullPath) ) {
74
+ $type = 'Plugin';
75
+ $id = $fullPath;
76
+ } else if ( $themeDirectory !== null ) {
77
+ $type = 'Theme';
78
+ $id = $themeDirectory;
79
+ } else {
80
+ throw new RuntimeException(sprintf(
81
+ 'The update checker cannot determine if "%s" is a plugin or a theme. ' .
82
+ 'This is a bug. Please contact the PUC developer.',
83
+ htmlentities($fullPath)
84
+ ));
85
+ }
86
+
87
+ //Which hosting service does the URL point to?
88
+ $service = self::getVcsService($metadataUrl);
89
+
90
+ $apiClass = null;
91
+ if ( empty($service) ) {
92
+ //The default is to get update information from a remote JSON file.
93
+ $checkerClass = $type . '_UpdateChecker';
94
+ } else {
95
+ //You can also use a VCS repository like GitHub.
96
+ $checkerClass = 'Vcs_' . $type . 'UpdateChecker';
97
+ $apiClass = $service . 'Api';
98
+ }
99
+
100
+ $checkerClass = self::getCompatibleClassVersion($checkerClass);
101
+ if ( $checkerClass === null ) {
102
+ trigger_error(
103
+ sprintf(
104
+ 'PUC %s does not support updates for %ss %s',
105
+ htmlentities(self::$latestCompatibleVersion),
106
+ strtolower($type),
107
+ $service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
108
+ ),
109
+ E_USER_ERROR
110
+ );
111
+ return null;
112
+ }
113
+
114
+ if ( !isset($apiClass) ) {
115
+ //Plain old update checker.
116
+ return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
117
+ } else {
118
+ //VCS checker + an API client.
119
+ $apiClass = self::getCompatibleClassVersion($apiClass);
120
+ if ( $apiClass === null ) {
121
+ trigger_error(sprintf(
122
+ 'PUC %s does not support %s',
123
+ htmlentities(self::$latestCompatibleVersion),
124
+ htmlentities($service)
125
+ ), E_USER_ERROR);
126
+ return null;
127
+ }
128
+
129
+ return new $checkerClass(
130
+ new $apiClass($metadataUrl),
131
+ $id,
132
+ $slug,
133
+ $checkPeriod,
134
+ $optionName,
135
+ $muPluginFile
136
+ );
137
+ }
138
+ }
139
+
140
+ /**
141
+ *
142
+ * Normalize a filesystem path. Introduced in WP 3.9.
143
+ * Copying here allows use of the class on earlier versions.
144
+ * This version adapted from WP 4.8.2 (unchanged since 4.5.0)
145
+ *
146
+ * @param string $path Path to normalize.
147
+ * @return string Normalized path.
148
+ */
149
+ public static function normalizePath($path) {
150
+ if ( function_exists('wp_normalize_path') ) {
151
+ return wp_normalize_path($path);
152
+ }
153
+ $path = str_replace('\\', '/', $path);
154
+ $path = preg_replace('|(?<=.)/+|', '/', $path);
155
+ if ( substr($path, 1, 1) === ':' ) {
156
+ $path = ucfirst($path);
157
+ }
158
+ return $path;
159
+ }
160
+
161
+ /**
162
+ * Check if the path points to a plugin file.
163
+ *
164
+ * @param string $absolutePath Normalized path.
165
+ * @return bool
166
+ */
167
+ protected static function isPluginFile($absolutePath) {
168
+ //Is the file inside the "plugins" or "mu-plugins" directory?
169
+ $pluginDir = self::normalizePath(WP_PLUGIN_DIR);
170
+ $muPluginDir = self::normalizePath(WPMU_PLUGIN_DIR);
171
+ if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
172
+ return true;
173
+ }
174
+
175
+ //Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
176
+ if ( !is_file($absolutePath) ) {
177
+ return false;
178
+ }
179
+
180
+ //Does it have a valid plugin header?
181
+ //This is a last-ditch check for plugins symlinked from outside the WP root.
182
+ if ( function_exists('get_file_data') ) {
183
+ $headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
184
+ return !empty($headers['Name']);
185
+ }
186
+
187
+ return false;
188
+ }
189
+
190
+ /**
191
+ * Get the name of the theme's directory from a full path to a file inside that directory.
192
+ * E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
193
+ *
194
+ * Note that subdirectories are currently not supported. For example,
195
+ * "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
196
+ *
197
+ * @param string $absolutePath Normalized path.
198
+ * @return string|null Directory name, or NULL if the path doesn't point to a theme.
199
+ */
200
+ protected static function getThemeDirectoryName($absolutePath) {
201
+ if ( is_file($absolutePath) ) {
202
+ $absolutePath = dirname($absolutePath);
203
+ }
204
+
205
+ if ( file_exists($absolutePath . '/style.css') ) {
206
+ return basename($absolutePath);
207
+ }
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Get the service URI from the file header.
213
+ *
214
+ * @param string $fullPath
215
+ * @return string
216
+ */
217
+ private static function getServiceURI($fullPath) {
218
+ //Look for the URI
219
+ if ( is_readable($fullPath) ) {
220
+ $seek = array(
221
+ 'github' => 'GitHub URI',
222
+ 'gitlab' => 'GitLab URI',
223
+ 'bucket' => 'BitBucket URI',
224
+ );
225
+ $seek = apply_filters('puc_get_source_uri', $seek);
226
+ $data = get_file_data($fullPath, $seek);
227
+ foreach ($data as $key => $uri) {
228
+ if ( $uri ) {
229
+ return $uri;
230
+ }
231
+ }
232
+ }
233
+
234
+ //URI was not found so throw an error.
235
+ throw new RuntimeException(
236
+ sprintf('Unable to locate URI in header of "%s"', htmlentities($fullPath))
237
+ );
238
+ }
239
+
240
+ /**
241
+ * Get the name of the hosting service that the URL points to.
242
+ *
243
+ * @param string $metadataUrl
244
+ * @return string|null
245
+ */
246
+ private static function getVcsService($metadataUrl) {
247
+ $service = null;
248
+
249
+ //Which hosting service does the URL point to?
250
+ $host = parse_url($metadataUrl, PHP_URL_HOST);
251
+ $path = parse_url($metadataUrl, PHP_URL_PATH);
252
+
253
+ //Check if the path looks like "/user-name/repository".
254
+ //For GitLab.com it can also be "/user/group1/group2/.../repository".
255
+ $repoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
256
+ if ( $host === 'gitlab.com' ) {
257
+ $repoRegex = '@^/?(?:[^/#?&]++/){1,20}(?:[^/#?&]++)/?$@';
258
+ }
259
+ if ( preg_match($repoRegex, $path) ) {
260
+ $knownServices = array(
261
+ 'github.com' => 'GitHub',
262
+ 'bitbucket.org' => 'BitBucket',
263
+ 'gitlab.com' => 'GitLab',
264
+ );
265
+ if ( isset($knownServices[$host]) ) {
266
+ $service = $knownServices[$host];
267
+ }
268
+ }
269
+
270
+ return apply_filters('puc_get_vcs_service', $service, $host, $path, $metadataUrl);
271
+ }
272
+
273
+ /**
274
+ * Get the latest version of the specified class that has the same major version number
275
+ * as this factory class.
276
+ *
277
+ * @param string $class Partial class name.
278
+ * @return string|null Full class name.
279
+ */
280
+ protected static function getCompatibleClassVersion($class) {
281
+ if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
282
+ return self::$classVersions[$class][self::$latestCompatibleVersion];
283
+ }
284
+ return null;
285
+ }
286
+
287
+ /**
288
+ * Get the specific class name for the latest available version of a class.
289
+ *
290
+ * @param string $class
291
+ * @return null|string
292
+ */
293
+ public static function getLatestClassVersion($class) {
294
+ if ( !self::$sorted ) {
295
+ self::sortVersions();
296
+ }
297
+
298
+ if ( isset(self::$classVersions[$class]) ) {
299
+ return reset(self::$classVersions[$class]);
300
+ } else {
301
+ return null;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Sort available class versions in descending order (i.e. newest first).
307
+ */
308
+ protected static function sortVersions() {
309
+ foreach ( self::$classVersions as $class => $versions ) {
310
+ uksort($versions, array(__CLASS__, 'compareVersions'));
311
+ self::$classVersions[$class] = $versions;
312
+ }
313
+ self::$sorted = true;
314
+ }
315
+
316
+ protected static function compareVersions($a, $b) {
317
+ return -version_compare($a, $b);
318
+ }
319
+
320
+ /**
321
+ * Register a version of a class.
322
+ *
323
+ * @access private This method is only for internal use by the library.
324
+ *
325
+ * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
326
+ * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
327
+ * @param string $version Version number, e.g. '1.2'.
328
+ */
329
+ public static function addVersion($generalClass, $versionedClass, $version) {
330
+ if ( empty(self::$myMajorVersion) ) {
331
+ $nameParts = explode('_', __CLASS__, 3);
332
+ self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
333
+ }
334
+
335
+ //Store the greatest version number that matches our major version.
336
+ $components = explode('.', $version);
337
+ if ( $components[0] === self::$myMajorVersion ) {
338
+
339
+ if (
340
+ empty(self::$latestCompatibleVersion)
341
+ || version_compare($version, self::$latestCompatibleVersion, '>')
342
+ ) {
343
+ self::$latestCompatibleVersion = $version;
344
+ }
345
+
346
+ }
347
+
348
+ if ( !isset(self::$classVersions[$generalClass]) ) {
349
+ self::$classVersions[$generalClass] = array();
350
+ }
351
+ self::$classVersions[$generalClass][$version] = $versionedClass;
352
+ self::$sorted = false;
353
+ }
354
+ }
355
+
356
+ endif;
updater/Puc/v4p10/InstalledPackage.php ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_InstalledPackage', false) ):
3
+
4
+ /**
5
+ * This class represents a currently installed plugin or theme.
6
+ *
7
+ * Not to be confused with the "package" field in WP update API responses that contains
8
+ * the download URL of a the new version.
9
+ */
10
+ abstract class Puc_v4p10_InstalledPackage {
11
+ /**
12
+ * @var Puc_v4p10_UpdateChecker
13
+ */
14
+ protected $updateChecker;
15
+
16
+ public function __construct($updateChecker) {
17
+ $this->updateChecker = $updateChecker;
18
+ }
19
+
20
+ /**
21
+ * Get the currently installed version of the plugin or theme.
22
+ *
23
+ * @return string|null Version number.
24
+ */
25
+ abstract public function getInstalledVersion();
26
+
27
+ /**
28
+ * Get the full path of the plugin or theme directory (without a trailing slash).
29
+ *
30
+ * @return string
31
+ */
32
+ abstract public function getAbsoluteDirectoryPath();
33
+
34
+ /**
35
+ * Check whether a regular file exists in the package's directory.
36
+ *
37
+ * @param string $relativeFileName File name relative to the package directory.
38
+ * @return bool
39
+ */
40
+ public function fileExists($relativeFileName) {
41
+ return is_file(
42
+ $this->getAbsoluteDirectoryPath()
43
+ . DIRECTORY_SEPARATOR
44
+ . ltrim($relativeFileName, '/\\')
45
+ );
46
+ }
47
+
48
+ /* -------------------------------------------------------------------
49
+ * File header parsing
50
+ * -------------------------------------------------------------------
51
+ */
52
+
53
+ /**
54
+ * Parse plugin or theme metadata from the header comment.
55
+ *
56
+ * This is basically a simplified version of the get_file_data() function from /wp-includes/functions.php.
57
+ * It's intended as a utility for subclasses that detect updates by parsing files in a VCS.
58
+ *
59
+ * @param string|null $content File contents.
60
+ * @return string[]
61
+ */
62
+ public function getFileHeader($content) {
63
+ $content = (string)$content;
64
+
65
+ //WordPress only looks at the first 8 KiB of the file, so we do the same.
66
+ $content = substr($content, 0, 8192);
67
+ //Normalize line endings.
68
+ $content = str_replace("\r", "\n", $content);
69
+
70
+ $headers = $this->getHeaderNames();
71
+ $results = array();
72
+ foreach ($headers as $field => $name) {
73
+ $success = preg_match('/^[ \t\/*#@]*' . preg_quote($name, '/') . ':(.*)$/mi', $content, $matches);
74
+
75
+ if ( ($success === 1) && $matches[1] ) {
76
+ $value = $matches[1];
77
+ if ( function_exists('_cleanup_header_comment') ) {
78
+ $value = _cleanup_header_comment($value);
79
+ }
80
+ $results[$field] = $value;
81
+ } else {
82
+ $results[$field] = '';
83
+ }
84
+ }
85
+
86
+ return $results;
87
+ }
88
+
89
+ /**
90
+ * @return array Format: ['HeaderKey' => 'Header Name']
91
+ */
92
+ abstract protected function getHeaderNames();
93
+
94
+ /**
95
+ * Get the value of a specific plugin or theme header.
96
+ *
97
+ * @param string $headerName
98
+ * @return string Either the value of the header, or an empty string if the header doesn't exist.
99
+ */
100
+ abstract public function getHeaderValue($headerName);
101
+
102
+ }
103
+ endif;
updater/Puc/v4p10/Metadata.php ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Metadata', false) ):
3
+
4
+ /**
5
+ * A base container for holding information about updates and plugin metadata.
6
+ *
7
+ * @author Janis Elsts
8
+ * @copyright 2016
9
+ * @access public
10
+ */
11
+ abstract class Puc_v4p10_Metadata {
12
+
13
+ /**
14
+ * Create an instance of this class from a JSON document.
15
+ *
16
+ * @abstract
17
+ * @param string $json
18
+ * @return self
19
+ */
20
+ public static function fromJson(/** @noinspection PhpUnusedParameterInspection */ $json) {
21
+ throw new LogicException('The ' . __METHOD__ . ' method must be implemented by subclasses');
22
+ }
23
+
24
+ /**
25
+ * @param string $json
26
+ * @param self $target
27
+ * @return bool
28
+ */
29
+ protected static function createFromJson($json, $target) {
30
+ /** @var StdClass $apiResponse */
31
+ $apiResponse = json_decode($json);
32
+ if ( empty($apiResponse) || !is_object($apiResponse) ){
33
+ $errorMessage = "Failed to parse update metadata. Try validating your .json file with http://jsonlint.com/";
34
+ do_action('puc_api_error', new WP_Error('puc-invalid-json', $errorMessage));
35
+ trigger_error($errorMessage, E_USER_NOTICE);
36
+ return false;
37
+ }
38
+
39
+ $valid = $target->validateMetadata($apiResponse);
40
+ if ( is_wp_error($valid) ){
41
+ do_action('puc_api_error', $valid);
42
+ trigger_error($valid->get_error_message(), E_USER_NOTICE);
43
+ return false;
44
+ }
45
+
46
+ foreach(get_object_vars($apiResponse) as $key => $value){
47
+ $target->$key = $value;
48
+ }
49
+
50
+ return true;
51
+ }
52
+
53
+ /**
54
+ * No validation by default! Subclasses should check that the required fields are present.
55
+ *
56
+ * @param StdClass $apiResponse
57
+ * @return bool|WP_Error
58
+ */
59
+ protected function validateMetadata(/** @noinspection PhpUnusedParameterInspection */ $apiResponse) {
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Create a new instance by copying the necessary fields from another object.
65
+ *
66
+ * @abstract
67
+ * @param StdClass|self $object The source object.
68
+ * @return self The new copy.
69
+ */
70
+ public static function fromObject(/** @noinspection PhpUnusedParameterInspection */ $object) {
71
+ throw new LogicException('The ' . __METHOD__ . ' method must be implemented by subclasses');
72
+ }
73
+
74
+ /**
75
+ * Create an instance of StdClass that can later be converted back to an
76
+ * update or info container. Useful for serialization and caching, as it
77
+ * avoids the "incomplete object" problem if the cached value is loaded
78
+ * before this class.
79
+ *
80
+ * @return StdClass
81
+ */
82
+ public function toStdClass() {
83
+ $object = new stdClass();
84
+ $this->copyFields($this, $object);
85
+ return $object;
86
+ }
87
+
88
+ /**
89
+ * Transform the metadata into the format used by WordPress core.
90
+ *
91
+ * @return object
92
+ */
93
+ abstract public function toWpFormat();
94
+
95
+ /**
96
+ * Copy known fields from one object to another.
97
+ *
98
+ * @param StdClass|self $from
99
+ * @param StdClass|self $to
100
+ */
101
+ protected function copyFields($from, $to) {
102
+ $fields = $this->getFieldNames();
103
+
104
+ if ( property_exists($from, 'slug') && !empty($from->slug) ) {
105
+ //Let plugins add extra fields without having to create subclasses.
106
+ $fields = apply_filters($this->getPrefixedFilter('retain_fields') . '-' . $from->slug, $fields);
107
+ }
108
+
109
+ foreach ($fields as $field) {
110
+ if ( property_exists($from, $field) ) {
111
+ $to->$field = $from->$field;
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * @return string[]
118
+ */
119
+ protected function getFieldNames() {
120
+ return array();
121
+ }
122
+
123
+ /**
124
+ * @param string $tag
125
+ * @return string
126
+ */
127
+ protected function getPrefixedFilter($tag) {
128
+ return 'puc_' . $tag;
129
+ }
130
+ }
131
+
132
+ endif;
updater/Puc/v4p10/OAuthSignature.php ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_OAuthSignature', false) ):
4
+
5
+ /**
6
+ * A basic signature generator for zero-legged OAuth 1.0.
7
+ */
8
+ class Puc_v4p10_OAuthSignature {
9
+ private $consumerKey = '';
10
+ private $consumerSecret = '';
11
+
12
+ public function __construct($consumerKey, $consumerSecret) {
13
+ $this->consumerKey = $consumerKey;
14
+ $this->consumerSecret = $consumerSecret;
15
+ }
16
+
17
+ /**
18
+ * Sign a URL using OAuth 1.0.
19
+ *
20
+ * @param string $url The URL to be signed. It may contain query parameters.
21
+ * @param string $method HTTP method such as "GET", "POST" and so on.
22
+ * @return string The signed URL.
23
+ */
24
+ public function sign($url, $method = 'GET') {
25
+ $parameters = array();
26
+
27
+ //Parse query parameters.
28
+ $query = parse_url($url, PHP_URL_QUERY);
29
+ if ( !empty($query) ) {
30
+ parse_str($query, $parsedParams);
31
+ if ( is_array($parameters) ) {
32
+ $parameters = $parsedParams;
33
+ }
34
+ //Remove the query string from the URL. We'll replace it later.
35
+ $url = substr($url, 0, strpos($url, '?'));
36
+ }
37
+
38
+ $parameters = array_merge(
39
+ $parameters,
40
+ array(
41
+ 'oauth_consumer_key' => $this->consumerKey,
42
+ 'oauth_nonce' => $this->nonce(),
43
+ 'oauth_signature_method' => 'HMAC-SHA1',
44
+ 'oauth_timestamp' => time(),
45
+ 'oauth_version' => '1.0',
46
+ )
47
+ );
48
+ unset($parameters['oauth_signature']);
49
+
50
+ //Parameters must be sorted alphabetically before signing.
51
+ ksort($parameters);
52
+
53
+ //The most complicated part of the request - generating the signature.
54
+ //The string to sign contains the HTTP method, the URL path, and all of
55
+ //our query parameters. Everything is URL encoded. Then we concatenate
56
+ //them with ampersands into a single string to hash.
57
+ $encodedVerb = urlencode($method);
58
+ $encodedUrl = urlencode($url);
59
+ $encodedParams = urlencode(http_build_query($parameters, '', '&'));
60
+
61
+ $stringToSign = $encodedVerb . '&' . $encodedUrl . '&' . $encodedParams;
62
+
63
+ //Since we only have one OAuth token (the consumer secret) we only have
64
+ //to use it as our HMAC key. However, we still have to append an & to it
65
+ //as if we were using it with additional tokens.
66
+ $secret = urlencode($this->consumerSecret) . '&';
67
+
68
+ //The signature is a hash of the consumer key and the base string. Note
69
+ //that we have to get the raw output from hash_hmac and base64 encode
70
+ //the binary data result.
71
+ $parameters['oauth_signature'] = base64_encode(hash_hmac('sha1', $stringToSign, $secret, true));
72
+
73
+ return ($url . '?' . http_build_query($parameters));
74
+ }
75
+
76
+ /**
77
+ * Generate a random nonce.
78
+ *
79
+ * @return string
80
+ */
81
+ private function nonce() {
82
+ $mt = microtime();
83
+
84
+ $rand = null;
85
+ if ( is_callable('random_bytes') ) {
86
+ try {
87
+ $rand = random_bytes(16);
88
+ } catch (Exception $ex) {
89
+ //Fall back to mt_rand (below).
90
+ }
91
+ }
92
+ if ( $rand === null ) {
93
+ $rand = mt_rand();
94
+ }
95
+
96
+ return md5($mt . '_' . $rand);
97
+ }
98
+ }
99
+
100
+ endif;
updater/Puc/v4p10/Plugin/Info.php ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Plugin_Info', false) ):
3
+
4
+ /**
5
+ * A container class for holding and transforming various plugin metadata.
6
+ *
7
+ * @author Janis Elsts
8
+ * @copyright 2016
9
+ * @access public
10
+ */
11
+ class Puc_v4p10_Plugin_Info extends Puc_v4p10_Metadata {
12
+ //Most fields map directly to the contents of the plugin's info.json file.
13
+ //See the relevant docs for a description of their meaning.
14
+ public $name;
15
+ public $slug;
16
+ public $version;
17
+ public $homepage;
18
+ public $sections = array();
19
+ public $download_url;
20
+
21
+ public $banners;
22
+ public $icons = array();
23
+ public $translations = array();
24
+
25
+ public $author;
26
+ public $author_homepage;
27
+
28
+ public $requires;
29
+ public $tested;
30
+ public $requires_php;
31
+ public $upgrade_notice;
32
+
33
+ public $rating;
34
+ public $num_ratings;
35
+ public $downloaded;
36
+ public $active_installs;
37
+ public $last_updated;
38
+
39
+ public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.
40
+
41
+ public $filename; //Plugin filename relative to the plugins directory.
42
+
43
+ /**
44
+ * Create a new instance of Plugin Info from JSON-encoded plugin info
45
+ * returned by an external update API.
46
+ *
47
+ * @param string $json Valid JSON string representing plugin info.
48
+ * @return self|null New instance of Plugin Info, or NULL on error.
49
+ */
50
+ public static function fromJson($json){
51
+ $instance = new self();
52
+
53
+ if ( !parent::createFromJson($json, $instance) ) {
54
+ return null;
55
+ }
56
+
57
+ //json_decode decodes assoc. arrays as objects. We want them as arrays.
58
+ $instance->sections = (array)$instance->sections;
59
+ $instance->icons = (array)$instance->icons;
60
+
61
+ return $instance;
62
+ }
63
+
64
+ /**
65
+ * Very, very basic validation.
66
+ *
67
+ * @param StdClass $apiResponse
68
+ * @return bool|WP_Error
69
+ */
70
+ protected function validateMetadata($apiResponse) {
71
+ if (
72
+ !isset($apiResponse->name, $apiResponse->version)
73
+ || empty($apiResponse->name)
74
+ || empty($apiResponse->version)
75
+ ) {
76
+ return new WP_Error(
77
+ 'puc-invalid-metadata',
78
+ "The plugin metadata file does not contain the required 'name' and/or 'version' keys."
79
+ );
80
+ }
81
+ return true;
82
+ }
83
+
84
+
85
+ /**
86
+ * Transform plugin info into the format used by the native WordPress.org API
87
+ *
88
+ * @return object
89
+ */
90
+ public function toWpFormat(){
91
+ $info = new stdClass;
92
+
93
+ //The custom update API is built so that many fields have the same name and format
94
+ //as those returned by the native WordPress.org API. These can be assigned directly.
95
+ $sameFormat = array(
96
+ 'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',
97
+ 'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',
98
+ 'requires_php',
99
+ );
100
+ foreach($sameFormat as $field){
101
+ if ( isset($this->$field) ) {
102
+ $info->$field = $this->$field;
103
+ } else {
104
+ $info->$field = null;
105
+ }
106
+ }
107
+
108
+ //Other fields need to be renamed and/or transformed.
109
+ $info->download_link = $this->download_url;
110
+ $info->author = $this->getFormattedAuthor();
111
+ $info->sections = array_merge(array('description' => ''), $this->sections);
112
+
113
+ if ( !empty($this->banners) ) {
114
+ //WP expects an array with two keys: "high" and "low". Both are optional.
115
+ //Docs: https://wordpress.org/plugins/about/faq/#banners
116
+ $info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners;
117
+ $info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true));
118
+ }
119
+
120
+ return $info;
121
+ }
122
+
123
+ protected function getFormattedAuthor() {
124
+ if ( !empty($this->author_homepage) ){
125
+ /** @noinspection HtmlUnknownTarget */
126
+ return sprintf('<a href="%s">%s</a>', $this->author_homepage, $this->author);
127
+ }
128
+ return $this->author;
129
+ }
130
+ }
131
+
132
+ endif;
updater/Puc/v4p10/Plugin/Package.php ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Plugin_Package', false) ):
3
+
4
+ class Puc_v4p10_Plugin_Package extends Puc_v4p10_InstalledPackage {
5
+ /**
6
+ * @var Puc_v4p10_Plugin_UpdateChecker
7
+ */
8
+ protected $updateChecker;
9
+
10
+ /**
11
+ * @var string Full path of the main plugin file.
12
+ */
13
+ protected $pluginAbsolutePath = '';
14
+
15
+ /**
16
+ * @var string Plugin basename.
17
+ */
18
+ private $pluginFile;
19
+
20
+ /**
21
+ * @var string|null
22
+ */
23
+ private $cachedInstalledVersion = null;
24
+
25
+ public function __construct($pluginAbsolutePath, $updateChecker) {
26
+ $this->pluginAbsolutePath = $pluginAbsolutePath;
27
+ $this->pluginFile = plugin_basename($this->pluginAbsolutePath);
28
+
29
+ parent::__construct($updateChecker);
30
+
31
+ //Clear the version number cache when something - anything - is upgraded or WP clears the update cache.
32
+ add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
33
+ add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
34
+ }
35
+
36
+ public function getInstalledVersion() {
37
+ if ( isset($this->cachedInstalledVersion) ) {
38
+ return $this->cachedInstalledVersion;
39
+ }
40
+
41
+ $pluginHeader = $this->getPluginHeader();
42
+ if ( isset($pluginHeader['Version']) ) {
43
+ $this->cachedInstalledVersion = $pluginHeader['Version'];
44
+ return $pluginHeader['Version'];
45
+ } else {
46
+ //This can happen if the filename points to something that is not a plugin.
47
+ $this->updateChecker->triggerError(
48
+ sprintf(
49
+ "Can't to read the Version header for '%s'. The filename is incorrect or is not a plugin.",
50
+ $this->updateChecker->pluginFile
51
+ ),
52
+ E_USER_WARNING
53
+ );
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Clear the cached plugin version. This method can be set up as a filter (hook) and will
60
+ * return the filter argument unmodified.
61
+ *
62
+ * @param mixed $filterArgument
63
+ * @return mixed
64
+ */
65
+ public function clearCachedVersion($filterArgument = null) {
66
+ $this->cachedInstalledVersion = null;
67
+ return $filterArgument;
68
+ }
69
+
70
+ public function getAbsoluteDirectoryPath() {
71
+ return dirname($this->pluginAbsolutePath);
72
+ }
73
+
74
+ /**
75
+ * Get the value of a specific plugin or theme header.
76
+ *
77
+ * @param string $headerName
78
+ * @param string $defaultValue
79
+ * @return string Either the value of the header, or $defaultValue if the header doesn't exist or is empty.
80
+ */
81
+ public function getHeaderValue($headerName, $defaultValue = '') {
82
+ $headers = $this->getPluginHeader();
83
+ if ( isset($headers[$headerName]) && ($headers[$headerName] !== '') ) {
84
+ return $headers[$headerName];
85
+ }
86
+ return $defaultValue;
87
+ }
88
+
89
+ protected function getHeaderNames() {
90
+ return array(
91
+ 'Name' => 'Plugin Name',
92
+ 'PluginURI' => 'Plugin URI',
93
+ 'Version' => 'Version',
94
+ 'Description' => 'Description',
95
+ 'Author' => 'Author',
96
+ 'AuthorURI' => 'Author URI',
97
+ 'TextDomain' => 'Text Domain',
98
+ 'DomainPath' => 'Domain Path',
99
+ 'Network' => 'Network',
100
+
101
+ //The newest WordPress version that this plugin requires or has been tested with.
102
+ //We support several different formats for compatibility with other libraries.
103
+ 'Tested WP' => 'Tested WP',
104
+ 'Requires WP' => 'Requires WP',
105
+ 'Tested up to' => 'Tested up to',
106
+ 'Requires at least' => 'Requires at least',
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Get the translated plugin title.
112
+ *
113
+ * @return string
114
+ */
115
+ public function getPluginTitle() {
116
+ $title = '';
117
+ $header = $this->getPluginHeader();
118
+ if ( $header && !empty($header['Name']) && isset($header['TextDomain']) ) {
119
+ $title = translate($header['Name'], $header['TextDomain']);
120
+ }
121
+ return $title;
122
+ }
123
+
124
+ /**
125
+ * Get plugin's metadata from its file header.
126
+ *
127
+ * @return array
128
+ */
129
+ public function getPluginHeader() {
130
+ if ( !is_file($this->pluginAbsolutePath) ) {
131
+ //This can happen if the plugin filename is wrong.
132
+ $this->updateChecker->triggerError(
133
+ sprintf(
134
+ "Can't to read the plugin header for '%s'. The file does not exist.",
135
+ $this->updateChecker->pluginFile
136
+ ),
137
+ E_USER_WARNING
138
+ );
139
+ return array();
140
+ }
141
+
142
+ if ( !function_exists('get_plugin_data') ) {
143
+ /** @noinspection PhpIncludeInspection */
144
+ require_once(ABSPATH . '/wp-admin/includes/plugin.php');
145
+ }
146
+ return get_plugin_data($this->pluginAbsolutePath, false, false);
147
+ }
148
+
149
+ public function removeHooks() {
150
+ remove_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
151
+ remove_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
152
+ }
153
+
154
+ /**
155
+ * Check if the plugin file is inside the mu-plugins directory.
156
+ *
157
+ * @return bool
158
+ */
159
+ public function isMuPlugin() {
160
+ static $cachedResult = null;
161
+
162
+ if ( $cachedResult === null ) {
163
+ if ( !defined('WPMU_PLUGIN_DIR') || !is_string(WPMU_PLUGIN_DIR) ) {
164
+ $cachedResult = false;
165
+ return $cachedResult;
166
+ }
167
+
168
+ //Convert both paths to the canonical form before comparison.
169
+ $muPluginDir = realpath(WPMU_PLUGIN_DIR);
170
+ $pluginPath = realpath($this->pluginAbsolutePath);
171
+ //If realpath() fails, just normalize the syntax instead.
172
+ if (($muPluginDir === false) || ($pluginPath === false)) {
173
+ $muPluginDir = Puc_v4p10_Factory::normalizePath(WPMU_PLUGIN_DIR);
174
+ $pluginPath = Puc_v4p10_Factory::normalizePath($this->pluginAbsolutePath);
175
+ }
176
+
177
+ $cachedResult = (strpos($pluginPath, $muPluginDir) === 0);
178
+ }
179
+
180
+ return $cachedResult;
181
+ }
182
+ }
183
+
184
+ endif;
updater/Puc/v4p10/Plugin/Ui.php ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Plugin_Ui', false) ):
3
+ /**
4
+ * Additional UI elements for plugins.
5
+ */
6
+ class Puc_v4p10_Plugin_Ui {
7
+ private $updateChecker;
8
+ private $manualCheckErrorTransient = '';
9
+
10
+ /**
11
+ * @param Puc_v4p10_Plugin_UpdateChecker $updateChecker
12
+ */
13
+ public function __construct($updateChecker) {
14
+ $this->updateChecker = $updateChecker;
15
+ $this->manualCheckErrorTransient = $this->updateChecker->getUniqueName('manual_check_errors');
16
+
17
+ add_action('admin_init', array($this, 'onAdminInit'));
18
+ }
19
+
20
+ public function onAdminInit() {
21
+ if ( $this->updateChecker->userCanInstallUpdates() ) {
22
+ $this->handleManualCheck();
23
+
24
+ add_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10, 3);
25
+ add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
26
+ add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Add a "View Details" link to the plugin row in the "Plugins" page. By default,
32
+ * the new link will appear before the "Visit plugin site" link (if present).
33
+ *
34
+ * You can change the link text by using the "puc_view_details_link-$slug" filter.
35
+ * Returning an empty string from the filter will disable the link.
36
+ *
37
+ * You can change the position of the link using the
38
+ * "puc_view_details_link_position-$slug" filter.
39
+ * Returning 'before' or 'after' will place the link immediately before/after
40
+ * the "Visit plugin site" link.
41
+ * Returning 'append' places the link after any existing links at the time of the hook.
42
+ * Returning 'replace' replaces the "Visit plugin site" link.
43
+ * Returning anything else disables the link when there is a "Visit plugin site" link.
44
+ *
45
+ * If there is no "Visit plugin site" link 'append' is always used!
46
+ *
47
+ * @param array $pluginMeta Array of meta links.
48
+ * @param string $pluginFile
49
+ * @param array $pluginData Array of plugin header data.
50
+ * @return array
51
+ */
52
+ public function addViewDetailsLink($pluginMeta, $pluginFile, $pluginData = array()) {
53
+ if ( $this->isMyPluginFile($pluginFile) && !isset($pluginData['slug']) ) {
54
+ $linkText = apply_filters($this->updateChecker->getUniqueName('view_details_link'), __('View details'));
55
+ if ( !empty($linkText) ) {
56
+ $viewDetailsLinkPosition = 'append';
57
+
58
+ //Find the "Visit plugin site" link (if present).
59
+ $visitPluginSiteLinkIndex = count($pluginMeta) - 1;
60
+ if ( $pluginData['PluginURI'] ) {
61
+ $escapedPluginUri = esc_url($pluginData['PluginURI']);
62
+ foreach ($pluginMeta as $linkIndex => $existingLink) {
63
+ if ( strpos($existingLink, $escapedPluginUri) !== false ) {
64
+ $visitPluginSiteLinkIndex = $linkIndex;
65
+ $viewDetailsLinkPosition = apply_filters(
66
+ $this->updateChecker->getUniqueName('view_details_link_position'),
67
+ 'before'
68
+ );
69
+ break;
70
+ }
71
+ }
72
+ }
73
+
74
+ $viewDetailsLink = sprintf('<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
75
+ esc_url(network_admin_url('plugin-install.php?tab=plugin-information&plugin=' . urlencode($this->updateChecker->slug) .
76
+ '&TB_iframe=true&width=600&height=550')),
77
+ esc_attr(sprintf(__('More information about %s'), $pluginData['Name'])),
78
+ esc_attr($pluginData['Name']),
79
+ $linkText
80
+ );
81
+ switch ($viewDetailsLinkPosition) {
82
+ case 'before':
83
+ array_splice($pluginMeta, $visitPluginSiteLinkIndex, 0, $viewDetailsLink);
84
+ break;
85
+ case 'after':
86
+ array_splice($pluginMeta, $visitPluginSiteLinkIndex + 1, 0, $viewDetailsLink);
87
+ break;
88
+ case 'replace':
89
+ $pluginMeta[$visitPluginSiteLinkIndex] = $viewDetailsLink;
90
+ break;
91
+ case 'append':
92
+ default:
93
+ $pluginMeta[] = $viewDetailsLink;
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ return $pluginMeta;
99
+ }
100
+
101
+ /**
102
+ * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
103
+ * the new link will appear after the "Visit plugin site" link if present, otherwise
104
+ * after the "View plugin details" link.
105
+ *
106
+ * You can change the link text by using the "puc_manual_check_link-$slug" filter.
107
+ * Returning an empty string from the filter will disable the link.
108
+ *
109
+ * @param array $pluginMeta Array of meta links.
110
+ * @param string $pluginFile
111
+ * @return array
112
+ */
113
+ public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {
114
+ if ( $this->isMyPluginFile($pluginFile) ) {
115
+ $linkUrl = wp_nonce_url(
116
+ add_query_arg(
117
+ array(
118
+ 'puc_check_for_updates' => 1,
119
+ 'puc_slug' => $this->updateChecker->slug,
120
+ ),
121
+ self_admin_url('plugins.php')
122
+ ),
123
+ 'puc_check_for_updates'
124
+ );
125
+
126
+ $linkText = apply_filters(
127
+ $this->updateChecker->getUniqueName('manual_check_link'),
128
+ __('Check for updates', 'plugin-update-checker')
129
+ );
130
+ if ( !empty($linkText) ) {
131
+ /** @noinspection HtmlUnknownTarget */
132
+ $pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);
133
+ }
134
+ }
135
+ return $pluginMeta;
136
+ }
137
+
138
+ protected function isMyPluginFile($pluginFile) {
139
+ return ($pluginFile == $this->updateChecker->pluginFile)
140
+ || (!empty($this->updateChecker->muPluginFile) && ($pluginFile == $this->updateChecker->muPluginFile));
141
+ }
142
+
143
+ /**
144
+ * Check for updates when the user clicks the "Check for updates" link.
145
+ *
146
+ * @see self::addCheckForUpdatesLink()
147
+ *
148
+ * @return void
149
+ */
150
+ public function handleManualCheck() {
151
+ $shouldCheck =
152
+ isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
153
+ && $_GET['puc_slug'] == $this->updateChecker->slug
154
+ && check_admin_referer('puc_check_for_updates');
155
+
156
+ if ( $shouldCheck ) {
157
+ $update = $this->updateChecker->checkForUpdates();
158
+ $status = ($update === null) ? 'no_update' : 'update_available';
159
+
160
+ if ( ($update === null) && !empty($this->lastRequestApiErrors) ) {
161
+ //Some errors are not critical. For example, if PUC tries to retrieve the readme.txt
162
+ //file from GitHub and gets a 404, that's an API error, but it doesn't prevent updates
163
+ //from working. Maybe the plugin simply doesn't have a readme.
164
+ //Let's only show important errors.
165
+ $foundCriticalErrors = false;
166
+ $questionableErrorCodes = array(
167
+ 'puc-github-http-error',
168
+ 'puc-gitlab-http-error',
169
+ 'puc-bitbucket-http-error',
170
+ );
171
+
172
+ foreach ($this->lastRequestApiErrors as $item) {
173
+ $wpError = $item['error'];
174
+ /** @var WP_Error $wpError */
175
+ if ( !in_array($wpError->get_error_code(), $questionableErrorCodes) ) {
176
+ $foundCriticalErrors = true;
177
+ break;
178
+ }
179
+ }
180
+
181
+ if ( $foundCriticalErrors ) {
182
+ $status = 'error';
183
+ set_site_transient($this->manualCheckErrorTransient, $this->lastRequestApiErrors, 60);
184
+ }
185
+ }
186
+
187
+ wp_redirect(add_query_arg(
188
+ array(
189
+ 'puc_update_check_result' => $status,
190
+ 'puc_slug' => $this->updateChecker->slug,
191
+ ),
192
+ self_admin_url('plugins.php')
193
+ ));
194
+ exit;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Display the results of a manual update check.
200
+ *
201
+ * @see self::handleManualCheck()
202
+ *
203
+ * You can change the result message by using the "puc_manual_check_message-$slug" filter.
204
+ */
205
+ public function displayManualCheckResult() {
206
+ if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->updateChecker->slug) ) {
207
+ $status = strval($_GET['puc_update_check_result']);
208
+ $title = $this->updateChecker->getInstalledPackage()->getPluginTitle();
209
+ $noticeClass = 'updated notice-success';
210
+ $details = '';
211
+
212
+ if ( $status == 'no_update' ) {
213
+ $message = sprintf(_x('The %s plugin is up to date.', 'the plugin title', 'plugin-update-checker'), $title);
214
+ } else if ( $status == 'update_available' ) {
215
+ $message = sprintf(_x('A new version of the %s plugin is available.', 'the plugin title', 'plugin-update-checker'), $title);
216
+ } else if ( $status === 'error' ) {
217
+ $message = sprintf(_x('Could not determine if updates are available for %s.', 'the plugin title', 'plugin-update-checker'), $title);
218
+ $noticeClass = 'error notice-error';
219
+
220
+ $details = $this->formatManualCheckErrors(get_site_transient($this->manualCheckErrorTransient));
221
+ delete_site_transient($this->manualCheckErrorTransient);
222
+ } else {
223
+ $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));
224
+ $noticeClass = 'error notice-error';
225
+ }
226
+ printf(
227
+ '<div class="notice %s is-dismissible"><p>%s</p>%s</div>',
228
+ $noticeClass,
229
+ apply_filters($this->updateChecker->getUniqueName('manual_check_message'), $message, $status),
230
+ $details
231
+ );
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Format the list of errors that were thrown during an update check.
237
+ *
238
+ * @param array $errors
239
+ * @return string
240
+ */
241
+ protected function formatManualCheckErrors($errors) {
242
+ if ( empty($errors) ) {
243
+ return '';
244
+ }
245
+ $output = '';
246
+
247
+ $showAsList = count($errors) > 1;
248
+ if ( $showAsList ) {
249
+ $output .= '<ol>';
250
+ $formatString = '<li>%1$s <code>%2$s</code></li>';
251
+ } else {
252
+ $formatString = '<p>%1$s <code>%2$s</code></p>';
253
+ }
254
+ foreach ($errors as $item) {
255
+ $wpError = $item['error'];
256
+ /** @var WP_Error $wpError */
257
+ $output .= sprintf(
258
+ $formatString,
259
+ $wpError->get_error_message(),
260
+ $wpError->get_error_code()
261
+ );
262
+ }
263
+ if ( $showAsList ) {
264
+ $output .= '</ol>';
265
+ }
266
+
267
+ return $output;
268
+ }
269
+
270
+ public function removeHooks() {
271
+ remove_action('admin_init', array($this, 'onAdminInit'));
272
+ remove_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10);
273
+ remove_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10);
274
+ remove_action('all_admin_notices', array($this, 'displayManualCheckResult'));
275
+ }
276
+ }
277
+ endif;
updater/Puc/v4p10/Plugin/Update.php ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Plugin_Update', false) ):
3
+
4
+ /**
5
+ * A simple container class for holding information about an available update.
6
+ *
7
+ * @author Janis Elsts
8
+ * @copyright 2016
9
+ * @access public
10
+ */
11
+ class Puc_v4p10_Plugin_Update extends Puc_v4p10_Update {
12
+ public $id = 0;
13
+ public $homepage;
14
+ public $upgrade_notice;
15
+ public $tested;
16
+ public $requires_php = false;
17
+ public $icons = array();
18
+ public $filename; //Plugin filename relative to the plugins directory.
19
+
20
+ protected static $extraFields = array(
21
+ 'id', 'homepage', 'tested', 'requires_php', 'upgrade_notice', 'icons', 'filename',
22
+ );
23
+
24
+ /**
25
+ * Create a new instance of PluginUpdate from its JSON-encoded representation.
26
+ *
27
+ * @param string $json
28
+ * @return Puc_v4p10_Plugin_Update|null
29
+ */
30
+ public static function fromJson($json){
31
+ //Since update-related information is simply a subset of the full plugin info,
32
+ //we can parse the update JSON as if it was a plugin info string, then copy over
33
+ //the parts that we care about.
34
+ $pluginInfo = Puc_v4p10_Plugin_Info::fromJson($json);
35
+ if ( $pluginInfo !== null ) {
36
+ return self::fromPluginInfo($pluginInfo);
37
+ } else {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Create a new instance of PluginUpdate based on an instance of PluginInfo.
44
+ * Basically, this just copies a subset of fields from one object to another.
45
+ *
46
+ * @param Puc_v4p10_Plugin_Info $info
47
+ * @return Puc_v4p10_Plugin_Update
48
+ */
49
+ public static function fromPluginInfo($info){
50
+ return self::fromObject($info);
51
+ }
52
+
53
+ /**
54
+ * Create a new instance by copying the necessary fields from another object.
55
+ *
56
+ * @param StdClass|Puc_v4p10_Plugin_Info|Puc_v4p10_Plugin_Update $object The source object.
57
+ * @return Puc_v4p10_Plugin_Update The new copy.
58
+ */
59
+ public static function fromObject($object) {
60
+ $update = new self();
61
+ $update->copyFields($object, $update);
62
+ return $update;
63
+ }
64
+
65
+ /**
66
+ * @return string[]
67
+ */
68
+ protected function getFieldNames() {
69
+ return array_merge(parent::getFieldNames(), self::$extraFields);
70
+ }
71
+
72
+ /**
73
+ * Transform the update into the format used by WordPress native plugin API.
74
+ *
75
+ * @return object
76
+ */
77
+ public function toWpFormat() {
78
+ $update = parent::toWpFormat();
79
+
80
+ $update->id = $this->id;
81
+ $update->url = $this->homepage;
82
+ $update->tested = $this->tested;
83
+ $update->requires_php = $this->requires_php;
84
+ $update->plugin = $this->filename;
85
+
86
+ if ( !empty($this->upgrade_notice) ) {
87
+ $update->upgrade_notice = $this->upgrade_notice;
88
+ }
89
+
90
+ if ( !empty($this->icons) && is_array($this->icons) ) {
91
+ //This should be an array with up to 4 keys: 'svg', '1x', '2x' and 'default'.
92
+ //Docs: https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
93
+ $icons = array_intersect_key(
94
+ $this->icons,
95
+ array('svg' => true, '1x' => true, '2x' => true, 'default' => true)
96
+ );
97
+ if ( !empty($icons) ) {
98
+ $update->icons = $icons;
99
+
100
+ //It appears that the 'default' icon isn't used anywhere in WordPress 4.9,
101
+ //but lets set it just in case a future release needs it.
102
+ if ( !isset($update->icons['default']) ) {
103
+ $update->icons['default'] = current($update->icons);
104
+ }
105
+ }
106
+ }
107
+
108
+ return $update;
109
+ }
110
+ }
111
+
112
+ endif;
updater/Puc/v4p10/Plugin/UpdateChecker.php ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Plugin_UpdateChecker', false) ):
3
+
4
+ /**
5
+ * A custom plugin update checker.
6
+ *
7
+ * @author Janis Elsts
8
+ * @copyright 2018
9
+ * @access public
10
+ */
11
+ class Puc_v4p10_Plugin_UpdateChecker extends Puc_v4p10_UpdateChecker {
12
+ protected $updateTransient = 'update_plugins';
13
+ protected $translationType = 'plugin';
14
+
15
+ public $pluginAbsolutePath = ''; //Full path of the main plugin file.
16
+ public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.
17
+ public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
18
+
19
+ /**
20
+ * @var Puc_v4p10_Plugin_Package
21
+ */
22
+ protected $package;
23
+
24
+ private $extraUi = null;
25
+
26
+ /**
27
+ * Class constructor.
28
+ *
29
+ * @param string $metadataUrl The URL of the plugin's metadata file.
30
+ * @param string $pluginFile Fully qualified path to the main plugin file.
31
+ * @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.
32
+ * @param integer $checkPeriod How often to check for updates (in hours). Defaults to checking every 12 hours. Set to 0 to disable automatic update checks.
33
+ * @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.
34
+ * @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.
35
+ */
36
+ public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){
37
+ $this->pluginAbsolutePath = $pluginFile;
38
+ $this->pluginFile = plugin_basename($this->pluginAbsolutePath);
39
+ $this->muPluginFile = $muPluginFile;
40
+
41
+ //If no slug is specified, use the name of the main plugin file as the slug.
42
+ //For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.
43
+ if ( empty($slug) ){
44
+ $slug = basename($this->pluginFile, '.php');
45
+ }
46
+
47
+ //Plugin slugs must be unique.
48
+ $slugCheckFilter = 'puc_is_slug_in_use-' . $slug;
49
+ $slugUsedBy = apply_filters($slugCheckFilter, false);
50
+ if ( $slugUsedBy ) {
51
+ $this->triggerError(sprintf(
52
+ 'Plugin slug "%s" is already in use by %s. Slugs must be unique.',
53
+ htmlentities($slug),
54
+ htmlentities($slugUsedBy)
55
+ ), E_USER_ERROR);
56
+ }
57
+ add_filter($slugCheckFilter, array($this, 'getAbsolutePath'));
58
+
59
+ parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
60
+
61
+ //Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume
62
+ //it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).
63
+ if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {
64
+ $this->muPluginFile = $this->pluginFile;
65
+ }
66
+
67
+ //To prevent a crash during plugin uninstallation, remove updater hooks when the user removes the plugin.
68
+ //Details: https://github.com/YahnisElsts/plugin-update-checker/issues/138#issuecomment-335590964
69
+ add_action('uninstall_' . $this->pluginFile, array($this, 'removeHooks'));
70
+
71
+ $this->extraUi = new Puc_v4p10_Plugin_Ui($this);
72
+ }
73
+
74
+ /**
75
+ * Create an instance of the scheduler.
76
+ *
77
+ * @param int $checkPeriod
78
+ * @return Puc_v4p10_Scheduler
79
+ */
80
+ protected function createScheduler($checkPeriod) {
81
+ $scheduler = new Puc_v4p10_Scheduler($this, $checkPeriod, array('load-plugins.php'));
82
+ register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
83
+ return $scheduler;
84
+ }
85
+
86
+ /**
87
+ * Install the hooks required to run periodic update checks and inject update info
88
+ * into WP data structures.
89
+ *
90
+ * @return void
91
+ */
92
+ protected function installHooks(){
93
+ //Override requests for plugin information
94
+ add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
95
+
96
+ parent::installHooks();
97
+ }
98
+
99
+ /**
100
+ * Remove update checker hooks.
101
+ *
102
+ * The intent is to prevent a fatal error that can happen if the plugin has an uninstall
103
+ * hook. During uninstallation, WP includes the main plugin file (which creates a PUC instance),
104
+ * the uninstall hook runs, WP deletes the plugin files and then updates some transients.
105
+ * If PUC hooks are still around at this time, they could throw an error while trying to
106
+ * autoload classes from files that no longer exist.
107
+ *
108
+ * The "site_transient_{$transient}" filter is the main problem here, but let's also remove
109
+ * most other PUC hooks to be safe.
110
+ *
111
+ * @internal
112
+ */
113
+ public function removeHooks() {
114
+ parent::removeHooks();
115
+ $this->extraUi->removeHooks();
116
+ $this->package->removeHooks();
117
+
118
+ remove_filter('plugins_api', array($this, 'injectInfo'), 20);
119
+ }
120
+
121
+ /**
122
+ * Retrieve plugin info from the configured API endpoint.
123
+ *
124
+ * @uses wp_remote_get()
125
+ *
126
+ * @param array $queryArgs Additional query arguments to append to the request. Optional.
127
+ * @return Puc_v4p10_Plugin_Info
128
+ */
129
+ public function requestInfo($queryArgs = array()) {
130
+ list($pluginInfo, $result) = $this->requestMetadata('Puc_v4p10_Plugin_Info', 'request_info', $queryArgs);
131
+
132
+ if ( $pluginInfo !== null ) {
133
+ /** @var Puc_v4p10_Plugin_Info $pluginInfo */
134
+ $pluginInfo->filename = $this->pluginFile;
135
+ $pluginInfo->slug = $this->slug;
136
+ }
137
+
138
+ $pluginInfo = apply_filters($this->getUniqueName('request_info_result'), $pluginInfo, $result);
139
+ return $pluginInfo;
140
+ }
141
+
142
+ /**
143
+ * Retrieve the latest update (if any) from the configured API endpoint.
144
+ *
145
+ * @uses PluginUpdateChecker::requestInfo()
146
+ *
147
+ * @return Puc_v4p10_Update|null An instance of Plugin_Update, or NULL when no updates are available.
148
+ */
149
+ public function requestUpdate() {
150
+ //For the sake of simplicity, this function just calls requestInfo()
151
+ //and transforms the result accordingly.
152
+ $pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));
153
+ if ( $pluginInfo === null ){
154
+ return null;
155
+ }
156
+ $update = Puc_v4p10_Plugin_Update::fromPluginInfo($pluginInfo);
157
+
158
+ $update = $this->filterUpdateResult($update);
159
+
160
+ return $update;
161
+ }
162
+
163
+ /**
164
+ * Intercept plugins_api() calls that request information about our plugin and
165
+ * use the configured API endpoint to satisfy them.
166
+ *
167
+ * @see plugins_api()
168
+ *
169
+ * @param mixed $result
170
+ * @param string $action
171
+ * @param array|object $args
172
+ * @return mixed
173
+ */
174
+ public function injectInfo($result, $action = null, $args = null){
175
+ $relevant = ($action == 'plugin_information') && isset($args->slug) && (
176
+ ($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))
177
+ );
178
+ if ( !$relevant ) {
179
+ return $result;
180
+ }
181
+
182
+ $pluginInfo = $this->requestInfo();
183
+ $this->fixSupportedWordpressVersion($pluginInfo);
184
+
185
+ $pluginInfo = apply_filters($this->getUniqueName('pre_inject_info'), $pluginInfo);
186
+ if ( $pluginInfo ) {
187
+ return $pluginInfo->toWpFormat();
188
+ }
189
+
190
+ return $result;
191
+ }
192
+
193
+ protected function shouldShowUpdates() {
194
+ //No update notifications for mu-plugins unless explicitly enabled. The MU plugin file
195
+ //is usually different from the main plugin file so the update wouldn't show up properly anyway.
196
+ return !$this->isUnknownMuPlugin();
197
+ }
198
+
199
+ /**
200
+ * @param stdClass|null $updates
201
+ * @param stdClass $updateToAdd
202
+ * @return stdClass
203
+ */
204
+ protected function addUpdateToList($updates, $updateToAdd) {
205
+ if ( $this->package->isMuPlugin() ) {
206
+ //WP does not support automatic update installation for mu-plugins, but we can
207
+ //still display a notice.
208
+ $updateToAdd->package = null;
209
+ }
210
+ return parent::addUpdateToList($updates, $updateToAdd);
211
+ }
212
+
213
+ /**
214
+ * @param stdClass|null $updates
215
+ * @return stdClass|null
216
+ */
217
+ protected function removeUpdateFromList($updates) {
218
+ $updates = parent::removeUpdateFromList($updates);
219
+ if ( !empty($this->muPluginFile) && isset($updates, $updates->response) ) {
220
+ unset($updates->response[$this->muPluginFile]);
221
+ }
222
+ return $updates;
223
+ }
224
+
225
+ /**
226
+ * For plugins, the update array is indexed by the plugin filename relative to the "plugins"
227
+ * directory. Example: "plugin-name/plugin.php".
228
+ *
229
+ * @return string
230
+ */
231
+ protected function getUpdateListKey() {
232
+ if ( $this->package->isMuPlugin() ) {
233
+ return $this->muPluginFile;
234
+ }
235
+ return $this->pluginFile;
236
+ }
237
+
238
+ protected function getNoUpdateItemFields() {
239
+ return array_merge(
240
+ parent::getNoUpdateItemFields(),
241
+ array(
242
+ 'id' => $this->pluginFile,
243
+ 'slug' => $this->slug,
244
+ 'plugin' => $this->pluginFile,
245
+ 'icons' => array(),
246
+ 'banners' => array(),
247
+ 'banners_rtl' => array(),
248
+ 'tested' => '',
249
+ 'compatibility' => new stdClass(),
250
+ )
251
+ );
252
+ }
253
+
254
+ /**
255
+ * Alias for isBeingUpgraded().
256
+ *
257
+ * @deprecated
258
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
259
+ * @return bool
260
+ */
261
+ public function isPluginBeingUpgraded($upgrader = null) {
262
+ return $this->isBeingUpgraded($upgrader);
263
+ }
264
+
265
+ /**
266
+ * Is there an update being installed for this plugin, right now?
267
+ *
268
+ * @param WP_Upgrader|null $upgrader
269
+ * @return bool
270
+ */
271
+ public function isBeingUpgraded($upgrader = null) {
272
+ return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);
273
+ }
274
+
275
+ /**
276
+ * Get the details of the currently available update, if any.
277
+ *
278
+ * If no updates are available, or if the last known update version is below or equal
279
+ * to the currently installed version, this method will return NULL.
280
+ *
281
+ * Uses cached update data. To retrieve update information straight from
282
+ * the metadata URL, call requestUpdate() instead.
283
+ *
284
+ * @return Puc_v4p10_Plugin_Update|null
285
+ */
286
+ public function getUpdate() {
287
+ $update = parent::getUpdate();
288
+ if ( isset($update) ) {
289
+ /** @var Puc_v4p10_Plugin_Update $update */
290
+ $update->filename = $this->pluginFile;
291
+ }
292
+ return $update;
293
+ }
294
+
295
+ /**
296
+ * Get the translated plugin title.
297
+ *
298
+ * @deprecated
299
+ * @return string
300
+ */
301
+ public function getPluginTitle() {
302
+ return $this->package->getPluginTitle();
303
+ }
304
+
305
+ /**
306
+ * Check if the current user has the required permissions to install updates.
307
+ *
308
+ * @return bool
309
+ */
310
+ public function userCanInstallUpdates() {
311
+ return current_user_can('update_plugins');
312
+ }
313
+
314
+ /**
315
+ * Check if the plugin file is inside the mu-plugins directory.
316
+ *
317
+ * @deprecated
318
+ * @return bool
319
+ */
320
+ protected function isMuPlugin() {
321
+ return $this->package->isMuPlugin();
322
+ }
323
+
324
+ /**
325
+ * MU plugins are partially supported, but only when we know which file in mu-plugins
326
+ * corresponds to this plugin.
327
+ *
328
+ * @return bool
329
+ */
330
+ protected function isUnknownMuPlugin() {
331
+ return empty($this->muPluginFile) && $this->package->isMuPlugin();
332
+ }
333
+
334
+ /**
335
+ * Get absolute path to the main plugin file.
336
+ *
337
+ * @return string
338
+ */
339
+ public function getAbsolutePath() {
340
+ return $this->pluginAbsolutePath;
341
+ }
342
+
343
+ /**
344
+ * Register a callback for filtering query arguments.
345
+ *
346
+ * The callback function should take one argument - an associative array of query arguments.
347
+ * It should return a modified array of query arguments.
348
+ *
349
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
350
+ *
351
+ * @param callable $callback
352
+ * @return void
353
+ */
354
+ public function addQueryArgFilter($callback){
355
+ $this->addFilter('request_info_query_args', $callback);
356
+ }
357
+
358
+ /**
359
+ * Register a callback for filtering arguments passed to wp_remote_get().
360
+ *
361
+ * The callback function should take one argument - an associative array of arguments -
362
+ * and return a modified array or arguments. See the WP documentation on wp_remote_get()
363
+ * for details on what arguments are available and how they work.
364
+ *
365
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
366
+ *
367
+ * @param callable $callback
368
+ * @return void
369
+ */
370
+ public function addHttpRequestArgFilter($callback) {
371
+ $this->addFilter('request_info_options', $callback);
372
+ }
373
+
374
+ /**
375
+ * Register a callback for filtering the plugin info retrieved from the external API.
376
+ *
377
+ * The callback function should take two arguments. If the plugin info was retrieved
378
+ * successfully, the first argument passed will be an instance of PluginInfo. Otherwise,
379
+ * it will be NULL. The second argument will be the corresponding return value of
380
+ * wp_remote_get (see WP docs for details).
381
+ *
382
+ * The callback function should return a new or modified instance of PluginInfo or NULL.
383
+ *
384
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
385
+ *
386
+ * @param callable $callback
387
+ * @return void
388
+ */
389
+ public function addResultFilter($callback) {
390
+ $this->addFilter('request_info_result', $callback, 10, 2);
391
+ }
392
+
393
+ protected function createDebugBarExtension() {
394
+ return new Puc_v4p10_DebugBar_PluginExtension($this);
395
+ }
396
+
397
+ /**
398
+ * Create a package instance that represents this plugin or theme.
399
+ *
400
+ * @return Puc_v4p10_InstalledPackage
401
+ */
402
+ protected function createInstalledPackage() {
403
+ return new Puc_v4p10_Plugin_Package($this->pluginAbsolutePath, $this);
404
+ }
405
+
406
+ /**
407
+ * @return Puc_v4p10_Plugin_Package
408
+ */
409
+ public function getInstalledPackage() {
410
+ return $this->package;
411
+ }
412
+ }
413
+
414
+ endif;
updater/Puc/v4p10/Scheduler.php ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Scheduler', false) ):
3
+
4
+ /**
5
+ * The scheduler decides when and how often to check for updates.
6
+ * It calls @see Puc_v4p10_UpdateChecker::checkForUpdates() to perform the actual checks.
7
+ */
8
+ class Puc_v4p10_Scheduler {
9
+ public $checkPeriod = 12; //How often to check for updates (in hours).
10
+ public $throttleRedundantChecks = false; //Check less often if we already know that an update is available.
11
+ public $throttledCheckPeriod = 72;
12
+
13
+ protected $hourlyCheckHooks = array('load-update.php');
14
+
15
+ /**
16
+ * @var Puc_v4p10_UpdateChecker
17
+ */
18
+ protected $updateChecker;
19
+
20
+ private $cronHook = null;
21
+
22
+ /**
23
+ * Scheduler constructor.
24
+ *
25
+ * @param Puc_v4p10_UpdateChecker $updateChecker
26
+ * @param int $checkPeriod How often to check for updates (in hours).
27
+ * @param array $hourlyHooks
28
+ */
29
+ public function __construct($updateChecker, $checkPeriod, $hourlyHooks = array('load-plugins.php')) {
30
+ $this->updateChecker = $updateChecker;
31
+ $this->checkPeriod = $checkPeriod;
32
+
33
+ //Set up the periodic update checks
34
+ $this->cronHook = $this->updateChecker->getUniqueName('cron_check_updates');
35
+ if ( $this->checkPeriod > 0 ){
36
+
37
+ //Trigger the check via Cron.
38
+ //Try to use one of the default schedules if possible as it's less likely to conflict
39
+ //with other plugins and their custom schedules.
40
+ $defaultSchedules = array(
41
+ 1 => 'hourly',
42
+ 12 => 'twicedaily',
43
+ 24 => 'daily',
44
+ );
45
+ if ( array_key_exists($this->checkPeriod, $defaultSchedules) ) {
46
+ $scheduleName = $defaultSchedules[$this->checkPeriod];
47
+ } else {
48
+ //Use a custom cron schedule.
49
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';
50
+ add_filter('cron_schedules', array($this, '_addCustomSchedule'));
51
+ }
52
+
53
+ if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) {
54
+ //Randomly offset the schedule to help prevent update server traffic spikes. Without this
55
+ //most checks may happen during times of day when people are most likely to install new plugins.
56
+ $firstCheckTime = time() - rand(0, max($this->checkPeriod * 3600 - 15 * 60, 1));
57
+ $firstCheckTime = apply_filters(
58
+ $this->updateChecker->getUniqueName('first_check_time'),
59
+ $firstCheckTime
60
+ );
61
+ wp_schedule_event($firstCheckTime, $scheduleName, $this->cronHook);
62
+ }
63
+ add_action($this->cronHook, array($this, 'maybeCheckForUpdates'));
64
+
65
+ //In case Cron is disabled or unreliable, we also manually trigger
66
+ //the periodic checks while the user is browsing the Dashboard.
67
+ add_action( 'admin_init', array($this, 'maybeCheckForUpdates') );
68
+
69
+ //Like WordPress itself, we check more often on certain pages.
70
+ /** @see wp_update_plugins */
71
+ add_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));
72
+ //"load-update.php" and "load-plugins.php" or "load-themes.php".
73
+ $this->hourlyCheckHooks = array_merge($this->hourlyCheckHooks, $hourlyHooks);
74
+ foreach($this->hourlyCheckHooks as $hook) {
75
+ add_action($hook, array($this, 'maybeCheckForUpdates'));
76
+ }
77
+ //This hook fires after a bulk update is complete.
78
+ add_action('upgrader_process_complete', array($this, 'upgraderProcessComplete'), 11, 2);
79
+
80
+ } else {
81
+ //Periodic checks are disabled.
82
+ wp_clear_scheduled_hook($this->cronHook);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Runs upon the WP action upgrader_process_complete.
88
+ *
89
+ * We look at the parameters to decide whether to call maybeCheckForUpdates() or not.
90
+ * We also check if the update checker has been removed by the update.
91
+ *
92
+ * @param WP_Upgrader $upgrader WP_Upgrader instance
93
+ * @param array $upgradeInfo extra information about the upgrade
94
+ */
95
+ public function upgraderProcessComplete(
96
+ /** @noinspection PhpUnusedParameterInspection */
97
+ $upgrader, $upgradeInfo
98
+ ) {
99
+ //Cancel all further actions if the current version of PUC has been deleted or overwritten
100
+ //by a different version during the upgrade. If we try to do anything more in that situation,
101
+ //we could trigger a fatal error by trying to autoload a deleted class.
102
+ clearstatcache();
103
+ if ( !file_exists(__FILE__) ) {
104
+ $this->removeHooks();
105
+ $this->updateChecker->removeHooks();
106
+ return;
107
+ }
108
+
109
+ //Sanity check and limitation to relevant types.
110
+ if (
111
+ !is_array($upgradeInfo) || !isset($upgradeInfo['type'], $upgradeInfo['action'])
112
+ || 'update' !== $upgradeInfo['action'] || !in_array($upgradeInfo['type'], array('plugin', 'theme'))
113
+ ) {
114
+ return;
115
+ }
116
+
117
+ //Filter out notifications of upgrades that should have no bearing upon whether or not our
118
+ //current info is up-to-date.
119
+ if ( is_a($this->updateChecker, 'Puc_v4p10_Theme_UpdateChecker') ) {
120
+ if ( 'theme' !== $upgradeInfo['type'] || !isset($upgradeInfo['themes']) ) {
121
+ return;
122
+ }
123
+
124
+ //Letting too many things going through for checks is not a real problem, so we compare widely.
125
+ if ( !in_array(
126
+ strtolower($this->updateChecker->directoryName),
127
+ array_map('strtolower', $upgradeInfo['themes'])
128
+ ) ) {
129
+ return;
130
+ }
131
+ }
132
+
133
+ if ( is_a($this->updateChecker, 'Puc_v4p10_Plugin_UpdateChecker') ) {
134
+ if ( 'plugin' !== $upgradeInfo['type'] || !isset($upgradeInfo['plugins']) ) {
135
+ return;
136
+ }
137
+
138
+ //Themes pass in directory names in the information array, but plugins use the relative plugin path.
139
+ if ( !in_array(
140
+ strtolower($this->updateChecker->directoryName),
141
+ array_map('dirname', array_map('strtolower', $upgradeInfo['plugins']))
142
+ ) ) {
143
+ return;
144
+ }
145
+ }
146
+
147
+ $this->maybeCheckForUpdates();
148
+ }
149
+
150
+ /**
151
+ * Check for updates if the configured check interval has already elapsed.
152
+ * Will use a shorter check interval on certain admin pages like "Dashboard -> Updates" or when doing cron.
153
+ *
154
+ * You can override the default behaviour by using the "puc_check_now-$slug" filter.
155
+ * The filter callback will be passed three parameters:
156
+ * - Current decision. TRUE = check updates now, FALSE = don't check now.
157
+ * - Last check time as a Unix timestamp.
158
+ * - Configured check period in hours.
159
+ * Return TRUE to check for updates immediately, or FALSE to cancel.
160
+ *
161
+ * This method is declared public because it's a hook callback. Calling it directly is not recommended.
162
+ */
163
+ public function maybeCheckForUpdates() {
164
+ if ( empty($this->checkPeriod) ){
165
+ return;
166
+ }
167
+
168
+ $state = $this->updateChecker->getUpdateState();
169
+ $shouldCheck = ($state->timeSinceLastCheck() >= $this->getEffectiveCheckPeriod());
170
+
171
+ //Let plugin authors substitute their own algorithm.
172
+ $shouldCheck = apply_filters(
173
+ $this->updateChecker->getUniqueName('check_now'),
174
+ $shouldCheck,
175
+ $state->getLastCheck(),
176
+ $this->checkPeriod
177
+ );
178
+
179
+ if ( $shouldCheck ) {
180
+ $this->updateChecker->checkForUpdates();
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Calculate the actual check period based on the current status and environment.
186
+ *
187
+ * @return int Check period in seconds.
188
+ */
189
+ protected function getEffectiveCheckPeriod() {
190
+ $currentFilter = current_filter();
191
+ if ( in_array($currentFilter, array('load-update-core.php', 'upgrader_process_complete')) ) {
192
+ //Check more often when the user visits "Dashboard -> Updates" or does a bulk update.
193
+ $period = 60;
194
+ } else if ( in_array($currentFilter, $this->hourlyCheckHooks) ) {
195
+ //Also check more often on /wp-admin/update.php and the "Plugins" or "Themes" page.
196
+ $period = 3600;
197
+ } else if ( $this->throttleRedundantChecks && ($this->updateChecker->getUpdate() !== null) ) {
198
+ //Check less frequently if it's already known that an update is available.
199
+ $period = $this->throttledCheckPeriod * 3600;
200
+ } else if ( defined('DOING_CRON') && constant('DOING_CRON') ) {
201
+ //WordPress cron schedules are not exact, so lets do an update check even
202
+ //if slightly less than $checkPeriod hours have elapsed since the last check.
203
+ $cronFuzziness = 20 * 60;
204
+ $period = $this->checkPeriod * 3600 - $cronFuzziness;
205
+ } else {
206
+ $period = $this->checkPeriod * 3600;
207
+ }
208
+
209
+ return $period;
210
+ }
211
+
212
+ /**
213
+ * Add our custom schedule to the array of Cron schedules used by WP.
214
+ *
215
+ * @param array $schedules
216
+ * @return array
217
+ */
218
+ public function _addCustomSchedule($schedules) {
219
+ if ( $this->checkPeriod && ($this->checkPeriod > 0) ){
220
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';
221
+ $schedules[$scheduleName] = array(
222
+ 'interval' => $this->checkPeriod * 3600,
223
+ 'display' => sprintf('Every %d hours', $this->checkPeriod),
224
+ );
225
+ }
226
+ return $schedules;
227
+ }
228
+
229
+ /**
230
+ * Remove the scheduled cron event that the library uses to check for updates.
231
+ *
232
+ * @return void
233
+ */
234
+ public function removeUpdaterCron() {
235
+ wp_clear_scheduled_hook($this->cronHook);
236
+ }
237
+
238
+ /**
239
+ * Get the name of the update checker's WP-cron hook. Mostly useful for debugging.
240
+ *
241
+ * @return string
242
+ */
243
+ public function getCronHookName() {
244
+ return $this->cronHook;
245
+ }
246
+
247
+ /**
248
+ * Remove most hooks added by the scheduler.
249
+ */
250
+ public function removeHooks() {
251
+ remove_filter('cron_schedules', array($this, '_addCustomSchedule'));
252
+ remove_action('admin_init', array($this, 'maybeCheckForUpdates'));
253
+ remove_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));
254
+
255
+ if ( $this->cronHook !== null ) {
256
+ remove_action($this->cronHook, array($this, 'maybeCheckForUpdates'));
257
+ }
258
+ if ( !empty($this->hourlyCheckHooks) ) {
259
+ foreach ($this->hourlyCheckHooks as $hook) {
260
+ remove_action($hook, array($this, 'maybeCheckForUpdates'));
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ endif;
updater/Puc/v4p10/StateStore.php ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_StateStore', false) ):
4
+
5
+ class Puc_v4p10_StateStore {
6
+ /**
7
+ * @var int Last update check timestamp.
8
+ */
9
+ protected $lastCheck = 0;
10
+
11
+ /**
12
+ * @var string Version number.
13
+ */
14
+ protected $checkedVersion = '';
15
+
16
+ /**
17
+ * @var Puc_v4p10_Update|null Cached update.
18
+ */
19
+ protected $update = null;
20
+
21
+ /**
22
+ * @var string Site option name.
23
+ */
24
+ private $optionName = '';
25
+
26
+ /**
27
+ * @var bool Whether we've already tried to load the state from the database.
28
+ */
29
+ private $isLoaded = false;
30
+
31
+ public function __construct($optionName) {
32
+ $this->optionName = $optionName;
33
+ }
34
+
35
+ /**
36
+ * Get time elapsed since the last update check.
37
+ *
38
+ * If there are no recorded update checks, this method returns a large arbitrary number
39
+ * (i.e. time since the Unix epoch).
40
+ *
41
+ * @return int Elapsed time in seconds.
42
+ */
43
+ public function timeSinceLastCheck() {
44
+ $this->lazyLoad();
45
+ return time() - $this->lastCheck;
46
+ }
47
+
48
+ /**
49
+ * @return int
50
+ */
51
+ public function getLastCheck() {
52
+ $this->lazyLoad();
53
+ return $this->lastCheck;
54
+ }
55
+
56
+ /**
57
+ * Set the time of the last update check to the current timestamp.
58
+ *
59
+ * @return $this
60
+ */
61
+ public function setLastCheckToNow() {
62
+ $this->lazyLoad();
63
+ $this->lastCheck = time();
64
+ return $this;
65
+ }
66
+
67
+ /**
68
+ * @return null|Puc_v4p10_Update
69
+ */
70
+ public function getUpdate() {
71
+ $this->lazyLoad();
72
+ return $this->update;
73
+ }
74
+
75
+ /**
76
+ * @param Puc_v4p10_Update|null $update
77
+ * @return $this
78
+ */
79
+ public function setUpdate(Puc_v4p10_Update $update = null) {
80
+ $this->lazyLoad();
81
+ $this->update = $update;
82
+ return $this;
83
+ }
84
+
85
+ /**
86
+ * @return string
87
+ */
88
+ public function getCheckedVersion() {
89
+ $this->lazyLoad();
90
+ return $this->checkedVersion;
91
+ }
92
+
93
+ /**
94
+ * @param string $version
95
+ * @return $this
96
+ */
97
+ public function setCheckedVersion($version) {
98
+ $this->lazyLoad();
99
+ $this->checkedVersion = strval($version);
100
+ return $this;
101
+ }
102
+
103
+ /**
104
+ * Get translation updates.
105
+ *
106
+ * @return array
107
+ */
108
+ public function getTranslations() {
109
+ $this->lazyLoad();
110
+ if ( isset($this->update, $this->update->translations) ) {
111
+ return $this->update->translations;
112
+ }
113
+ return array();
114
+ }
115
+
116
+ /**
117
+ * Set translation updates.
118
+ *
119
+ * @param array $translationUpdates
120
+ */
121
+ public function setTranslations($translationUpdates) {
122
+ $this->lazyLoad();
123
+ if ( isset($this->update) ) {
124
+ $this->update->translations = $translationUpdates;
125
+ $this->save();
126
+ }
127
+ }
128
+
129
+ public function save() {
130
+ $state = new stdClass();
131
+
132
+ $state->lastCheck = $this->lastCheck;
133
+ $state->checkedVersion = $this->checkedVersion;
134
+
135
+ if ( isset($this->update)) {
136
+ $state->update = $this->update->toStdClass();
137
+
138
+ $updateClass = get_class($this->update);
139
+ $state->updateClass = $updateClass;
140
+ $prefix = $this->getLibPrefix();
141
+ if ( Puc_v4p10_Utils::startsWith($updateClass, $prefix) ) {
142
+ $state->updateBaseClass = substr($updateClass, strlen($prefix));
143
+ }
144
+ }
145
+
146
+ update_site_option($this->optionName, $state);
147
+ $this->isLoaded = true;
148
+ }
149
+
150
+ /**
151
+ * @return $this
152
+ */
153
+ public function lazyLoad() {
154
+ if ( !$this->isLoaded ) {
155
+ $this->load();
156
+ }
157
+ return $this;
158
+ }
159
+
160
+ protected function load() {
161
+ $this->isLoaded = true;
162
+
163
+ $state = get_site_option($this->optionName, null);
164
+
165
+ if ( !is_object($state) ) {
166
+ $this->lastCheck = 0;
167
+ $this->checkedVersion = '';
168
+ $this->update = null;
169
+ return;
170
+ }
171
+
172
+ $this->lastCheck = intval(Puc_v4p10_Utils::get($state, 'lastCheck', 0));
173
+ $this->checkedVersion = Puc_v4p10_Utils::get($state, 'checkedVersion', '');
174
+ $this->update = null;
175
+
176
+ if ( isset($state->update) ) {
177
+ //This mess is due to the fact that the want the update class from this version
178
+ //of the library, not the version that saved the update.
179
+
180
+ $updateClass = null;
181
+ if ( isset($state->updateBaseClass) ) {
182
+ $updateClass = $this->getLibPrefix() . $state->updateBaseClass;
183
+ } else if ( isset($state->updateClass) && class_exists($state->updateClass) ) {
184
+ $updateClass = $state->updateClass;
185
+ }
186
+
187
+ if ( $updateClass !== null ) {
188
+ $this->update = call_user_func(array($updateClass, 'fromObject'), $state->update);
189
+ }
190
+ }
191
+ }
192
+
193
+ public function delete() {
194
+ delete_site_option($this->optionName);
195
+
196
+ $this->lastCheck = 0;
197
+ $this->checkedVersion = '';
198
+ $this->update = null;
199
+ }
200
+
201
+ private function getLibPrefix() {
202
+ $parts = explode('_', __CLASS__, 3);
203
+ return $parts[0] . '_' . $parts[1] . '_';
204
+ }
205
+ }
206
+
207
+ endif;
updater/Puc/v4p10/Theme/Package.php ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Theme_Package', false) ):
3
+
4
+ class Puc_v4p10_Theme_Package extends Puc_v4p10_InstalledPackage {
5
+ /**
6
+ * @var string Theme directory name.
7
+ */
8
+ protected $stylesheet;
9
+
10
+ /**
11
+ * @var WP_Theme Theme object.
12
+ */
13
+ protected $theme;
14
+
15
+ public function __construct($stylesheet, $updateChecker) {
16
+ $this->stylesheet = $stylesheet;
17
+ $this->theme = wp_get_theme($this->stylesheet);
18
+
19
+ parent::__construct($updateChecker);
20
+ }
21
+
22
+ public function getInstalledVersion() {
23
+ return $this->theme->get('Version');
24
+ }
25
+
26
+ public function getAbsoluteDirectoryPath() {
27
+ if ( method_exists($this->theme, 'get_stylesheet_directory') ) {
28
+ return $this->theme->get_stylesheet_directory(); //Available since WP 3.4.
29
+ }
30
+ return get_theme_root($this->stylesheet) . '/' . $this->stylesheet;
31
+ }
32
+
33
+ /**
34
+ * Get the value of a specific plugin or theme header.
35
+ *
36
+ * @param string $headerName
37
+ * @param string $defaultValue
38
+ * @return string Either the value of the header, or $defaultValue if the header doesn't exist or is empty.
39
+ */
40
+ public function getHeaderValue($headerName, $defaultValue = '') {
41
+ $value = $this->theme->get($headerName);
42
+ if ( ($headerName === false) || ($headerName === '') ) {
43
+ return $defaultValue;
44
+ }
45
+ return $value;
46
+ }
47
+
48
+ protected function getHeaderNames() {
49
+ return array(
50
+ 'Name' => 'Theme Name',
51
+ 'ThemeURI' => 'Theme URI',
52
+ 'Description' => 'Description',
53
+ 'Author' => 'Author',
54
+ 'AuthorURI' => 'Author URI',
55
+ 'Version' => 'Version',
56
+ 'Template' => 'Template',
57
+ 'Status' => 'Status',
58
+ 'Tags' => 'Tags',
59
+ 'TextDomain' => 'Text Domain',
60
+ 'DomainPath' => 'Domain Path',
61
+ );
62
+ }
63
+ }
64
+
65
+ endif;
updater/Puc/v4p10/Theme/Update.php ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_Theme_Update', false) ):
4
+
5
+ class Puc_v4p10_Theme_Update extends Puc_v4p10_Update {
6
+ public $details_url = '';
7
+
8
+ protected static $extraFields = array('details_url');
9
+
10
+ /**
11
+ * Transform the metadata into the format used by WordPress core.
12
+ * Note the inconsistency: WP stores plugin updates as objects and theme updates as arrays.
13
+ *
14
+ * @return array
15
+ */
16
+ public function toWpFormat() {
17
+ $update = array(
18
+ 'theme' => $this->slug,
19
+ 'new_version' => $this->version,
20
+ 'url' => $this->details_url,
21
+ );
22
+
23
+ if ( !empty($this->download_url) ) {
24
+ $update['package'] = $this->download_url;
25
+ }
26
+
27
+ return $update;
28
+ }
29
+
30
+ /**
31
+ * Create a new instance of Theme_Update from its JSON-encoded representation.
32
+ *
33
+ * @param string $json Valid JSON string representing a theme information object.
34
+ * @return self New instance of ThemeUpdate, or NULL on error.
35
+ */
36
+ public static function fromJson($json) {
37
+ $instance = new self();
38
+ if ( !parent::createFromJson($json, $instance) ) {
39
+ return null;
40
+ }
41
+ return $instance;
42
+ }
43
+
44
+ /**
45
+ * Create a new instance by copying the necessary fields from another object.
46
+ *
47
+ * @param StdClass|Puc_v4p10_Theme_Update $object The source object.
48
+ * @return Puc_v4p10_Theme_Update The new copy.
49
+ */
50
+ public static function fromObject($object) {
51
+ $update = new self();
52
+ $update->copyFields($object, $update);
53
+ return $update;
54
+ }
55
+
56
+ /**
57
+ * Basic validation.
58
+ *
59
+ * @param StdClass $apiResponse
60
+ * @return bool|WP_Error
61
+ */
62
+ protected function validateMetadata($apiResponse) {
63
+ $required = array('version', 'details_url');
64
+ foreach($required as $key) {
65
+ if ( !isset($apiResponse->$key) || empty($apiResponse->$key) ) {
66
+ return new WP_Error(
67
+ 'tuc-invalid-metadata',
68
+ sprintf('The theme metadata is missing the required "%s" key.', $key)
69
+ );
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+
75
+ protected function getFieldNames() {
76
+ return array_merge(parent::getFieldNames(), self::$extraFields);
77
+ }
78
+
79
+ protected function getPrefixedFilter($tag) {
80
+ return parent::getPrefixedFilter($tag) . '_theme';
81
+ }
82
+ }
83
+
84
+ endif;
updater/Puc/v4p10/Theme/UpdateChecker.php ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_Theme_UpdateChecker', false) ):
4
+
5
+ class Puc_v4p10_Theme_UpdateChecker extends Puc_v4p10_UpdateChecker {
6
+ protected $filterSuffix = 'theme';
7
+ protected $updateTransient = 'update_themes';
8
+ protected $translationType = 'theme';
9
+
10
+ /**
11
+ * @var string Theme directory name.
12
+ */
13
+ protected $stylesheet;
14
+
15
+ public function __construct($metadataUrl, $stylesheet = null, $customSlug = null, $checkPeriod = 12, $optionName = '') {
16
+ if ( $stylesheet === null ) {
17
+ $stylesheet = get_stylesheet();
18
+ }
19
+ $this->stylesheet = $stylesheet;
20
+
21
+ parent::__construct(
22
+ $metadataUrl,
23
+ $stylesheet,
24
+ $customSlug ? $customSlug : $stylesheet,
25
+ $checkPeriod,
26
+ $optionName
27
+ );
28
+ }
29
+
30
+ /**
31
+ * For themes, the update array is indexed by theme directory name.
32
+ *
33
+ * @return string
34
+ */
35
+ protected function getUpdateListKey() {
36
+ return $this->directoryName;
37
+ }
38
+
39
+ /**
40
+ * Retrieve the latest update (if any) from the configured API endpoint.
41
+ *
42
+ * @return Puc_v4p10_Update|null An instance of Update, or NULL when no updates are available.
43
+ */
44
+ public function requestUpdate() {
45
+ list($themeUpdate, $result) = $this->requestMetadata('Puc_v4p10_Theme_Update', 'request_update');
46
+
47
+ if ( $themeUpdate !== null ) {
48
+ /** @var Puc_v4p10_Theme_Update $themeUpdate */
49
+ $themeUpdate->slug = $this->slug;
50
+ }
51
+
52
+ $themeUpdate = $this->filterUpdateResult($themeUpdate, $result);
53
+ return $themeUpdate;
54
+ }
55
+
56
+ protected function getNoUpdateItemFields() {
57
+ return array_merge(
58
+ parent::getNoUpdateItemFields(),
59
+ array(
60
+ 'theme' => $this->directoryName,
61
+ 'requires' => '',
62
+ )
63
+ );
64
+ }
65
+
66
+ public function userCanInstallUpdates() {
67
+ return current_user_can('update_themes');
68
+ }
69
+
70
+ /**
71
+ * Create an instance of the scheduler.
72
+ *
73
+ * @param int $checkPeriod
74
+ * @return Puc_v4p10_Scheduler
75
+ */
76
+ protected function createScheduler($checkPeriod) {
77
+ return new Puc_v4p10_Scheduler($this, $checkPeriod, array('load-themes.php'));
78
+ }
79
+
80
+ /**
81
+ * Is there an update being installed right now for this theme?
82
+ *
83
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
84
+ * @return bool
85
+ */
86
+ public function isBeingUpgraded($upgrader = null) {
87
+ return $this->upgraderStatus->isThemeBeingUpgraded($this->stylesheet, $upgrader);
88
+ }
89
+
90
+ protected function createDebugBarExtension() {
91
+ return new Puc_v4p10_DebugBar_Extension($this, 'Puc_v4p10_DebugBar_ThemePanel');
92
+ }
93
+
94
+ /**
95
+ * Register a callback for filtering query arguments.
96
+ *
97
+ * The callback function should take one argument - an associative array of query arguments.
98
+ * It should return a modified array of query arguments.
99
+ *
100
+ * @param callable $callback
101
+ * @return void
102
+ */
103
+ public function addQueryArgFilter($callback){
104
+ $this->addFilter('request_update_query_args', $callback);
105
+ }
106
+
107
+ /**
108
+ * Register a callback for filtering arguments passed to wp_remote_get().
109
+ *
110
+ * The callback function should take one argument - an associative array of arguments -
111
+ * and return a modified array or arguments. See the WP documentation on wp_remote_get()
112
+ * for details on what arguments are available and how they work.
113
+ *
114
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
115
+ *
116
+ * @param callable $callback
117
+ * @return void
118
+ */
119
+ public function addHttpRequestArgFilter($callback) {
120
+ $this->addFilter('request_update_options', $callback);
121
+ }
122
+
123
+ /**
124
+ * Register a callback for filtering theme updates retrieved from the external API.
125
+ *
126
+ * The callback function should take two arguments. If the theme update was retrieved
127
+ * successfully, the first argument passed will be an instance of Theme_Update. Otherwise,
128
+ * it will be NULL. The second argument will be the corresponding return value of
129
+ * wp_remote_get (see WP docs for details).
130
+ *
131
+ * The callback function should return a new or modified instance of Theme_Update or NULL.
132
+ *
133
+ * @uses add_filter() This method is a convenience wrapper for add_filter().
134
+ *
135
+ * @param callable $callback
136
+ * @return void
137
+ */
138
+ public function addResultFilter($callback) {
139
+ $this->addFilter('request_update_result', $callback, 10, 2);
140
+ }
141
+
142
+ /**
143
+ * Create a package instance that represents this plugin or theme.
144
+ *
145
+ * @return Puc_v4p10_InstalledPackage
146
+ */
147
+ protected function createInstalledPackage() {
148
+ return new Puc_v4p10_Theme_Package($this->stylesheet, $this);
149
+ }
150
+ }
151
+
152
+ endif;
updater/Puc/v4p10/Update.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Update', false) ):
3
+
4
+ /**
5
+ * A simple container class for holding information about an available update.
6
+ *
7
+ * @author Janis Elsts
8
+ * @access public
9
+ */
10
+ abstract class Puc_v4p10_Update extends Puc_v4p10_Metadata {
11
+ public $slug;
12
+ public $version;
13
+ public $download_url;
14
+ public $translations = array();
15
+
16
+ /**
17
+ * @return string[]
18
+ */
19
+ protected function getFieldNames() {
20
+ return array('slug', 'version', 'download_url', 'translations');
21
+ }
22
+
23
+ public function toWpFormat() {
24
+ $update = new stdClass();
25
+
26
+ $update->slug = $this->slug;
27
+ $update->new_version = $this->version;
28
+ $update->package = $this->download_url;
29
+
30
+ return $update;
31
+ }
32
+ }
33
+
34
+ endif;
updater/Puc/v4p10/UpdateChecker.php ADDED
@@ -0,0 +1,994 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_UpdateChecker', false) ):
4
+
5
+ abstract class Puc_v4p10_UpdateChecker {
6
+ protected $filterSuffix = '';
7
+ protected $updateTransient = '';
8
+ protected $translationType = ''; //"plugin" or "theme".
9
+
10
+ /**
11
+ * Set to TRUE to enable error reporting. Errors are raised using trigger_error()
12
+ * and should be logged to the standard PHP error log.
13
+ * @var bool
14
+ */
15
+ public $debugMode = null;
16
+
17
+ /**
18
+ * @var string Where to store the update info.
19
+ */
20
+ public $optionName = '';
21
+
22
+ /**
23
+ * @var string The URL of the metadata file.
24
+ */
25
+ public $metadataUrl = '';
26
+
27
+ /**
28
+ * @var string Plugin or theme directory name.
29
+ */
30
+ public $directoryName = '';
31
+
32
+ /**
33
+ * @var string The slug that will be used in update checker hooks and remote API requests.
34
+ * Usually matches the directory name unless the plugin/theme directory has been renamed.
35
+ */
36
+ public $slug = '';
37
+
38
+ /**
39
+ * @var Puc_v4p10_InstalledPackage
40
+ */
41
+ protected $package;
42
+
43
+ /**
44
+ * @var Puc_v4p10_Scheduler
45
+ */
46
+ public $scheduler;
47
+
48
+ /**
49
+ * @var Puc_v4p10_UpgraderStatus
50
+ */
51
+ protected $upgraderStatus;
52
+
53
+ /**
54
+ * @var Puc_v4p10_StateStore
55
+ */
56
+ protected $updateState;
57
+
58
+ /**
59
+ * @var array List of API errors triggered during the last checkForUpdates() call.
60
+ */
61
+ protected $lastRequestApiErrors = array();
62
+
63
+ /**
64
+ * @var string|mixed The default is 0 because parse_url() can return NULL or FALSE.
65
+ */
66
+ protected $cachedMetadataHost = 0;
67
+
68
+ /**
69
+ * @var Puc_v4p10_DebugBar_Extension|null
70
+ */
71
+ protected $debugBarExtension = null;
72
+
73
+ public function __construct($metadataUrl, $directoryName, $slug = null, $checkPeriod = 12, $optionName = '') {
74
+ $this->debugMode = (bool)(constant('WP_DEBUG'));
75
+ $this->metadataUrl = $metadataUrl;
76
+ $this->directoryName = $directoryName;
77
+ $this->slug = !empty($slug) ? $slug : $this->directoryName;
78
+
79
+ $this->optionName = $optionName;
80
+ if ( empty($this->optionName) ) {
81
+ //BC: Initially the library only supported plugin updates and didn't use type prefixes
82
+ //in the option name. Lets use the same prefix-less name when possible.
83
+ if ( $this->filterSuffix === '' ) {
84
+ $this->optionName = 'external_updates-' . $this->slug;
85
+ } else {
86
+ $this->optionName = $this->getUniqueName('external_updates');
87
+ }
88
+ }
89
+
90
+ $this->package = $this->createInstalledPackage();
91
+ $this->scheduler = $this->createScheduler($checkPeriod);
92
+ $this->upgraderStatus = new Puc_v4p10_UpgraderStatus();
93
+ $this->updateState = new Puc_v4p10_StateStore($this->optionName);
94
+
95
+ if ( did_action('init') ) {
96
+ $this->loadTextDomain();
97
+ } else {
98
+ add_action('init', array($this, 'loadTextDomain'));
99
+ }
100
+
101
+ $this->installHooks();
102
+ }
103
+
104
+ /**
105
+ * @internal
106
+ */
107
+ public function loadTextDomain() {
108
+ //We're not using load_plugin_textdomain() or its siblings because figuring out where
109
+ //the library is located (plugin, mu-plugin, theme, custom wp-content paths) is messy.
110
+ $domain = 'plugin-update-checker';
111
+ $locale = apply_filters(
112
+ 'plugin_locale',
113
+ (is_admin() && function_exists('get_user_locale')) ? get_user_locale() : get_locale(),
114
+ $domain
115
+ );
116
+
117
+ $moFile = $domain . '-' . $locale . '.mo';
118
+ $path = realpath(dirname(__FILE__) . '/../../languages');
119
+
120
+ if ($path && file_exists($path)) {
121
+ load_textdomain($domain, $path . '/' . $moFile);
122
+ }
123
+ }
124
+
125
+ protected function installHooks() {
126
+ //Insert our update info into the update array maintained by WP.
127
+ add_filter('site_transient_' . $this->updateTransient, array($this,'injectUpdate'));
128
+
129
+ //Insert translation updates into the update list.
130
+ add_filter('site_transient_' . $this->updateTransient, array($this, 'injectTranslationUpdates'));
131
+
132
+ //Clear translation updates when WP clears the update cache.
133
+ //This needs to be done directly because the library doesn't actually remove obsolete plugin updates,
134
+ //it just hides them (see getUpdate()). We can't do that with translations - too much disk I/O.
135
+ add_action(
136
+ 'delete_site_transient_' . $this->updateTransient,
137
+ array($this, 'clearCachedTranslationUpdates')
138
+ );
139
+
140
+ //Rename the update directory to be the same as the existing directory.
141
+ if ( $this->directoryName !== '.' ) {
142
+ add_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10, 3);
143
+ }
144
+
145
+ //Allow HTTP requests to the metadata URL even if it's on a local host.
146
+ add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
147
+
148
+ //DebugBar integration.
149
+ if ( did_action('plugins_loaded') ) {
150
+ $this->maybeInitDebugBar();
151
+ } else {
152
+ add_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Remove hooks that were added by this update checker instance.
158
+ */
159
+ public function removeHooks() {
160
+ remove_filter('site_transient_' . $this->updateTransient, array($this,'injectUpdate'));
161
+ remove_filter('site_transient_' . $this->updateTransient, array($this, 'injectTranslationUpdates'));
162
+ remove_action(
163
+ 'delete_site_transient_' . $this->updateTransient,
164
+ array($this, 'clearCachedTranslationUpdates')
165
+ );
166
+
167
+ remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10);
168
+ remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10);
169
+ remove_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
170
+
171
+ remove_action('init', array($this, 'loadTextDomain'));
172
+
173
+ if ( $this->scheduler ) {
174
+ $this->scheduler->removeHooks();
175
+ }
176
+
177
+ if ( $this->debugBarExtension ) {
178
+ $this->debugBarExtension->removeHooks();
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Check if the current user has the required permissions to install updates.
184
+ *
185
+ * @return bool
186
+ */
187
+ abstract public function userCanInstallUpdates();
188
+
189
+ /**
190
+ * Explicitly allow HTTP requests to the metadata URL.
191
+ *
192
+ * WordPress has a security feature where the HTTP API will reject all requests that are sent to
193
+ * another site hosted on the same server as the current site (IP match), a local host, or a local
194
+ * IP, unless the host exactly matches the current site.
195
+ *
196
+ * This feature is opt-in (at least in WP 4.4). Apparently some people enable it.
197
+ *
198
+ * That can be a problem when you're developing your plugin and you decide to host the update information
199
+ * on the same server as your test site. Update requests will mysteriously fail.
200
+ *
201
+ * We fix that by adding an exception for the metadata host.
202
+ *
203
+ * @param bool $allow
204
+ * @param string $host
205
+ * @return bool
206
+ */
207
+ public function allowMetadataHost($allow, $host) {
208
+ if ( $this->cachedMetadataHost === 0 ) {
209
+ $this->cachedMetadataHost = parse_url($this->metadataUrl, PHP_URL_HOST);
210
+ }
211
+
212
+ if ( is_string($this->cachedMetadataHost) && (strtolower($host) === strtolower($this->cachedMetadataHost)) ) {
213
+ return true;
214
+ }
215
+ return $allow;
216
+ }
217
+
218
+ /**
219
+ * Create a package instance that represents this plugin or theme.
220
+ *
221
+ * @return Puc_v4p10_InstalledPackage
222
+ */
223
+ abstract protected function createInstalledPackage();
224
+
225
+ /**
226
+ * @return Puc_v4p10_InstalledPackage
227
+ */
228
+ public function getInstalledPackage() {
229
+ return $this->package;
230
+ }
231
+
232
+ /**
233
+ * Create an instance of the scheduler.
234
+ *
235
+ * This is implemented as a method to make it possible for plugins to subclass the update checker
236
+ * and substitute their own scheduler.
237
+ *
238
+ * @param int $checkPeriod
239
+ * @return Puc_v4p10_Scheduler
240
+ */
241
+ abstract protected function createScheduler($checkPeriod);
242
+
243
+ /**
244
+ * Check for updates. The results are stored in the DB option specified in $optionName.
245
+ *
246
+ * @return Puc_v4p10_Update|null
247
+ */
248
+ public function checkForUpdates() {
249
+ $installedVersion = $this->getInstalledVersion();
250
+ //Fail silently if we can't find the plugin/theme or read its header.
251
+ if ( $installedVersion === null ) {
252
+ $this->triggerError(
253
+ sprintf('Skipping update check for %s - installed version unknown.', $this->slug),
254
+ E_USER_WARNING
255
+ );
256
+ return null;
257
+ }
258
+
259
+ //Start collecting API errors.
260
+ $this->lastRequestApiErrors = array();
261
+ add_action('puc_api_error', array($this, 'collectApiErrors'), 10, 4);
262
+
263
+ $state = $this->updateState;
264
+ $state->setLastCheckToNow()
265
+ ->setCheckedVersion($installedVersion)
266
+ ->save(); //Save before checking in case something goes wrong
267
+
268
+ $state->setUpdate($this->requestUpdate());
269
+ $state->save();
270
+
271
+ //Stop collecting API errors.
272
+ remove_action('puc_api_error', array($this, 'collectApiErrors'), 10);
273
+
274
+ return $this->getUpdate();
275
+ }
276
+
277
+ /**
278
+ * Load the update checker state from the DB.
279
+ *
280
+ * @return Puc_v4p10_StateStore
281
+ */
282
+ public function getUpdateState() {
283
+ return $this->updateState->lazyLoad();
284
+ }
285
+
286
+ /**
287
+ * Reset update checker state - i.e. last check time, cached update data and so on.
288
+ *
289
+ * Call this when your plugin is being uninstalled, or if you want to
290
+ * clear the update cache.
291
+ */
292
+ public function resetUpdateState() {
293
+ $this->updateState->delete();
294
+ }
295
+
296
+ /**
297
+ * Get the details of the currently available update, if any.
298
+ *
299
+ * If no updates are available, or if the last known update version is below or equal
300
+ * to the currently installed version, this method will return NULL.
301
+ *
302
+ * Uses cached update data. To retrieve update information straight from
303
+ * the metadata URL, call requestUpdate() instead.
304
+ *
305
+ * @return Puc_v4p10_Update|null
306
+ */
307
+ public function getUpdate() {
308
+ $update = $this->updateState->getUpdate();
309
+
310
+ //Is there an update available?
311
+ if ( isset($update) ) {
312
+ //Check if the update is actually newer than the currently installed version.
313
+ $installedVersion = $this->getInstalledVersion();
314
+ if ( ($installedVersion !== null) && version_compare($update->version, $installedVersion, '>') ){
315
+ return $update;
316
+ }
317
+ }
318
+ return null;
319
+ }
320
+
321
+ /**
322
+ * Retrieve the latest update (if any) from the configured API endpoint.
323
+ *
324
+ * Subclasses should run the update through filterUpdateResult before returning it.
325
+ *
326
+ * @return Puc_v4p10_Update An instance of Update, or NULL when no updates are available.
327
+ */
328
+ abstract public function requestUpdate();
329
+
330
+ /**
331
+ * Filter the result of a requestUpdate() call.
332
+ *
333
+ * @param Puc_v4p10_Update|null $update
334
+ * @param array|WP_Error|null $httpResult The value returned by wp_remote_get(), if any.
335
+ * @return Puc_v4p10_Update
336
+ */
337
+ protected function filterUpdateResult($update, $httpResult = null) {
338
+ //Let plugins/themes modify the update.
339
+ $update = apply_filters($this->getUniqueName('request_update_result'), $update, $httpResult);
340
+
341
+ $this->fixSupportedWordpressVersion($update);
342
+
343
+ if ( isset($update, $update->translations) ) {
344
+ //Keep only those translation updates that apply to this site.
345
+ $update->translations = $this->filterApplicableTranslations($update->translations);
346
+ }
347
+
348
+ return $update;
349
+ }
350
+
351
+ /**
352
+ * The "Tested up to" field in the plugin metadata is supposed to be in the form of "major.minor",
353
+ * while WordPress core's list_plugin_updates() expects the $update->tested field to be an exact
354
+ * version, e.g. "major.minor.patch", to say it's compatible. In other case it shows
355
+ * "Compatibility: Unknown".
356
+ * The function mimics how wordpress.org API crafts the "tested" field out of "Tested up to".
357
+ *
358
+ * @param Puc_v4p10_Metadata|null $update
359
+ */
360
+ protected function fixSupportedWordpressVersion(Puc_v4p10_Metadata $update = null) {
361
+ if ( !isset($update->tested) || !preg_match('/^\d++\.\d++$/', $update->tested) ) {
362
+ return;
363
+ }
364
+
365
+ $actualWpVersions = array();
366
+
367
+ $wpVersion = $GLOBALS['wp_version'];
368
+
369
+ if ( function_exists('get_core_updates') ) {
370
+ $coreUpdates = get_core_updates();
371
+ if ( is_array($coreUpdates) ) {
372
+ foreach ($coreUpdates as $coreUpdate) {
373
+ if ( isset($coreUpdate->current) ) {
374
+ $actualWpVersions[] = $coreUpdate->current;
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ $actualWpVersions[] = $wpVersion;
381
+
382
+ $actualWpPatchNumber = null;
383
+ foreach ($actualWpVersions as $version) {
384
+ if ( preg_match('/^(?P<majorMinor>\d++\.\d++)(?:\.(?P<patch>\d++))?/', $version, $versionParts) ) {
385
+ if ( $versionParts['majorMinor'] === $update->tested ) {
386
+ $patch = isset($versionParts['patch']) ? intval($versionParts['patch']) : 0;
387
+ if ( $actualWpPatchNumber === null ) {
388
+ $actualWpPatchNumber = $patch;
389
+ } else {
390
+ $actualWpPatchNumber = max($actualWpPatchNumber, $patch);
391
+ }
392
+ }
393
+ }
394
+ }
395
+ if ( $actualWpPatchNumber === null ) {
396
+ $actualWpPatchNumber = 999;
397
+ }
398
+
399
+ if ( $actualWpPatchNumber > 0 ) {
400
+ $update->tested .= '.' . $actualWpPatchNumber;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Get the currently installed version of the plugin or theme.
406
+ *
407
+ * @return string|null Version number.
408
+ */
409
+ public function getInstalledVersion() {
410
+ return $this->package->getInstalledVersion();
411
+ }
412
+
413
+ /**
414
+ * Get the full path of the plugin or theme directory.
415
+ *
416
+ * @return string
417
+ */
418
+ public function getAbsoluteDirectoryPath() {
419
+ return $this->package->getAbsoluteDirectoryPath();
420
+ }
421
+
422
+ /**
423
+ * Trigger a PHP error, but only when $debugMode is enabled.
424
+ *
425
+ * @param string $message
426
+ * @param int $errorType
427
+ */
428
+ public function triggerError($message, $errorType) {
429
+ if ( $this->isDebugModeEnabled() ) {
430
+ trigger_error($message, $errorType);
431
+ }
432
+ }
433
+
434
+ /**
435
+ * @return bool
436
+ */
437
+ protected function isDebugModeEnabled() {
438
+ if ( $this->debugMode === null ) {
439
+ $this->debugMode = (bool)(constant('WP_DEBUG'));
440
+ }
441
+ return $this->debugMode;
442
+ }
443
+
444
+ /**
445
+ * Get the full name of an update checker filter, action or DB entry.
446
+ *
447
+ * This method adds the "puc_" prefix and the "-$slug" suffix to the filter name.
448
+ * For example, "pre_inject_update" becomes "puc_pre_inject_update-plugin-slug".
449
+ *
450
+ * @param string $baseTag
451
+ * @return string
452
+ */
453
+ public function getUniqueName($baseTag) {
454
+ $name = 'puc_' . $baseTag;
455
+ if ( $this->filterSuffix !== '' ) {
456
+ $name .= '_' . $this->filterSuffix;
457
+ }
458
+ return $name . '-' . $this->slug;
459
+ }
460
+
461
+ /**
462
+ * Store API errors that are generated when checking for updates.
463
+ *
464
+ * @internal
465
+ * @param WP_Error $error
466
+ * @param array|null $httpResponse
467
+ * @param string|null $url
468
+ * @param string|null $slug
469
+ */
470
+ public function collectApiErrors($error, $httpResponse = null, $url = null, $slug = null) {
471
+ if ( isset($slug) && ($slug !== $this->slug) ) {
472
+ return;
473
+ }
474
+
475
+ $this->lastRequestApiErrors[] = array(
476
+ 'error' => $error,
477
+ 'httpResponse' => $httpResponse,
478
+ 'url' => $url,
479
+ );
480
+ }
481
+
482
+ /**
483
+ * @return array
484
+ */
485
+ public function getLastRequestApiErrors() {
486
+ return $this->lastRequestApiErrors;
487
+ }
488
+
489
+ /* -------------------------------------------------------------------
490
+ * PUC filters and filter utilities
491
+ * -------------------------------------------------------------------
492
+ */
493
+
494
+ /**
495
+ * Register a callback for one of the update checker filters.
496
+ *
497
+ * Identical to add_filter(), except it automatically adds the "puc_" prefix
498
+ * and the "-$slug" suffix to the filter name. For example, "request_info_result"
499
+ * becomes "puc_request_info_result-your_plugin_slug".
500
+ *
501
+ * @param string $tag
502
+ * @param callable $callback
503
+ * @param int $priority
504
+ * @param int $acceptedArgs
505
+ */
506
+ public function addFilter($tag, $callback, $priority = 10, $acceptedArgs = 1) {
507
+ add_filter($this->getUniqueName($tag), $callback, $priority, $acceptedArgs);
508
+ }
509
+
510
+ /* -------------------------------------------------------------------
511
+ * Inject updates
512
+ * -------------------------------------------------------------------
513
+ */
514
+
515
+ /**
516
+ * Insert the latest update (if any) into the update list maintained by WP.
517
+ *
518
+ * @param stdClass $updates Update list.
519
+ * @return stdClass Modified update list.
520
+ */
521
+ public function injectUpdate($updates) {
522
+ //Is there an update to insert?
523
+ $update = $this->getUpdate();
524
+
525
+ if ( !$this->shouldShowUpdates() ) {
526
+ $update = null;
527
+ }
528
+
529
+ if ( !empty($update) ) {
530
+ //Let plugins filter the update info before it's passed on to WordPress.
531
+ $update = apply_filters($this->getUniqueName('pre_inject_update'), $update);
532
+ $updates = $this->addUpdateToList($updates, $update->toWpFormat());
533
+ } else {
534
+ //Clean up any stale update info.
535
+ $updates = $this->removeUpdateFromList($updates);
536
+ //Add a placeholder item to the "no_update" list to enable auto-update support.
537
+ //If we don't do this, the option to enable automatic updates will only show up
538
+ //when an update is available.
539
+ $updates = $this->addNoUpdateItem($updates);
540
+ }
541
+
542
+ return $updates;
543
+ }
544
+
545
+ /**
546
+ * @param stdClass|null $updates
547
+ * @param stdClass|array $updateToAdd
548
+ * @return stdClass
549
+ */
550
+ protected function addUpdateToList($updates, $updateToAdd) {
551
+ if ( !is_object($updates) ) {
552
+ $updates = new stdClass();
553
+ $updates->response = array();
554
+ }
555
+
556
+ $updates->response[$this->getUpdateListKey()] = $updateToAdd;
557
+ return $updates;
558
+ }
559
+
560
+ /**
561
+ * @param stdClass|null $updates
562
+ * @return stdClass|null
563
+ */
564
+ protected function removeUpdateFromList($updates) {
565
+ if ( isset($updates, $updates->response) ) {
566
+ unset($updates->response[$this->getUpdateListKey()]);
567
+ }
568
+ return $updates;
569
+ }
570
+
571
+ /**
572
+ * See this post for more information:
573
+ * @link https://make.wordpress.org/core/2020/07/30/recommended-usage-of-the-updates-api-to-support-the-auto-updates-ui-for-plugins-and-themes-in-wordpress-5-5/
574
+ *
575
+ * @param stdClass|null $updates
576
+ * @return stdClass
577
+ */
578
+ protected function addNoUpdateItem($updates) {
579
+ if ( !is_object($updates) ) {
580
+ $updates = new stdClass();
581
+ $updates->response = array();
582
+ $updates->no_update = array();
583
+ } else if ( !isset($updates->no_update) ) {
584
+ $updates->no_update = array();
585
+ }
586
+
587
+ $updates->no_update[$this->getUpdateListKey()] = (object) $this->getNoUpdateItemFields();
588
+
589
+ return $updates;
590
+ }
591
+
592
+ /**
593
+ * Subclasses should override this method to add fields that are specific to plugins or themes.
594
+ * @return array
595
+ */
596
+ protected function getNoUpdateItemFields() {
597
+ return array(
598
+ 'new_version' => $this->getInstalledVersion(),
599
+ 'url' => '',
600
+ 'package' => '',
601
+ 'requires_php' => '',
602
+ );
603
+ }
604
+
605
+ /**
606
+ * Get the key that will be used when adding updates to the update list that's maintained
607
+ * by the WordPress core. The list is always an associative array, but the key is different
608
+ * for plugins and themes.
609
+ *
610
+ * @return string
611
+ */
612
+ abstract protected function getUpdateListKey();
613
+
614
+ /**
615
+ * Should we show available updates?
616
+ *
617
+ * Usually the answer is "yes", but there are exceptions. For example, WordPress doesn't
618
+ * support automatic updates installation for mu-plugins, so PUC usually won't show update
619
+ * notifications in that case. See the plugin-specific subclass for details.
620
+ *
621
+ * Note: This method only applies to updates that are displayed (or not) in the WordPress
622
+ * admin. It doesn't affect APIs like requestUpdate and getUpdate.
623
+ *
624
+ * @return bool
625
+ */
626
+ protected function shouldShowUpdates() {
627
+ return true;
628
+ }
629
+
630
+ /* -------------------------------------------------------------------
631
+ * JSON-based update API
632
+ * -------------------------------------------------------------------
633
+ */
634
+
635
+ /**
636
+ * Retrieve plugin or theme metadata from the JSON document at $this->metadataUrl.
637
+ *
638
+ * @param string $metaClass Parse the JSON as an instance of this class. It must have a static fromJson method.
639
+ * @param string $filterRoot
640
+ * @param array $queryArgs Additional query arguments.
641
+ * @return array [Puc_v4p10_Metadata|null, array|WP_Error] A metadata instance and the value returned by wp_remote_get().
642
+ */
643
+ protected function requestMetadata($metaClass, $filterRoot, $queryArgs = array()) {
644
+ //Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).
645
+ $queryArgs = array_merge(
646
+ array(
647
+ 'installed_version' => strval($this->getInstalledVersion()),
648
+ 'php' => phpversion(),
649
+ 'locale' => get_locale(),
650
+ ),
651
+ $queryArgs
652
+ );
653
+ $queryArgs = apply_filters($this->getUniqueName($filterRoot . '_query_args'), $queryArgs);
654
+
655
+ //Various options for the wp_remote_get() call. Plugins can filter these, too.
656
+ $options = array(
657
+ 'timeout' => 10, //seconds
658
+ 'headers' => array(
659
+ 'Accept' => 'application/json',
660
+ ),
661
+ );
662
+ $options = apply_filters($this->getUniqueName($filterRoot . '_options'), $options);
663
+
664
+ //The metadata file should be at 'http://your-api.com/url/here/$slug/info.json'
665
+ $url = $this->metadataUrl;
666
+ if ( !empty($queryArgs) ){
667
+ $url = add_query_arg($queryArgs, $url);
668
+ }
669
+
670
+ $result = wp_remote_get($url, $options);
671
+
672
+ $result = apply_filters($this->getUniqueName('request_metadata_http_result'), $result, $url, $options);
673
+
674
+ //Try to parse the response
675
+ $status = $this->validateApiResponse($result);
676
+ $metadata = null;
677
+ if ( !is_wp_error($status) ){
678
+ $metadata = call_user_func(array($metaClass, 'fromJson'), $result['body']);
679
+ } else {
680
+ do_action('puc_api_error', $status, $result, $url, $this->slug);
681
+ $this->triggerError(
682
+ sprintf('The URL %s does not point to a valid metadata file. ', $url)
683
+ . $status->get_error_message(),
684
+ E_USER_WARNING
685
+ );
686
+ }
687
+
688
+ return array($metadata, $result);
689
+ }
690
+
691
+ /**
692
+ * Check if $result is a successful update API response.
693
+ *
694
+ * @param array|WP_Error $result
695
+ * @return true|WP_Error
696
+ */
697
+ protected function validateApiResponse($result) {
698
+ if ( is_wp_error($result) ) { /** @var WP_Error $result */
699
+ return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());
700
+ }
701
+
702
+ if ( !isset($result['response']['code']) ) {
703
+ return new WP_Error(
704
+ 'puc_no_response_code',
705
+ 'wp_remote_get() returned an unexpected result.'
706
+ );
707
+ }
708
+
709
+ if ( $result['response']['code'] !== 200 ) {
710
+ return new WP_Error(
711
+ 'puc_unexpected_response_code',
712
+ 'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'
713
+ );
714
+ }
715
+
716
+ if ( empty($result['body']) ) {
717
+ return new WP_Error('puc_empty_response', 'The metadata file appears to be empty.');
718
+ }
719
+
720
+ return true;
721
+ }
722
+
723
+ /* -------------------------------------------------------------------
724
+ * Language packs / Translation updates
725
+ * -------------------------------------------------------------------
726
+ */
727
+
728
+ /**
729
+ * Filter a list of translation updates and return a new list that contains only updates
730
+ * that apply to the current site.
731
+ *
732
+ * @param array $translations
733
+ * @return array
734
+ */
735
+ protected function filterApplicableTranslations($translations) {
736
+ $languages = array_flip(array_values(get_available_languages()));
737
+ $installedTranslations = $this->getInstalledTranslations();
738
+
739
+ $applicableTranslations = array();
740
+ foreach ($translations as $translation) {
741
+ //Does it match one of the available core languages?
742
+ $isApplicable = array_key_exists($translation->language, $languages);
743
+ //Is it more recent than an already-installed translation?
744
+ if ( isset($installedTranslations[$translation->language]) ) {
745
+ $updateTimestamp = strtotime($translation->updated);
746
+ $installedTimestamp = strtotime($installedTranslations[$translation->language]['PO-Revision-Date']);
747
+ $isApplicable = $updateTimestamp > $installedTimestamp;
748
+ }
749
+
750
+ if ( $isApplicable ) {
751
+ $applicableTranslations[] = $translation;
752
+ }
753
+ }
754
+
755
+ return $applicableTranslations;
756
+ }
757
+
758
+ /**
759
+ * Get a list of installed translations for this plugin or theme.
760
+ *
761
+ * @return array
762
+ */
763
+ protected function getInstalledTranslations() {
764
+ if ( !function_exists('wp_get_installed_translations') ) {
765
+ return array();
766
+ }
767
+ $installedTranslations = wp_get_installed_translations($this->translationType . 's');
768
+ if ( isset($installedTranslations[$this->directoryName]) ) {
769
+ $installedTranslations = $installedTranslations[$this->directoryName];
770
+ } else {
771
+ $installedTranslations = array();
772
+ }
773
+ return $installedTranslations;
774
+ }
775
+
776
+ /**
777
+ * Insert translation updates into the list maintained by WordPress.
778
+ *
779
+ * @param stdClass $updates
780
+ * @return stdClass
781
+ */
782
+ public function injectTranslationUpdates($updates) {
783
+ $translationUpdates = $this->getTranslationUpdates();
784
+ if ( empty($translationUpdates) ) {
785
+ return $updates;
786
+ }
787
+
788
+ //Being defensive.
789
+ if ( !is_object($updates) ) {
790
+ $updates = new stdClass();
791
+ }
792
+ if ( !isset($updates->translations) ) {
793
+ $updates->translations = array();
794
+ }
795
+
796
+ //In case there's a name collision with a plugin or theme hosted on wordpress.org,
797
+ //remove any preexisting updates that match our thing.
798
+ $updates->translations = array_values(array_filter(
799
+ $updates->translations,
800
+ array($this, 'isNotMyTranslation')
801
+ ));
802
+
803
+ //Add our updates to the list.
804
+ foreach($translationUpdates as $update) {
805
+ $convertedUpdate = array_merge(
806
+ array(
807
+ 'type' => $this->translationType,
808
+ 'slug' => $this->directoryName,
809
+ 'autoupdate' => 0,
810
+ //AFAICT, WordPress doesn't actually use the "version" field for anything.
811
+ //But lets make sure it's there, just in case.
812
+ 'version' => isset($update->version) ? $update->version : ('1.' . strtotime($update->updated)),
813
+ ),
814
+ (array)$update
815
+ );
816
+
817
+ $updates->translations[] = $convertedUpdate;
818
+ }
819
+
820
+ return $updates;
821
+ }
822
+
823
+ /**
824
+ * Get a list of available translation updates.
825
+ *
826
+ * This method will return an empty array if there are no updates.
827
+ * Uses cached update data.
828
+ *
829
+ * @return array
830
+ */
831
+ public function getTranslationUpdates() {
832
+ return $this->updateState->getTranslations();
833
+ }
834
+
835
+ /**
836
+ * Remove all cached translation updates.
837
+ *
838
+ * @see wp_clean_update_cache
839
+ */
840
+ public function clearCachedTranslationUpdates() {
841
+ $this->updateState->setTranslations(array());
842
+ }
843
+
844
+ /**
845
+ * Filter callback. Keeps only translations that *don't* match this plugin or theme.
846
+ *
847
+ * @param array $translation
848
+ * @return bool
849
+ */
850
+ protected function isNotMyTranslation($translation) {
851
+ $isMatch = isset($translation['type'], $translation['slug'])
852
+ && ($translation['type'] === $this->translationType)
853
+ && ($translation['slug'] === $this->directoryName);
854
+
855
+ return !$isMatch;
856
+ }
857
+
858
+ /* -------------------------------------------------------------------
859
+ * Fix directory name when installing updates
860
+ * -------------------------------------------------------------------
861
+ */
862
+
863
+ /**
864
+ * Rename the update directory to match the existing plugin/theme directory.
865
+ *
866
+ * When WordPress installs a plugin or theme update, it assumes that the ZIP file will contain
867
+ * exactly one directory, and that the directory name will be the same as the directory where
868
+ * the plugin or theme is currently installed.
869
+ *
870
+ * GitHub and other repositories provide ZIP downloads, but they often use directory names like
871
+ * "project-branch" or "project-tag-hash". We need to change the name to the actual plugin folder.
872
+ *
873
+ * This is a hook callback. Don't call it from a plugin.
874
+ *
875
+ * @access protected
876
+ *
877
+ * @param string $source The directory to copy to /wp-content/plugins or /wp-content/themes. Usually a subdirectory of $remoteSource.
878
+ * @param string $remoteSource WordPress has extracted the update to this directory.
879
+ * @param WP_Upgrader $upgrader
880
+ * @return string|WP_Error
881
+ */
882
+ public function fixDirectoryName($source, $remoteSource, $upgrader) {
883
+ global $wp_filesystem;
884
+ /** @var WP_Filesystem_Base $wp_filesystem */
885
+
886
+ //Basic sanity checks.
887
+ if ( !isset($source, $remoteSource, $upgrader, $upgrader->skin, $wp_filesystem) ) {
888
+ return $source;
889
+ }
890
+
891
+ //If WordPress is upgrading anything other than our plugin/theme, leave the directory name unchanged.
892
+ if ( !$this->isBeingUpgraded($upgrader) ) {
893
+ return $source;
894
+ }
895
+
896
+ //Rename the source to match the existing directory.
897
+ $correctedSource = trailingslashit($remoteSource) . $this->directoryName . '/';
898
+ if ( $source !== $correctedSource ) {
899
+ //The update archive should contain a single directory that contains the rest of plugin/theme files.
900
+ //Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
901
+ //We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
902
+ //after update.
903
+ if ( $this->isBadDirectoryStructure($remoteSource) ) {
904
+ return new WP_Error(
905
+ 'puc-incorrect-directory-structure',
906
+ sprintf(
907
+ 'The directory structure of the update is incorrect. All files should be inside ' .
908
+ 'a directory named <span class="code">%s</span>, not at the root of the ZIP archive.',
909
+ htmlentities($this->slug)
910
+ )
911
+ );
912
+ }
913
+
914
+ /** @var WP_Upgrader_Skin $upgrader ->skin */
915
+ $upgrader->skin->feedback(sprintf(
916
+ 'Renaming %s to %s&#8230;',
917
+ '<span class="code">' . basename($source) . '</span>',
918
+ '<span class="code">' . $this->directoryName . '</span>'
919
+ ));
920
+
921
+ if ( $wp_filesystem->move($source, $correctedSource, true) ) {
922
+ $upgrader->skin->feedback('Directory successfully renamed.');
923
+ return $correctedSource;
924
+ } else {
925
+ return new WP_Error(
926
+ 'puc-rename-failed',
927
+ 'Unable to rename the update to match the existing directory.'
928
+ );
929
+ }
930
+ }
931
+
932
+ return $source;
933
+ }
934
+
935
+ /**
936
+ * Is there an update being installed right now, for this plugin or theme?
937
+ *
938
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
939
+ * @return bool
940
+ */
941
+ abstract public function isBeingUpgraded($upgrader = null);
942
+
943
+ /**
944
+ * Check for incorrect update directory structure. An update must contain a single directory,
945
+ * all other files should be inside that directory.
946
+ *
947
+ * @param string $remoteSource Directory path.
948
+ * @return bool
949
+ */
950
+ protected function isBadDirectoryStructure($remoteSource) {
951
+ global $wp_filesystem;
952
+ /** @var WP_Filesystem_Base $wp_filesystem */
953
+
954
+ $sourceFiles = $wp_filesystem->dirlist($remoteSource);
955
+ if ( is_array($sourceFiles) ) {
956
+ $sourceFiles = array_keys($sourceFiles);
957
+ $firstFilePath = trailingslashit($remoteSource) . $sourceFiles[0];
958
+ return (count($sourceFiles) > 1) || (!$wp_filesystem->is_dir($firstFilePath));
959
+ }
960
+
961
+ //Assume it's fine.
962
+ return false;
963
+ }
964
+
965
+ /* -------------------------------------------------------------------
966
+ * DebugBar integration
967
+ * -------------------------------------------------------------------
968
+ */
969
+
970
+ /**
971
+ * Initialize the update checker Debug Bar plugin/add-on thingy.
972
+ */
973
+ public function maybeInitDebugBar() {
974
+ if ( class_exists('Debug_Bar', false) && file_exists(dirname(__FILE__) . '/DebugBar') ) {
975
+ $this->debugBarExtension = $this->createDebugBarExtension();
976
+ }
977
+ }
978
+
979
+ protected function createDebugBarExtension() {
980
+ return new Puc_v4p10_DebugBar_Extension($this);
981
+ }
982
+
983
+ /**
984
+ * Display additional configuration details in the Debug Bar panel.
985
+ *
986
+ * @param Puc_v4p10_DebugBar_Panel $panel
987
+ */
988
+ public function onDisplayConfiguration($panel) {
989
+ //Do nothing. Subclasses can use this to add additional info to the panel.
990
+ }
991
+
992
+ }
993
+
994
+ endif;
updater/Puc/v4p10/UpgraderStatus.php ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_UpgraderStatus', false) ):
3
+
4
+ /**
5
+ * A utility class that helps figure out which plugin or theme WordPress is upgrading.
6
+ *
7
+ * It may seem strange to have a separate class just for that, but the task is surprisingly complicated.
8
+ * Core classes like Plugin_Upgrader don't expose the plugin file name during an in-progress update (AFAICT).
9
+ * This class uses a few workarounds and heuristics to get the file name.
10
+ */
11
+ class Puc_v4p10_UpgraderStatus {
12
+ private $currentType = null; //"plugin" or "theme".
13
+ private $currentId = null; //Plugin basename or theme directory name.
14
+
15
+ public function __construct() {
16
+ //Keep track of which plugin/theme WordPress is currently upgrading.
17
+ add_filter('upgrader_pre_install', array($this, 'setUpgradedThing'), 10, 2);
18
+ add_filter('upgrader_package_options', array($this, 'setUpgradedPluginFromOptions'), 10, 1);
19
+ add_filter('upgrader_post_install', array($this, 'clearUpgradedThing'), 10, 1);
20
+ add_action('upgrader_process_complete', array($this, 'clearUpgradedThing'), 10, 1);
21
+ }
22
+
23
+ /**
24
+ * Is there and update being installed RIGHT NOW, for a specific plugin?
25
+ *
26
+ * Caution: This method is unreliable. WordPress doesn't make it easy to figure out what it is upgrading,
27
+ * and upgrader implementations are liable to change without notice.
28
+ *
29
+ * @param string $pluginFile The plugin to check.
30
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
31
+ * @return bool True if the plugin identified by $pluginFile is being upgraded.
32
+ */
33
+ public function isPluginBeingUpgraded($pluginFile, $upgrader = null) {
34
+ return $this->isBeingUpgraded('plugin', $pluginFile, $upgrader);
35
+ }
36
+
37
+ /**
38
+ * Is there an update being installed for a specific theme?
39
+ *
40
+ * @param string $stylesheet Theme directory name.
41
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
42
+ * @return bool
43
+ */
44
+ public function isThemeBeingUpgraded($stylesheet, $upgrader = null) {
45
+ return $this->isBeingUpgraded('theme', $stylesheet, $upgrader);
46
+ }
47
+
48
+ /**
49
+ * Check if a specific theme or plugin is being upgraded.
50
+ *
51
+ * @param string $type
52
+ * @param string $id
53
+ * @param Plugin_Upgrader|WP_Upgrader|null $upgrader
54
+ * @return bool
55
+ */
56
+ protected function isBeingUpgraded($type, $id, $upgrader = null) {
57
+ if ( isset($upgrader) ) {
58
+ list($currentType, $currentId) = $this->getThingBeingUpgradedBy($upgrader);
59
+ if ( $currentType !== null ) {
60
+ $this->currentType = $currentType;
61
+ $this->currentId = $currentId;
62
+ }
63
+ }
64
+ return ($this->currentType === $type) && ($this->currentId === $id);
65
+ }
66
+
67
+ /**
68
+ * Figure out which theme or plugin is being upgraded by a WP_Upgrader instance.
69
+ *
70
+ * Returns an array with two items. The first item is the type of the thing that's being
71
+ * upgraded: "plugin" or "theme". The second item is either the plugin basename or
72
+ * the theme directory name. If we can't determine what the upgrader is doing, both items
73
+ * will be NULL.
74
+ *
75
+ * Examples:
76
+ * ['plugin', 'plugin-dir-name/plugin.php']
77
+ * ['theme', 'theme-dir-name']
78
+ *
79
+ * @param Plugin_Upgrader|WP_Upgrader $upgrader
80
+ * @return array
81
+ */
82
+ private function getThingBeingUpgradedBy($upgrader) {
83
+ if ( !isset($upgrader, $upgrader->skin) ) {
84
+ return array(null, null);
85
+ }
86
+
87
+ //Figure out which plugin or theme is being upgraded.
88
+ $pluginFile = null;
89
+ $themeDirectoryName = null;
90
+
91
+ $skin = $upgrader->skin;
92
+ if ( isset($skin->theme_info) && ($skin->theme_info instanceof WP_Theme) ) {
93
+ $themeDirectoryName = $skin->theme_info->get_stylesheet();
94
+ } elseif ( $skin instanceof Plugin_Upgrader_Skin ) {
95
+ if ( isset($skin->plugin) && is_string($skin->plugin) && ($skin->plugin !== '') ) {
96
+ $pluginFile = $skin->plugin;
97
+ }
98
+ } elseif ( $skin instanceof Theme_Upgrader_Skin ) {
99
+ if ( isset($skin->theme) && is_string($skin->theme) && ($skin->theme !== '') ) {
100
+ $themeDirectoryName = $skin->theme;
101
+ }
102
+ } elseif ( isset($skin->plugin_info) && is_array($skin->plugin_info) ) {
103
+ //This case is tricky because Bulk_Plugin_Upgrader_Skin (etc) doesn't actually store the plugin
104
+ //filename anywhere. Instead, it has the plugin headers in $plugin_info. So the best we can
105
+ //do is compare those headers to the headers of installed plugins.
106
+ $pluginFile = $this->identifyPluginByHeaders($skin->plugin_info);
107
+ }
108
+
109
+ if ( $pluginFile !== null ) {
110
+ return array('plugin', $pluginFile);
111
+ } elseif ( $themeDirectoryName !== null ) {
112
+ return array('theme', $themeDirectoryName);
113
+ }
114
+ return array(null, null);
115
+ }
116
+
117
+ /**
118
+ * Identify an installed plugin based on its headers.
119
+ *
120
+ * @param array $searchHeaders The plugin file header to look for.
121
+ * @return string|null Plugin basename ("foo/bar.php"), or NULL if we can't identify the plugin.
122
+ */
123
+ private function identifyPluginByHeaders($searchHeaders) {
124
+ if ( !function_exists('get_plugins') ){
125
+ /** @noinspection PhpIncludeInspection */
126
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
127
+ }
128
+
129
+ $installedPlugins = get_plugins();
130
+ $matches = array();
131
+ foreach($installedPlugins as $pluginBasename => $headers) {
132
+ $diff1 = array_diff_assoc($headers, $searchHeaders);
133
+ $diff2 = array_diff_assoc($searchHeaders, $headers);
134
+ if ( empty($diff1) && empty($diff2) ) {
135
+ $matches[] = $pluginBasename;
136
+ }
137
+ }
138
+
139
+ //It's possible (though very unlikely) that there could be two plugins with identical
140
+ //headers. In that case, we can't unambiguously identify the plugin that's being upgraded.
141
+ if ( count($matches) !== 1 ) {
142
+ return null;
143
+ }
144
+
145
+ return reset($matches);
146
+ }
147
+
148
+ /**
149
+ * @access private
150
+ *
151
+ * @param mixed $input
152
+ * @param array $hookExtra
153
+ * @return mixed Returns $input unaltered.
154
+ */
155
+ public function setUpgradedThing($input, $hookExtra) {
156
+ if ( !empty($hookExtra['plugin']) && is_string($hookExtra['plugin']) ) {
157
+ $this->currentId = $hookExtra['plugin'];
158
+ $this->currentType = 'plugin';
159
+ } elseif ( !empty($hookExtra['theme']) && is_string($hookExtra['theme']) ) {
160
+ $this->currentId = $hookExtra['theme'];
161
+ $this->currentType = 'theme';
162
+ } else {
163
+ $this->currentType = null;
164
+ $this->currentId = null;
165
+ }
166
+ return $input;
167
+ }
168
+
169
+ /**
170
+ * @access private
171
+ *
172
+ * @param array $options
173
+ * @return array
174
+ */
175
+ public function setUpgradedPluginFromOptions($options) {
176
+ if ( isset($options['hook_extra']['plugin']) && is_string($options['hook_extra']['plugin']) ) {
177
+ $this->currentType = 'plugin';
178
+ $this->currentId = $options['hook_extra']['plugin'];
179
+ } else {
180
+ $this->currentType = null;
181
+ $this->currentId = null;
182
+ }
183
+ return $options;
184
+ }
185
+
186
+ /**
187
+ * @access private
188
+ *
189
+ * @param mixed $input
190
+ * @return mixed Returns $input unaltered.
191
+ */
192
+ public function clearUpgradedThing($input = null) {
193
+ $this->currentId = null;
194
+ $this->currentType = null;
195
+ return $input;
196
+ }
197
+ }
198
+
199
+ endif;
updater/Puc/v4p10/Utils.php ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_Utils', false) ):
4
+
5
+ class Puc_v4p10_Utils {
6
+ /**
7
+ * Get a value from a nested array or object based on a path.
8
+ *
9
+ * @param array|object|null $collection Get an entry from this array.
10
+ * @param array|string $path A list of array keys in hierarchy order, or a string path like "foo.bar.baz".
11
+ * @param mixed $default The value to return if the specified path is not found.
12
+ * @param string $separator Path element separator. Only applies to string paths.
13
+ * @return mixed
14
+ */
15
+ public static function get($collection, $path, $default = null, $separator = '.') {
16
+ if ( is_string($path) ) {
17
+ $path = explode($separator, $path);
18
+ }
19
+
20
+ //Follow the $path into $input as far as possible.
21
+ $currentValue = $collection;
22
+ foreach ($path as $node) {
23
+ if ( is_array($currentValue) && isset($currentValue[$node]) ) {
24
+ $currentValue = $currentValue[$node];
25
+ } else if ( is_object($currentValue) && isset($currentValue->$node) ) {
26
+ $currentValue = $currentValue->$node;
27
+ } else {
28
+ return $default;
29
+ }
30
+ }
31
+
32
+ return $currentValue;
33
+ }
34
+
35
+ /**
36
+ * Get the first array element that is not empty.
37
+ *
38
+ * @param array $values
39
+ * @param mixed|null $default Returns this value if there are no non-empty elements.
40
+ * @return mixed|null
41
+ */
42
+ public static function findNotEmpty($values, $default = null) {
43
+ if ( empty($values) ) {
44
+ return $default;
45
+ }
46
+
47
+ foreach ($values as $value) {
48
+ if ( !empty($value) ) {
49
+ return $value;
50
+ }
51
+ }
52
+
53
+ return $default;
54
+ }
55
+
56
+ /**
57
+ * Check if the input string starts with the specified prefix.
58
+ *
59
+ * @param string $input
60
+ * @param string $prefix
61
+ * @return bool
62
+ */
63
+ public static function startsWith($input, $prefix) {
64
+ $length = strlen($prefix);
65
+ return (substr($input, 0, $length) === $prefix);
66
+ }
67
+ }
68
+
69
+ endif;
updater/Puc/v4p10/Vcs/Api.php ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Vcs_Api') ):
3
+
4
+ abstract class Puc_v4p10_Vcs_Api {
5
+ protected $tagNameProperty = 'name';
6
+ protected $slug = '';
7
+
8
+ /**
9
+ * @var string
10
+ */
11
+ protected $repositoryUrl = '';
12
+
13
+ /**
14
+ * @var mixed Authentication details for private repositories. Format depends on service.
15
+ */
16
+ protected $credentials = null;
17
+
18
+ /**
19
+ * @var string The filter tag that's used to filter options passed to wp_remote_get.
20
+ * For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug".
21
+ */
22
+ protected $httpFilterName = '';
23
+
24
+ /**
25
+ * @var string|null
26
+ */
27
+ protected $localDirectory = null;
28
+
29
+ /**
30
+ * Puc_v4p10_Vcs_Api constructor.
31
+ *
32
+ * @param string $repositoryUrl
33
+ * @param array|string|null $credentials
34
+ */
35
+ public function __construct($repositoryUrl, $credentials = null) {
36
+ $this->repositoryUrl = $repositoryUrl;
37
+ $this->setAuthentication($credentials);
38
+ }
39
+
40
+ /**
41
+ * @return string
42
+ */
43
+ public function getRepositoryUrl() {
44
+ return $this->repositoryUrl;
45
+ }
46
+
47
+ /**
48
+ * Figure out which reference (i.e tag or branch) contains the latest version.
49
+ *
50
+ * @param string $configBranch Start looking in this branch.
51
+ * @return null|Puc_v4p10_Vcs_Reference
52
+ */
53
+ abstract public function chooseReference($configBranch);
54
+
55
+ /**
56
+ * Get the readme.txt file from the remote repository and parse it
57
+ * according to the plugin readme standard.
58
+ *
59
+ * @param string $ref Tag or branch name.
60
+ * @return array Parsed readme.
61
+ */
62
+ public function getRemoteReadme($ref = 'master') {
63
+ $fileContents = $this->getRemoteFile($this->getLocalReadmeName(), $ref);
64
+ if ( empty($fileContents) ) {
65
+ return array();
66
+ }
67
+
68
+ $parser = new PucReadmeParser();
69
+ return $parser->parse_readme_contents($fileContents);
70
+ }
71
+
72
+ /**
73
+ * Get the case-sensitive name of the local readme.txt file.
74
+ *
75
+ * In most cases it should just be called "readme.txt", but some plugins call it "README.txt",
76
+ * "README.TXT", or even "Readme.txt". Most VCS are case-sensitive so we need to know the correct
77
+ * capitalization.
78
+ *
79
+ * Defaults to "readme.txt" (all lowercase).
80
+ *
81
+ * @return string
82
+ */
83
+ public function getLocalReadmeName() {
84
+ static $fileName = null;
85
+ if ( $fileName !== null ) {
86
+ return $fileName;
87
+ }
88
+
89
+ $fileName = 'readme.txt';
90
+ if ( isset($this->localDirectory) ) {
91
+ $files = scandir($this->localDirectory);
92
+ if ( !empty($files) ) {
93
+ foreach ($files as $possibleFileName) {
94
+ if ( strcasecmp($possibleFileName, 'readme.txt') === 0 ) {
95
+ $fileName = $possibleFileName;
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return $fileName;
102
+ }
103
+
104
+ /**
105
+ * Get a branch.
106
+ *
107
+ * @param string $branchName
108
+ * @return Puc_v4p10_Vcs_Reference|null
109
+ */
110
+ abstract public function getBranch($branchName);
111
+
112
+ /**
113
+ * Get a specific tag.
114
+ *
115
+ * @param string $tagName
116
+ * @return Puc_v4p10_Vcs_Reference|null
117
+ */
118
+ abstract public function getTag($tagName);
119
+
120
+ /**
121
+ * Get the tag that looks like the highest version number.
122
+ * (Implementations should skip pre-release versions if possible.)
123
+ *
124
+ * @return Puc_v4p10_Vcs_Reference|null
125
+ */
126
+ abstract public function getLatestTag();
127
+
128
+ /**
129
+ * Check if a tag name string looks like a version number.
130
+ *
131
+ * @param string $name
132
+ * @return bool
133
+ */
134
+ protected function looksLikeVersion($name) {
135
+ //Tag names may be prefixed with "v", e.g. "v1.2.3".
136
+ $name = ltrim($name, 'v');
137
+
138
+ //The version string must start with a number.
139
+ if ( !is_numeric(substr($name, 0, 1)) ) {
140
+ return false;
141
+ }
142
+
143
+ //The goal is to accept any SemVer-compatible or "PHP-standardized" version number.
144
+ return (preg_match('@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name) === 1);
145
+ }
146
+
147
+ /**
148
+ * Check if a tag appears to be named like a version number.
149
+ *
150
+ * @param stdClass $tag
151
+ * @return bool
152
+ */
153
+ protected function isVersionTag($tag) {
154
+ $property = $this->tagNameProperty;
155
+ return isset($tag->$property) && $this->looksLikeVersion($tag->$property);
156
+ }
157
+
158
+ /**
159
+ * Sort a list of tags as if they were version numbers.
160
+ * Tags that don't look like version number will be removed.
161
+ *
162
+ * @param stdClass[] $tags Array of tag objects.
163
+ * @return stdClass[] Filtered array of tags sorted in descending order.
164
+ */
165
+ protected function sortTagsByVersion($tags) {
166
+ //Keep only those tags that look like version numbers.
167
+ $versionTags = array_filter($tags, array($this, 'isVersionTag'));
168
+ //Sort them in descending order.
169
+ usort($versionTags, array($this, 'compareTagNames'));
170
+
171
+ return $versionTags;
172
+ }
173
+
174
+ /**
175
+ * Compare two tags as if they were version number.
176
+ *
177
+ * @param stdClass $tag1 Tag object.
178
+ * @param stdClass $tag2 Another tag object.
179
+ * @return int
180
+ */
181
+ protected function compareTagNames($tag1, $tag2) {
182
+ $property = $this->tagNameProperty;
183
+ if ( !isset($tag1->$property) ) {
184
+ return 1;
185
+ }
186
+ if ( !isset($tag2->$property) ) {
187
+ return -1;
188
+ }
189
+ return -version_compare(ltrim($tag1->$property, 'v'), ltrim($tag2->$property, 'v'));
190
+ }
191
+
192
+ /**
193
+ * Get the contents of a file from a specific branch or tag.
194
+ *
195
+ * @param string $path File name.
196
+ * @param string $ref
197
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
198
+ */
199
+ abstract public function getRemoteFile($path, $ref = 'master');
200
+
201
+ /**
202
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
203
+ *
204
+ * @param string $ref Reference name (e.g. branch or tag).
205
+ * @return string|null
206
+ */
207
+ abstract public function getLatestCommitTime($ref);
208
+
209
+ /**
210
+ * Get the contents of the changelog file from the repository.
211
+ *
212
+ * @param string $ref
213
+ * @param string $localDirectory Full path to the local plugin or theme directory.
214
+ * @return null|string The HTML contents of the changelog.
215
+ */
216
+ public function getRemoteChangelog($ref, $localDirectory) {
217
+ $filename = $this->findChangelogName($localDirectory);
218
+ if ( empty($filename) ) {
219
+ return null;
220
+ }
221
+
222
+ $changelog = $this->getRemoteFile($filename, $ref);
223
+ if ( $changelog === null ) {
224
+ return null;
225
+ }
226
+
227
+ /** @noinspection PhpUndefinedClassInspection */
228
+ return Parsedown::instance()->text($changelog);
229
+ }
230
+
231
+ /**
232
+ * Guess the name of the changelog file.
233
+ *
234
+ * @param string $directory
235
+ * @return string|null
236
+ */
237
+ protected function findChangelogName($directory = null) {
238
+ if ( !isset($directory) ) {
239
+ $directory = $this->localDirectory;
240
+ }
241
+ if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
242
+ return null;
243
+ }
244
+
245
+ $possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
246
+ $files = scandir($directory);
247
+ $foundNames = array_intersect($possibleNames, $files);
248
+
249
+ if ( !empty($foundNames) ) {
250
+ return reset($foundNames);
251
+ }
252
+ return null;
253
+ }
254
+
255
+ /**
256
+ * Set authentication credentials.
257
+ *
258
+ * @param $credentials
259
+ */
260
+ public function setAuthentication($credentials) {
261
+ $this->credentials = $credentials;
262
+ }
263
+
264
+ public function isAuthenticationEnabled() {
265
+ return !empty($this->credentials);
266
+ }
267
+
268
+ /**
269
+ * @param string $url
270
+ * @return string
271
+ */
272
+ public function signDownloadUrl($url) {
273
+ return $url;
274
+ }
275
+
276
+ /**
277
+ * @param string $filterName
278
+ */
279
+ public function setHttpFilterName($filterName) {
280
+ $this->httpFilterName = $filterName;
281
+ }
282
+
283
+ /**
284
+ * @param string $directory
285
+ */
286
+ public function setLocalDirectory($directory) {
287
+ if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
288
+ $this->localDirectory = null;
289
+ } else {
290
+ $this->localDirectory = $directory;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * @param string $slug
296
+ */
297
+ public function setSlug($slug) {
298
+ $this->slug = $slug;
299
+ }
300
+ }
301
+
302
+ endif;
updater/Puc/v4p10/Vcs/BaseChecker.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !interface_exists('Puc_v4p10_Vcs_BaseChecker', false) ):
3
+
4
+ interface Puc_v4p10_Vcs_BaseChecker {
5
+ /**
6
+ * Set the repository branch to use for updates. Defaults to 'master'.
7
+ *
8
+ * @param string $branch
9
+ * @return $this
10
+ */
11
+ public function setBranch($branch);
12
+
13
+ /**
14
+ * Set authentication credentials.
15
+ *
16
+ * @param array|string $credentials
17
+ * @return $this
18
+ */
19
+ public function setAuthentication($credentials);
20
+
21
+ /**
22
+ * @return Puc_v4p10_Vcs_Api
23
+ */
24
+ public function getVcsApi();
25
+ }
26
+
27
+ endif;
updater/Puc/v4p10/Vcs/BitBucketApi.php ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Vcs_BitBucketApi', false) ):
3
+
4
+ class Puc_v4p10_Vcs_BitBucketApi extends Puc_v4p10_Vcs_Api {
5
+ /**
6
+ * @var Puc_v4p10_OAuthSignature
7
+ */
8
+ private $oauth = null;
9
+
10
+ /**
11
+ * @var string
12
+ */
13
+ private $username;
14
+
15
+ /**
16
+ * @var string
17
+ */
18
+ private $repository;
19
+
20
+ public function __construct($repositoryUrl, $credentials = array()) {
21
+ $path = parse_url($repositoryUrl, PHP_URL_PATH);
22
+ if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
23
+ $this->username = $matches['username'];
24
+ $this->repository = $matches['repository'];
25
+ } else {
26
+ throw new InvalidArgumentException('Invalid BitBucket repository URL: "' . $repositoryUrl . '"');
27
+ }
28
+
29
+ parent::__construct($repositoryUrl, $credentials);
30
+ }
31
+
32
+ /**
33
+ * Figure out which reference (i.e tag or branch) contains the latest version.
34
+ *
35
+ * @param string $configBranch Start looking in this branch.
36
+ * @return null|Puc_v4p10_Vcs_Reference
37
+ */
38
+ public function chooseReference($configBranch) {
39
+ $updateSource = null;
40
+
41
+ //Check if there's a "Stable tag: 1.2.3" header that points to a valid tag.
42
+ $updateSource = $this->getStableTag($configBranch);
43
+
44
+ //Look for version-like tags.
45
+ if ( !$updateSource && ($configBranch === 'master') ) {
46
+ $updateSource = $this->getLatestTag();
47
+ }
48
+ //If all else fails, use the specified branch itself.
49
+ if ( !$updateSource ) {
50
+ $updateSource = $this->getBranch($configBranch);
51
+ }
52
+
53
+ return $updateSource;
54
+ }
55
+
56
+ public function getBranch($branchName) {
57
+ $branch = $this->api('/refs/branches/' . $branchName);
58
+ if ( is_wp_error($branch) || empty($branch) ) {
59
+ return null;
60
+ }
61
+
62
+ return new Puc_v4p10_Vcs_Reference(array(
63
+ 'name' => $branch->name,
64
+ 'updated' => $branch->target->date,
65
+ 'downloadUrl' => $this->getDownloadUrl($branch->name),
66
+ ));
67
+ }
68
+
69
+ /**
70
+ * Get a specific tag.
71
+ *
72
+ * @param string $tagName
73
+ * @return Puc_v4p10_Vcs_Reference|null
74
+ */
75
+ public function getTag($tagName) {
76
+ $tag = $this->api('/refs/tags/' . $tagName);
77
+ if ( is_wp_error($tag) || empty($tag) ) {
78
+ return null;
79
+ }
80
+
81
+ return new Puc_v4p10_Vcs_Reference(array(
82
+ 'name' => $tag->name,
83
+ 'version' => ltrim($tag->name, 'v'),
84
+ 'updated' => $tag->target->date,
85
+ 'downloadUrl' => $this->getDownloadUrl($tag->name),
86
+ ));
87
+ }
88
+
89
+ /**
90
+ * Get the tag that looks like the highest version number.
91
+ *
92
+ * @return Puc_v4p10_Vcs_Reference|null
93
+ */
94
+ public function getLatestTag() {
95
+ $tags = $this->api('/refs/tags?sort=-target.date');
96
+ if ( !isset($tags, $tags->values) || !is_array($tags->values) ) {
97
+ return null;
98
+ }
99
+
100
+ //Filter and sort the list of tags.
101
+ $versionTags = $this->sortTagsByVersion($tags->values);
102
+
103
+ //Return the first result.
104
+ if ( !empty($versionTags) ) {
105
+ $tag = $versionTags[0];
106
+ return new Puc_v4p10_Vcs_Reference(array(
107
+ 'name' => $tag->name,
108
+ 'version' => ltrim($tag->name, 'v'),
109
+ 'updated' => $tag->target->date,
110
+ 'downloadUrl' => $this->getDownloadUrl($tag->name),
111
+ ));
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Get the tag/ref specified by the "Stable tag" header in the readme.txt of a given branch.
118
+ *
119
+ * @param string $branch
120
+ * @return null|Puc_v4p10_Vcs_Reference
121
+ */
122
+ protected function getStableTag($branch) {
123
+ $remoteReadme = $this->getRemoteReadme($branch);
124
+ if ( !empty($remoteReadme['stable_tag']) ) {
125
+ $tag = $remoteReadme['stable_tag'];
126
+
127
+ //You can explicitly opt out of using tags by setting "Stable tag" to
128
+ //"trunk" or the name of the current branch.
129
+ if ( ($tag === $branch) || ($tag === 'trunk') ) {
130
+ return $this->getBranch($branch);
131
+ }
132
+
133
+ return $this->getTag($tag);
134
+ }
135
+
136
+ return null;
137
+ }
138
+
139
+ /**
140
+ * @param string $ref
141
+ * @return string
142
+ */
143
+ protected function getDownloadUrl($ref) {
144
+ return sprintf(
145
+ 'https://bitbucket.org/%s/%s/get/%s.zip',
146
+ $this->username,
147
+ $this->repository,
148
+ $ref
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Get the contents of a file from a specific branch or tag.
154
+ *
155
+ * @param string $path File name.
156
+ * @param string $ref
157
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
158
+ */
159
+ public function getRemoteFile($path, $ref = 'master') {
160
+ $response = $this->api('src/' . $ref . '/' . ltrim($path));
161
+ if ( is_wp_error($response) || !is_string($response) ) {
162
+ return null;
163
+ }
164
+ return $response;
165
+ }
166
+
167
+ /**
168
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
169
+ *
170
+ * @param string $ref Reference name (e.g. branch or tag).
171
+ * @return string|null
172
+ */
173
+ public function getLatestCommitTime($ref) {
174
+ $response = $this->api('commits/' . $ref);
175
+ if ( isset($response->values, $response->values[0], $response->values[0]->date) ) {
176
+ return $response->values[0]->date;
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Perform a BitBucket API 2.0 request.
183
+ *
184
+ * @param string $url
185
+ * @param string $version
186
+ * @return mixed|WP_Error
187
+ */
188
+ public function api($url, $version = '2.0') {
189
+ $url = ltrim($url, '/');
190
+ $isSrcResource = Puc_v4p10_Utils::startsWith($url, 'src/');
191
+
192
+ $url = implode('/', array(
193
+ 'https://api.bitbucket.org',
194
+ $version,
195
+ 'repositories',
196
+ $this->username,
197
+ $this->repository,
198
+ $url
199
+ ));
200
+ $baseUrl = $url;
201
+
202
+ if ( $this->oauth ) {
203
+ $url = $this->oauth->sign($url,'GET');
204
+ }
205
+
206
+ $options = array('timeout' => 10);
207
+ if ( !empty($this->httpFilterName) ) {
208
+ $options = apply_filters($this->httpFilterName, $options);
209
+ }
210
+ $response = wp_remote_get($url, $options);
211
+ if ( is_wp_error($response) ) {
212
+ do_action('puc_api_error', $response, null, $url, $this->slug);
213
+ return $response;
214
+ }
215
+
216
+ $code = wp_remote_retrieve_response_code($response);
217
+ $body = wp_remote_retrieve_body($response);
218
+ if ( $code === 200 ) {
219
+ if ( $isSrcResource ) {
220
+ //Most responses are JSON-encoded, but src resources just
221
+ //return raw file contents.
222
+ $document = $body;
223
+ } else {
224
+ $document = json_decode($body);
225
+ }
226
+ return $document;
227
+ }
228
+
229
+ $error = new WP_Error(
230
+ 'puc-bitbucket-http-error',
231
+ sprintf('BitBucket API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
232
+ );
233
+ do_action('puc_api_error', $error, $response, $url, $this->slug);
234
+
235
+ return $error;
236
+ }
237
+
238
+ /**
239
+ * @param array $credentials
240
+ */
241
+ public function setAuthentication($credentials) {
242
+ parent::setAuthentication($credentials);
243
+
244
+ if ( !empty($credentials) && !empty($credentials['consumer_key']) ) {
245
+ $this->oauth = new Puc_v4p10_OAuthSignature(
246
+ $credentials['consumer_key'],
247
+ $credentials['consumer_secret']
248
+ );
249
+ } else {
250
+ $this->oauth = null;
251
+ }
252
+ }
253
+
254
+ public function signDownloadUrl($url) {
255
+ //Add authentication data to download URLs. Since OAuth signatures incorporate
256
+ //timestamps, we have to do this immediately before inserting the update. Otherwise
257
+ //authentication could fail due to a stale timestamp.
258
+ if ( $this->oauth ) {
259
+ $url = $this->oauth->sign($url);
260
+ }
261
+ return $url;
262
+ }
263
+ }
264
+
265
+ endif;
updater/Puc/v4p10/Vcs/GitHubApi.php ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_Vcs_GitHubApi', false) ):
4
+
5
+ class Puc_v4p10_Vcs_GitHubApi extends Puc_v4p10_Vcs_Api {
6
+ /**
7
+ * @var string GitHub username.
8
+ */
9
+ protected $userName;
10
+ /**
11
+ * @var string GitHub repository name.
12
+ */
13
+ protected $repositoryName;
14
+
15
+ /**
16
+ * @var string Either a fully qualified repository URL, or just "user/repo-name".
17
+ */
18
+ protected $repositoryUrl;
19
+
20
+ /**
21
+ * @var string GitHub authentication token. Optional.
22
+ */
23
+ protected $accessToken;
24
+
25
+ /**
26
+ * @var bool Whether to download release assets instead of the auto-generated source code archives.
27
+ */
28
+ protected $releaseAssetsEnabled = false;
29
+
30
+ /**
31
+ * @var string|null Regular expression that's used to filter release assets by name. Optional.
32
+ */
33
+ protected $assetFilterRegex = null;
34
+
35
+ /**
36
+ * @var string|null The unchanging part of a release asset URL. Used to identify download attempts.
37
+ */
38
+ protected $assetApiBaseUrl = null;
39
+
40
+ /**
41
+ * @var bool
42
+ */
43
+ private $downloadFilterAdded = false;
44
+
45
+ public function __construct($repositoryUrl, $accessToken = null) {
46
+ $path = parse_url($repositoryUrl, PHP_URL_PATH);
47
+ if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
48
+ $this->userName = $matches['username'];
49
+ $this->repositoryName = $matches['repository'];
50
+ } else {
51
+ throw new InvalidArgumentException('Invalid GitHub repository URL: "' . $repositoryUrl . '"');
52
+ }
53
+
54
+ parent::__construct($repositoryUrl, $accessToken);
55
+ }
56
+
57
+ /**
58
+ * Get the latest release from GitHub.
59
+ *
60
+ * @return Puc_v4p10_Vcs_Reference|null
61
+ */
62
+ public function getLatestRelease() {
63
+ $release = $this->api('/repos/:user/:repo/releases/latest');
64
+ if ( is_wp_error($release) || !is_object($release) || !isset($release->tag_name) ) {
65
+ return null;
66
+ }
67
+
68
+ $reference = new Puc_v4p10_Vcs_Reference(array(
69
+ 'name' => $release->tag_name,
70
+ 'version' => ltrim($release->tag_name, 'v'), //Remove the "v" prefix from "v1.2.3".
71
+ 'downloadUrl' => $release->zipball_url,
72
+ 'updated' => $release->created_at,
73
+ 'apiResponse' => $release,
74
+ ));
75
+
76
+ if ( isset($release->assets[0]) ) {
77
+ $reference->downloadCount = $release->assets[0]->download_count;
78
+ }
79
+
80
+ if ( $this->releaseAssetsEnabled && isset($release->assets, $release->assets[0]) ) {
81
+ //Use the first release asset that matches the specified regular expression.
82
+ $matchingAssets = array_filter($release->assets, array($this, 'matchesAssetFilter'));
83
+ if ( !empty($matchingAssets) ) {
84
+ if ( $this->isAuthenticationEnabled() ) {
85
+ /**
86
+ * Keep in mind that we'll need to add an "Accept" header to download this asset.
87
+ *
88
+ * @see setUpdateDownloadHeaders()
89
+ */
90
+ $reference->downloadUrl = $matchingAssets[0]->url;
91
+ } else {
92
+ //It seems that browser_download_url only works for public repositories.
93
+ //Using an access_token doesn't help. Maybe OAuth would work?
94
+ $reference->downloadUrl = $matchingAssets[0]->browser_download_url;
95
+ }
96
+
97
+ $reference->downloadCount = $matchingAssets[0]->download_count;
98
+ }
99
+ }
100
+
101
+ if ( !empty($release->body) ) {
102
+ /** @noinspection PhpUndefinedClassInspection */
103
+ $reference->changelog = Parsedown::instance()->text($release->body);
104
+ }
105
+
106
+ return $reference;
107
+ }
108
+
109
+ /**
110
+ * Get the tag that looks like the highest version number.
111
+ *
112
+ * @return Puc_v4p10_Vcs_Reference|null
113
+ */
114
+ public function getLatestTag() {
115
+ $tags = $this->api('/repos/:user/:repo/tags');
116
+
117
+ if ( is_wp_error($tags) || !is_array($tags) ) {
118
+ return null;
119
+ }
120
+
121
+ $versionTags = $this->sortTagsByVersion($tags);
122
+ if ( empty($versionTags) ) {
123
+ return null;
124
+ }
125
+
126
+ $tag = $versionTags[0];
127
+ return new Puc_v4p10_Vcs_Reference(array(
128
+ 'name' => $tag->name,
129
+ 'version' => ltrim($tag->name, 'v'),
130
+ 'downloadUrl' => $tag->zipball_url,
131
+ 'apiResponse' => $tag,
132
+ ));
133
+ }
134
+
135
+ /**
136
+ * Get a branch by name.
137
+ *
138
+ * @param string $branchName
139
+ * @return null|Puc_v4p10_Vcs_Reference
140
+ */
141
+ public function getBranch($branchName) {
142
+ $branch = $this->api('/repos/:user/:repo/branches/' . $branchName);
143
+ if ( is_wp_error($branch) || empty($branch) ) {
144
+ return null;
145
+ }
146
+
147
+ $reference = new Puc_v4p10_Vcs_Reference(array(
148
+ 'name' => $branch->name,
149
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
150
+ 'apiResponse' => $branch,
151
+ ));
152
+
153
+ if ( isset($branch->commit, $branch->commit->commit, $branch->commit->commit->author->date) ) {
154
+ $reference->updated = $branch->commit->commit->author->date;
155
+ }
156
+
157
+ return $reference;
158
+ }
159
+
160
+ /**
161
+ * Get the latest commit that changed the specified file.
162
+ *
163
+ * @param string $filename
164
+ * @param string $ref Reference name (e.g. branch or tag).
165
+ * @return StdClass|null
166
+ */
167
+ public function getLatestCommit($filename, $ref = 'master') {
168
+ $commits = $this->api(
169
+ '/repos/:user/:repo/commits',
170
+ array(
171
+ 'path' => $filename,
172
+ 'sha' => $ref,
173
+ )
174
+ );
175
+ if ( !is_wp_error($commits) && isset($commits[0]) ) {
176
+ return $commits[0];
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
183
+ *
184
+ * @param string $ref Reference name (e.g. branch or tag).
185
+ * @return string|null
186
+ */
187
+ public function getLatestCommitTime($ref) {
188
+ $commits = $this->api('/repos/:user/:repo/commits', array('sha' => $ref));
189
+ if ( !is_wp_error($commits) && isset($commits[0]) ) {
190
+ return $commits[0]->commit->author->date;
191
+ }
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Perform a GitHub API request.
197
+ *
198
+ * @param string $url
199
+ * @param array $queryParams
200
+ * @return mixed|WP_Error
201
+ */
202
+ protected function api($url, $queryParams = array()) {
203
+ $baseUrl = $url;
204
+ $url = $this->buildApiUrl($url, $queryParams);
205
+
206
+ $options = array('timeout' => 10);
207
+ if ( $this->isAuthenticationEnabled() ) {
208
+ $options['headers'] = array('Authorization' => $this->getAuthorizationHeader());
209
+ }
210
+
211
+ if ( !empty($this->httpFilterName) ) {
212
+ $options = apply_filters($this->httpFilterName, $options);
213
+ }
214
+ $response = wp_remote_get($url, $options);
215
+ if ( is_wp_error($response) ) {
216
+ do_action('puc_api_error', $response, null, $url, $this->slug);
217
+ return $response;
218
+ }
219
+
220
+ $code = wp_remote_retrieve_response_code($response);
221
+ $body = wp_remote_retrieve_body($response);
222
+ if ( $code === 200 ) {
223
+ $document = json_decode($body);
224
+ return $document;
225
+ }
226
+
227
+ $error = new WP_Error(
228
+ 'puc-github-http-error',
229
+ sprintf('GitHub API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
230
+ );
231
+ do_action('puc_api_error', $error, $response, $url, $this->slug);
232
+
233
+ return $error;
234
+ }
235
+
236
+ /**
237
+ * Build a fully qualified URL for an API request.
238
+ *
239
+ * @param string $url
240
+ * @param array $queryParams
241
+ * @return string
242
+ */
243
+ protected function buildApiUrl($url, $queryParams) {
244
+ $variables = array(
245
+ 'user' => $this->userName,
246
+ 'repo' => $this->repositoryName,
247
+ );
248
+ foreach ($variables as $name => $value) {
249
+ $url = str_replace('/:' . $name, '/' . urlencode($value), $url);
250
+ }
251
+ $url = 'https://api.github.com' . $url;
252
+
253
+ if ( !empty($queryParams) ) {
254
+ $url = add_query_arg($queryParams, $url);
255
+ }
256
+
257
+ return $url;
258
+ }
259
+
260
+ /**
261
+ * Get the contents of a file from a specific branch or tag.
262
+ *
263
+ * @param string $path File name.
264
+ * @param string $ref
265
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
266
+ */
267
+ public function getRemoteFile($path, $ref = 'master') {
268
+ $apiUrl = '/repos/:user/:repo/contents/' . $path;
269
+ $response = $this->api($apiUrl, array('ref' => $ref));
270
+
271
+ if ( is_wp_error($response) || !isset($response->content) || ($response->encoding !== 'base64') ) {
272
+ return null;
273
+ }
274
+ return base64_decode($response->content);
275
+ }
276
+
277
+ /**
278
+ * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
279
+ *
280
+ * @param string $ref
281
+ * @return string
282
+ */
283
+ public function buildArchiveDownloadUrl($ref = 'master') {
284
+ $url = sprintf(
285
+ 'https://api.github.com/repos/%1$s/%2$s/zipball/%3$s',
286
+ urlencode($this->userName),
287
+ urlencode($this->repositoryName),
288
+ urlencode($ref)
289
+ );
290
+ return $url;
291
+ }
292
+
293
+ /**
294
+ * Get a specific tag.
295
+ *
296
+ * @param string $tagName
297
+ * @return void
298
+ */
299
+ public function getTag($tagName) {
300
+ //The current GitHub update checker doesn't use getTag, so I didn't bother to implement it.
301
+ throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
302
+ }
303
+
304
+ public function setAuthentication($credentials) {
305
+ parent::setAuthentication($credentials);
306
+ $this->accessToken = is_string($credentials) ? $credentials : null;
307
+
308
+ //Optimization: Instead of filtering all HTTP requests, let's do it only when
309
+ //WordPress is about to download an update.
310
+ add_filter('upgrader_pre_download', array($this, 'addHttpRequestFilter'), 10, 1); //WP 3.7+
311
+ }
312
+
313
+ /**
314
+ * Figure out which reference (i.e tag or branch) contains the latest version.
315
+ *
316
+ * @param string $configBranch Start looking in this branch.
317
+ * @return null|Puc_v4p10_Vcs_Reference
318
+ */
319
+ public function chooseReference($configBranch) {
320
+ $updateSource = null;
321
+
322
+ if ( $configBranch === 'master' ) {
323
+ //Use the latest release.
324
+ $updateSource = $this->getLatestRelease();
325
+ if ( $updateSource === null ) {
326
+ //Failing that, use the tag with the highest version number.
327
+ $updateSource = $this->getLatestTag();
328
+ }
329
+ }
330
+ //Alternatively, just use the branch itself.
331
+ if ( empty($updateSource) ) {
332
+ $updateSource = $this->getBranch($configBranch);
333
+ }
334
+
335
+ return $updateSource;
336
+ }
337
+
338
+ /**
339
+ * Enable updating via release assets.
340
+ *
341
+ * If the latest release contains no usable assets, the update checker
342
+ * will fall back to using the automatically generated ZIP archive.
343
+ *
344
+ * Private repositories will only work with WordPress 3.7 or later.
345
+ *
346
+ * @param string|null $fileNameRegex Optional. Use only those assets where the file name matches this regex.
347
+ */
348
+ public function enableReleaseAssets($fileNameRegex = null) {
349
+ $this->releaseAssetsEnabled = true;
350
+ $this->assetFilterRegex = $fileNameRegex;
351
+ $this->assetApiBaseUrl = sprintf(
352
+ '//api.github.com/repos/%1$s/%2$s/releases/assets/',
353
+ $this->userName,
354
+ $this->repositoryName
355
+ );
356
+ }
357
+
358
+ /**
359
+ * Does this asset match the file name regex?
360
+ *
361
+ * @param stdClass $releaseAsset
362
+ * @return bool
363
+ */
364
+ protected function matchesAssetFilter($releaseAsset) {
365
+ if ( $this->assetFilterRegex === null ) {
366
+ //The default is to accept all assets.
367
+ return true;
368
+ }
369
+ return isset($releaseAsset->name) && preg_match($this->assetFilterRegex, $releaseAsset->name);
370
+ }
371
+
372
+ /**
373
+ * @internal
374
+ * @param bool $result
375
+ * @return bool
376
+ */
377
+ public function addHttpRequestFilter($result) {
378
+ if ( !$this->downloadFilterAdded && $this->isAuthenticationEnabled() ) {
379
+ add_filter('http_request_args', array($this, 'setUpdateDownloadHeaders'), 10, 2);
380
+ add_action('requests-requests.before_redirect', array($this, 'removeAuthHeaderFromRedirects'), 10, 4);
381
+ $this->downloadFilterAdded = true;
382
+ }
383
+ return $result;
384
+ }
385
+
386
+ /**
387
+ * Set the HTTP headers that are necessary to download updates from private repositories.
388
+ *
389
+ * See GitHub docs:
390
+ * @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
391
+ * @link https://developer.github.com/v3/auth/#basic-authentication
392
+ *
393
+ * @internal
394
+ * @param array $requestArgs
395
+ * @param string $url
396
+ * @return array
397
+ */
398
+ public function setUpdateDownloadHeaders($requestArgs, $url = '') {
399
+ //Is WordPress trying to download one of our release assets?
400
+ if ( $this->releaseAssetsEnabled && (strpos($url, $this->assetApiBaseUrl) !== false) ) {
401
+ $requestArgs['headers']['Accept'] = 'application/octet-stream';
402
+ }
403
+ //Use Basic authentication, but only if the download is from our repository.
404
+ $repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
405
+ if ( $this->isAuthenticationEnabled() && (strpos($url, $repoApiBaseUrl)) === 0 ) {
406
+ $requestArgs['headers']['Authorization'] = $this->getAuthorizationHeader();
407
+ }
408
+ return $requestArgs;
409
+ }
410
+
411
+ /**
412
+ * When following a redirect, the Requests library will automatically forward
413
+ * the authorization header to other hosts. We don't want that because it breaks
414
+ * AWS downloads and can leak authorization information.
415
+ *
416
+ * @internal
417
+ * @param string $location
418
+ * @param array $headers
419
+ */
420
+ public function removeAuthHeaderFromRedirects(&$location, &$headers) {
421
+ $repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
422
+ if ( strpos($location, $repoApiBaseUrl) === 0 ) {
423
+ return; //This request is going to GitHub, so it's fine.
424
+ }
425
+ //Remove the header.
426
+ if ( isset($headers['Authorization']) ) {
427
+ unset($headers['Authorization']);
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Generate the value of the "Authorization" header.
433
+ *
434
+ * @return string
435
+ */
436
+ protected function getAuthorizationHeader() {
437
+ return 'Basic ' . base64_encode($this->userName . ':' . $this->accessToken);
438
+ }
439
+ }
440
+
441
+ endif;
updater/Puc/v4p10/Vcs/GitLabApi.php ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_Vcs_GitLabApi', false) ):
4
+
5
+ class Puc_v4p10_Vcs_GitLabApi extends Puc_v4p10_Vcs_Api {
6
+ /**
7
+ * @var string GitLab username.
8
+ */
9
+ protected $userName;
10
+
11
+ /**
12
+ * @var string GitLab server host.
13
+ */
14
+ protected $repositoryHost;
15
+
16
+ /**
17
+ * @var string Protocol used by this GitLab server: "http" or "https".
18
+ */
19
+ protected $repositoryProtocol = 'https';
20
+
21
+ /**
22
+ * @var string GitLab repository name.
23
+ */
24
+ protected $repositoryName;
25
+
26
+ /**
27
+ * @var string GitLab authentication token. Optional.
28
+ */
29
+ protected $accessToken;
30
+
31
+ public function __construct($repositoryUrl, $accessToken = null, $subgroup = null) {
32
+ //Parse the repository host to support custom hosts.
33
+ $port = parse_url($repositoryUrl, PHP_URL_PORT);
34
+ if ( !empty($port) ) {
35
+ $port = ':' . $port;
36
+ }
37
+ $this->repositoryHost = parse_url($repositoryUrl, PHP_URL_HOST) . $port;
38
+
39
+ if ( $this->repositoryHost !== 'gitlab.com' ) {
40
+ $this->repositoryProtocol = parse_url($repositoryUrl, PHP_URL_SCHEME);
41
+ }
42
+
43
+ //Find the repository information
44
+ $path = parse_url($repositoryUrl, PHP_URL_PATH);
45
+ if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
46
+ $this->userName = $matches['username'];
47
+ $this->repositoryName = $matches['repository'];
48
+ } elseif ( ($this->repositoryHost === 'gitlab.com') ) {
49
+ //This is probably a repository in a subgroup, e.g. "/organization/category/repo".
50
+ $parts = explode('/', trim($path, '/'));
51
+ if ( count($parts) < 3 ) {
52
+ throw new InvalidArgumentException('Invalid GitLab.com repository URL: "' . $repositoryUrl . '"');
53
+ }
54
+ $lastPart = array_pop($parts);
55
+ $this->userName = implode('/', $parts);
56
+ $this->repositoryName = $lastPart;
57
+ } else {
58
+ //There could be subgroups in the URL: gitlab.domain.com/group/subgroup/subgroup2/repository
59
+ if ( $subgroup !== null ) {
60
+ $path = str_replace(trailingslashit($subgroup), '', $path);
61
+ }
62
+
63
+ //This is not a traditional url, it could be gitlab is in a deeper subdirectory.
64
+ //Get the path segments.
65
+ $segments = explode('/', untrailingslashit(ltrim($path, '/')));
66
+
67
+ //We need at least /user-name/repository-name/
68
+ if ( count($segments) < 2 ) {
69
+ throw new InvalidArgumentException('Invalid GitLab repository URL: "' . $repositoryUrl . '"');
70
+ }
71
+
72
+ //Get the username and repository name.
73
+ $usernameRepo = array_splice($segments, -2, 2);
74
+ $this->userName = $usernameRepo[0];
75
+ $this->repositoryName = $usernameRepo[1];
76
+
77
+ //Append the remaining segments to the host if there are segments left.
78
+ if ( count($segments) > 0 ) {
79
+ $this->repositoryHost = trailingslashit($this->repositoryHost) . implode('/', $segments);
80
+ }
81
+
82
+ //Add subgroups to username.
83
+ if ( $subgroup !== null ) {
84
+ $this->userName = $usernameRepo[0] . '/' . untrailingslashit($subgroup);
85
+ }
86
+ }
87
+
88
+ parent::__construct($repositoryUrl, $accessToken);
89
+ }
90
+
91
+ /**
92
+ * Get the latest release from GitLab.
93
+ *
94
+ * @return Puc_v4p10_Vcs_Reference|null
95
+ */
96
+ public function getLatestRelease() {
97
+ return $this->getLatestTag();
98
+ }
99
+
100
+ /**
101
+ * Get the tag that looks like the highest version number.
102
+ *
103
+ * @return Puc_v4p10_Vcs_Reference|null
104
+ */
105
+ public function getLatestTag() {
106
+ $tags = $this->api('/:id/repository/tags');
107
+ if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
108
+ return null;
109
+ }
110
+
111
+ $versionTags = $this->sortTagsByVersion($tags);
112
+ if ( empty($versionTags) ) {
113
+ return null;
114
+ }
115
+
116
+ $tag = $versionTags[0];
117
+ return new Puc_v4p10_Vcs_Reference(array(
118
+ 'name' => $tag->name,
119
+ 'version' => ltrim($tag->name, 'v'),
120
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($tag->name),
121
+ 'apiResponse' => $tag,
122
+ ));
123
+ }
124
+
125
+ /**
126
+ * Get a branch by name.
127
+ *
128
+ * @param string $branchName
129
+ * @return null|Puc_v4p10_Vcs_Reference
130
+ */
131
+ public function getBranch($branchName) {
132
+ $branch = $this->api('/:id/repository/branches/' . $branchName);
133
+ if ( is_wp_error($branch) || empty($branch) ) {
134
+ return null;
135
+ }
136
+
137
+ $reference = new Puc_v4p10_Vcs_Reference(array(
138
+ 'name' => $branch->name,
139
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
140
+ 'apiResponse' => $branch,
141
+ ));
142
+
143
+ if ( isset($branch->commit, $branch->commit->committed_date) ) {
144
+ $reference->updated = $branch->commit->committed_date;
145
+ }
146
+
147
+ return $reference;
148
+ }
149
+
150
+ /**
151
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
152
+ *
153
+ * @param string $ref Reference name (e.g. branch or tag).
154
+ * @return string|null
155
+ */
156
+ public function getLatestCommitTime($ref) {
157
+ $commits = $this->api('/:id/repository/commits/', array('ref_name' => $ref));
158
+ if ( is_wp_error($commits) || !is_array($commits) || !isset($commits[0]) ) {
159
+ return null;
160
+ }
161
+
162
+ return $commits[0]->committed_date;
163
+ }
164
+
165
+ /**
166
+ * Perform a GitLab API request.
167
+ *
168
+ * @param string $url
169
+ * @param array $queryParams
170
+ * @return mixed|WP_Error
171
+ */
172
+ protected function api($url, $queryParams = array()) {
173
+ $baseUrl = $url;
174
+ $url = $this->buildApiUrl($url, $queryParams);
175
+
176
+ $options = array('timeout' => 10);
177
+ if ( !empty($this->httpFilterName) ) {
178
+ $options = apply_filters($this->httpFilterName, $options);
179
+ }
180
+
181
+ $response = wp_remote_get($url, $options);
182
+ if ( is_wp_error($response) ) {
183
+ do_action('puc_api_error', $response, null, $url, $this->slug);
184
+ return $response;
185
+ }
186
+
187
+ $code = wp_remote_retrieve_response_code($response);
188
+ $body = wp_remote_retrieve_body($response);
189
+ if ( $code === 200 ) {
190
+ return json_decode($body);
191
+ }
192
+
193
+ $error = new WP_Error(
194
+ 'puc-gitlab-http-error',
195
+ sprintf('GitLab API error. URL: "%s", HTTP status code: %d.', $baseUrl, $code)
196
+ );
197
+ do_action('puc_api_error', $error, $response, $url, $this->slug);
198
+
199
+ return $error;
200
+ }
201
+
202
+ /**
203
+ * Build a fully qualified URL for an API request.
204
+ *
205
+ * @param string $url
206
+ * @param array $queryParams
207
+ * @return string
208
+ */
209
+ protected function buildApiUrl($url, $queryParams) {
210
+ $variables = array(
211
+ 'user' => $this->userName,
212
+ 'repo' => $this->repositoryName,
213
+ 'id' => $this->userName . '/' . $this->repositoryName,
214
+ );
215
+
216
+ foreach ($variables as $name => $value) {
217
+ $url = str_replace("/:{$name}", '/' . urlencode($value), $url);
218
+ }
219
+
220
+ $url = substr($url, 1);
221
+ $url = sprintf('%1$s://%2$s/api/v4/projects/%3$s', $this->repositoryProtocol, $this->repositoryHost, $url);
222
+
223
+ if ( !empty($this->accessToken) ) {
224
+ $queryParams['private_token'] = $this->accessToken;
225
+ }
226
+
227
+ if ( !empty($queryParams) ) {
228
+ $url = add_query_arg($queryParams, $url);
229
+ }
230
+
231
+ return $url;
232
+ }
233
+
234
+ /**
235
+ * Get the contents of a file from a specific branch or tag.
236
+ *
237
+ * @param string $path File name.
238
+ * @param string $ref
239
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
240
+ */
241
+ public function getRemoteFile($path, $ref = 'master') {
242
+ $response = $this->api('/:id/repository/files/' . $path, array('ref' => $ref));
243
+ if ( is_wp_error($response) || !isset($response->content) || $response->encoding !== 'base64' ) {
244
+ return null;
245
+ }
246
+
247
+ return base64_decode($response->content);
248
+ }
249
+
250
+ /**
251
+ * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
252
+ *
253
+ * @param string $ref
254
+ * @return string
255
+ */
256
+ public function buildArchiveDownloadUrl($ref = 'master') {
257
+ $url = sprintf(
258
+ '%1$s://%2$s/api/v4/projects/%3$s/repository/archive.zip',
259
+ $this->repositoryProtocol,
260
+ $this->repositoryHost,
261
+ urlencode($this->userName . '/' . $this->repositoryName)
262
+ );
263
+ $url = add_query_arg('sha', urlencode($ref), $url);
264
+
265
+ if ( !empty($this->accessToken) ) {
266
+ $url = add_query_arg('private_token', $this->accessToken, $url);
267
+ }
268
+
269
+ return $url;
270
+ }
271
+
272
+ /**
273
+ * Get a specific tag.
274
+ *
275
+ * @param string $tagName
276
+ * @return void
277
+ */
278
+ public function getTag($tagName) {
279
+ throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
280
+ }
281
+
282
+ /**
283
+ * Figure out which reference (i.e tag or branch) contains the latest version.
284
+ *
285
+ * @param string $configBranch Start looking in this branch.
286
+ * @return null|Puc_v4p10_Vcs_Reference
287
+ */
288
+ public function chooseReference($configBranch) {
289
+ $updateSource = null;
290
+
291
+ // GitLab doesn't handle releases the same as GitHub so just use the latest tag
292
+ if ( $configBranch === 'master' ) {
293
+ $updateSource = $this->getLatestTag();
294
+ }
295
+
296
+ if ( empty($updateSource) ) {
297
+ $updateSource = $this->getBranch($configBranch);
298
+ }
299
+
300
+ return $updateSource;
301
+ }
302
+
303
+ public function setAuthentication($credentials) {
304
+ parent::setAuthentication($credentials);
305
+ $this->accessToken = is_string($credentials) ? $credentials : null;
306
+ }
307
+ }
308
+
309
+ endif;
updater/Puc/v4p10/Vcs/PluginUpdateChecker.php ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Vcs_PluginUpdateChecker') ):
3
+
4
+ class Puc_v4p10_Vcs_PluginUpdateChecker extends Puc_v4p10_Plugin_UpdateChecker implements Puc_v4p10_Vcs_BaseChecker {
5
+ /**
6
+ * @var string The branch where to look for updates. Defaults to "master".
7
+ */
8
+ protected $branch = 'master';
9
+
10
+ /**
11
+ * @var Puc_v4p10_Vcs_Api Repository API client.
12
+ */
13
+ protected $api = null;
14
+
15
+ /**
16
+ * Puc_v4p10_Vcs_PluginUpdateChecker constructor.
17
+ *
18
+ * @param Puc_v4p10_Vcs_Api $api
19
+ * @param string $pluginFile
20
+ * @param string $slug
21
+ * @param int $checkPeriod
22
+ * @param string $optionName
23
+ * @param string $muPluginFile
24
+ */
25
+ public function __construct($api, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
26
+ $this->api = $api;
27
+ $this->api->setHttpFilterName($this->getUniqueName('request_info_options'));
28
+
29
+ parent::__construct($api->getRepositoryUrl(), $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);
30
+
31
+ $this->api->setSlug($this->slug);
32
+ }
33
+
34
+ public function requestInfo($unusedParameter = null) {
35
+ //We have to make several remote API requests to gather all the necessary info
36
+ //which can take a while on slow networks.
37
+ if ( function_exists('set_time_limit') ) {
38
+ @set_time_limit(60);
39
+ }
40
+
41
+ $api = $this->api;
42
+ $api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
43
+
44
+ $info = new Puc_v4p10_Plugin_Info();
45
+ $info->filename = $this->pluginFile;
46
+ $info->slug = $this->slug;
47
+
48
+ $this->setInfoFromHeader($this->package->getPluginHeader(), $info);
49
+
50
+ //Pick a branch or tag.
51
+ $updateSource = $api->chooseReference($this->branch);
52
+ if ( $updateSource ) {
53
+ $ref = $updateSource->name;
54
+ $info->version = $updateSource->version;
55
+ $info->last_updated = $updateSource->updated;
56
+ $info->download_url = $updateSource->downloadUrl;
57
+
58
+ if ( !empty($updateSource->changelog) ) {
59
+ $info->sections['changelog'] = $updateSource->changelog;
60
+ }
61
+ if ( isset($updateSource->downloadCount) ) {
62
+ $info->downloaded = $updateSource->downloadCount;
63
+ }
64
+ } else {
65
+ //There's probably a network problem or an authentication error.
66
+ do_action(
67
+ 'puc_api_error',
68
+ new WP_Error(
69
+ 'puc-no-update-source',
70
+ 'Could not retrieve version information from the repository. '
71
+ . 'This usually means that the update checker either can\'t connect '
72
+ . 'to the repository or it\'s configured incorrectly.'
73
+ ),
74
+ null, null, $this->slug
75
+ );
76
+ return null;
77
+ }
78
+
79
+ //Get headers from the main plugin file in this branch/tag. Its "Version" header and other metadata
80
+ //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
81
+ $mainPluginFile = basename($this->pluginFile);
82
+ $remotePlugin = $api->getRemoteFile($mainPluginFile, $ref);
83
+ if ( !empty($remotePlugin) ) {
84
+ $remoteHeader = $this->package->getFileHeader($remotePlugin);
85
+ $this->setInfoFromHeader($remoteHeader, $info);
86
+ }
87
+
88
+ //Try parsing readme.txt. If it's formatted according to WordPress.org standards, it will contain
89
+ //a lot of useful information like the required/tested WP version, changelog, and so on.
90
+ if ( $this->readmeTxtExistsLocally() ) {
91
+ $this->setInfoFromRemoteReadme($ref, $info);
92
+ }
93
+
94
+ //The changelog might be in a separate file.
95
+ if ( empty($info->sections['changelog']) ) {
96
+ $info->sections['changelog'] = $api->getRemoteChangelog($ref, $this->package->getAbsoluteDirectoryPath());
97
+ if ( empty($info->sections['changelog']) ) {
98
+ $info->sections['changelog'] = __('There is no changelog available.', 'plugin-update-checker');
99
+ }
100
+ }
101
+
102
+ if ( empty($info->last_updated) ) {
103
+ //Fetch the latest commit that changed the tag or branch and use it as the "last_updated" date.
104
+ $latestCommitTime = $api->getLatestCommitTime($ref);
105
+ if ( $latestCommitTime !== null ) {
106
+ $info->last_updated = $latestCommitTime;
107
+ }
108
+ }
109
+
110
+ $info = apply_filters($this->getUniqueName('request_info_result'), $info, null);
111
+ return $info;
112
+ }
113
+
114
+ /**
115
+ * Check if the currently installed version has a readme.txt file.
116
+ *
117
+ * @return bool
118
+ */
119
+ protected function readmeTxtExistsLocally() {
120
+ return $this->package->fileExists($this->api->getLocalReadmeName());
121
+ }
122
+
123
+ /**
124
+ * Copy plugin metadata from a file header to a Plugin Info object.
125
+ *
126
+ * @param array $fileHeader
127
+ * @param Puc_v4p10_Plugin_Info $pluginInfo
128
+ */
129
+ protected function setInfoFromHeader($fileHeader, $pluginInfo) {
130
+ $headerToPropertyMap = array(
131
+ 'Version' => 'version',
132
+ 'Name' => 'name',
133
+ 'PluginURI' => 'homepage',
134
+ 'Author' => 'author',
135
+ 'AuthorName' => 'author',
136
+ 'AuthorURI' => 'author_homepage',
137
+
138
+ 'Requires WP' => 'requires',
139
+ 'Tested WP' => 'tested',
140
+ 'Requires at least' => 'requires',
141
+ 'Tested up to' => 'tested',
142
+
143
+ 'Requires PHP' => 'requires_php',
144
+ );
145
+ foreach ($headerToPropertyMap as $headerName => $property) {
146
+ if ( isset($fileHeader[$headerName]) && !empty($fileHeader[$headerName]) ) {
147
+ $pluginInfo->$property = $fileHeader[$headerName];
148
+ }
149
+ }
150
+
151
+ if ( !empty($fileHeader['Description']) ) {
152
+ $pluginInfo->sections['description'] = $fileHeader['Description'];
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Copy plugin metadata from the remote readme.txt file.
158
+ *
159
+ * @param string $ref GitHub tag or branch where to look for the readme.
160
+ * @param Puc_v4p10_Plugin_Info $pluginInfo
161
+ */
162
+ protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
163
+ $readme = $this->api->getRemoteReadme($ref);
164
+ if ( empty($readme) ) {
165
+ return;
166
+ }
167
+
168
+ if ( isset($readme['sections']) ) {
169
+ $pluginInfo->sections = array_merge($pluginInfo->sections, $readme['sections']);
170
+ }
171
+ if ( !empty($readme['tested_up_to']) ) {
172
+ $pluginInfo->tested = $readme['tested_up_to'];
173
+ }
174
+ if ( !empty($readme['requires_at_least']) ) {
175
+ $pluginInfo->requires = $readme['requires_at_least'];
176
+ }
177
+ if ( !empty($readme['requires_php']) ) {
178
+ $pluginInfo->requires_php = $readme['requires_php'];
179
+ }
180
+
181
+ if ( isset($readme['upgrade_notice'], $readme['upgrade_notice'][$pluginInfo->version]) ) {
182
+ $pluginInfo->upgrade_notice = $readme['upgrade_notice'][$pluginInfo->version];
183
+ }
184
+ }
185
+
186
+ public function setBranch($branch) {
187
+ $this->branch = $branch;
188
+ return $this;
189
+ }
190
+
191
+ public function setAuthentication($credentials) {
192
+ $this->api->setAuthentication($credentials);
193
+ return $this;
194
+ }
195
+
196
+ public function getVcsApi() {
197
+ return $this->api;
198
+ }
199
+
200
+ public function getUpdate() {
201
+ $update = parent::getUpdate();
202
+
203
+ if ( isset($update) && !empty($update->download_url) ) {
204
+ $update->download_url = $this->api->signDownloadUrl($update->download_url);
205
+ }
206
+
207
+ return $update;
208
+ }
209
+
210
+ public function onDisplayConfiguration($panel) {
211
+ parent::onDisplayConfiguration($panel);
212
+ $panel->row('Branch', $this->branch);
213
+ $panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
214
+ $panel->row('API client', get_class($this->api));
215
+ }
216
+ }
217
+
218
+ endif;
updater/Puc/v4p10/Vcs/Reference.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p10_Vcs_Reference', false) ):
3
+
4
+ /**
5
+ * This class represents a VCS branch or tag. It's intended as a read only, short-lived container
6
+ * that only exists to provide a limited degree of type checking.
7
+ *
8
+ * @property string $name
9
+ * @property string|null version
10
+ * @property string $downloadUrl
11
+ * @property string $updated
12
+ *
13
+ * @property string|null $changelog
14
+ * @property int|null $downloadCount
15
+ */
16
+ class Puc_v4p10_Vcs_Reference {
17
+ private $properties = array();
18
+
19
+ public function __construct($properties = array()) {
20
+ $this->properties = $properties;
21
+ }
22
+
23
+ /**
24
+ * @param string $name
25
+ * @return mixed|null
26
+ */
27
+ public function __get($name) {
28
+ return array_key_exists($name, $this->properties) ? $this->properties[$name] : null;
29
+ }
30
+
31
+ /**
32
+ * @param string $name
33
+ * @param mixed $value
34
+ */
35
+ public function __set($name, $value) {
36
+ $this->properties[$name] = $value;
37
+ }
38
+
39
+ /**
40
+ * @param string $name
41
+ * @return bool
42
+ */
43
+ public function __isset($name) {
44
+ return isset($this->properties[$name]);
45
+ }
46
+
47
+ }
48
+
49
+ endif;
updater/Puc/v4p10/Vcs/ThemeUpdateChecker.php ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p10_Vcs_ThemeUpdateChecker', false) ):
4
+
5
+ class Puc_v4p10_Vcs_ThemeUpdateChecker extends Puc_v4p10_Theme_UpdateChecker implements Puc_v4p10_Vcs_BaseChecker {
6
+ /**
7
+ * @var string The branch where to look for updates. Defaults to "master".
8
+ */
9
+ protected $branch = 'master';
10
+
11
+ /**
12
+ * @var Puc_v4p10_Vcs_Api Repository API client.
13
+ */
14
+ protected $api = null;
15
+
16
+ /**
17
+ * Puc_v4p10_Vcs_ThemeUpdateChecker constructor.
18
+ *
19
+ * @param Puc_v4p10_Vcs_Api $api
20
+ * @param null $stylesheet
21
+ * @param null $customSlug
22
+ * @param int $checkPeriod
23
+ * @param string $optionName
24
+ */
25
+ public function __construct($api, $stylesheet = null, $customSlug = null, $checkPeriod = 12, $optionName = '') {
26
+ $this->api = $api;
27
+ $this->api->setHttpFilterName($this->getUniqueName('request_update_options'));
28
+
29
+ parent::__construct($api->getRepositoryUrl(), $stylesheet, $customSlug, $checkPeriod, $optionName);
30
+
31
+ $this->api->setSlug($this->slug);
32
+ }
33
+
34
+ public function requestUpdate() {
35
+ $api = $this->api;
36
+ $api->setLocalDirectory($this->package->getAbsoluteDirectoryPath());
37
+
38
+ $update = new Puc_v4p10_Theme_Update();
39
+ $update->slug = $this->slug;
40
+
41
+ //Figure out which reference (tag or branch) we'll use to get the latest version of the theme.
42
+ $updateSource = $api->chooseReference($this->branch);
43
+ if ( $updateSource ) {
44
+ $ref = $updateSource->name;
45
+ $update->download_url = $updateSource->downloadUrl;
46
+ } else {
47
+ do_action(
48
+ 'puc_api_error',
49
+ new WP_Error(
50
+ 'puc-no-update-source',
51
+ 'Could not retrieve version information from the repository. '
52
+ . 'This usually means that the update checker either can\'t connect '
53
+ . 'to the repository or it\'s configured incorrectly.'
54
+ ),
55
+ null, null, $this->slug
56
+ );
57
+ $ref = $this->branch;
58
+ }
59
+
60
+ //Get headers from the main stylesheet in this branch/tag. Its "Version" header and other metadata
61
+ //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
62
+ $remoteHeader = $this->package->getFileHeader($api->getRemoteFile('style.css', $ref));
63
+ $update->version = Puc_v4p10_Utils::findNotEmpty(array(
64
+ $remoteHeader['Version'],
65
+ Puc_v4p10_Utils::get($updateSource, 'version'),
66
+ ));
67
+
68
+ //The details URL defaults to the Theme URI header or the repository URL.
69
+ $update->details_url = Puc_v4p10_Utils::findNotEmpty(array(
70
+ $remoteHeader['ThemeURI'],
71
+ $this->package->getHeaderValue('ThemeURI'),
72
+ $this->metadataUrl,
73
+ ));
74
+
75
+ if ( empty($update->version) ) {
76
+ //It looks like we didn't find a valid update after all.
77
+ $update = null;
78
+ }
79
+
80
+ $update = $this->filterUpdateResult($update);
81
+ return $update;
82
+ }
83
+
84
+ //FIXME: This is duplicated code. Both theme and plugin subclasses that use VCS share these methods.
85
+
86
+ public function setBranch($branch) {
87
+ $this->branch = $branch;
88
+ return $this;
89
+ }
90
+
91
+ public function setAuthentication($credentials) {
92
+ $this->api->setAuthentication($credentials);
93
+ return $this;
94
+ }
95
+
96
+ public function getVcsApi() {
97
+ return $this->api;
98
+ }
99
+
100
+ public function getUpdate() {
101
+ $update = parent::getUpdate();
102
+
103
+ if ( isset($update) && !empty($update->download_url) ) {
104
+ $update->download_url = $this->api->signDownloadUrl($update->download_url);
105
+ }
106
+
107
+ return $update;
108
+ }
109
+
110
+ public function onDisplayConfiguration($panel) {
111
+ parent::onDisplayConfiguration($panel);
112
+ $panel->row('Branch', $this->branch);
113
+ $panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
114
+ $panel->row('API client', get_class($this->api));
115
+ }
116
+ }
117
+
118
+ endif;
updater/css/puc-debug-bar.css ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .puc-debug-bar-panel-v4 pre {
2
+ margin-top: 0;
3
+ }
4
+
5
+ /* Style the debug data table to match "widefat" table style used by WordPress. */
6
+ table.puc-debug-data {
7
+ width: 100%;
8
+ clear: both;
9
+ margin: 0;
10
+
11
+ border-spacing: 0;
12
+ background-color: #f9f9f9;
13
+
14
+ border-radius: 3px;
15
+ border: 1px solid #dfdfdf;
16
+ border-collapse: separate;
17
+ }
18
+
19
+ table.puc-debug-data * {
20
+ word-wrap: break-word;
21
+ }
22
+
23
+ table.puc-debug-data th {
24
+ width: 11em;
25
+ padding: 7px 7px 8px;
26
+ text-align: left;
27
+
28
+ font-family: "Georgia", "Times New Roman", "Bitstream Charter", "Times", serif;
29
+ font-weight: 400;
30
+ font-size: 14px;
31
+ line-height: 1.3em;
32
+ text-shadow: rgba(255, 255, 255, 0.804) 0 1px 0;
33
+ }
34
+
35
+ table.puc-debug-data td, table.puc-debug-data th {
36
+ border-width: 1px 0;
37
+ border-style: solid;
38
+
39
+ border-top-color: #fff;
40
+ border-bottom-color: #dfdfdf;
41
+
42
+ text-transform: none;
43
+ }
44
+
45
+ table.puc-debug-data td {
46
+ color: #555;
47
+ font-size: 12px;
48
+ padding: 4px 7px 2px;
49
+ vertical-align: top;
50
+ }
51
+
52
+ .puc-ajax-response {
53
+ border: 1px solid #dfdfdf;
54
+ border-radius: 3px;
55
+ padding: 0.5em;
56
+ margin: 5px 0;
57
+ background-color: white;
58
+ }
59
+
60
+ .puc-ajax-nonce {
61
+ display: none;
62
+ }
63
+
64
+ .puc-ajax-response dt {
65
+ margin: 0;
66
+ }
67
+
68
+ .puc-ajax-response dd {
69
+ margin: 0 0 1em;
70
+ }
updater/js/debug-bar.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery(function($) {
2
+
3
+ function runAjaxAction(button, action) {
4
+ button = $(button);
5
+ var panel = button.closest('.puc-debug-bar-panel-v4');
6
+ var responseBox = button.closest('td').find('.puc-ajax-response');
7
+
8
+ responseBox.text('Processing...').show();
9
+ $.post(
10
+ ajaxurl,
11
+ {
12
+ action : action,
13
+ uid : panel.data('uid'),
14
+ _wpnonce: panel.data('nonce')
15
+ },
16
+ function(data) {
17
+ responseBox.html(data);
18
+ },
19
+ 'html'
20
+ );
21
+ }
22
+
23
+ $('.puc-debug-bar-panel-v4 input[name="puc-check-now-button"]').click(function() {
24
+ runAjaxAction(this, 'puc_v4_debug_check_now');
25
+ return false;
26
+ });
27
+
28
+ $('.puc-debug-bar-panel-v4 input[name="puc-request-info-button"]').click(function() {
29
+ runAjaxAction(this, 'puc_v4_debug_request_info');
30
+ return false;
31
+ });
32
+
33
+
34
+ // Debug Bar uses the panel class name as part of its link and container IDs. This means we can
35
+ // end up with multiple identical IDs if more than one plugin uses the update checker library.
36
+ // Fix it by replacing the class name with the plugin slug.
37
+ var panels = $('#debug-menu-targets').find('.puc-debug-bar-panel-v4');
38
+ panels.each(function() {
39
+ var panel = $(this);
40
+ var uid = panel.data('uid');
41
+ var target = panel.closest('.debug-menu-target');
42
+
43
+ //Change the panel wrapper ID.
44
+ target.attr('id', 'debug-menu-target-puc-' + uid);
45
+
46
+ //Change the menu link ID as well and point it at the new target ID.
47
+ $('#debug-bar-menu').find('.puc-debug-menu-link-' + uid)
48
+ .closest('.debug-menu-link')
49
+ .attr('id', 'debug-menu-link-puc-' + uid)
50
+ .attr('href', '#' + target.attr('id'));
51
+ });
52
+ });
updater/languages/plugin-update-checker-ca.mo ADDED
Binary file
updater/languages/plugin-update-checker-ca.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2019-09-25 18:15+0200\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: ca\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprova si hi ha actualitzacions"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "L’extensió %s està actualitzada."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nova versió de l’extensió %s està disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No s’ha pogut determinar si hi ha actualitzacions per a %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estat del comprovador d’actualitzacions desconegut \"%s\""
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hi ha cap registre de canvis disponible."
updater/languages/plugin-update-checker-cs_CZ.mo ADDED
Binary file
updater/languages/plugin-update-checker-cs_CZ.po ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: 2017-05-20 10:53+0300\n"
6
+ "PO-Revision-Date: 2017-07-05 15:39+0000\n"
7
+ "Last-Translator: Vojtěch Sajdl <vojtech@sajdl.com>\n"
8
+ "Language-Team: Czech (Czech Republic)\n"
9
+ "Language: cs-CZ\n"
10
+ "Plural-Forms: nplurals=2; plural=(n != 1)\n"
11
+ "MIME-Version: 1.0\n"
12
+ "Content-Type: text/plain; charset=UTF-8\n"
13
+ "Content-Transfer-Encoding: 8bit\n"
14
+ "X-Loco-Source-Locale: cs_CZ\n"
15
+ "X-Generator: Loco - https://localise.biz/\n"
16
+ "X-Poedit-Basepath: ..\n"
17
+ "X-Poedit-SourceCharset: UTF-8\n"
18
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
19
+ "X-Poedit-SearchPath-0: .\n"
20
+ "X-Loco-Parser: loco_parse_po"
21
+
22
+ #: Puc/v4p1/Plugin/UpdateChecker.php:358
23
+ msgid "Check for updates"
24
+ msgstr "Zkontrolovat aktualizace"
25
+
26
+ #: Puc/v4p1/Plugin/UpdateChecker.php:405
27
+ #, php-format
28
+ msgctxt "the plugin title"
29
+ msgid "The %s plugin is up to date."
30
+ msgstr "Plugin %s je aktuální."
31
+
32
+ #: Puc/v4p1/Plugin/UpdateChecker.php:407
33
+ #, php-format
34
+ msgctxt "the plugin title"
35
+ msgid "A new version of the %s plugin is available."
36
+ msgstr "Nová verze pluginu %s je dostupná."
37
+
38
+ #: Puc/v4p1/Plugin/UpdateChecker.php:409
39
+ #, php-format
40
+ msgid "Unknown update checker status \"%s\""
41
+ msgstr "Neznámý status kontroly aktualizací \"%s\""
42
+
43
+ #: Puc/v4p1/Vcs/PluginUpdateChecker.php:83
44
+ msgid "There is no changelog available."
45
+ msgstr "Changelog není dostupný."
updater/languages/plugin-update-checker-da_DK.mo ADDED
Binary file
updater/languages/plugin-update-checker-da_DK.po ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-05-20 10:53+0300\n"
5
+ "PO-Revision-Date: 2017-10-17 11:07+0200\n"
6
+ "Last-Translator: Mikk3lRo\n"
7
+ "Language-Team: Mikk3lRo\n"
8
+ "MIME-Version: 1.0\n"
9
+ "Content-Type: text/plain; charset=UTF-8\n"
10
+ "Content-Transfer-Encoding: 8bit\n"
11
+ "X-Generator: Poedit 2.0.4\n"
12
+ "X-Poedit-Basepath: ..\n"
13
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
14
+ "X-Poedit-SourceCharset: UTF-8\n"
15
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
16
+ "Language: da_DK\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p1/Plugin/UpdateChecker.php:358
20
+ msgid "Check for updates"
21
+ msgstr "Undersøg for opdateringer"
22
+
23
+ #: Puc/v4p1/Plugin/UpdateChecker.php:405
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "Plugin'et %s er allerede opdateret."
28
+
29
+ #: Puc/v4p1/Plugin/UpdateChecker.php:407
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "En ny version af plugin'et %s er tilgængelig."
34
+
35
+ #: Puc/v4p1/Plugin/UpdateChecker.php:409
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Ukendt opdateringsstatus: \"%s\""
39
+
40
+ #: Puc/v4p1/Vcs/PluginUpdateChecker.php:83
41
+ msgid "There is no changelog available."
42
+ msgstr "Der er ingen ændringslog tilgængelig."
updater/languages/plugin-update-checker-de_DE.mo ADDED
Binary file
updater/languages/plugin-update-checker-de_DE.po ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2016-06-29 20:21+0100\n"
5
+ "PO-Revision-Date: 2016-06-29 20:23+0100\n"
6
+ "Last-Translator: Igor Lückel <info@igorlueckel.de>\n"
7
+ "Language-Team: \n"
8
+ "MIME-Version: 1.0\n"
9
+ "Content-Type: text/plain; charset=UTF-8\n"
10
+ "Content-Transfer-Encoding: 8bit\n"
11
+ "X-Generator: Poedit 1.8.1\n"
12
+ "X-Poedit-Basepath: ..\n"
13
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
14
+ "X-Poedit-SourceCharset: UTF-8\n"
15
+ "X-Poedit-KeywordsList: __;_e\n"
16
+ "Language: de_DE\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: github-checker.php:137
20
+ msgid "There is no changelog available."
21
+ msgstr "Es ist keine Liste von Programmänderungen verfügbar."
22
+
23
+ #: plugin-update-checker.php:852
24
+ msgid "Check for updates"
25
+ msgstr "Nach Update suchen"
26
+
27
+ #: plugin-update-checker.php:896
28
+ msgid "This plugin is up to date."
29
+ msgstr "Das Plugin ist aktuell."
30
+
31
+ #: plugin-update-checker.php:898
32
+ msgid "A new version of this plugin is available."
33
+ msgstr "Es ist eine neue Version für das Plugin verfügbar."
34
+
35
+ #: plugin-update-checker.php:900
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Unbekannter Update Status \"%s\""
updater/languages/plugin-update-checker-es_AR.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_AR.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:13-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_CL.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_CL.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:14-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_CO.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_CO.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:14-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_CR.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_CR.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:14-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_DO.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_DO.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:14-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_ES.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_ES.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 14:56-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_GT.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_GT.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:14-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_HN.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_HN.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:14-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_MX.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_MX.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 14:57-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_PE.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_PE.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:15-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_PR.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_PR.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:15-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_UY.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_UY.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 15:15-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-es_VE.mo ADDED
Binary file
updater/languages/plugin-update-checker-es_VE.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-03-21 14:57-0400\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.3\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: es_ES\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Comprobar si hay actualizaciones"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "El plugin %s está actualizado."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Una nueva versión del %s plugin está disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "No se pudo determinar si hay actualizaciones disponibles para %s."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Estado del comprobador de actualización desconocido «%s»"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "No hay un registro de cambios disponible."
updater/languages/plugin-update-checker-fa_IR.mo ADDED
Binary file
updater/languages/plugin-update-checker-fa_IR.po ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2016-02-17 14:21+0100\n"
5
+ "PO-Revision-Date: 2016-10-28 14:30+0330\n"
6
+ "Last-Translator: studio RVOLA <hello@rvola.com>\n"
7
+ "Language-Team: Pro Style <info@prostyle.ir>\n"
8
+ "Language: fa_IR\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "X-Generator: Poedit 1.8.8\n"
13
+ "X-Poedit-Basepath: ..\n"
14
+ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
15
+ "X-Poedit-SourceCharset: UTF-8\n"
16
+ "X-Poedit-KeywordsList: __;_e\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: github-checker.php:120
20
+ msgid "There is no changelog available."
21
+ msgstr "شرحی برای تغییرات یافت نشد"
22
+
23
+ #: plugin-update-checker.php:637
24
+ msgid "Check for updates"
25
+ msgstr "بررسی برای بروزرسانی "
26
+
27
+ #: plugin-update-checker.php:681
28
+ msgid "This plugin is up to date."
29
+ msgstr "شما از آخرین نسخه استفاده میکنید . به‌روز باشید"
30
+
31
+ #: plugin-update-checker.php:683
32
+ msgid "A new version of this plugin is available."
33
+ msgstr "نسخه جدیدی برای افزونه ارائه شده است ."
34
+
35
+ #: plugin-update-checker.php:685
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "وضعیت ناشناخته برای بروزرسانی \"%s\""
updater/languages/plugin-update-checker-fr_CA.mo ADDED
Binary file
updater/languages/plugin-update-checker-fr_CA.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2018-02-12 10:32-0500\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.0.4\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: Eric Gagnon <eric.gagnon@banq.qc.ca>\n"
16
+ "Language: fr_CA\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Vérifier les mises à jour"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "L’extension %s est à jour."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Une nouvelle version de l’extension %s est disponible."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "Impossible de déterminer si une mise à jour est disponible pour \"%s\""
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Un problème inconnu est survenu \"%s\""
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "Il n’y a aucun journal de mise à jour disponible."
updater/languages/plugin-update-checker-fr_FR.mo ADDED
Binary file
updater/languages/plugin-update-checker-fr_FR.po ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-07-07 14:53+0200\n"
5
+ "PO-Revision-Date: 2017-07-07 14:54+0200\n"
6
+ "Language-Team: studio RVOLA <http://www.rvola.com>\n"
7
+ "Language: fr_FR\n"
8
+ "MIME-Version: 1.0\n"
9
+ "Content-Type: text/plain; charset=UTF-8\n"
10
+ "Content-Transfer-Encoding: 8bit\n"
11
+ "X-Generator: Poedit 2.0.2\n"
12
+ "X-Poedit-Basepath: ..\n"
13
+ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
14
+ "X-Poedit-SourceCharset: UTF-8\n"
15
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
16
+ "Last-Translator: Nicolas GEHIN\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p1/Plugin/UpdateChecker.php:358
20
+ msgid "Check for updates"
21
+ msgstr "Vérifier les mises à jour"
22
+
23
+ #: Puc/v4p1/Plugin/UpdateChecker.php:405
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "L’extension %s est à jour."
28
+
29
+ #: Puc/v4p1/Plugin/UpdateChecker.php:407
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Une nouvelle version de l’extension %s est disponible."
34
+
35
+ #: Puc/v4p1/Plugin/UpdateChecker.php:409
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Un problème inconnu est survenu \"%s\""
39
+
40
+ #: Puc/v4p1/Vcs/PluginUpdateChecker.php:85
41
+ msgid "There is no changelog available."
42
+ msgstr "Il n’y a aucun journal de mise à jour disponible."
updater/languages/plugin-update-checker-hu_HU.mo ADDED
Binary file
updater/languages/plugin-update-checker-hu_HU.po ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2016-01-11 21:23+0100\n"
5
+ "PO-Revision-Date: 2016-01-11 21:25+0100\n"
6
+ "Last-Translator: Tamás András Horváth <htomy92@gmail.com>\n"
7
+ "Language-Team: \n"
8
+ "Language: hu_HU\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "X-Generator: Poedit 1.8.6\n"
13
+ "X-Poedit-Basepath: ..\n"
14
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
15
+ "X-Poedit-SourceCharset: UTF-8\n"
16
+ "X-Poedit-KeywordsList: __;_e\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: github-checker.php:137
20
+ msgid "There is no changelog available."
21
+ msgstr "Nem érhető el a changelog."
22
+
23
+ #: plugin-update-checker.php:852
24
+ msgid "Check for updates"
25
+ msgstr "Frissítés ellenőrzése"
26
+
27
+ #: plugin-update-checker.php:896
28
+ msgid "This plugin is up to date."
29
+ msgstr "Ez a plugin naprakész."
30
+
31
+ #: plugin-update-checker.php:898
32
+ msgid "A new version of this plugin is available."
33
+ msgstr "Új verzió érhető el a kiegészítőhöz"
34
+
35
+ #: plugin-update-checker.php:900
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Ismeretlen a frissítés ellenőrző státusza \"%s\""
39
+
40
+ #~ msgid "Every %d hours"
41
+ #~ msgstr "Minden %d órában"
updater/languages/plugin-update-checker-it_IT.mo ADDED
Binary file
updater/languages/plugin-update-checker-it_IT.po ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2016-06-29 20:21+0100\n"
5
+ "PO-Revision-Date: 2017-01-15 12:24+0100\n"
6
+ "Last-Translator: Igor Lückel <info@igorlueckel.de>\n"
7
+ "Language-Team: \n"
8
+ "MIME-Version: 1.0\n"
9
+ "Content-Type: text/plain; charset=UTF-8\n"
10
+ "Content-Transfer-Encoding: 8bit\n"
11
+ "X-Generator: Poedit 1.5.5\n"
12
+ "X-Poedit-Basepath: ..\n"
13
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
14
+ "X-Poedit-SourceCharset: UTF-8\n"
15
+ "X-Poedit-KeywordsList: __;_e\n"
16
+ "Language: de_DE\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: github-checker.php:137
20
+ msgid "There is no changelog available."
21
+ msgstr "Non c'è alcuna sezione di aggiornamento disponibile"
22
+
23
+ #: plugin-update-checker.php:852
24
+ msgid "Check for updates"
25
+ msgstr "Verifica aggiornamenti"
26
+
27
+ #: plugin-update-checker.php:896
28
+ msgid "This plugin is up to date."
29
+ msgstr "Il plugin è aggiornato"
30
+
31
+ #: plugin-update-checker.php:898
32
+ msgid "A new version of this plugin is available."
33
+ msgstr "Una nuova versione del plugin è disponibile"
34
+
35
+ #: plugin-update-checker.php:900
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Si è verificato un problema sconosciuto \"%s\""
updater/languages/plugin-update-checker-ja.mo ADDED
Binary file
updater/languages/plugin-update-checker-ja.po ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: \n"
4
+ "POT-Creation-Date: 2019-07-15 17:07+0900\n"
5
+ "PO-Revision-Date: 2019-07-15 17:12+0900\n"
6
+ "Last-Translator: tak <tak7725@gmail.com>\n"
7
+ "Language-Team: \n"
8
+ "Language: ja_JP\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "X-Generator: Poedit 2.2.3\n"
13
+ "X-Poedit-Basepath: ../../../../../../Applications/XAMPP/xamppfiles/htdocs/"
14
+ "kisagai/wordpress/wp-content/plugins/simple-stripe-gateway/Puc\n"
15
+ "Plural-Forms: nplurals=1; plural=0;\n"
16
+ "X-Poedit-KeywordsList: __;_x:1,2c\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: v4p7/Plugin/Ui.php:54
20
+ msgid "View details"
21
+ msgstr "詳細を表示"
22
+
23
+ #: v4p7/Plugin/Ui.php:77
24
+ #, php-format
25
+ msgid "More information about %s"
26
+ msgstr "%sについての詳細"
27
+
28
+ #: v4p7/Plugin/Ui.php:128
29
+ msgid "Check for updates"
30
+ msgstr "アップデートを確認"
31
+
32
+ #: v4p7/Plugin/Ui.php:213
33
+ #, php-format
34
+ msgctxt "the plugin title"
35
+ msgid "The %s plugin is up to date."
36
+ msgstr "%s プラグインは、最新バージョンです。"
37
+
38
+ #: v4p7/Plugin/Ui.php:215
39
+ #, php-format
40
+ msgctxt "the plugin title"
41
+ msgid "A new version of the %s plugin is available."
42
+ msgstr "%s プラグインの最新バージョンがあります。"
43
+
44
+ #: v4p7/Plugin/Ui.php:217
45
+ #, php-format
46
+ msgctxt "the plugin title"
47
+ msgid "Could not determine if updates are available for %s."
48
+ msgstr "%s のアップデートがあるかどうかを判断できませんでした。"
49
+
50
+ #: v4p7/Plugin/Ui.php:223
51
+ #, php-format
52
+ msgid "Unknown update checker status \"%s\""
53
+ msgstr "バージョンアップの確認で想定外の状態になりました。ステータス:”%s”"
54
+
55
+ #: v4p7/Vcs/PluginUpdateChecker.php:98
56
+ msgid "There is no changelog available."
57
+ msgstr "更新履歴はありません。"
updater/languages/plugin-update-checker-nl_BE.mo ADDED
Binary file
updater/languages/plugin-update-checker-nl_BE.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2018-03-25 18:15+0200\n"
5
+ "PO-Revision-Date: 2018-03-25 18:32+0200\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 1.8.7.1\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: Frank Goossens <frank@optimizingmatters.com>\n"
16
+ "Language: nl_BE\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Controleer op nieuwe versies"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "De meest recente %s versie is geïnstalleerd."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Er is een nieuwe versie van %s beschikbaar."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "Kon niet bepalen of er nieuwe versie van %s beschikbaar is."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Ongekende status bij controle op nieuwe versie: \"%s\""
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "Er is geen changelog beschikbaar."
updater/languages/plugin-update-checker-nl_NL.mo ADDED
Binary file
updater/languages/plugin-update-checker-nl_NL.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2018-03-25 18:15+0200\n"
5
+ "PO-Revision-Date: 2018-03-25 18:32+0200\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 1.8.7.1\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: Frank Goossens <frank@optimizingmatters.com>\n"
16
+ "Language: nl_NL\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Controleer op nieuwe versies"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "De meest recente %s versie is geïnstalleerd."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Er is een nieuwe versie van %s beschikbaar."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "Kon niet bepalen of er nieuwe versie van %s beschikbaar is."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Ongekende status bij controle op nieuwe versie: \"%s\""
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "Er is geen changelog beschikbaar."
updater/languages/plugin-update-checker-pt_BR.mo ADDED
Binary file
updater/languages/plugin-update-checker-pt_BR.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-05-19 15:41-0300\n"
5
+ "PO-Revision-Date: 2017-05-19 15:42-0300\n"
6
+ "Last-Translator: \n"
7
+ "Language-Team: \n"
8
+ "Language: pt_BR\n"
9
+ "MIME-Version: 1.0\n"
10
+ "Content-Type: text/plain; charset=UTF-8\n"
11
+ "Content-Transfer-Encoding: 8bit\n"
12
+ "X-Generator: Poedit 1.8.8\n"
13
+ "X-Poedit-Basepath: ..\n"
14
+ "Plural-Forms: nplurals=2; plural=(n > 1);\n"
15
+ "X-Poedit-SourceCharset: UTF-8\n"
16
+ "X-Poedit-KeywordsList: __;_e;_x;_x:1,2c\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p1/Plugin/UpdateChecker.php:358
20
+ msgid "Check for updates"
21
+ msgstr "Verificar Atualizações"
22
+
23
+ #: Puc/v4p1/Plugin/UpdateChecker.php:401 Puc/v4p1/Plugin/UpdateChecker.php:406
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "O plugin %s já está na sua versão mais recente."
28
+
29
+ #: Puc/v4p1/Plugin/UpdateChecker.php:408
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Há uma nova versão para o plugin %s disponível para download."
34
+
35
+ #: Puc/v4p1/Plugin/UpdateChecker.php:410
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Status \"%s\" desconhecido."
39
+
40
+ #: Puc/v4p1/Vcs/PluginUpdateChecker.php:83
41
+ msgid "There is no changelog available."
42
+ msgstr "Não há um changelog disponível."
43
+
44
+ #~ msgid "The %s plugin is up to date."
45
+ #~ msgstr "O plugin %s já está na sua versão mais recente."
46
+
47
+ #~ msgid "A new version of the %s plugin is available."
48
+ #~ msgstr "Há uma nova versão para o plugin %s disponível para download."
updater/languages/plugin-update-checker-sl_SI.mo ADDED
Binary file
updater/languages/plugin-update-checker-sl_SI.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2018-10-27 20:36+0200\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.2\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: Igor Funa\n"
16
+ "Language: sl_SI\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "Preveri posodobitve"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "Vtičnik %s je že posodobljen."
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Nova različica vtičnika %s je na razpolago."
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "Ne morem ugotoviti če se za vtičnik %s na razpolago posodobitve."
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "Neznan status preverjanja posodobitev za \"%s\""
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "Dnevnik sprememb ni na razpolago."
updater/languages/plugin-update-checker-sv_SE.mo ADDED
Binary file
updater/languages/plugin-update-checker-sv_SE.po ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-05-20 10:53+0300\n"
5
+ "PO-Revision-Date: 2017-10-16 15:02+0200\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.0.4\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: \n"
16
+ "Language: sv_SE\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p1/Plugin/UpdateChecker.php:358
20
+ msgid "Check for updates"
21
+ msgstr "Sök efter uppdateringar"
22
+
23
+ #: Puc/v4p1/Plugin/UpdateChecker.php:405
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "Tillägget %s är uppdaterat."
28
+
29
+ #: Puc/v4p1/Plugin/UpdateChecker.php:407
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "Det finns en ny version av tillägget %s."
34
+
35
+ #: Puc/v4p1/Plugin/UpdateChecker.php:409
36
+ #, php-format
37
+ msgid "Unknown update checker status \"%s\""
38
+ msgstr "Okänd status för kontroll av uppdatering “%s”"
39
+
40
+ #: Puc/v4p1/Vcs/PluginUpdateChecker.php:83
41
+ msgid "There is no changelog available."
42
+ msgstr "Det finns ingen ändringslogg tillgänglig."
updater/languages/plugin-update-checker-zh_CN.mo ADDED
Binary file
updater/languages/plugin-update-checker-zh_CN.po ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: plugin-update-checker\n"
4
+ "POT-Creation-Date: 2017-11-24 17:02+0200\n"
5
+ "PO-Revision-Date: 2020-08-04 08:10+0800\n"
6
+ "Language-Team: \n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=UTF-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "X-Generator: Poedit 2.4\n"
11
+ "X-Poedit-Basepath: ..\n"
12
+ "Plural-Forms: nplurals=1; plural=0;\n"
13
+ "X-Poedit-SourceCharset: UTF-8\n"
14
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
15
+ "Last-Translator: Seaton Jiang <seaton@vtrois.com>\n"
16
+ "Language: zh_CN\n"
17
+ "X-Poedit-SearchPath-0: .\n"
18
+
19
+ #: Puc/v4p3/Plugin/UpdateChecker.php:395
20
+ msgid "Check for updates"
21
+ msgstr "检查更新"
22
+
23
+ #: Puc/v4p3/Plugin/UpdateChecker.php:548
24
+ #, php-format
25
+ msgctxt "the plugin title"
26
+ msgid "The %s plugin is up to date."
27
+ msgstr "%s 目前是最新版本。"
28
+
29
+ #: Puc/v4p3/Plugin/UpdateChecker.php:550
30
+ #, php-format
31
+ msgctxt "the plugin title"
32
+ msgid "A new version of the %s plugin is available."
33
+ msgstr "%s 当前有可用的更新。"
34
+
35
+ #: Puc/v4p3/Plugin/UpdateChecker.php:552
36
+ #, php-format
37
+ msgctxt "the plugin title"
38
+ msgid "Could not determine if updates are available for %s."
39
+ msgstr "%s 无法确定是否有可用的更新。"
40
+
41
+ #: Puc/v4p3/Plugin/UpdateChecker.php:558
42
+ #, php-format
43
+ msgid "Unknown update checker status \"%s\""
44
+ msgstr "未知的更新检查状态:%s"
45
+
46
+ #: Puc/v4p3/Vcs/PluginUpdateChecker.php:95
47
+ msgid "There is no changelog available."
48
+ msgstr "没有可用的更新日志。"
updater/languages/plugin-update-checker.pot ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #, fuzzy
2
+ msgid ""
3
+ msgstr ""
4
+ "Project-Id-Version: plugin-update-checker\n"
5
+ "POT-Creation-Date: 2020-08-08 14:36+0300\n"
6
+ "PO-Revision-Date: 2016-01-10 20:59+0100\n"
7
+ "Last-Translator: Tamás András Horváth <htomy92@gmail.com>\n"
8
+ "Language-Team: \n"
9
+ "Language: en_US\n"
10
+ "MIME-Version: 1.0\n"
11
+ "Content-Type: text/plain; charset=UTF-8\n"
12
+ "Content-Transfer-Encoding: 8bit\n"
13
+ "X-Generator: Poedit 2.4\n"
14
+ "X-Poedit-Basepath: ..\n"
15
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
16
+ "X-Poedit-SourceCharset: UTF-8\n"
17
+ "X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
18
+ "X-Poedit-SearchPath-0: .\n"
19
+
20
+ #: Puc/v4p10/Plugin/Ui.php:128
21
+ msgid "Check for updates"
22
+ msgstr ""
23
+
24
+ #: Puc/v4p10/Plugin/Ui.php:213
25
+ #, php-format
26
+ msgctxt "the plugin title"
27
+ msgid "The %s plugin is up to date."
28
+ msgstr ""
29
+
30
+ #: Puc/v4p10/Plugin/Ui.php:215
31
+ #, php-format
32
+ msgctxt "the plugin title"
33
+ msgid "A new version of the %s plugin is available."
34
+ msgstr ""
35
+
36
+ #: Puc/v4p10/Plugin/Ui.php:217
37
+ #, php-format
38
+ msgctxt "the plugin title"
39
+ msgid "Could not determine if updates are available for %s."
40
+ msgstr ""
41
+
42
+ #: Puc/v4p10/Plugin/Ui.php:223
43
+ #, php-format
44
+ msgid "Unknown update checker status \"%s\""
45
+ msgstr ""
46
+
47
+ #: Puc/v4p10/Vcs/PluginUpdateChecker.php:98
48
+ msgid "There is no changelog available."
49
+ msgstr ""
updater/load-v4p10.php ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ require dirname(__FILE__) . '/Puc/v4p10/Autoloader.php';
3
+ new Puc_v4p10_Autoloader();
4
+
5
+ require dirname(__FILE__) . '/Puc/v4p10/Factory.php';
6
+ require dirname(__FILE__) . '/Puc/v4/Factory.php';
7
+
8
+ //Register classes defined in this version with the factory.
9
+ foreach (
10
+ array(
11
+ 'Plugin_UpdateChecker' => 'Puc_v4p10_Plugin_UpdateChecker',
12
+ 'Theme_UpdateChecker' => 'Puc_v4p10_Theme_UpdateChecker',
13
+
14
+ 'Vcs_PluginUpdateChecker' => 'Puc_v4p10_Vcs_PluginUpdateChecker',
15
+ 'Vcs_ThemeUpdateChecker' => 'Puc_v4p10_Vcs_ThemeUpdateChecker',
16
+
17
+ 'GitHubApi' => 'Puc_v4p10_Vcs_GitHubApi',
18
+ 'BitBucketApi' => 'Puc_v4p10_Vcs_BitBucketApi',
19
+ 'GitLabApi' => 'Puc_v4p10_Vcs_GitLabApi',
20
+ )
21
+ as $pucGeneralClass => $pucVersionedClass
22
+ ) {
23
+ Puc_v4_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.10');
24
+ //Also add it to the minor-version factory in case the major-version factory
25
+ //was already defined by another, older version of the update checker.
26
+ Puc_v4p10_Factory::addVersion($pucGeneralClass, $pucVersionedClass, '4.10');
27
+ }
28
+
updater/plugin-update-checker.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Update Checker Library 4.10
4
+ * http://w-shadow.com/
5
+ *
6
+ * Copyright 2020 Janis Elsts
7
+ * Released under the MIT license. See license.txt for details.
8
+ */
9
+
10
+ require dirname(__FILE__) . '/load-v4p10.php';
updater/vendor/Parsedown.php ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Parsedown', false) ) {
3
+ //Load the Parsedown version that's compatible with the current PHP version.
4
+ if ( version_compare(PHP_VERSION, '5.3.0', '>=') ) {
5
+ require __DIR__ . '/ParsedownModern.php';
6
+ } else {
7
+ require __DIR__ . '/ParsedownLegacy.php';
8
+ }
9
+ }
updater/vendor/ParsedownLegacy.php ADDED
@@ -0,0 +1,1535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ #
4
+ #
5
+ # Parsedown
6
+ # http://parsedown.org
7
+ #
8
+ # (c) Emanuil Rusev
9
+ # http://erusev.com
10
+ #
11
+ # For the full license information, view the LICENSE file that was distributed
12
+ # with this source code.
13
+ #
14
+ #
15
+
16
+ class Parsedown
17
+ {
18
+ # ~
19
+
20
+ const version = '1.5.0';
21
+
22
+ # ~
23
+
24
+ function text($text)
25
+ {
26
+ # make sure no definitions are set
27
+ $this->DefinitionData = array();
28
+
29
+ # standardize line breaks
30
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
31
+
32
+ # remove surrounding line breaks
33
+ $text = trim($text, "\n");
34
+
35
+ # split text into lines
36
+ $lines = explode("\n", $text);
37
+
38
+ # iterate through lines to identify blocks
39
+ $markup = $this->lines($lines);
40
+
41
+ # trim line breaks
42
+ $markup = trim($markup, "\n");
43
+
44
+ return $markup;
45
+ }
46
+
47
+ #
48
+ # Setters
49
+ #
50
+
51
+ function setBreaksEnabled($breaksEnabled)
52
+ {
53
+ $this->breaksEnabled = $breaksEnabled;
54
+
55
+ return $this;
56
+ }
57
+
58
+ protected $breaksEnabled;
59
+
60
+ function setMarkupEscaped($markupEscaped)
61
+ {
62
+ $this->markupEscaped = $markupEscaped;
63
+
64
+ return $this;
65
+ }
66
+
67
+ protected $markupEscaped;
68
+
69
+ function setUrlsLinked($urlsLinked)
70
+ {
71
+ $this->urlsLinked = $urlsLinked;
72
+
73
+ return $this;
74
+ }
75
+
76
+ protected $urlsLinked = true;
77
+
78
+ #
79
+ # Lines
80
+ #
81
+
82
+ protected $BlockTypes = array(
83
+ '#' => array('Header'),
84
+ '*' => array('Rule', 'List'),
85
+ '+' => array('List'),
86
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
87
+ '0' => array('List'),
88
+ '1' => array('List'),
89
+ '2' => array('List'),
90
+ '3' => array('List'),
91
+ '4' => array('List'),
92
+ '5' => array('List'),
93
+ '6' => array('List'),
94
+ '7' => array('List'),
95
+ '8' => array('List'),
96
+ '9' => array('List'),
97
+ ':' => array('Table'),
98
+ '<' => array('Comment', 'Markup'),
99
+ '=' => array('SetextHeader'),
100
+ '>' => array('Quote'),
101
+ '[' => array('Reference'),
102
+ '_' => array('Rule'),
103
+ '`' => array('FencedCode'),
104
+ '|' => array('Table'),
105
+ '~' => array('FencedCode'),
106
+ );
107
+
108
+ # ~
109
+
110
+ protected $DefinitionTypes = array(
111
+ '[' => array('Reference'),
112
+ );
113
+
114
+ # ~
115
+
116
+ protected $unmarkedBlockTypes = array(
117
+ 'Code',
118
+ );
119
+
120
+ #
121
+ # Blocks
122
+ #
123
+
124
+ private function lines(array $lines)
125
+ {
126
+ $CurrentBlock = null;
127
+
128
+ foreach ($lines as $line)
129
+ {
130
+ if (chop($line) === '')
131
+ {
132
+ if (isset($CurrentBlock))
133
+ {
134
+ $CurrentBlock['interrupted'] = true;
135
+ }
136
+
137
+ continue;
138
+ }
139
+
140
+ if (strpos($line, "\t") !== false)
141
+ {
142
+ $parts = explode("\t", $line);
143
+
144
+ $line = $parts[0];
145
+
146
+ unset($parts[0]);
147
+
148
+ foreach ($parts as $part)
149
+ {
150
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
151
+
152
+ $line .= str_repeat(' ', $shortage);
153
+ $line .= $part;
154
+ }
155
+ }
156
+
157
+ $indent = 0;
158
+
159
+ while (isset($line[$indent]) and $line[$indent] === ' ')
160
+ {
161
+ $indent ++;
162
+ }
163
+
164
+ $text = $indent > 0 ? substr($line, $indent) : $line;
165
+
166
+ # ~
167
+
168
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
169
+
170
+ # ~
171
+
172
+ if (isset($CurrentBlock['incomplete']))
173
+ {
174
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
175
+
176
+ if (isset($Block))
177
+ {
178
+ $CurrentBlock = $Block;
179
+
180
+ continue;
181
+ }
182
+ else
183
+ {
184
+ if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
185
+ {
186
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
187
+ }
188
+
189
+ unset($CurrentBlock['incomplete']);
190
+ }
191
+ }
192
+
193
+ # ~
194
+
195
+ $marker = $text[0];
196
+
197
+ # ~
198
+
199
+ $blockTypes = $this->unmarkedBlockTypes;
200
+
201
+ if (isset($this->BlockTypes[$marker]))
202
+ {
203
+ foreach ($this->BlockTypes[$marker] as $blockType)
204
+ {
205
+ $blockTypes []= $blockType;
206
+ }
207
+ }
208
+
209
+ #
210
+ # ~
211
+
212
+ foreach ($blockTypes as $blockType)
213
+ {
214
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
215
+
216
+ if (isset($Block))
217
+ {
218
+ $Block['type'] = $blockType;
219
+
220
+ if ( ! isset($Block['identified']))
221
+ {
222
+ $Blocks []= $CurrentBlock;
223
+
224
+ $Block['identified'] = true;
225
+ }
226
+
227
+ if (method_exists($this, 'block'.$blockType.'Continue'))
228
+ {
229
+ $Block['incomplete'] = true;
230
+ }
231
+
232
+ $CurrentBlock = $Block;
233
+
234
+ continue 2;
235
+ }
236
+ }
237
+
238
+ # ~
239
+
240
+ if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
241
+ {
242
+ $CurrentBlock['element']['text'] .= "\n".$text;
243
+ }
244
+ else
245
+ {
246
+ $Blocks []= $CurrentBlock;
247
+
248
+ $CurrentBlock = $this->paragraph($Line);
249
+
250
+ $CurrentBlock['identified'] = true;
251
+ }
252
+ }
253
+
254
+ # ~
255
+
256
+ if (isset($CurrentBlock['incomplete']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
257
+ {
258
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
259
+ }
260
+
261
+ # ~
262
+
263
+ $Blocks []= $CurrentBlock;
264
+
265
+ unset($Blocks[0]);
266
+
267
+ # ~
268
+
269
+ $markup = '';
270
+
271
+ foreach ($Blocks as $Block)
272
+ {
273
+ if (isset($Block['hidden']))
274
+ {
275
+ continue;
276
+ }
277
+
278
+ $markup .= "\n";
279
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
280
+ }
281
+
282
+ $markup .= "\n";
283
+
284
+ # ~
285
+
286
+ return $markup;
287
+ }
288
+
289
+ #
290
+ # Code
291
+
292
+ protected function blockCode($Line, $Block = null)
293
+ {
294
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
295
+ {
296
+ return;
297
+ }
298
+
299
+ if ($Line['indent'] >= 4)
300
+ {
301
+ $text = substr($Line['body'], 4);
302
+
303
+ $Block = array(
304
+ 'element' => array(
305
+ 'name' => 'pre',
306
+ 'handler' => 'element',
307
+ 'text' => array(
308
+ 'name' => 'code',
309
+ 'text' => $text,
310
+ ),
311
+ ),
312
+ );
313
+
314
+ return $Block;
315
+ }
316
+ }
317
+
318
+ protected function blockCodeContinue($Line, $Block)
319
+ {
320
+ if ($Line['indent'] >= 4)
321
+ {
322
+ if (isset($Block['interrupted']))
323
+ {
324
+ $Block['element']['text']['text'] .= "\n";
325
+
326
+ unset($Block['interrupted']);
327
+ }
328
+
329
+ $Block['element']['text']['text'] .= "\n";
330
+
331
+ $text = substr($Line['body'], 4);
332
+
333
+ $Block['element']['text']['text'] .= $text;
334
+
335
+ return $Block;
336
+ }
337
+ }
338
+
339
+ protected function blockCodeComplete($Block)
340
+ {
341
+ $text = $Block['element']['text']['text'];
342
+
343
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
344
+
345
+ $Block['element']['text']['text'] = $text;
346
+
347
+ return $Block;
348
+ }
349
+
350
+ #
351
+ # Comment
352
+
353
+ protected function blockComment($Line)
354
+ {
355
+ if ($this->markupEscaped)
356
+ {
357
+ return;
358
+ }
359
+
360
+ if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
361
+ {
362
+ $Block = array(
363
+ 'markup' => $Line['body'],
364
+ );
365
+
366
+ if (preg_match('/-->$/', $Line['text']))
367
+ {
368
+ $Block['closed'] = true;
369
+ }
370
+
371
+ return $Block;
372
+ }
373
+ }
374
+
375
+ protected function blockCommentContinue($Line, array $Block)
376
+ {
377
+ if (isset($Block['closed']))
378
+ {
379
+ return;
380
+ }
381
+
382
+ $Block['markup'] .= "\n" . $Line['body'];
383
+
384
+ if (preg_match('/-->$/', $Line['text']))
385
+ {
386
+ $Block['closed'] = true;
387
+ }
388
+
389
+ return $Block;
390
+ }
391
+
392
+ #
393
+ # Fenced Code
394
+
395
+ protected function blockFencedCode($Line)
396
+ {
397
+ if (preg_match('/^(['.$Line['text'][0].']{3,})[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
398
+ {
399
+ $Element = array(
400
+ 'name' => 'code',
401
+ 'text' => '',
402
+ );
403
+
404
+ if (isset($matches[2]))
405
+ {
406
+ $class = 'language-'.$matches[2];
407
+
408
+ $Element['attributes'] = array(
409
+ 'class' => $class,
410
+ );
411
+ }
412
+
413
+ $Block = array(
414
+ 'char' => $Line['text'][0],
415
+ 'element' => array(
416
+ 'name' => 'pre',
417
+ 'handler' => 'element',
418
+ 'text' => $Element,
419
+ ),
420
+ );
421
+
422
+ return $Block;
423
+ }
424
+ }
425
+
426
+ protected function blockFencedCodeContinue($Line, $Block)
427
+ {
428
+ if (isset($Block['complete']))
429
+ {
430
+ return;
431
+ }
432
+
433
+ if (isset($Block['interrupted']))
434
+ {
435
+ $Block['element']['text']['text'] .= "\n";
436
+
437
+ unset($Block['interrupted']);
438
+ }
439
+
440
+ if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
441
+ {
442
+ $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
443
+
444
+ $Block['complete'] = true;
445
+
446
+ return $Block;
447
+ }
448
+
449
+ $Block['element']['text']['text'] .= "\n".$Line['body'];;
450
+
451
+ return $Block;
452
+ }
453
+
454
+ protected function blockFencedCodeComplete($Block)
455
+ {
456
+ $text = $Block['element']['text']['text'];
457
+
458
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
459
+
460
+ $Block['element']['text']['text'] = $text;
461
+
462
+ return $Block;
463
+ }
464
+
465
+ #
466
+ # Header
467
+
468
+ protected function blockHeader($Line)
469
+ {
470
+ if (isset($Line['text'][1]))
471
+ {
472
+ $level = 1;
473
+
474
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
475
+ {
476
+ $level ++;
477
+ }
478
+
479
+ if ($level > 6)
480
+ {
481
+ return;
482
+ }
483
+
484
+ $text = trim($Line['text'], '# ');
485
+
486
+ $Block = array(
487
+ 'element' => array(
488
+ 'name' => 'h' . min(6, $level),
489
+ 'text' => $text,
490
+ 'handler' => 'line',
491
+ ),
492
+ );
493
+
494
+ return $Block;
495
+ }
496
+ }
497
+
498
+ #
499
+ # List
500
+
501
+ protected function blockList($Line)
502
+ {
503
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
504
+
505
+ if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
506
+ {
507
+ $Block = array(
508
+ 'indent' => $Line['indent'],
509
+ 'pattern' => $pattern,
510
+ 'element' => array(
511
+ 'name' => $name,
512
+ 'handler' => 'elements',
513
+ ),
514
+ );
515
+
516
+ $Block['li'] = array(
517
+ 'name' => 'li',
518
+ 'handler' => 'li',
519
+ 'text' => array(
520
+ $matches[2],
521
+ ),
522
+ );
523
+
524
+ $Block['element']['text'] []= & $Block['li'];
525
+
526
+ return $Block;
527
+ }
528
+ }
529
+
530
+ protected function blockListContinue($Line, array $Block)
531
+ {
532
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
533
+ {
534
+ if (isset($Block['interrupted']))
535
+ {
536
+ $Block['li']['text'] []= '';
537
+
538
+ unset($Block['interrupted']);
539
+ }
540
+
541
+ unset($Block['li']);
542
+
543
+ $text = isset($matches[1]) ? $matches[1] : '';
544
+
545
+ $Block['li'] = array(
546
+ 'name' => 'li',
547
+ 'handler' => 'li',
548
+ 'text' => array(
549
+ $text,
550
+ ),
551
+ );
552
+
553
+ $Block['element']['text'] []= & $Block['li'];
554
+
555
+ return $Block;
556
+ }
557
+
558
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
559
+ {
560
+ return $Block;
561
+ }
562
+
563
+ if ( ! isset($Block['interrupted']))
564
+ {
565
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
566
+
567
+ $Block['li']['text'] []= $text;
568
+
569
+ return $Block;
570
+ }
571
+
572
+ if ($Line['indent'] > 0)
573
+ {
574
+ $Block['li']['text'] []= '';
575
+
576
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
577
+
578
+ $Block['li']['text'] []= $text;
579
+
580
+ unset($Block['interrupted']);
581
+
582
+ return $Block;
583
+ }
584
+ }
585
+
586
+ #
587
+ # Quote
588
+
589
+ protected function blockQuote($Line)
590
+ {
591
+ if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
592
+ {
593
+ $Block = array(
594
+ 'element' => array(
595
+ 'name' => 'blockquote',
596
+ 'handler' => 'lines',
597
+ 'text' => (array) $matches[1],
598
+ ),
599
+ );
600
+
601
+ return $Block;
602
+ }
603
+ }
604
+
605
+ protected function blockQuoteContinue($Line, array $Block)
606
+ {
607
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
608
+ {
609
+ if (isset($Block['interrupted']))
610
+ {
611
+ $Block['element']['text'] []= '';
612
+
613
+ unset($Block['interrupted']);
614
+ }
615
+
616
+ $Block['element']['text'] []= $matches[1];
617
+
618
+ return $Block;
619
+ }
620
+
621
+ if ( ! isset($Block['interrupted']))
622
+ {
623
+ $Block['element']['text'] []= $Line['text'];
624
+
625
+ return $Block;
626
+ }
627
+ }
628
+
629
+ #
630
+ # Rule
631
+
632
+ protected function blockRule($Line)
633
+ {
634
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
635
+ {
636
+ $Block = array(
637
+ 'element' => array(
638
+ 'name' => 'hr'
639
+ ),
640
+ );
641
+
642
+ return $Block;
643
+ }
644
+ }
645
+
646
+ #
647
+ # Setext
648
+
649
+ protected function blockSetextHeader($Line, array $Block = null)
650
+ {
651
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
652
+ {
653
+ return;
654
+ }
655
+
656
+ if (chop($Line['text'], $Line['text'][0]) === '')
657
+ {
658
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
659
+
660
+ return $Block;
661
+ }
662
+ }
663
+
664
+ #
665
+ # Markup
666
+
667
+ protected function blockMarkup($Line)
668
+ {
669
+ if ($this->markupEscaped)
670
+ {
671
+ return;
672
+ }
673
+
674
+ if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
675
+ {
676
+ if (in_array($matches[1], $this->textLevelElements))
677
+ {
678
+ return;
679
+ }
680
+
681
+ $Block = array(
682
+ 'name' => $matches[1],
683
+ 'depth' => 0,
684
+ 'markup' => $Line['text'],
685
+ );
686
+
687
+ $length = strlen($matches[0]);
688
+
689
+ $remainder = substr($Line['text'], $length);
690
+
691
+ if (trim($remainder) === '')
692
+ {
693
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
694
+ {
695
+ $Block['closed'] = true;
696
+
697
+ $Block['void'] = true;
698
+ }
699
+ }
700
+ else
701
+ {
702
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
703
+ {
704
+ return;
705
+ }
706
+
707
+ if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
708
+ {
709
+ $Block['closed'] = true;
710
+ }
711
+ }
712
+
713
+ return $Block;
714
+ }
715
+ }
716
+
717
+ protected function blockMarkupContinue($Line, array $Block)
718
+ {
719
+ if (isset($Block['closed']))
720
+ {
721
+ return;
722
+ }
723
+
724
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
725
+ {
726
+ $Block['depth'] ++;
727
+ }
728
+
729
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
730
+ {
731
+ if ($Block['depth'] > 0)
732
+ {
733
+ $Block['depth'] --;
734
+ }
735
+ else
736
+ {
737
+ $Block['closed'] = true;
738
+ }
739
+
740
+ $Block['markup'] .= $matches[1];
741
+ }
742
+
743
+ if (isset($Block['interrupted']))
744
+ {
745
+ $Block['markup'] .= "\n";
746
+
747
+ unset($Block['interrupted']);
748
+ }
749
+
750
+ $Block['markup'] .= "\n".$Line['body'];
751
+
752
+ return $Block;
753
+ }
754
+
755
+ #
756
+ # Reference
757
+
758
+ protected function blockReference($Line)
759
+ {
760
+ if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
761
+ {
762
+ $id = strtolower($matches[1]);
763
+
764
+ $Data = array(
765
+ 'url' => $matches[2],
766
+ 'title' => null,
767
+ );
768
+
769
+ if (isset($matches[3]))
770
+ {
771
+ $Data['title'] = $matches[3];
772
+ }
773
+
774
+ $this->DefinitionData['Reference'][$id] = $Data;
775
+
776
+ $Block = array(
777
+ 'hidden' => true,
778
+ );
779
+
780
+ return $Block;
781
+ }
782
+ }
783
+
784
+ #
785
+ # Table
786
+
787
+ protected function blockTable($Line, array $Block = null)
788
+ {
789
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
790
+ {
791
+ return;
792
+ }
793
+
794
+ if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
795
+ {
796
+ $alignments = array();
797
+
798
+ $divider = $Line['text'];
799
+
800
+ $divider = trim($divider);
801
+ $divider = trim($divider, '|');
802
+
803
+ $dividerCells = explode('|', $divider);
804
+
805
+ foreach ($dividerCells as $dividerCell)
806
+ {
807
+ $dividerCell = trim($dividerCell);
808
+
809
+ if ($dividerCell === '')
810
+ {
811
+ continue;
812
+ }
813
+
814
+ $alignment = null;
815
+
816
+ if ($dividerCell[0] === ':')
817
+ {
818
+ $alignment = 'left';
819
+ }
820
+
821
+ if (substr($dividerCell, - 1) === ':')
822
+ {
823
+ $alignment = $alignment === 'left' ? 'center' : 'right';
824
+ }
825
+
826
+ $alignments []= $alignment;
827
+ }
828
+
829
+ # ~
830
+
831
+ $HeaderElements = array();
832
+
833
+ $header = $Block['element']['text'];
834
+
835
+ $header = trim($header);
836
+ $header = trim($header, '|');
837
+
838
+ $headerCells = explode('|', $header);
839
+
840
+ foreach ($headerCells as $index => $headerCell)
841
+ {
842
+ $headerCell = trim($headerCell);
843
+
844
+ $HeaderElement = array(
845
+ 'name' => 'th',
846
+ 'text' => $headerCell,
847
+ 'handler' => 'line',
848
+ );
849
+
850
+ if (isset($alignments[$index]))
851
+ {
852
+ $alignment = $alignments[$index];
853
+
854
+ $HeaderElement['attributes'] = array(
855
+ 'style' => 'text-align: '.$alignment.';',
856
+ );
857
+ }
858
+
859
+ $HeaderElements []= $HeaderElement;
860
+ }
861
+
862
+ # ~
863
+
864
+ $Block = array(
865
+ 'alignments' => $alignments,
866
+ 'identified' => true,
867
+ 'element' => array(
868
+ 'name' => 'table',
869
+ 'handler' => 'elements',
870
+ ),
871
+ );
872
+
873
+ $Block['element']['text'] []= array(
874
+ 'name' => 'thead',
875
+ 'handler' => 'elements',
876
+ );
877
+
878
+ $Block['element']['text'] []= array(
879
+ 'name' => 'tbody',
880
+ 'handler' => 'elements',
881
+ 'text' => array(),
882
+ );
883
+
884
+ $Block['element']['text'][0]['text'] []= array(
885
+ 'name' => 'tr',
886
+ 'handler' => 'elements',
887
+ 'text' => $HeaderElements,
888
+ );
889
+
890
+ return $Block;
891
+ }
892
+ }
893
+
894
+ protected function blockTableContinue($Line, array $Block)
895
+ {
896
+ if (isset($Block['interrupted']))
897
+ {
898
+ return;
899
+ }
900
+
901
+ if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
902
+ {
903
+ $Elements = array();
904
+
905
+ $row = $Line['text'];
906
+
907
+ $row = trim($row);
908
+ $row = trim($row, '|');
909
+
910
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
911
+
912
+ foreach ($matches[0] as $index => $cell)
913
+ {
914
+ $cell = trim($cell);
915
+
916
+ $Element = array(
917
+ 'name' => 'td',
918
+ 'handler' => 'line',
919
+ 'text' => $cell,
920
+ );
921
+
922
+ if (isset($Block['alignments'][$index]))
923
+ {
924
+ $Element['attributes'] = array(
925
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
926
+ );
927
+ }
928
+
929
+ $Elements []= $Element;
930
+ }
931
+
932
+ $Element = array(
933
+ 'name' => 'tr',
934
+ 'handler' => 'elements',
935
+ 'text' => $Elements,
936
+ );
937
+
938
+ $Block['element']['text'][1]['text'] []= $Element;
939
+
940
+ return $Block;
941
+ }
942
+ }
943
+
944
+ #
945
+ # ~
946
+ #
947
+
948
+ protected function paragraph($Line)
949
+ {
950
+ $Block = array(
951
+ 'element' => array(
952
+ 'name' => 'p',
953
+ 'text' => $Line['text'],
954
+ 'handler' => 'line',
955
+ ),
956
+ );
957
+
958
+ return $Block;
959
+ }
960
+
961
+ #
962
+ # Inline Elements
963
+ #
964
+
965
+ protected $InlineTypes = array(
966
+ '"' => array('SpecialCharacter'),
967
+ '!' => array('Image'),
968
+ '&' => array('SpecialCharacter'),
969
+ '*' => array('Emphasis'),
970
+ ':' => array('Url'),
971
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
972
+ '>' => array('SpecialCharacter'),
973
+ '[' => array('Link'),
974
+ '_' => array('Emphasis'),
975
+ '`' => array('Code'),
976
+ '~' => array('Strikethrough'),
977
+ '\\' => array('EscapeSequence'),
978
+ );
979
+
980
+ # ~
981
+
982
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
983
+
984
+ #
985
+ # ~
986
+ #
987
+
988
+ public function line($text)
989
+ {
990
+ $markup = '';
991
+
992
+ $unexaminedText = $text;
993
+
994
+ $markerPosition = 0;
995
+
996
+ while ($excerpt = strpbrk($unexaminedText, $this->inlineMarkerList))
997
+ {
998
+ $marker = $excerpt[0];
999
+
1000
+ $markerPosition += strpos($unexaminedText, $marker);
1001
+
1002
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
1003
+
1004
+ foreach ($this->InlineTypes[$marker] as $inlineType)
1005
+ {
1006
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
1007
+
1008
+ if ( ! isset($Inline))
1009
+ {
1010
+ continue;
1011
+ }
1012
+
1013
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition) # position is ahead of marker
1014
+ {
1015
+ continue;
1016
+ }
1017
+
1018
+ if ( ! isset($Inline['position']))
1019
+ {
1020
+ $Inline['position'] = $markerPosition;
1021
+ }
1022
+
1023
+ $unmarkedText = substr($text, 0, $Inline['position']);
1024
+
1025
+ $markup .= $this->unmarkedText($unmarkedText);
1026
+
1027
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1028
+
1029
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
1030
+
1031
+ $unexaminedText = $text;
1032
+
1033
+ $markerPosition = 0;
1034
+
1035
+ continue 2;
1036
+ }
1037
+
1038
+ $unexaminedText = substr($excerpt, 1);
1039
+
1040
+ $markerPosition ++;
1041
+ }
1042
+
1043
+ $markup .= $this->unmarkedText($text);
1044
+
1045
+ return $markup;
1046
+ }
1047
+
1048
+ #
1049
+ # ~
1050
+ #
1051
+
1052
+ protected function inlineCode($Excerpt)
1053
+ {
1054
+ $marker = $Excerpt['text'][0];
1055
+
1056
+ if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1057
+ {
1058
+ $text = $matches[2];
1059
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
1060
+ $text = preg_replace("/[ ]*\n/", ' ', $text);
1061
+
1062
+ return array(
1063
+ 'extent' => strlen($matches[0]),
1064
+ 'element' => array(
1065
+ 'name' => 'code',
1066
+ 'text' => $text,
1067
+ ),
1068
+ );
1069
+ }
1070
+ }
1071
+
1072
+ protected function inlineEmailTag($Excerpt)
1073
+ {
1074
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1075
+ {
1076
+ $url = $matches[1];
1077
+
1078
+ if ( ! isset($matches[2]))
1079
+ {
1080
+ $url = 'mailto:' . $url;
1081
+ }
1082
+
1083
+ return array(
1084
+ 'extent' => strlen($matches[0]),
1085
+ 'element' => array(
1086
+ 'name' => 'a',
1087
+ 'text' => $matches[1],
1088
+ 'attributes' => array(
1089
+ 'href' => $url,
1090
+ ),
1091
+ ),
1092
+ );
1093
+ }
1094
+ }
1095
+
1096
+ protected function inlineEmphasis($Excerpt)
1097
+ {
1098
+ if ( ! isset($Excerpt['text'][1]))
1099
+ {
1100
+ return;
1101
+ }
1102
+
1103
+ $marker = $Excerpt['text'][0];
1104
+
1105
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1106
+ {
1107
+ $emphasis = 'strong';
1108
+ }
1109
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1110
+ {
1111
+ $emphasis = 'em';
1112
+ }
1113
+ else
1114
+ {
1115
+ return;
1116
+ }
1117
+
1118
+ return array(
1119
+ 'extent' => strlen($matches[0]),
1120
+ 'element' => array(
1121
+ 'name' => $emphasis,
1122
+ 'handler' => 'line',
1123
+ 'text' => $matches[1],
1124
+ ),
1125
+ );
1126
+ }
1127
+
1128
+ protected function inlineEscapeSequence($Excerpt)
1129
+ {
1130
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1131
+ {
1132
+ return array(
1133
+ 'markup' => $Excerpt['text'][1],
1134
+ 'extent' => 2,
1135
+ );
1136
+ }
1137
+ }
1138
+
1139
+ protected function inlineImage($Excerpt)
1140
+ {
1141
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1142
+ {
1143
+ return;
1144
+ }
1145
+
1146
+ $Excerpt['text']= substr($Excerpt['text'], 1);
1147
+
1148
+ $Link = $this->inlineLink($Excerpt);
1149
+
1150
+ if ($Link === null)
1151
+ {
1152
+ return;
1153
+ }
1154
+
1155
+ $Inline = array(
1156
+ 'extent' => $Link['extent'] + 1,
1157
+ 'element' => array(
1158
+ 'name' => 'img',
1159
+ 'attributes' => array(
1160
+ 'src' => $Link['element']['attributes']['href'],
1161
+ 'alt' => $Link['element']['text'],
1162
+ ),
1163
+ ),
1164
+ );
1165
+
1166
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
1167
+
1168
+ unset($Inline['element']['attributes']['href']);
1169
+
1170
+ return $Inline;
1171
+ }
1172
+
1173
+ protected function inlineLink($Excerpt)
1174
+ {
1175
+ $Element = array(
1176
+ 'name' => 'a',
1177
+ 'handler' => 'line',
1178
+ 'text' => null,
1179
+ 'attributes' => array(
1180
+ 'href' => null,
1181
+ 'title' => null,
1182
+ ),
1183
+ );
1184
+
1185
+ $extent = 0;
1186
+
1187
+ $remainder = $Excerpt['text'];
1188
+
1189
+ if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
1190
+ {
1191
+ $Element['text'] = $matches[1];
1192
+
1193
+ $extent += strlen($matches[0]);
1194
+
1195
+ $remainder = substr($remainder, $extent);
1196
+ }
1197
+ else
1198
+ {
1199
+ return;
1200
+ }
1201
+
1202
+ if (preg_match('/^[(]((?:[^ (]|[(][^ )]+[)])+)(?:[ ]+("[^"]+"|\'[^\']+\'))?[)]/', $remainder, $matches))
1203
+ {
1204
+ $Element['attributes']['href'] = $matches[1];
1205
+
1206
+ if (isset($matches[2]))
1207
+ {
1208
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1209
+ }
1210
+
1211
+ $extent += strlen($matches[0]);
1212
+ }
1213
+ else
1214
+ {
1215
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1216
+ {
1217
+ $definition = $matches[1] ? $matches[1] : $Element['text'];
1218
+ $definition = strtolower($definition);
1219
+
1220
+ $extent += strlen($matches[0]);
1221
+ }
1222
+ else
1223
+ {
1224
+ $definition = strtolower($Element['text']);
1225
+ }
1226
+
1227
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
1228
+ {
1229
+ return;
1230
+ }
1231
+
1232
+ $Definition = $this->DefinitionData['Reference'][$definition];
1233
+
1234
+ $Element['attributes']['href'] = $Definition['url'];
1235
+ $Element['attributes']['title'] = $Definition['title'];
1236
+ }
1237
+
1238
+ $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
1239
+
1240
+ return array(
1241
+ 'extent' => $extent,
1242
+ 'element' => $Element,
1243
+ );
1244
+ }
1245
+
1246
+ protected function inlineMarkup($Excerpt)
1247
+ {
1248
+ if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
1249
+ {
1250
+ return;
1251
+ }
1252
+
1253
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
1254
+ {
1255
+ return array(
1256
+ 'markup' => $matches[0],
1257
+ 'extent' => strlen($matches[0]),
1258
+ );
1259
+ }
1260
+
1261
+ if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1262
+ {
1263
+ return array(
1264
+ 'markup' => $matches[0],
1265
+ 'extent' => strlen($matches[0]),
1266
+ );
1267
+ }
1268
+
1269
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1270
+ {
1271
+ return array(
1272
+ 'markup' => $matches[0],
1273
+ 'extent' => strlen($matches[0]),
1274
+ );
1275
+ }
1276
+ }
1277
+
1278
+ protected function inlineSpecialCharacter($Excerpt)
1279
+ {
1280
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1281
+ {
1282
+ return array(
1283
+ 'markup' => '&amp;',
1284
+ 'extent' => 1,
1285
+ );
1286
+ }
1287
+
1288
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1289
+
1290
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1291
+ {
1292
+ return array(
1293
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1294
+ 'extent' => 1,
1295
+ );
1296
+ }
1297
+ }
1298
+
1299
+ protected function inlineStrikethrough($Excerpt)
1300
+ {
1301
+ if ( ! isset($Excerpt['text'][1]))
1302
+ {
1303
+ return;
1304
+ }
1305
+
1306
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1307
+ {
1308
+ return array(
1309
+ 'extent' => strlen($matches[0]),
1310
+ 'element' => array(
1311
+ 'name' => 'del',
1312
+ 'text' => $matches[1],
1313
+ 'handler' => 'line',
1314
+ ),
1315
+ );
1316
+ }
1317
+ }
1318
+
1319
+ protected function inlineUrl($Excerpt)
1320
+ {
1321
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1322
+ {
1323
+ return;
1324
+ }
1325
+
1326
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1327
+ {
1328
+ $Inline = array(
1329
+ 'extent' => strlen($matches[0][0]),
1330
+ 'position' => $matches[0][1],
1331
+ 'element' => array(
1332
+ 'name' => 'a',
1333
+ 'text' => $matches[0][0],
1334
+ 'attributes' => array(
1335
+ 'href' => $matches[0][0],
1336
+ ),
1337
+ ),
1338
+ );
1339
+
1340
+ return $Inline;
1341
+ }
1342
+ }
1343
+
1344
+ protected function inlineUrlTag($Excerpt)
1345
+ {
1346
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1347
+ {
1348
+ $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
1349
+
1350
+ return array(
1351
+ 'extent' => strlen($matches[0]),
1352
+ 'element' => array(
1353
+ 'name' => 'a',
1354
+ 'text' => $url,
1355
+ 'attributes' => array(
1356
+ 'href' => $url,
1357
+ ),
1358
+ ),
1359
+ );
1360
+ }
1361
+ }
1362
+
1363
+ #
1364
+ # ~
1365
+
1366
+ protected $unmarkedInlineTypes = array("\n" => 'Break', '://' => 'Url');
1367
+
1368
+ # ~
1369
+
1370
+ protected function unmarkedText($text)
1371
+ {
1372
+ if ($this->breaksEnabled)
1373
+ {
1374
+ $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1375
+ }
1376
+ else
1377
+ {
1378
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1379
+ $text = str_replace(" \n", "\n", $text);
1380
+ }
1381
+
1382
+ return $text;
1383
+ }
1384
+
1385
+ #
1386
+ # Handlers
1387
+ #
1388
+
1389
+ protected function element(array $Element)
1390
+ {
1391
+ $markup = '<'.$Element['name'];
1392
+
1393
+ if (isset($Element['attributes']))
1394
+ {
1395
+ foreach ($Element['attributes'] as $name => $value)
1396
+ {
1397
+ if ($value === null)
1398
+ {
1399
+ continue;
1400
+ }
1401
+
1402
+ $markup .= ' '.$name.'="'.$value.'"';
1403
+ }
1404
+ }
1405
+
1406
+ if (isset($Element['text']))
1407
+ {
1408
+ $markup .= '>';
1409
+
1410
+ if (isset($Element['handler']))
1411
+ {
1412
+ $markup .= $this->{$Element['handler']}($Element['text']);
1413
+ }
1414
+ else
1415
+ {
1416
+ $markup .= $Element['text'];
1417
+ }
1418
+
1419
+ $markup .= '</'.$Element['name'].'>';
1420
+ }
1421
+ else
1422
+ {
1423
+ $markup .= ' />';
1424
+ }
1425
+
1426
+ return $markup;
1427
+ }
1428
+
1429
+ protected function elements(array $Elements)
1430
+ {
1431
+ $markup = '';
1432
+
1433
+ foreach ($Elements as $Element)
1434
+ {
1435
+ $markup .= "\n" . $this->element($Element);
1436
+ }
1437
+
1438
+ $markup .= "\n";
1439
+
1440
+ return $markup;
1441
+ }
1442
+
1443
+ # ~
1444
+
1445
+ protected function li($lines)
1446
+ {
1447
+ $markup = $this->lines($lines);
1448
+
1449
+ $trimmedMarkup = trim($markup);
1450
+
1451
+ if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1452
+ {
1453
+ $markup = $trimmedMarkup;
1454
+ $markup = substr($markup, 3);
1455
+
1456
+ $position = strpos($markup, "</p>");
1457
+
1458
+ $markup = substr_replace($markup, '', $position, 4);
1459
+ }
1460
+
1461
+ return $markup;
1462
+ }
1463
+
1464
+ #
1465
+ # Deprecated Methods
1466
+ #
1467
+
1468
+ function parse($text)
1469
+ {
1470
+ $markup = $this->text($text);
1471
+
1472
+ return $markup;
1473
+ }
1474
+
1475
+ #
1476
+ # Static Methods
1477
+ #
1478
+
1479
+ static function instance($name = 'default')
1480
+ {
1481
+ if (isset(self::$instances[$name]))
1482
+ {
1483
+ return self::$instances[$name];
1484
+ }
1485
+
1486
+ $instance = new self();
1487
+
1488
+ self::$instances[$name] = $instance;
1489
+
1490
+ return $instance;
1491
+ }
1492
+
1493
+ private static $instances = array();
1494
+
1495
+ #
1496
+ # Fields
1497
+ #
1498
+
1499
+ protected $DefinitionData;
1500
+
1501
+ #
1502
+ # Read-Only
1503
+
1504
+ protected $specialCharacters = array(
1505
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1506
+ );
1507
+
1508
+ protected $StrongRegex = array(
1509
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1510
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1511
+ );
1512
+
1513
+ protected $EmRegex = array(
1514
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1515
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1516
+ );
1517
+
1518
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1519
+
1520
+ protected $voidElements = array(
1521
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1522
+ );
1523
+
1524
+ protected $textLevelElements = array(
1525
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1526
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1527
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1528
+ 'q', 'rt', 'ins', 'font', 'strong',
1529
+ 's', 'tt', 'sub', 'mark',
1530
+ 'u', 'xm', 'sup', 'nobr',
1531
+ 'var', 'ruby',
1532
+ 'wbr', 'span',
1533
+ 'time',
1534
+ );
1535
+ }
updater/vendor/ParsedownModern.php ADDED
@@ -0,0 +1,1538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ #
4
+ #
5
+ # Parsedown
6
+ # http://parsedown.org
7
+ #
8
+ # (c) Emanuil Rusev
9
+ # http://erusev.com
10
+ #
11
+ # For the full license information, view the LICENSE file that was distributed
12
+ # with this source code.
13
+ #
14
+ #
15
+
16
+ class Parsedown
17
+ {
18
+ # ~
19
+
20
+ const version = '1.6.0';
21
+
22
+ # ~
23
+
24
+ function text($text)
25
+ {
26
+ # make sure no definitions are set
27
+ $this->DefinitionData = array();
28
+
29
+ # standardize line breaks
30
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
31
+
32
+ # remove surrounding line breaks
33
+ $text = trim($text, "\n");
34
+
35
+ # split text into lines
36
+ $lines = explode("\n", $text);
37
+
38
+ # iterate through lines to identify blocks
39
+ $markup = $this->lines($lines);
40
+
41
+ # trim line breaks
42
+ $markup = trim($markup, "\n");
43
+
44
+ return $markup;
45
+ }
46
+
47
+ #
48
+ # Setters
49
+ #
50
+
51
+ function setBreaksEnabled($breaksEnabled)
52
+ {
53
+ $this->breaksEnabled = $breaksEnabled;
54
+
55
+ return $this;
56
+ }
57
+
58
+ protected $breaksEnabled;
59
+
60
+ function setMarkupEscaped($markupEscaped)
61
+ {
62
+ $this->markupEscaped = $markupEscaped;
63
+
64
+ return $this;
65
+ }
66
+
67
+ protected $markupEscaped;
68
+
69
+ function setUrlsLinked($urlsLinked)
70
+ {
71
+ $this->urlsLinked = $urlsLinked;
72
+
73
+ return $this;
74
+ }
75
+
76
+ protected $urlsLinked = true;
77
+
78
+ #
79
+ # Lines
80
+ #
81
+
82
+ protected $BlockTypes = array(
83
+ '#' => array('Header'),
84
+ '*' => array('Rule', 'List'),
85
+ '+' => array('List'),
86
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
87
+ '0' => array('List'),
88
+ '1' => array('List'),
89
+ '2' => array('List'),
90
+ '3' => array('List'),
91
+ '4' => array('List'),
92
+ '5' => array('List'),
93
+ '6' => array('List'),
94
+ '7' => array('List'),
95
+ '8' => array('List'),
96
+ '9' => array('List'),
97
+ ':' => array('Table'),
98
+ '<' => array('Comment', 'Markup'),
99
+ '=' => array('SetextHeader'),
100
+ '>' => array('Quote'),
101
+ '[' => array('Reference'),
102
+ '_' => array('Rule'),
103
+ '`' => array('FencedCode'),
104
+ '|' => array('Table'),
105
+ '~' => array('FencedCode'),
106
+ );
107
+
108
+ # ~
109
+
110
+ protected $unmarkedBlockTypes = array(
111
+ 'Code',
112
+ );
113
+
114
+ #
115
+ # Blocks
116
+ #
117
+
118
+ protected function lines(array $lines)
119
+ {
120
+ $CurrentBlock = null;
121
+
122
+ foreach ($lines as $line)
123
+ {
124
+ if (chop($line) === '')
125
+ {
126
+ if (isset($CurrentBlock))
127
+ {
128
+ $CurrentBlock['interrupted'] = true;
129
+ }
130
+
131
+ continue;
132
+ }
133
+
134
+ if (strpos($line, "\t") !== false)
135
+ {
136
+ $parts = explode("\t", $line);
137
+
138
+ $line = $parts[0];
139
+
140
+ unset($parts[0]);
141
+
142
+ foreach ($parts as $part)
143
+ {
144
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
145
+
146
+ $line .= str_repeat(' ', $shortage);
147
+ $line .= $part;
148
+ }
149
+ }
150
+
151
+ $indent = 0;
152
+
153
+ while (isset($line[$indent]) and $line[$indent] === ' ')
154
+ {
155
+ $indent ++;
156
+ }
157
+
158
+ $text = $indent > 0 ? substr($line, $indent) : $line;
159
+
160
+ # ~
161
+
162
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
163
+
164
+ # ~
165
+
166
+ if (isset($CurrentBlock['continuable']))
167
+ {
168
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
169
+
170
+ if (isset($Block))
171
+ {
172
+ $CurrentBlock = $Block;
173
+
174
+ continue;
175
+ }
176
+ else
177
+ {
178
+ if ($this->isBlockCompletable($CurrentBlock['type']))
179
+ {
180
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
181
+ }
182
+ }
183
+ }
184
+
185
+ # ~
186
+
187
+ $marker = $text[0];
188
+
189
+ # ~
190
+
191
+ $blockTypes = $this->unmarkedBlockTypes;
192
+
193
+ if (isset($this->BlockTypes[$marker]))
194
+ {
195
+ foreach ($this->BlockTypes[$marker] as $blockType)
196
+ {
197
+ $blockTypes []= $blockType;
198
+ }
199
+ }
200
+
201
+ #
202
+ # ~
203
+
204
+ foreach ($blockTypes as $blockType)
205
+ {
206
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
207
+
208
+ if (isset($Block))
209
+ {
210
+ $Block['type'] = $blockType;
211
+
212
+ if ( ! isset($Block['identified']))
213
+ {
214
+ $Blocks []= $CurrentBlock;
215
+
216
+ $Block['identified'] = true;
217
+ }
218
+
219
+ if ($this->isBlockContinuable($blockType))
220
+ {
221
+ $Block['continuable'] = true;
222
+ }
223
+
224
+ $CurrentBlock = $Block;
225
+
226
+ continue 2;
227
+ }
228
+ }
229
+
230
+ # ~
231
+
232
+ if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
233
+ {
234
+ $CurrentBlock['element']['text'] .= "\n".$text;
235
+ }
236
+ else
237
+ {
238
+ $Blocks []= $CurrentBlock;
239
+
240
+ $CurrentBlock = $this->paragraph($Line);
241
+
242
+ $CurrentBlock['identified'] = true;
243
+ }
244
+ }
245
+
246
+ # ~
247
+
248
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
249
+ {
250
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
251
+ }
252
+
253
+ # ~
254
+
255
+ $Blocks []= $CurrentBlock;
256
+
257
+ unset($Blocks[0]);
258
+
259
+ # ~
260
+
261
+ $markup = '';
262
+
263
+ foreach ($Blocks as $Block)
264
+ {
265
+ if (isset($Block['hidden']))
266
+ {
267
+ continue;
268
+ }
269
+
270
+ $markup .= "\n";
271
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
272
+ }
273
+
274
+ $markup .= "\n";
275
+
276
+ # ~
277
+
278
+ return $markup;
279
+ }
280
+
281
+ protected function isBlockContinuable($Type)
282
+ {
283
+ return method_exists($this, 'block'.$Type.'Continue');
284
+ }
285
+
286
+ protected function isBlockCompletable($Type)
287
+ {
288
+ return method_exists($this, 'block'.$Type.'Complete');
289
+ }
290
+
291
+ #
292
+ # Code
293
+
294
+ protected function blockCode($Line, $Block = null)
295
+ {
296
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
297
+ {
298
+ return;
299
+ }
300
+
301
+ if ($Line['indent'] >= 4)
302
+ {
303
+ $text = substr($Line['body'], 4);
304
+
305
+ $Block = array(
306
+ 'element' => array(
307
+ 'name' => 'pre',
308
+ 'handler' => 'element',
309
+ 'text' => array(
310
+ 'name' => 'code',
311
+ 'text' => $text,
312
+ ),
313
+ ),
314
+ );
315
+
316
+ return $Block;
317
+ }
318
+ }
319
+
320
+ protected function blockCodeContinue($Line, $Block)
321
+ {
322
+ if ($Line['indent'] >= 4)
323
+ {
324
+ if (isset($Block['interrupted']))
325
+ {
326
+ $Block['element']['text']['text'] .= "\n";
327
+
328
+ unset($Block['interrupted']);
329
+ }
330
+
331
+ $Block['element']['text']['text'] .= "\n";
332
+
333
+ $text = substr($Line['body'], 4);
334
+
335
+ $Block['element']['text']['text'] .= $text;
336
+
337
+ return $Block;
338
+ }
339
+ }
340
+
341
+ protected function blockCodeComplete($Block)
342
+ {
343
+ $text = $Block['element']['text']['text'];
344
+
345
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
346
+
347
+ $Block['element']['text']['text'] = $text;
348
+
349
+ return $Block;
350
+ }
351
+
352
+ #
353
+ # Comment
354
+
355
+ protected function blockComment($Line)
356
+ {
357
+ if ($this->markupEscaped)
358
+ {
359
+ return;
360
+ }
361
+
362
+ if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
363
+ {
364
+ $Block = array(
365
+ 'markup' => $Line['body'],
366
+ );
367
+
368
+ if (preg_match('/-->$/', $Line['text']))
369
+ {
370
+ $Block['closed'] = true;
371
+ }
372
+
373
+ return $Block;
374
+ }
375
+ }
376
+
377
+ protected function blockCommentContinue($Line, array $Block)
378
+ {
379
+ if (isset($Block['closed']))
380
+ {
381
+ return;
382
+ }
383
+
384
+ $Block['markup'] .= "\n" . $Line['body'];
385
+
386
+ if (preg_match('/-->$/', $Line['text']))
387
+ {
388
+ $Block['closed'] = true;
389
+ }
390
+
391
+ return $Block;
392
+ }
393
+
394
+ #
395
+ # Fenced Code
396
+
397
+ protected function blockFencedCode($Line)
398
+ {
399
+ if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
400
+ {
401
+ $Element = array(
402
+ 'name' => 'code',
403
+ 'text' => '',
404
+ );
405
+
406
+ if (isset($matches[1]))
407
+ {
408
+ $class = 'language-'.$matches[1];
409
+
410
+ $Element['attributes'] = array(
411
+ 'class' => $class,
412
+ );
413
+ }
414
+
415
+ $Block = array(
416
+ 'char' => $Line['text'][0],
417
+ 'element' => array(
418
+ 'name' => 'pre',
419
+ 'handler' => 'element',
420
+ 'text' => $Element,
421
+ ),
422
+ );
423
+
424
+ return $Block;
425
+ }
426
+ }
427
+
428
+ protected function blockFencedCodeContinue($Line, $Block)
429
+ {
430
+ if (isset($Block['complete']))
431
+ {
432
+ return;
433
+ }
434
+
435
+ if (isset($Block['interrupted']))
436
+ {
437
+ $Block['element']['text']['text'] .= "\n";
438
+
439
+ unset($Block['interrupted']);
440
+ }
441
+
442
+ if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
443
+ {
444
+ $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
445
+
446
+ $Block['complete'] = true;
447
+
448
+ return $Block;
449
+ }
450
+
451
+ $Block['element']['text']['text'] .= "\n".$Line['body'];;
452
+
453
+ return $Block;
454
+ }
455
+
456
+ protected function blockFencedCodeComplete($Block)
457
+ {
458
+ $text = $Block['element']['text']['text'];
459
+
460
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
461
+
462
+ $Block['element']['text']['text'] = $text;
463
+
464
+ return $Block;
465
+ }
466
+
467
+ #
468
+ # Header
469
+
470
+ protected function blockHeader($Line)
471
+ {
472
+ if (isset($Line['text'][1]))
473
+ {
474
+ $level = 1;
475
+
476
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
477
+ {
478
+ $level ++;
479
+ }
480
+
481
+ if ($level > 6)
482
+ {
483
+ return;
484
+ }
485
+
486
+ $text = trim($Line['text'], '# ');
487
+
488
+ $Block = array(
489
+ 'element' => array(
490
+ 'name' => 'h' . min(6, $level),
491
+ 'text' => $text,
492
+ 'handler' => 'line',
493
+ ),
494
+ );
495
+
496
+ return $Block;
497
+ }
498
+ }
499
+
500
+ #
501
+ # List
502
+
503
+ protected function blockList($Line)
504
+ {
505
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
506
+
507
+ if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
508
+ {
509
+ $Block = array(
510
+ 'indent' => $Line['indent'],
511
+ 'pattern' => $pattern,
512
+ 'element' => array(
513
+ 'name' => $name,
514
+ 'handler' => 'elements',
515
+ ),
516
+ );
517
+
518
+ $Block['li'] = array(
519
+ 'name' => 'li',
520
+ 'handler' => 'li',
521
+ 'text' => array(
522
+ $matches[2],
523
+ ),
524
+ );
525
+
526
+ $Block['element']['text'] []= & $Block['li'];
527
+
528
+ return $Block;
529
+ }
530
+ }
531
+
532
+ protected function blockListContinue($Line, array $Block)
533
+ {
534
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
535
+ {
536
+ if (isset($Block['interrupted']))
537
+ {
538
+ $Block['li']['text'] []= '';
539
+
540
+ unset($Block['interrupted']);
541
+ }
542
+
543
+ unset($Block['li']);
544
+
545
+ $text = isset($matches[1]) ? $matches[1] : '';
546
+
547
+ $Block['li'] = array(
548
+ 'name' => 'li',
549
+ 'handler' => 'li',
550
+ 'text' => array(
551
+ $text,
552
+ ),
553
+ );
554
+
555
+ $Block['element']['text'] []= & $Block['li'];
556
+
557
+ return $Block;
558
+ }
559
+
560
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
561
+ {
562
+ return $Block;
563
+ }
564
+
565
+ if ( ! isset($Block['interrupted']))
566
+ {
567
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
568
+
569
+ $Block['li']['text'] []= $text;
570
+
571
+ return $Block;
572
+ }
573
+
574
+ if ($Line['indent'] > 0)
575
+ {
576
+ $Block['li']['text'] []= '';
577
+
578
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
579
+
580
+ $Block['li']['text'] []= $text;
581
+
582
+ unset($Block['interrupted']);
583
+
584
+ return $Block;
585
+ }
586
+ }
587
+
588
+ #
589
+ # Quote
590
+
591
+ protected function blockQuote($Line)
592
+ {
593
+ if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
594
+ {
595
+ $Block = array(
596
+ 'element' => array(
597
+ 'name' => 'blockquote',
598
+ 'handler' => 'lines',
599
+ 'text' => (array) $matches[1],
600
+ ),
601
+ );
602
+
603
+ return $Block;
604
+ }
605
+ }
606
+
607
+ protected function blockQuoteContinue($Line, array $Block)
608
+ {
609
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
610
+ {
611
+ if (isset($Block['interrupted']))
612
+ {
613
+ $Block['element']['text'] []= '';
614
+
615
+ unset($Block['interrupted']);
616
+ }
617
+
618
+ $Block['element']['text'] []= $matches[1];
619
+
620
+ return $Block;
621
+ }
622
+
623
+ if ( ! isset($Block['interrupted']))
624
+ {
625
+ $Block['element']['text'] []= $Line['text'];
626
+
627
+ return $Block;
628
+ }
629
+ }
630
+
631
+ #
632
+ # Rule
633
+
634
+ protected function blockRule($Line)
635
+ {
636
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
637
+ {
638
+ $Block = array(
639
+ 'element' => array(
640
+ 'name' => 'hr'
641
+ ),
642
+ );
643
+
644
+ return $Block;
645
+ }
646
+ }
647
+
648
+ #
649
+ # Setext
650
+
651
+ protected function blockSetextHeader($Line, array $Block = null)
652
+ {
653
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
654
+ {
655
+ return;
656
+ }
657
+
658
+ if (chop($Line['text'], $Line['text'][0]) === '')
659
+ {
660
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
661
+
662
+ return $Block;
663
+ }
664
+ }
665
+
666
+ #
667
+ # Markup
668
+
669
+ protected function blockMarkup($Line)
670
+ {
671
+ if ($this->markupEscaped)
672
+ {
673
+ return;
674
+ }
675
+
676
+ if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
677
+ {
678
+ $element = strtolower($matches[1]);
679
+
680
+ if (in_array($element, $this->textLevelElements))
681
+ {
682
+ return;
683
+ }
684
+
685
+ $Block = array(
686
+ 'name' => $matches[1],
687
+ 'depth' => 0,
688
+ 'markup' => $Line['text'],
689
+ );
690
+
691
+ $length = strlen($matches[0]);
692
+
693
+ $remainder = substr($Line['text'], $length);
694
+
695
+ if (trim($remainder) === '')
696
+ {
697
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
698
+ {
699
+ $Block['closed'] = true;
700
+
701
+ $Block['void'] = true;
702
+ }
703
+ }
704
+ else
705
+ {
706
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
707
+ {
708
+ return;
709
+ }
710
+
711
+ if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
712
+ {
713
+ $Block['closed'] = true;
714
+ }
715
+ }
716
+
717
+ return $Block;
718
+ }
719
+ }
720
+
721
+ protected function blockMarkupContinue($Line, array $Block)
722
+ {
723
+ if (isset($Block['closed']))
724
+ {
725
+ return;
726
+ }
727
+
728
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
729
+ {
730
+ $Block['depth'] ++;
731
+ }
732
+
733
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
734
+ {
735
+ if ($Block['depth'] > 0)
736
+ {
737
+ $Block['depth'] --;
738
+ }
739
+ else
740
+ {
741
+ $Block['closed'] = true;
742
+ }
743
+ }
744
+
745
+ if (isset($Block['interrupted']))
746
+ {
747
+ $Block['markup'] .= "\n";
748
+
749
+ unset($Block['interrupted']);
750
+ }
751
+
752
+ $Block['markup'] .= "\n".$Line['body'];
753
+
754
+ return $Block;
755
+ }
756
+
757
+ #
758
+ # Reference
759
+
760
+ protected function blockReference($Line)
761
+ {
762
+ if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
763
+ {
764
+ $id = strtolower($matches[1]);
765
+
766
+ $Data = array(
767
+ 'url' => $matches[2],
768
+ 'title' => null,
769
+ );
770
+
771
+ if (isset($matches[3]))
772
+ {
773
+ $Data['title'] = $matches[3];
774
+ }
775
+
776
+ $this->DefinitionData['Reference'][$id] = $Data;
777
+
778
+ $Block = array(
779
+ 'hidden' => true,
780
+ );
781
+
782
+ return $Block;
783
+ }
784
+ }
785
+
786
+ #
787
+ # Table
788
+
789
+ protected function blockTable($Line, array $Block = null)
790
+ {
791
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
792
+ {
793
+ return;
794
+ }
795
+
796
+ if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
797
+ {
798
+ $alignments = array();
799
+
800
+ $divider = $Line['text'];
801
+
802
+ $divider = trim($divider);
803
+ $divider = trim($divider, '|');
804
+
805
+ $dividerCells = explode('|', $divider);
806
+
807
+ foreach ($dividerCells as $dividerCell)
808
+ {
809
+ $dividerCell = trim($dividerCell);
810
+
811
+ if ($dividerCell === '')
812
+ {
813
+ continue;
814
+ }
815
+
816
+ $alignment = null;
817
+
818
+ if ($dividerCell[0] === ':')
819
+ {
820
+ $alignment = 'left';
821
+ }
822
+
823
+ if (substr($dividerCell, - 1) === ':')
824
+ {
825
+ $alignment = $alignment === 'left' ? 'center' : 'right';
826
+ }
827
+
828
+ $alignments []= $alignment;
829
+ }
830
+
831
+ # ~
832
+
833
+ $HeaderElements = array();
834
+
835
+ $header = $Block['element']['text'];
836
+
837
+ $header = trim($header);
838
+ $header = trim($header, '|');
839
+
840
+ $headerCells = explode('|', $header);
841
+
842
+ foreach ($headerCells as $index => $headerCell)
843
+ {
844
+ $headerCell = trim($headerCell);
845
+
846
+ $HeaderElement = array(
847
+ 'name' => 'th',
848
+ 'text' => $headerCell,
849
+ 'handler' => 'line',
850
+ );
851
+
852
+ if (isset($alignments[$index]))
853
+ {
854
+ $alignment = $alignments[$index];
855
+
856
+ $HeaderElement['attributes'] = array(
857
+ 'style' => 'text-align: '.$alignment.';',
858
+ );
859
+ }
860
+
861
+ $HeaderElements []= $HeaderElement;
862
+ }
863
+
864
+ # ~
865
+
866
+ $Block = array(
867
+ 'alignments' => $alignments,
868
+ 'identified' => true,
869
+ 'element' => array(
870
+ 'name' => 'table',
871
+ 'handler' => 'elements',
872
+ ),
873
+ );
874
+
875
+ $Block['element']['text'] []= array(
876
+ 'name' => 'thead',
877
+ 'handler' => 'elements',
878
+ );
879
+
880
+ $Block['element']['text'] []= array(
881
+ 'name' => 'tbody',
882
+ 'handler' => 'elements',
883
+ 'text' => array(),
884
+ );
885
+
886
+ $Block['element']['text'][0]['text'] []= array(
887
+ 'name' => 'tr',
888
+ 'handler' => 'elements',
889
+ 'text' => $HeaderElements,
890
+ );
891
+
892
+ return $Block;
893
+ }
894
+ }
895
+
896
+ protected function blockTableContinue($Line, array $Block)
897
+ {
898
+ if (isset($Block['interrupted']))
899
+ {
900
+ return;
901
+ }
902
+
903
+ if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
904
+ {
905
+ $Elements = array();
906
+
907
+ $row = $Line['text'];
908
+
909
+ $row = trim($row);
910
+ $row = trim($row, '|');
911
+
912
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
913
+
914
+ foreach ($matches[0] as $index => $cell)
915
+ {
916
+ $cell = trim($cell);
917
+
918
+ $Element = array(
919
+ 'name' => 'td',
920
+ 'handler' => 'line',
921
+ 'text' => $cell,
922
+ );
923
+
924
+ if (isset($Block['alignments'][$index]))
925
+ {
926
+ $Element['attributes'] = array(
927
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
928
+ );
929
+ }
930
+
931
+ $Elements []= $Element;
932
+ }
933
+
934
+ $Element = array(
935
+ 'name' => 'tr',
936
+ 'handler' => 'elements',
937
+ 'text' => $Elements,
938
+ );
939
+
940
+ $Block['element']['text'][1]['text'] []= $Element;
941
+
942
+ return $Block;
943
+ }
944
+ }
945
+
946
+ #
947
+ # ~
948
+ #
949
+
950
+ protected function paragraph($Line)
951
+ {
952
+ $Block = array(
953
+ 'element' => array(
954
+ 'name' => 'p',
955
+ 'text' => $Line['text'],
956
+ 'handler' => 'line',
957
+ ),
958
+ );
959
+
960
+ return $Block;
961
+ }
962
+
963
+ #
964
+ # Inline Elements
965
+ #
966
+
967
+ protected $InlineTypes = array(
968
+ '"' => array('SpecialCharacter'),
969
+ '!' => array('Image'),
970
+ '&' => array('SpecialCharacter'),
971
+ '*' => array('Emphasis'),
972
+ ':' => array('Url'),
973
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
974
+ '>' => array('SpecialCharacter'),
975
+ '[' => array('Link'),
976
+ '_' => array('Emphasis'),
977
+ '`' => array('Code'),
978
+ '~' => array('Strikethrough'),
979
+ '\\' => array('EscapeSequence'),
980
+ );
981
+
982
+ # ~
983
+
984
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
985
+
986
+ #
987
+ # ~
988
+ #
989
+
990
+ public function line($text)
991
+ {
992
+ $markup = '';
993
+
994
+ # $excerpt is based on the first occurrence of a marker
995
+
996
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
997
+ {
998
+ $marker = $excerpt[0];
999
+
1000
+ $markerPosition = strpos($text, $marker);
1001
+
1002
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
1003
+
1004
+ foreach ($this->InlineTypes[$marker] as $inlineType)
1005
+ {
1006
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
1007
+
1008
+ if ( ! isset($Inline))
1009
+ {
1010
+ continue;
1011
+ }
1012
+
1013
+ # makes sure that the inline belongs to "our" marker
1014
+
1015
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1016
+ {
1017
+ continue;
1018
+ }
1019
+
1020
+ # sets a default inline position
1021
+
1022
+ if ( ! isset($Inline['position']))
1023
+ {
1024
+ $Inline['position'] = $markerPosition;
1025
+ }
1026
+
1027
+ # the text that comes before the inline
1028
+ $unmarkedText = substr($text, 0, $Inline['position']);
1029
+
1030
+ # compile the unmarked text
1031
+ $markup .= $this->unmarkedText($unmarkedText);
1032
+
1033
+ # compile the inline
1034
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1035
+
1036
+ # remove the examined text
1037
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
1038
+
1039
+ continue 2;
1040
+ }
1041
+
1042
+ # the marker does not belong to an inline
1043
+
1044
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
1045
+
1046
+ $markup .= $this->unmarkedText($unmarkedText);
1047
+
1048
+ $text = substr($text, $markerPosition + 1);
1049
+ }
1050
+
1051
+ $markup .= $this->unmarkedText($text);
1052
+
1053
+ return $markup;
1054
+ }
1055
+
1056
+ #
1057
+ # ~
1058
+ #
1059
+
1060
+ protected function inlineCode($Excerpt)
1061
+ {
1062
+ $marker = $Excerpt['text'][0];
1063
+
1064
+ if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1065
+ {
1066
+ $text = $matches[2];
1067
+ $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
1068
+ $text = preg_replace("/[ ]*\n/", ' ', $text);
1069
+
1070
+ return array(
1071
+ 'extent' => strlen($matches[0]),
1072
+ 'element' => array(
1073
+ 'name' => 'code',
1074
+ 'text' => $text,
1075
+ ),
1076
+ );
1077
+ }
1078
+ }
1079
+
1080
+ protected function inlineEmailTag($Excerpt)
1081
+ {
1082
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1083
+ {
1084
+ $url = $matches[1];
1085
+
1086
+ if ( ! isset($matches[2]))
1087
+ {
1088
+ $url = 'mailto:' . $url;
1089
+ }
1090
+
1091
+ return array(
1092
+ 'extent' => strlen($matches[0]),
1093
+ 'element' => array(
1094
+ 'name' => 'a',
1095
+ 'text' => $matches[1],
1096
+ 'attributes' => array(
1097
+ 'href' => $url,
1098
+ ),
1099
+ ),
1100
+ );
1101
+ }
1102
+ }
1103
+
1104
+ protected function inlineEmphasis($Excerpt)
1105
+ {
1106
+ if ( ! isset($Excerpt['text'][1]))
1107
+ {
1108
+ return;
1109
+ }
1110
+
1111
+ $marker = $Excerpt['text'][0];
1112
+
1113
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1114
+ {
1115
+ $emphasis = 'strong';
1116
+ }
1117
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1118
+ {
1119
+ $emphasis = 'em';
1120
+ }
1121
+ else
1122
+ {
1123
+ return;
1124
+ }
1125
+
1126
+ return array(
1127
+ 'extent' => strlen($matches[0]),
1128
+ 'element' => array(
1129
+ 'name' => $emphasis,
1130
+ 'handler' => 'line',
1131
+ 'text' => $matches[1],
1132
+ ),
1133
+ );
1134
+ }
1135
+
1136
+ protected function inlineEscapeSequence($Excerpt)
1137
+ {
1138
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1139
+ {
1140
+ return array(
1141
+ 'markup' => $Excerpt['text'][1],
1142
+ 'extent' => 2,
1143
+ );
1144
+ }
1145
+ }
1146
+
1147
+ protected function inlineImage($Excerpt)
1148
+ {
1149
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1150
+ {
1151
+ return;
1152
+ }
1153
+
1154
+ $Excerpt['text']= substr($Excerpt['text'], 1);
1155
+
1156
+ $Link = $this->inlineLink($Excerpt);
1157
+
1158
+ if ($Link === null)
1159
+ {
1160
+ return;
1161
+ }
1162
+
1163
+ $Inline = array(
1164
+ 'extent' => $Link['extent'] + 1,
1165
+ 'element' => array(
1166
+ 'name' => 'img',
1167
+ 'attributes' => array(
1168
+ 'src' => $Link['element']['attributes']['href'],
1169
+ 'alt' => $Link['element']['text'],
1170
+ ),
1171
+ ),
1172
+ );
1173
+
1174
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
1175
+
1176
+ unset($Inline['element']['attributes']['href']);
1177
+
1178
+ return $Inline;
1179
+ }
1180
+
1181
+ protected function inlineLink($Excerpt)
1182
+ {
1183
+ $Element = array(
1184
+ 'name' => 'a',
1185
+ 'handler' => 'line',
1186
+ 'text' => null,
1187
+ 'attributes' => array(
1188
+ 'href' => null,
1189
+ 'title' => null,
1190
+ ),
1191
+ );
1192
+
1193
+ $extent = 0;
1194
+
1195
+ $remainder = $Excerpt['text'];
1196
+
1197
+ if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
1198
+ {
1199
+ $Element['text'] = $matches[1];
1200
+
1201
+ $extent += strlen($matches[0]);
1202
+
1203
+ $remainder = substr($remainder, $extent);
1204
+ }
1205
+ else
1206
+ {
1207
+ return;
1208
+ }
1209
+
1210
+ if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
1211
+ {
1212
+ $Element['attributes']['href'] = $matches[1];
1213
+
1214
+ if (isset($matches[2]))
1215
+ {
1216
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1217
+ }
1218
+
1219
+ $extent += strlen($matches[0]);
1220
+ }
1221
+ else
1222
+ {
1223
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1224
+ {
1225
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1226
+ $definition = strtolower($definition);
1227
+
1228
+ $extent += strlen($matches[0]);
1229
+ }
1230
+ else
1231
+ {
1232
+ $definition = strtolower($Element['text']);
1233
+ }
1234
+
1235
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
1236
+ {
1237
+ return;
1238
+ }
1239
+
1240
+ $Definition = $this->DefinitionData['Reference'][$definition];
1241
+
1242
+ $Element['attributes']['href'] = $Definition['url'];
1243
+ $Element['attributes']['title'] = $Definition['title'];
1244
+ }
1245
+
1246
+ $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
1247
+
1248
+ return array(
1249
+ 'extent' => $extent,
1250
+ 'element' => $Element,
1251
+ );
1252
+ }
1253
+
1254
+ protected function inlineMarkup($Excerpt)
1255
+ {
1256
+ if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
1257
+ {
1258
+ return;
1259
+ }
1260
+
1261
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
1262
+ {
1263
+ return array(
1264
+ 'markup' => $matches[0],
1265
+ 'extent' => strlen($matches[0]),
1266
+ );
1267
+ }
1268
+
1269
+ if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1270
+ {
1271
+ return array(
1272
+ 'markup' => $matches[0],
1273
+ 'extent' => strlen($matches[0]),
1274
+ );
1275
+ }
1276
+
1277
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1278
+ {
1279
+ return array(
1280
+ 'markup' => $matches[0],
1281
+ 'extent' => strlen($matches[0]),
1282
+ );
1283
+ }
1284
+ }
1285
+
1286
+ protected function inlineSpecialCharacter($Excerpt)
1287
+ {
1288
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1289
+ {
1290
+ return array(
1291
+ 'markup' => '&amp;',
1292
+ 'extent' => 1,
1293
+ );
1294
+ }
1295
+
1296
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1297
+
1298
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1299
+ {
1300
+ return array(
1301
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1302
+ 'extent' => 1,
1303
+ );
1304
+ }
1305
+ }
1306
+
1307
+ protected function inlineStrikethrough($Excerpt)
1308
+ {
1309
+ if ( ! isset($Excerpt['text'][1]))
1310
+ {
1311
+ return;
1312
+ }
1313
+
1314
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1315
+ {
1316
+ return array(
1317
+ 'extent' => strlen($matches[0]),
1318
+ 'element' => array(
1319
+ 'name' => 'del',
1320
+ 'text' => $matches[1],
1321
+ 'handler' => 'line',
1322
+ ),
1323
+ );
1324
+ }
1325
+ }
1326
+
1327
+ protected function inlineUrl($Excerpt)
1328
+ {
1329
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1330
+ {
1331
+ return;
1332
+ }
1333
+
1334
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1335
+ {
1336
+ $Inline = array(
1337
+ 'extent' => strlen($matches[0][0]),
1338
+ 'position' => $matches[0][1],
1339
+ 'element' => array(
1340
+ 'name' => 'a',
1341
+ 'text' => $matches[0][0],
1342
+ 'attributes' => array(
1343
+ 'href' => $matches[0][0],
1344
+ ),
1345
+ ),
1346
+ );
1347
+
1348
+ return $Inline;
1349
+ }
1350
+ }
1351
+
1352
+ protected function inlineUrlTag($Excerpt)
1353
+ {
1354
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1355
+ {
1356
+ $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
1357
+
1358
+ return array(
1359
+ 'extent' => strlen($matches[0]),
1360
+ 'element' => array(
1361
+ 'name' => 'a',
1362
+ 'text' => $url,
1363
+ 'attributes' => array(
1364
+ 'href' => $url,
1365
+ ),
1366
+ ),
1367
+ );
1368
+ }
1369
+ }
1370
+
1371
+ # ~
1372
+
1373
+ protected function unmarkedText($text)
1374
+ {
1375
+ if ($this->breaksEnabled)
1376
+ {
1377
+ $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1378
+ }
1379
+ else
1380
+ {
1381
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1382
+ $text = str_replace(" \n", "\n", $text);
1383
+ }
1384
+
1385
+ return $text;
1386
+ }
1387
+
1388
+ #
1389
+ # Handlers
1390
+ #
1391
+
1392
+ protected function element(array $Element)
1393
+ {
1394
+ $markup = '<'.$Element['name'];
1395
+
1396
+ if (isset($Element['attributes']))
1397
+ {
1398
+ foreach ($Element['attributes'] as $name => $value)
1399
+ {
1400
+ if ($value === null)
1401
+ {
1402
+ continue;
1403
+ }
1404
+
1405
+ $markup .= ' '.$name.'="'.$value.'"';
1406
+ }
1407
+ }
1408
+
1409
+ if (isset($Element['text']))
1410
+ {
1411
+ $markup .= '>';
1412
+
1413
+ if (isset($Element['handler']))
1414
+ {
1415
+ $markup .= $this->{$Element['handler']}($Element['text']);
1416
+ }
1417
+ else
1418
+ {
1419
+ $markup .= $Element['text'];
1420
+ }
1421
+
1422
+ $markup .= '</'.$Element['name'].'>';
1423
+ }
1424
+ else
1425
+ {
1426
+ $markup .= ' />';
1427
+ }
1428
+
1429
+ return $markup;
1430
+ }
1431
+
1432
+ protected function elements(array $Elements)
1433
+ {
1434
+ $markup = '';
1435
+
1436
+ foreach ($Elements as $Element)
1437
+ {
1438
+ $markup .= "\n" . $this->element($Element);
1439
+ }
1440
+
1441
+ $markup .= "\n";
1442
+
1443
+ return $markup;
1444
+ }
1445
+
1446
+ # ~
1447
+
1448
+ protected function li($lines)
1449
+ {
1450
+ $markup = $this->lines($lines);
1451
+
1452
+ $trimmedMarkup = trim($markup);
1453
+
1454
+ if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1455
+ {
1456
+ $markup = $trimmedMarkup;
1457
+ $markup = substr($markup, 3);
1458
+
1459
+ $position = strpos($markup, "</p>");
1460
+
1461
+ $markup = substr_replace($markup, '', $position, 4);
1462
+ }
1463
+
1464
+ return $markup;
1465
+ }
1466
+
1467
+ #
1468
+ # Deprecated Methods
1469
+ #
1470
+
1471
+ function parse($text)
1472
+ {
1473
+ $markup = $this->text($text);
1474
+
1475
+ return $markup;
1476
+ }
1477
+
1478
+ #
1479
+ # Static Methods
1480
+ #
1481
+
1482
+ static function instance($name = 'default')
1483
+ {
1484
+ if (isset(self::$instances[$name]))
1485
+ {
1486
+ return self::$instances[$name];
1487
+ }
1488
+
1489
+ $instance = new static();
1490
+
1491
+ self::$instances[$name] = $instance;
1492
+
1493
+ return $instance;
1494
+ }
1495
+
1496
+ private static $instances = array();
1497
+
1498
+ #
1499
+ # Fields
1500
+ #
1501
+
1502
+ protected $DefinitionData;
1503
+
1504
+ #
1505
+ # Read-Only
1506
+
1507
+ protected $specialCharacters = array(
1508
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1509
+ );
1510
+
1511
+ protected $StrongRegex = array(
1512
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1513
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1514
+ );
1515
+
1516
+ protected $EmRegex = array(
1517
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1518
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1519
+ );
1520
+
1521
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1522
+
1523
+ protected $voidElements = array(
1524
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1525
+ );
1526
+
1527
+ protected $textLevelElements = array(
1528
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1529
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1530
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1531
+ 'q', 'rt', 'ins', 'font', 'strong',
1532
+ 's', 'tt', 'sub', 'mark',
1533
+ 'u', 'xm', 'sup', 'nobr',
1534
+ 'var', 'ruby',
1535
+ 'wbr', 'span',
1536
+ 'time',
1537
+ );
1538
+ }
updater/vendor/PucReadmeParser.php ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('PucReadmeParser', false) ):
4
+
5
+ /**
6
+ * This is a slightly modified version of github.com/markjaquith/WordPress-Plugin-Readme-Parser
7
+ * It uses Parsedown instead of the "Markdown Extra" parser.
8
+ */
9
+
10
+ class PucReadmeParser {
11
+
12
+ function __construct() {
13
+ // This space intentionally blank
14
+ }
15
+
16
+ function parse_readme( $file ) {
17
+ $file_contents = @implode('', @file($file));
18
+ return $this->parse_readme_contents( $file_contents );
19
+ }
20
+
21
+ function parse_readme_contents( $file_contents ) {
22
+ $file_contents = str_replace(array("\r\n", "\r"), "\n", $file_contents);
23
+ $file_contents = trim($file_contents);
24
+ if ( 0 === strpos( $file_contents, "\xEF\xBB\xBF" ) )
25
+ $file_contents = substr( $file_contents, 3 );
26
+
27
+ // Markdown transformations
28
+ $file_contents = preg_replace( "|^###([^#]+)#*?\s*?\n|im", '=$1='."\n", $file_contents );
29
+ $file_contents = preg_replace( "|^##([^#]+)#*?\s*?\n|im", '==$1=='."\n", $file_contents );
30
+ $file_contents = preg_replace( "|^#([^#]+)#*?\s*?\n|im", '===$1==='."\n", $file_contents );
31
+
32
+ // === Plugin Name ===
33
+ // Must be the very first thing.
34
+ if ( !preg_match('|^===(.*)===|', $file_contents, $_name) )
35
+ return array(); // require a name
36
+ $name = trim($_name[1], '=');
37
+ $name = $this->sanitize_text( $name );
38
+
39
+ $file_contents = $this->chop_string( $file_contents, $_name[0] );
40
+
41
+
42
+ // Requires at least: 1.5
43
+ if ( preg_match('|Requires at least:(.*)|i', $file_contents, $_requires_at_least) )
44
+ $requires_at_least = $this->sanitize_text($_requires_at_least[1]);
45
+ else
46
+ $requires_at_least = NULL;
47
+
48
+
49
+ // Tested up to: 2.1
50
+ if ( preg_match('|Tested up to:(.*)|i', $file_contents, $_tested_up_to) )
51
+ $tested_up_to = $this->sanitize_text( $_tested_up_to[1] );
52
+ else
53
+ $tested_up_to = NULL;
54
+
55
+ // Requires PHP: 5.2.4
56
+ if ( preg_match('|Requires PHP:(.*)|i', $file_contents, $_requires_php) ) {
57
+ $requires_php = $this->sanitize_text( $_requires_php[1] );
58
+ } else {
59
+ $requires_php = null;
60
+ }
61
+
62
+ // Stable tag: 10.4-ride-the-fire-eagle-danger-day
63
+ if ( preg_match('|Stable tag:(.*)|i', $file_contents, $_stable_tag) )
64
+ $stable_tag = $this->sanitize_text( $_stable_tag[1] );
65
+ else
66
+ $stable_tag = NULL; // we assume trunk, but don't set it here to tell the difference between specified trunk and default trunk
67
+
68
+
69
+ // Tags: some tag, another tag, we like tags
70
+ if ( preg_match('|Tags:(.*)|i', $file_contents, $_tags) ) {
71
+ $tags = preg_split('|,[\s]*?|', trim($_tags[1]));
72
+ foreach ( array_keys($tags) as $t )
73
+ $tags[$t] = $this->sanitize_text( $tags[$t] );
74
+ } else {
75
+ $tags = array();
76
+ }
77
+
78
+
79
+ // Contributors: markjaquith, mdawaffe, zefrank
80
+ $contributors = array();
81
+ if ( preg_match('|Contributors:(.*)|i', $file_contents, $_contributors) ) {
82
+ $temp_contributors = preg_split('|,[\s]*|', trim($_contributors[1]));
83
+ foreach ( array_keys($temp_contributors) as $c ) {
84
+ $tmp_sanitized = $this->user_sanitize( $temp_contributors[$c] );
85
+ if ( strlen(trim($tmp_sanitized)) > 0 )
86
+ $contributors[$c] = $tmp_sanitized;
87
+ unset($tmp_sanitized);
88
+ }
89
+ }
90
+
91
+
92
+ // Donate Link: URL
93
+ if ( preg_match('|Donate link:(.*)|i', $file_contents, $_donate_link) )
94
+ $donate_link = esc_url( $_donate_link[1] );
95
+ else
96
+ $donate_link = NULL;
97
+
98
+
99
+ // togs, conts, etc are optional and order shouldn't matter. So we chop them only after we've grabbed their values.
100
+ foreach ( array('tags', 'contributors', 'requires_at_least', 'tested_up_to', 'stable_tag', 'donate_link') as $chop ) {
101
+ if ( $$chop ) {
102
+ $_chop = '_' . $chop;
103
+ $file_contents = $this->chop_string( $file_contents, ${$_chop}[0] );
104
+ }
105
+ }
106
+
107
+ $file_contents = trim($file_contents);
108
+
109
+
110
+ // short-description fu
111
+ if ( !preg_match('/(^(.*?))^[\s]*=+?[\s]*.+?[\s]*=+?/ms', $file_contents, $_short_description) )
112
+ $_short_description = array( 1 => &$file_contents, 2 => &$file_contents );
113
+ $short_desc_filtered = $this->sanitize_text( $_short_description[2] );
114
+ $short_desc_length = strlen($short_desc_filtered);
115
+ $short_description = substr($short_desc_filtered, 0, 150);
116
+ if ( $short_desc_length > strlen($short_description) )
117
+ $truncated = true;
118
+ else
119
+ $truncated = false;
120
+ if ( $_short_description[1] )
121
+ $file_contents = $this->chop_string( $file_contents, $_short_description[1] ); // yes, the [1] is intentional
122
+
123
+ // == Section ==
124
+ // Break into sections
125
+ // $_sections[0] will be the title of the first section, $_sections[1] will be the content of the first section
126
+ // the array alternates from there: title2, content2, title3, content3... and so forth
127
+ $_sections = preg_split('/^[\s]*==[\s]*(.+?)[\s]*==/m', $file_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
128
+
129
+ $sections = array();
130
+ for ( $i=0; $i < count($_sections); $i +=2 ) {
131
+ $title = $this->sanitize_text( $_sections[$i] );
132
+ if ( isset($_sections[$i+1]) ) {
133
+ $content = preg_replace('/(^[\s]*)=[\s]+(.+?)[\s]+=/m', '$1<h4>$2</h4>', $_sections[$i+1]);
134
+ $content = $this->filter_text( $content, true );
135
+ } else {
136
+ $content = '';
137
+ }
138
+ $sections[str_replace(' ', '_', strtolower($title))] = array('title' => $title, 'content' => $content);
139
+ }
140
+
141
+
142
+ // Special sections
143
+ // This is where we nab our special sections, so we can enforce their order and treat them differently, if needed
144
+ // upgrade_notice is not a section, but parse it like it is for now
145
+ $final_sections = array();
146
+ foreach ( array('description', 'installation', 'frequently_asked_questions', 'screenshots', 'changelog', 'change_log', 'upgrade_notice') as $special_section ) {
147
+ if ( isset($sections[$special_section]) ) {
148
+ $final_sections[$special_section] = $sections[$special_section]['content'];
149
+ unset($sections[$special_section]);
150
+ }
151
+ }
152
+ if ( isset($final_sections['change_log']) && empty($final_sections['changelog']) )
153
+ $final_sections['changelog'] = $final_sections['change_log'];
154
+
155
+
156
+ $final_screenshots = array();
157
+ if ( isset($final_sections['screenshots']) ) {
158
+ preg_match_all('|<li>(.*?)</li>|s', $final_sections['screenshots'], $screenshots, PREG_SET_ORDER);
159
+ if ( $screenshots ) {
160
+ foreach ( (array) $screenshots as $ss )
161
+ $final_screenshots[] = $ss[1];
162
+ }
163
+ }
164
+
165
+ // Parse the upgrade_notice section specially:
166
+ // 1.0 => blah, 1.1 => fnord
167
+ $upgrade_notice = array();
168
+ if ( isset($final_sections['upgrade_notice']) ) {
169
+ $split = preg_split( '#<h4>(.*?)</h4>#', $final_sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
170
+ if ( count($split) >= 2 ) {
171
+ for ( $i = 0; $i < count( $split ); $i += 2 ) {
172
+ $upgrade_notice[$this->sanitize_text( $split[$i] )] = substr( $this->sanitize_text( $split[$i + 1] ), 0, 300 );
173
+ }
174
+ }
175
+ unset( $final_sections['upgrade_notice'] );
176
+ }
177
+
178
+ // No description?
179
+ // No problem... we'll just fall back to the old style of description
180
+ // We'll even let you use markup this time!
181
+ $excerpt = false;
182
+ if ( !isset($final_sections['description']) ) {
183
+ $final_sections = array_merge(array('description' => $this->filter_text( $_short_description[2], true )), $final_sections);
184
+ $excerpt = true;
185
+ }
186
+
187
+
188
+ // dump the non-special sections into $remaining_content
189
+ // their order will be determined by their original order in the readme.txt
190
+ $remaining_content = '';
191
+ foreach ( $sections as $s_name => $s_data ) {
192
+ $remaining_content .= "\n<h3>{$s_data['title']}</h3>\n{$s_data['content']}";
193
+ }
194
+ $remaining_content = trim($remaining_content);
195
+
196
+
197
+ // All done!
198
+ // $r['tags'] and $r['contributors'] are simple arrays
199
+ // $r['sections'] is an array with named elements
200
+ $r = array(
201
+ 'name' => $name,
202
+ 'tags' => $tags,
203
+ 'requires_at_least' => $requires_at_least,
204
+ 'tested_up_to' => $tested_up_to,
205
+ 'requires_php' => $requires_php,
206
+ 'stable_tag' => $stable_tag,
207
+ 'contributors' => $contributors,
208
+ 'donate_link' => $donate_link,
209
+ 'short_description' => $short_description,
210
+ 'screenshots' => $final_screenshots,
211
+ 'is_excerpt' => $excerpt,
212
+ 'is_truncated' => $truncated,
213
+ 'sections' => $final_sections,
214
+ 'remaining_content' => $remaining_content,
215
+ 'upgrade_notice' => $upgrade_notice
216
+ );
217
+
218
+ return $r;
219
+ }
220
+
221
+ function chop_string( $string, $chop ) { // chop a "prefix" from a string: Agressive! uses strstr not 0 === strpos
222
+ if ( $_string = strstr($string, $chop) ) {
223
+ $_string = substr($_string, strlen($chop));
224
+ return trim($_string);
225
+ } else {
226
+ return trim($string);
227
+ }
228
+ }
229
+
230
+ function user_sanitize( $text, $strict = false ) { // whitelisted chars
231
+ if ( function_exists('user_sanitize') ) // bbPress native
232
+ return user_sanitize( $text, $strict );
233
+
234
+ if ( $strict ) {
235
+ $text = preg_replace('/[^a-z0-9-]/i', '', $text);
236
+ $text = preg_replace('|-+|', '-', $text);
237
+ } else {
238
+ $text = preg_replace('/[^a-z0-9_-]/i', '', $text);
239
+ }
240
+ return $text;
241
+ }
242
+
243
+ function sanitize_text( $text ) { // not fancy
244
+ $text = strip_tags($text);
245
+ $text = esc_html($text);
246
+ $text = trim($text);
247
+ return $text;
248
+ }
249
+
250
+ function filter_text( $text, $markdown = false ) { // fancy, Markdown
251
+ $text = trim($text);
252
+
253
+ $text = call_user_func( array( __CLASS__, 'code_trick' ), $text, $markdown ); // A better parser than Markdown's for: backticks -> CODE
254
+
255
+ if ( $markdown ) { // Parse markdown.
256
+ if ( !class_exists('Parsedown', false) ) {
257
+ /** @noinspection PhpIncludeInspection */
258
+ require_once(dirname(__FILE__) . '/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php');
259
+ }
260
+ $instance = Parsedown::instance();
261
+ $text = $instance->text($text);
262
+ }
263
+
264
+ $allowed = array(
265
+ 'a' => array(
266
+ 'href' => array(),
267
+ 'title' => array(),
268
+ 'rel' => array()),
269
+ 'blockquote' => array('cite' => array()),
270
+ 'br' => array(),
271
+ 'p' => array(),
272
+ 'code' => array(),
273
+ 'pre' => array(),
274
+ 'em' => array(),
275
+ 'strong' => array(),
276
+ 'ul' => array(),
277
+ 'ol' => array(),
278
+ 'li' => array(),
279
+ 'h3' => array(),
280
+ 'h4' => array()
281
+ );
282
+
283
+ $text = balanceTags($text);
284
+
285
+ $text = wp_kses( $text, $allowed );
286
+ $text = trim($text);
287
+ return $text;
288
+ }
289
+
290
+ function code_trick( $text, $markdown ) { // Don't use bbPress native function - it's incompatible with Markdown
291
+ // If doing markdown, first take any user formatted code blocks and turn them into backticks so that
292
+ // markdown will preserve things like underscores in code blocks
293
+ if ( $markdown )
294
+ $text = preg_replace_callback("!(<pre><code>|<code>)(.*?)(</code></pre>|</code>)!s", array( __CLASS__,'decodeit'), $text);
295
+
296
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
297
+ if ( !$markdown ) {
298
+ // This gets the "inline" code blocks, but can't be used with Markdown.
299
+ $text = preg_replace_callback("|(`)(.*?)`|", array( __CLASS__, 'encodeit'), $text);
300
+ // This gets the "block level" code blocks and converts them to PRE CODE
301
+ $text = preg_replace_callback("!(^|\n)`(.*?)`!s", array( __CLASS__, 'encodeit'), $text);
302
+ } else {
303
+ // Markdown can do inline code, we convert bbPress style block level code to Markdown style
304
+ $text = preg_replace_callback("!(^|\n)([ \t]*?)`(.*?)`!s", array( __CLASS__, 'indent'), $text);
305
+ }
306
+ return $text;
307
+ }
308
+
309
+ function indent( $matches ) {
310
+ $text = $matches[3];
311
+ $text = preg_replace('|^|m', $matches[2] . ' ', $text);
312
+ return $matches[1] . $text;
313
+ }
314
+
315
+ function encodeit( $matches ) {
316
+ if ( function_exists('encodeit') ) // bbPress native
317
+ return encodeit( $matches );
318
+
319
+ $text = trim($matches[2]);
320
+ $text = htmlspecialchars($text, ENT_QUOTES);
321
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
322
+ $text = preg_replace("|\n\n\n+|", "\n\n", $text);
323
+ $text = str_replace('&amp;lt;', '&lt;', $text);
324
+ $text = str_replace('&amp;gt;', '&gt;', $text);
325
+ $text = "<code>$text</code>";
326
+ if ( "`" != $matches[1] )
327
+ $text = "<pre>$text</pre>";
328
+ return $text;
329
+ }
330
+
331
+ function decodeit( $matches ) {
332
+ if ( function_exists('decodeit') ) // bbPress native
333
+ return decodeit( $matches );
334
+
335
+ $text = $matches[2];
336
+ $trans_table = array_flip(get_html_translation_table(HTML_ENTITIES));
337
+ $text = strtr($text, $trans_table);
338
+ $text = str_replace('<br />', '', $text);
339
+ $text = str_replace('&#38;', '&', $text);
340
+ $text = str_replace('&#39;', "'", $text);
341
+ if ( '<pre><code>' == $matches[1] )
342
+ $text = "\n$text\n";
343
+ return "`$text`";
344
+ }
345
+
346
+ } // end class
347
+
348
+ endif;