Slimstat Analytics - Version 4.7.5.3

Version Description

  • [New] For those who can't manage to automatically update their local copy of our premium add-ons, we've added a link to manually download the zip file. You'll find it under the Plugins screen, once you've entered and saved the corresponding license key under Slimstat > Add-ons.
  • [Update] The Update Checker library has been updated to version 4.4.
  • [Fix] The world map was not being displayed correctly if no data points were available for the selected time range (thank you, Michel).
Download this release

Release Info

Developer coolmann
Plugin Icon 128x128 Slimstat Analytics
Version 4.7.5.3
Comparing to
See all releases

Code changes from version 4.7.5.2 to 4.7.5.3

Files changed (37) hide show
  1. admin/update-checker/Puc/v4/Factory.php +1 -266
  2. admin/update-checker/Puc/{v4p2 → v4p4}/Autoloader.php +2 -2
  3. admin/update-checker/Puc/v4p4/DebugBar/Extension.php +177 -0
  4. admin/update-checker/Puc/v4p4/DebugBar/Panel.php +165 -0
  5. admin/update-checker/Puc/v4p4/DebugBar/PluginExtension.php +33 -0
  6. admin/update-checker/Puc/v4p4/DebugBar/PluginPanel.php +38 -0
  7. admin/update-checker/Puc/v4p4/DebugBar/ThemePanel.php +21 -0
  8. admin/update-checker/Puc/v4p4/Factory.php +292 -0
  9. admin/update-checker/Puc/{v4p2 → v4p4}/Metadata.php +6 -6
  10. admin/update-checker/Puc/v4p4/OAuthSignature.php +88 -0
  11. admin/update-checker/Puc/{v4p2 → v4p4}/Plugin/Info.php +7 -4
  12. admin/update-checker/Puc/{v4p2 → v4p4}/Plugin/Update.php +30 -11
  13. admin/update-checker/Puc/{v4p2 → v4p4}/Plugin/UpdateChecker.php +214 -16
  14. admin/update-checker/Puc/{v4p2 → v4p4}/Scheduler.php +5 -5
  15. admin/update-checker/Puc/{v4p2 → v4p4}/StateStore.php +9 -9
  16. admin/update-checker/Puc/{v4p2 → v4p4}/Theme/Update.php +4 -4
  17. admin/update-checker/Puc/{v4p2 → v4p4}/Theme/UpdateChecker.php +21 -7
  18. admin/update-checker/Puc/{v4p2 → v4p4}/Update.php +2 -2
  19. admin/update-checker/Puc/{v4p2 → v4p4}/UpdateChecker.php +119 -15
  20. admin/update-checker/Puc/{v4p2 → v4p4}/UpgraderStatus.php +2 -2
  21. admin/update-checker/Puc/{v4p2 → v4p4}/Utils.php +2 -2
  22. admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/Api.php +68 -9
  23. admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/BaseChecker.php +7 -2
  24. admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/BitBucketApi.php +18 -13
  25. admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/GitHubApi.php +133 -22
  26. admin/update-checker/Puc/v4p4/Vcs/GitLabApi.php +274 -0
  27. admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/PluginUpdateChecker.php +35 -11
  28. admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/Reference.php +2 -2
  29. admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/ThemeUpdateChecker.php +33 -9
  30. admin/update-checker/license.txt +2 -2
  31. admin/update-checker/plugin-update-checker.php +11 -9
  32. admin/update-checker/vendor/ParsedownLegacy.php +1535 -0
  33. admin/view/addons.php +1 -1
  34. admin/view/wp-slimstat-reports.php +2 -2
  35. admin/wp-slimstat-admin.php +2 -2
  36. readme.txt +6 -1
  37. wp-slimstat.php +31 -5
admin/update-checker/Puc/v4/Factory.php CHANGED
@@ -1,271 +1,6 @@
1
  <?php
2
  if ( !class_exists('Puc_v4_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_v4_Factory {
15
- protected static $classVersions = array();
16
- protected static $sorted = false;
17
-
18
- protected static $myMajorVersion = '';
19
- protected static $latestCompatibleVersion = '';
20
-
21
- /**
22
- * Create a new instance of the update checker.
23
- *
24
- * This method automatically detects if you're using it for a plugin or a theme and chooses
25
- * the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
26
- *
27
- * @see Puc_v4p2_UpdateChecker::__construct
28
- *
29
- * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
30
- * @param string $fullPath Full path to the main plugin file or to the theme directory.
31
- * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
32
- * @param int $checkPeriod How often to check for updates (in hours).
33
- * @param string $optionName Where to store book-keeping info about update checks.
34
- * @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
35
- * @return Puc_v4p2_Plugin_UpdateChecker|Puc_v4p2_Theme_UpdateChecker|Puc_v4p2_Vcs_BaseChecker
36
- */
37
- public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
38
- $fullPath = wp_normalize_path($fullPath);
39
- $id = null;
40
-
41
- //Plugin or theme?
42
- $themeDirectory = self::getThemeDirectoryName($fullPath);
43
- if ( self::isPluginFile($fullPath) ) {
44
- $type = 'Plugin';
45
- $id = $fullPath;
46
- } else if ( $themeDirectory !== null ) {
47
- $type = 'Theme';
48
- $id = $themeDirectory;
49
- } else {
50
- throw new RuntimeException(sprintf(
51
- 'The update checker cannot determine if "%s" is a plugin or a theme. ' .
52
- 'This is a bug. Please contact the PUC developer.',
53
- htmlentities($fullPath)
54
- ));
55
- }
56
-
57
- //Which hosting service does the URL point to?
58
- $service = self::getVcsService($metadataUrl);
59
-
60
- $apiClass = null;
61
- if ( empty($service) ) {
62
- //The default is to get update information from a remote JSON file.
63
- $checkerClass = $type . '_UpdateChecker';
64
- } else {
65
- //You can also use a VCS repository like GitHub.
66
- $checkerClass = 'Vcs_' . $type . 'UpdateChecker';
67
- $apiClass = $service . 'Api';
68
- }
69
-
70
- $checkerClass = self::getCompatibleClassVersion($checkerClass);
71
- if ( $checkerClass === null ) {
72
- trigger_error(
73
- sprintf(
74
- 'PUC %s does not support updates for %ss %s',
75
- htmlentities(self::$latestCompatibleVersion),
76
- strtolower($type),
77
- $service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
78
- ),
79
- E_USER_ERROR
80
- );
81
- return null;
82
- }
83
-
84
- if ( !isset($apiClass) ) {
85
- //Plain old update checker.
86
- return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
87
- } else {
88
- //VCS checker + an API client.
89
- $apiClass = self::getCompatibleClassVersion($apiClass);
90
- if ( $apiClass === null ) {
91
- trigger_error(sprintf(
92
- 'PUC %s does not support %s',
93
- htmlentities(self::$latestCompatibleVersion),
94
- htmlentities($service)
95
- ), E_USER_ERROR);
96
- return null;
97
- }
98
-
99
- return new $checkerClass(
100
- new $apiClass($metadataUrl),
101
- $id,
102
- $slug,
103
- $checkPeriod,
104
- $optionName,
105
- $muPluginFile
106
- );
107
- }
108
- }
109
-
110
- /**
111
- * Check if the path points to a plugin file.
112
- *
113
- * @param string $absolutePath Normalized path.
114
- * @return bool
115
- */
116
- protected static function isPluginFile($absolutePath) {
117
- //Is the file inside the "plugins" or "mu-plugins" directory?
118
- $pluginDir = wp_normalize_path(WP_PLUGIN_DIR);
119
- $muPluginDir = wp_normalize_path(WPMU_PLUGIN_DIR);
120
- if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
121
- return true;
122
- }
123
-
124
- //Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
125
- if ( !is_file($absolutePath) ) {
126
- return false;
127
- }
128
-
129
- //Does it have a valid plugin header?
130
- //This is a last-ditch check for plugins symlinked from outside the WP root.
131
- if ( function_exists('get_file_data') ) {
132
- $headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
133
- return !empty($headers['Name']);
134
- }
135
-
136
- return false;
137
- }
138
-
139
- /**
140
- * Get the name of the theme's directory from a full path to a file inside that directory.
141
- * E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
142
- *
143
- * Note that subdirectories are currently not supported. For example,
144
- * "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
145
- *
146
- * @param string $absolutePath Normalized path.
147
- * @return string|null Directory name, or NULL if the path doesn't point to a theme.
148
- */
149
- protected static function getThemeDirectoryName($absolutePath) {
150
- if ( is_file($absolutePath) ) {
151
- $absolutePath = dirname($absolutePath);
152
- }
153
-
154
- if ( file_exists($absolutePath . '/style.css') ) {
155
- return basename($absolutePath);
156
- }
157
- return null;
158
- }
159
-
160
- /**
161
- * Get the name of the hosting service that the URL points to.
162
- *
163
- * @param string $metadataUrl
164
- * @return string|null
165
- */
166
- private static function getVcsService($metadataUrl) {
167
- $service = null;
168
-
169
- //Which hosting service does the URL point to?
170
- $host = @parse_url($metadataUrl, PHP_URL_HOST);
171
- $path = @parse_url($metadataUrl, PHP_URL_PATH);
172
- //Check if the path looks like "/user-name/repository".
173
- $usernameRepoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
174
- if ( preg_match($usernameRepoRegex, $path) ) {
175
- $knownServices = array(
176
- 'github.com' => 'GitHub',
177
- 'bitbucket.org' => 'BitBucket',
178
- 'gitlab.com' => 'GitLab',
179
- );
180
- if ( isset($knownServices[$host]) ) {
181
- $service = $knownServices[$host];
182
- }
183
- }
184
-
185
- return $service;
186
- }
187
-
188
- /**
189
- * Get the latest version of the specified class that has the same major version number
190
- * as this factory class.
191
- *
192
- * @param string $class Partial class name.
193
- * @return string|null Full class name.
194
- */
195
- protected static function getCompatibleClassVersion($class) {
196
- if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
197
- return self::$classVersions[$class][self::$latestCompatibleVersion];
198
- }
199
- return null;
200
- }
201
-
202
- /**
203
- * Get the specific class name for the latest available version of a class.
204
- *
205
- * @param string $class
206
- * @return null|string
207
- */
208
- public static function getLatestClassVersion($class) {
209
- if ( !self::$sorted ) {
210
- self::sortVersions();
211
- }
212
-
213
- if ( isset(self::$classVersions[$class]) ) {
214
- return reset(self::$classVersions[$class]);
215
- } else {
216
- return null;
217
- }
218
- }
219
-
220
- /**
221
- * Sort available class versions in descending order (i.e. newest first).
222
- */
223
- protected static function sortVersions() {
224
- foreach ( self::$classVersions as $class => $versions ) {
225
- uksort($versions, array(__CLASS__, 'compareVersions'));
226
- self::$classVersions[$class] = $versions;
227
- }
228
- self::$sorted = true;
229
- }
230
-
231
- protected static function compareVersions($a, $b) {
232
- return -version_compare($a, $b);
233
- }
234
-
235
- /**
236
- * Register a version of a class.
237
- *
238
- * @access private This method is only for internal use by the library.
239
- *
240
- * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
241
- * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
242
- * @param string $version Version number, e.g. '1.2'.
243
- */
244
- public static function addVersion($generalClass, $versionedClass, $version) {
245
- if ( empty(self::$myMajorVersion) ) {
246
- $nameParts = explode('_', __CLASS__, 3);
247
- self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
248
- }
249
-
250
- //Store the greatest version number that matches our major version.
251
- $components = explode('.', $version);
252
- if ( $components[0] === self::$myMajorVersion ) {
253
-
254
- if (
255
- empty(self::$latestCompatibleVersion)
256
- || version_compare($version, self::$latestCompatibleVersion, '>')
257
- ) {
258
- self::$latestCompatibleVersion = $version;
259
- }
260
-
261
- }
262
-
263
- if ( !isset(self::$classVersions[$generalClass]) ) {
264
- self::$classVersions[$generalClass] = array();
265
- }
266
- self::$classVersions[$generalClass][$version] = $versionedClass;
267
- self::$sorted = false;
268
- }
269
- }
270
 
271
  endif;
1
  <?php
2
  if ( !class_exists('Puc_v4_Factory', false) ):
3
 
4
+ class Puc_v4_Factory extends Puc_v4p4_Factory { }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  endif;
admin/update-checker/Puc/{v4p2 → v4p4}/Autoloader.php RENAMED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_Autoloader', false) ):
4
 
5
- class Puc_v4p2_Autoloader {
6
  private $prefix = '';
7
  private $rootDir = '';
8
  private $libraryDir = '';
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_Autoloader', false) ):
4
 
5
+ class Puc_v4p4_Autoloader {
6
  private $prefix = '';
7
  private $rootDir = '';
8
  private $libraryDir = '';
admin/update-checker/Puc/v4p4/DebugBar/Extension.php ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p4_DebugBar_Extension', false) ):
3
+
4
+ class Puc_v4p4_DebugBar_Extension {
5
+ const RESPONSE_BODY_LENGTH_LIMIT = 4000;
6
+
7
+ /** @var Puc_v4p4_UpdateChecker */
8
+ protected $updateChecker;
9
+ protected $panelClass = 'Puc_v4p4_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
+ * @param string $filePath
147
+ * @return string
148
+ */
149
+ private function getLibraryUrl($filePath) {
150
+ $absolutePath = realpath(dirname(__FILE__) . '/../../../' . ltrim($filePath, '/'));
151
+
152
+ //Where is the library located inside the WordPress directory structure?
153
+ $absolutePath = Puc_v4p4_Factory::normalizePath($absolutePath);
154
+
155
+ $pluginDir = Puc_v4p4_Factory::normalizePath(WP_PLUGIN_DIR);
156
+ $muPluginDir = Puc_v4p4_Factory::normalizePath(WPMU_PLUGIN_DIR);
157
+ $themeDir = Puc_v4p4_Factory::normalizePath(get_theme_root());
158
+
159
+ if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
160
+ //It's part of a plugin.
161
+ return plugins_url(basename($absolutePath), $absolutePath);
162
+ } else if ( strpos($absolutePath, $themeDir) === 0 ) {
163
+ //It's part of a theme.
164
+ $relativePath = substr($absolutePath, strlen($themeDir) + 1);
165
+ $template = substr($relativePath, 0, strpos($relativePath, '/'));
166
+ $baseUrl = get_theme_root_uri($template);
167
+
168
+ if ( !empty($baseUrl) && $relativePath ) {
169
+ return $baseUrl . '/' . $relativePath;
170
+ }
171
+ }
172
+
173
+ return '';
174
+ }
175
+ }
176
+
177
+ endif;
admin/update-checker/Puc/v4p4/DebugBar/Panel.php ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p4_DebugBar_Panel', false) && class_exists('Debug_Bar_Panel', false) ):
4
+
5
+ class Puc_v4p4_DebugBar_Panel extends Debug_Bar_Panel {
6
+ /** @var Puc_v4p4_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;
admin/update-checker/Puc/v4p4/DebugBar/PluginExtension.php ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p4_DebugBar_PluginExtension', false) ):
3
+
4
+ class Puc_v4p4_DebugBar_PluginExtension extends Puc_v4p4_DebugBar_Extension {
5
+ /** @var Puc_v4p4_Plugin_UpdateChecker */
6
+ protected $updateChecker;
7
+
8
+ public function __construct($updateChecker) {
9
+ parent::__construct($updateChecker, 'Puc_v4p4_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;
admin/update-checker/Puc/v4p4/DebugBar/PluginPanel.php ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p4_DebugBar_PluginPanel', false) ):
4
+
5
+ class Puc_v4p4_DebugBar_PluginPanel extends Puc_v4p4_DebugBar_Panel {
6
+ /**
7
+ * @var Puc_v4p4_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;
admin/update-checker/Puc/v4p4/DebugBar/ThemePanel.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p4_DebugBar_ThemePanel', false) ):
4
+
5
+ class Puc_v4p4_DebugBar_ThemePanel extends Puc_v4p4_DebugBar_Panel {
6
+ /**
7
+ * @var Puc_v4p4_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;
admin/update-checker/Puc/v4p4/Factory.php ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( !class_exists('Puc_v4p4_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_v4p4_Factory {
15
+ protected static $classVersions = array();
16
+ protected static $sorted = false;
17
+
18
+ protected static $myMajorVersion = '';
19
+ protected static $latestCompatibleVersion = '';
20
+
21
+ /**
22
+ * Create a new instance of the update checker.
23
+ *
24
+ * This method automatically detects if you're using it for a plugin or a theme and chooses
25
+ * the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
26
+ *
27
+ * @see Puc_v4p4_UpdateChecker::__construct
28
+ *
29
+ * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
30
+ * @param string $fullPath Full path to the main plugin file or to the theme directory.
31
+ * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
32
+ * @param int $checkPeriod How often to check for updates (in hours).
33
+ * @param string $optionName Where to store book-keeping info about update checks.
34
+ * @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
35
+ * @return Puc_v4p4_Plugin_UpdateChecker|Puc_v4p4_Theme_UpdateChecker|Puc_v4p4_Vcs_BaseChecker
36
+ */
37
+ public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
38
+ $fullPath = self::normalizePath($fullPath);
39
+ $id = null;
40
+
41
+ //Plugin or theme?
42
+ $themeDirectory = self::getThemeDirectoryName($fullPath);
43
+ if ( self::isPluginFile($fullPath) ) {
44
+ $type = 'Plugin';
45
+ $id = $fullPath;
46
+ } else if ( $themeDirectory !== null ) {
47
+ $type = 'Theme';
48
+ $id = $themeDirectory;
49
+ } else {
50
+ throw new RuntimeException(sprintf(
51
+ 'The update checker cannot determine if "%s" is a plugin or a theme. ' .
52
+ 'This is a bug. Please contact the PUC developer.',
53
+ htmlentities($fullPath)
54
+ ));
55
+ }
56
+
57
+ //Which hosting service does the URL point to?
58
+ $service = self::getVcsService($metadataUrl);
59
+
60
+ $apiClass = null;
61
+ if ( empty($service) ) {
62
+ //The default is to get update information from a remote JSON file.
63
+ $checkerClass = $type . '_UpdateChecker';
64
+ } else {
65
+ //You can also use a VCS repository like GitHub.
66
+ $checkerClass = 'Vcs_' . $type . 'UpdateChecker';
67
+ $apiClass = $service . 'Api';
68
+ }
69
+
70
+ $checkerClass = self::getCompatibleClassVersion($checkerClass);
71
+ if ( $checkerClass === null ) {
72
+ trigger_error(
73
+ sprintf(
74
+ 'PUC %s does not support updates for %ss %s',
75
+ htmlentities(self::$latestCompatibleVersion),
76
+ strtolower($type),
77
+ $service ? ('hosted on ' . htmlentities($service)) : 'using JSON metadata'
78
+ ),
79
+ E_USER_ERROR
80
+ );
81
+ return null;
82
+ }
83
+
84
+ if ( !isset($apiClass) ) {
85
+ //Plain old update checker.
86
+ return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
87
+ } else {
88
+ //VCS checker + an API client.
89
+ $apiClass = self::getCompatibleClassVersion($apiClass);
90
+ if ( $apiClass === null ) {
91
+ trigger_error(sprintf(
92
+ 'PUC %s does not support %s',
93
+ htmlentities(self::$latestCompatibleVersion),
94
+ htmlentities($service)
95
+ ), E_USER_ERROR);
96
+ return null;
97
+ }
98
+
99
+ return new $checkerClass(
100
+ new $apiClass($metadataUrl),
101
+ $id,
102
+ $slug,
103
+ $checkPeriod,
104
+ $optionName,
105
+ $muPluginFile
106
+ );
107
+ }
108
+ }
109
+
110
+ /**
111
+ *
112
+ * Normalize a filesystem path. Introduced in WP 3.9.
113
+ * Copying here allows use of the class on earlier versions.
114
+ * This version adapted from WP 4.8.2 (unchanged since 4.5.0)
115
+ *
116
+ * @param string $path Path to normalize.
117
+ * @return string Normalized path.
118
+ */
119
+ public static function normalizePath($path) {
120
+ if ( function_exists('wp_normalize_path') ) {
121
+ return wp_normalize_path($path);
122
+ }
123
+ $path = str_replace('\\', '/', $path);
124
+ $path = preg_replace('|(?<=.)/+|', '/', $path);
125
+ if ( substr($path, 1, 1) === ':' ) {
126
+ $path = ucfirst($path);
127
+ }
128
+ return $path;
129
+ }
130
+
131
+ /**
132
+ * Check if the path points to a plugin file.
133
+ *
134
+ * @param string $absolutePath Normalized path.
135
+ * @return bool
136
+ */
137
+ protected static function isPluginFile($absolutePath) {
138
+ //Is the file inside the "plugins" or "mu-plugins" directory?
139
+ $pluginDir = self::normalizePath(WP_PLUGIN_DIR);
140
+ $muPluginDir = self::normalizePath(WPMU_PLUGIN_DIR);
141
+ if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
142
+ return true;
143
+ }
144
+
145
+ //Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
146
+ if ( !is_file($absolutePath) ) {
147
+ return false;
148
+ }
149
+
150
+ //Does it have a valid plugin header?
151
+ //This is a last-ditch check for plugins symlinked from outside the WP root.
152
+ if ( function_exists('get_file_data') ) {
153
+ $headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
154
+ return !empty($headers['Name']);
155
+ }
156
+
157
+ return false;
158
+ }
159
+
160
+ /**
161
+ * Get the name of the theme's directory from a full path to a file inside that directory.
162
+ * E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
163
+ *
164
+ * Note that subdirectories are currently not supported. For example,
165
+ * "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
166
+ *
167
+ * @param string $absolutePath Normalized path.
168
+ * @return string|null Directory name, or NULL if the path doesn't point to a theme.
169
+ */
170
+ protected static function getThemeDirectoryName($absolutePath) {
171
+ if ( is_file($absolutePath) ) {
172
+ $absolutePath = dirname($absolutePath);
173
+ }
174
+
175
+ if ( file_exists($absolutePath . '/style.css') ) {
176
+ return basename($absolutePath);
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Get the name of the hosting service that the URL points to.
183
+ *
184
+ * @param string $metadataUrl
185
+ * @return string|null
186
+ */
187
+ private static function getVcsService($metadataUrl) {
188
+ $service = null;
189
+
190
+ //Which hosting service does the URL point to?
191
+ $host = @parse_url($metadataUrl, PHP_URL_HOST);
192
+ $path = @parse_url($metadataUrl, PHP_URL_PATH);
193
+ //Check if the path looks like "/user-name/repository".
194
+ $usernameRepoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
195
+ if ( preg_match($usernameRepoRegex, $path) ) {
196
+ $knownServices = array(
197
+ 'github.com' => 'GitHub',
198
+ 'bitbucket.org' => 'BitBucket',
199
+ 'gitlab.com' => 'GitLab',
200
+ );
201
+ if ( isset($knownServices[$host]) ) {
202
+ $service = $knownServices[$host];
203
+ }
204
+ }
205
+
206
+ return $service;
207
+ }
208
+
209
+ /**
210
+ * Get the latest version of the specified class that has the same major version number
211
+ * as this factory class.
212
+ *
213
+ * @param string $class Partial class name.
214
+ * @return string|null Full class name.
215
+ */
216
+ protected static function getCompatibleClassVersion($class) {
217
+ if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
218
+ return self::$classVersions[$class][self::$latestCompatibleVersion];
219
+ }
220
+ return null;
221
+ }
222
+
223
+ /**
224
+ * Get the specific class name for the latest available version of a class.
225
+ *
226
+ * @param string $class
227
+ * @return null|string
228
+ */
229
+ public static function getLatestClassVersion($class) {
230
+ if ( !self::$sorted ) {
231
+ self::sortVersions();
232
+ }
233
+
234
+ if ( isset(self::$classVersions[$class]) ) {
235
+ return reset(self::$classVersions[$class]);
236
+ } else {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Sort available class versions in descending order (i.e. newest first).
243
+ */
244
+ protected static function sortVersions() {
245
+ foreach ( self::$classVersions as $class => $versions ) {
246
+ uksort($versions, array(__CLASS__, 'compareVersions'));
247
+ self::$classVersions[$class] = $versions;
248
+ }
249
+ self::$sorted = true;
250
+ }
251
+
252
+ protected static function compareVersions($a, $b) {
253
+ return -version_compare($a, $b);
254
+ }
255
+
256
+ /**
257
+ * Register a version of a class.
258
+ *
259
+ * @access private This method is only for internal use by the library.
260
+ *
261
+ * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
262
+ * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
263
+ * @param string $version Version number, e.g. '1.2'.
264
+ */
265
+ public static function addVersion($generalClass, $versionedClass, $version) {
266
+ if ( empty(self::$myMajorVersion) ) {
267
+ $nameParts = explode('_', __CLASS__, 3);
268
+ self::$myMajorVersion = substr(ltrim($nameParts[1], 'v'), 0, 1);
269
+ }
270
+
271
+ //Store the greatest version number that matches our major version.
272
+ $components = explode('.', $version);
273
+ if ( $components[0] === self::$myMajorVersion ) {
274
+
275
+ if (
276
+ empty(self::$latestCompatibleVersion)
277
+ || version_compare($version, self::$latestCompatibleVersion, '>')
278
+ ) {
279
+ self::$latestCompatibleVersion = $version;
280
+ }
281
+
282
+ }
283
+
284
+ if ( !isset(self::$classVersions[$generalClass]) ) {
285
+ self::$classVersions[$generalClass] = array();
286
+ }
287
+ self::$classVersions[$generalClass][$version] = $versionedClass;
288
+ self::$sorted = false;
289
+ }
290
+ }
291
+
292
+ endif;
admin/update-checker/Puc/{v4p2 → v4p4}/Metadata.php RENAMED
@@ -1,5 +1,5 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Metadata', false) ):
3
 
4
  /**
5
  * A base container for holding information about updates and plugin metadata.
@@ -8,7 +8,7 @@ if ( !class_exists('Puc_v4p2_Metadata', false) ):
8
  * @copyright 2016
9
  * @access public
10
  */
11
- abstract class Puc_v4p2_Metadata {
12
 
13
  /**
14
  * Create an instance of this class from a JSON document.
@@ -30,15 +30,15 @@ if ( !class_exists('Puc_v4p2_Metadata', false) ):
30
  /** @var StdClass $apiResponse */
31
  $apiResponse = json_decode($json);
32
  if ( empty($apiResponse) || !is_object($apiResponse) ){
33
- trigger_error(
34
- "Failed to parse update metadata. Try validating your .json file with http://jsonlint.com/",
35
- E_USER_NOTICE
36
- );
37
  return false;
38
  }
39
 
40
  $valid = $target->validateMetadata($apiResponse);
41
  if ( is_wp_error($valid) ){
 
42
  trigger_error($valid->get_error_message(), E_USER_NOTICE);
43
  return false;
44
  }
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Metadata', false) ):
3
 
4
  /**
5
  * A base container for holding information about updates and plugin metadata.
8
  * @copyright 2016
9
  * @access public
10
  */
11
+ abstract class Puc_v4p4_Metadata {
12
 
13
  /**
14
  * Create an instance of this class from a JSON document.
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
  }
admin/update-checker/Puc/v4p4/OAuthSignature.php ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p4_OAuthSignature', false) ):
4
+
5
+ /**
6
+ * A basic signature generator for zero-legged OAuth 1.0.
7
+ */
8
+ class Puc_v4p4_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
+ $rand = mt_rand();
84
+ return md5($mt . '_' . $rand);
85
+ }
86
+ }
87
+
88
+ endif;
admin/update-checker/Puc/{v4p2 → v4p4}/Plugin/Info.php RENAMED
@@ -1,5 +1,5 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Plugin_Info', false) ):
3
 
4
  /**
5
  * A container class for holding and transforming various plugin metadata.
@@ -8,7 +8,7 @@ if ( !class_exists('Puc_v4p2_Plugin_Info', false) ):
8
  * @copyright 2016
9
  * @access public
10
  */
11
- class Puc_v4p2_Plugin_Info extends Puc_v4p2_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;
@@ -16,9 +16,11 @@ if ( !class_exists('Puc_v4p2_Plugin_Info', false) ):
16
  public $version;
17
  public $homepage;
18
  public $sections = array();
 
 
19
  public $banners;
 
20
  public $translations = array();
21
- public $download_url;
22
 
23
  public $author;
24
  public $author_homepage;
@@ -51,8 +53,9 @@ if ( !class_exists('Puc_v4p2_Plugin_Info', false) ):
51
  return null;
52
  }
53
 
54
- //json_decode decodes assoc. arrays as objects. We want it as an array.
55
  $instance->sections = (array)$instance->sections;
 
56
 
57
  return $instance;
58
  }
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Plugin_Info', false) ):
3
 
4
  /**
5
  * A container class for holding and transforming various plugin metadata.
8
  * @copyright 2016
9
  * @access public
10
  */
11
+ class Puc_v4p4_Plugin_Info extends Puc_v4p4_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;
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;
53
  return null;
54
  }
55
 
56
+ //json_decode decodes assoc. arrays as objects. We want them as arrays.
57
  $instance->sections = (array)$instance->sections;
58
+ $instance->icons = (array)$instance->icons;
59
 
60
  return $instance;
61
  }
admin/update-checker/Puc/{v4p2 → v4p4}/Plugin/Update.php RENAMED
@@ -1,5 +1,5 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Plugin_Update', false) ):
3
 
4
  /**
5
  * A simple container class for holding information about an available update.
@@ -8,28 +8,29 @@ if ( !class_exists('Puc_v4p2_Plugin_Update', false) ):
8
  * @copyright 2016
9
  * @access public
10
  */
11
- class Puc_v4p2_Plugin_Update extends Puc_v4p2_Update {
12
  public $id = 0;
13
  public $homepage;
14
  public $upgrade_notice;
15
  public $tested;
 
16
  public $filename; //Plugin filename relative to the plugins directory.
17
 
18
  protected static $extraFields = array(
19
- 'id', 'homepage', 'tested', 'upgrade_notice', 'filename',
20
  );
21
 
22
  /**
23
  * Create a new instance of PluginUpdate from its JSON-encoded representation.
24
  *
25
  * @param string $json
26
- * @return Puc_v4p2_Plugin_Update|null
27
  */
28
  public static function fromJson($json){
29
  //Since update-related information is simply a subset of the full plugin info,
30
  //we can parse the update JSON as if it was a plugin info string, then copy over
31
  //the parts that we care about.
32
- $pluginInfo = Puc_v4p2_Plugin_Info::fromJson($json);
33
  if ( $pluginInfo !== null ) {
34
  return self::fromPluginInfo($pluginInfo);
35
  } else {
@@ -41,8 +42,8 @@ if ( !class_exists('Puc_v4p2_Plugin_Update', false) ):
41
  * Create a new instance of PluginUpdate based on an instance of PluginInfo.
42
  * Basically, this just copies a subset of fields from one object to another.
43
  *
44
- * @param Puc_v4p2_Plugin_Info $info
45
- * @return Puc_v4p2_Plugin_Update
46
  */
47
  public static function fromPluginInfo($info){
48
  return self::fromObject($info);
@@ -51,8 +52,8 @@ if ( !class_exists('Puc_v4p2_Plugin_Update', false) ):
51
  /**
52
  * Create a new instance by copying the necessary fields from another object.
53
  *
54
- * @param StdClass|Puc_v4p2_Plugin_Info|Puc_v4p2_Plugin_Update $object The source object.
55
- * @return Puc_v4p2_Plugin_Update The new copy.
56
  */
57
  public static function fromObject($object) {
58
  $update = new self();
@@ -72,7 +73,7 @@ if ( !class_exists('Puc_v4p2_Plugin_Update', false) ):
72
  *
73
  * @return object
74
  */
75
- public function toWpFormat(){
76
  $update = parent::toWpFormat();
77
 
78
  $update->id = $this->id;
@@ -80,10 +81,28 @@ if ( !class_exists('Puc_v4p2_Plugin_Update', false) ):
80
  $update->tested = $this->tested;
81
  $update->plugin = $this->filename;
82
 
83
- if ( !empty($this->upgrade_notice) ){
84
  $update->upgrade_notice = $this->upgrade_notice;
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  return $update;
88
  }
89
  }
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Plugin_Update', false) ):
3
 
4
  /**
5
  * A simple container class for holding information about an available update.
8
  * @copyright 2016
9
  * @access public
10
  */
11
+ class Puc_v4p4_Plugin_Update extends Puc_v4p4_Update {
12
  public $id = 0;
13
  public $homepage;
14
  public $upgrade_notice;
15
  public $tested;
16
+ public $icons = array();
17
  public $filename; //Plugin filename relative to the plugins directory.
18
 
19
  protected static $extraFields = array(
20
+ 'id', 'homepage', 'tested', 'upgrade_notice', 'icons', 'filename',
21
  );
22
 
23
  /**
24
  * Create a new instance of PluginUpdate from its JSON-encoded representation.
25
  *
26
  * @param string $json
27
+ * @return Puc_v4p4_Plugin_Update|null
28
  */
29
  public static function fromJson($json){
30
  //Since update-related information is simply a subset of the full plugin info,
31
  //we can parse the update JSON as if it was a plugin info string, then copy over
32
  //the parts that we care about.
33
+ $pluginInfo = Puc_v4p4_Plugin_Info::fromJson($json);
34
  if ( $pluginInfo !== null ) {
35
  return self::fromPluginInfo($pluginInfo);
36
  } else {
42
  * Create a new instance of PluginUpdate based on an instance of PluginInfo.
43
  * Basically, this just copies a subset of fields from one object to another.
44
  *
45
+ * @param Puc_v4p4_Plugin_Info $info
46
+ * @return Puc_v4p4_Plugin_Update
47
  */
48
  public static function fromPluginInfo($info){
49
  return self::fromObject($info);
52
  /**
53
  * Create a new instance by copying the necessary fields from another object.
54
  *
55
+ * @param StdClass|Puc_v4p4_Plugin_Info|Puc_v4p4_Plugin_Update $object The source object.
56
+ * @return Puc_v4p4_Plugin_Update The new copy.
57
  */
58
  public static function fromObject($object) {
59
  $update = new self();
73
  *
74
  * @return object
75
  */
76
+ public function toWpFormat() {
77
  $update = parent::toWpFormat();
78
 
79
  $update->id = $this->id;
81
  $update->tested = $this->tested;
82
  $update->plugin = $this->filename;
83
 
84
+ if ( !empty($this->upgrade_notice) ) {
85
  $update->upgrade_notice = $this->upgrade_notice;
86
  }
87
 
88
+ if ( !empty($this->icons) && is_array($this->icons) ) {
89
+ //This should be an array with up to 4 keys: 'svg', '1x', '2x' and 'default'.
90
+ //Docs: https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
91
+ $icons = array_intersect_key(
92
+ $this->icons,
93
+ array('svg' => true, '1x' => true, '2x' => true, 'default' => true)
94
+ );
95
+ if ( !empty($icons) ) {
96
+ $update->icons = $icons;
97
+
98
+ //It appears that the 'default' icon isn't used anywhere in WordPress 4.9,
99
+ //but lets set it just in case a future release needs it.
100
+ if ( !isset($update->icons['default']) ) {
101
+ $update->icons['default'] = current($update->icons);
102
+ }
103
+ }
104
+ }
105
+
106
  return $update;
107
  }
108
  }
admin/update-checker/Puc/{v4p2 → v4p4}/Plugin/UpdateChecker.php RENAMED
@@ -1,5 +1,5 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
3
 
4
  /**
5
  * A custom plugin update checker.
@@ -8,7 +8,7 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
8
  * @copyright 2016
9
  * @access public
10
  */
11
- class Puc_v4p2_Plugin_UpdateChecker extends Puc_v4p2_UpdateChecker {
12
  protected $updateTransient = 'update_plugins';
13
  protected $translationType = 'plugin';
14
 
@@ -17,6 +17,7 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
17
  public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
18
 
19
  private $cachedInstalledVersion = null;
 
20
 
21
  /**
22
  * Class constructor.
@@ -57,6 +58,12 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
57
  $this->muPluginFile = $this->pluginFile;
58
  }
59
 
 
 
 
 
 
 
60
  parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
61
  }
62
 
@@ -64,10 +71,10 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
64
  * Create an instance of the scheduler.
65
  *
66
  * @param int $checkPeriod
67
- * @return Puc_v4p2_Scheduler
68
  */
69
  protected function createScheduler($checkPeriod) {
70
- $scheduler = new Puc_v4p2_Scheduler($this, $checkPeriod, array('load-plugins.php'));
71
  register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
72
  return $scheduler;
73
  }
@@ -82,6 +89,7 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
82
  //Override requests for plugin information
83
  add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
84
 
 
85
  add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
86
  add_action('admin_init', array($this, 'handleManualCheck'));
87
  add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
@@ -93,19 +101,47 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
93
  parent::installHooks();
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  /**
97
  * Retrieve plugin info from the configured API endpoint.
98
  *
99
  * @uses wp_remote_get()
100
  *
101
  * @param array $queryArgs Additional query arguments to append to the request. Optional.
102
- * @return Puc_v4p2_Plugin_Info
103
  */
104
  public function requestInfo($queryArgs = array()) {
105
- list($pluginInfo, $result) = $this->requestMetadata('Puc_v4p2_Plugin_Info', 'request_info', $queryArgs);
106
 
107
  if ( $pluginInfo !== null ) {
108
- /** @var Puc_v4p2_Plugin_Info $pluginInfo */
109
  $pluginInfo->filename = $this->pluginFile;
110
  $pluginInfo->slug = $this->slug;
111
  }
@@ -119,7 +155,7 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
119
  *
120
  * @uses PluginUpdateChecker::requestInfo()
121
  *
122
- * @return Puc_v4p2_Update|null An instance of Plugin_Update, or NULL when no updates are available.
123
  */
124
  public function requestUpdate() {
125
  //For the sake of simplicity, this function just calls requestInfo()
@@ -128,7 +164,7 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
128
  if ( $pluginInfo === null ){
129
  return null;
130
  }
131
- $update = Puc_v4p2_Plugin_Update::fromPluginInfo($pluginInfo);
132
 
133
  $update = $this->filterUpdateResult($update);
134
 
@@ -315,12 +351,12 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
315
  * Uses cached update data. To retrieve update information straight from
316
  * the metadata URL, call requestUpdate() instead.
317
  *
318
- * @return Puc_v4p2_Plugin_Update|null
319
  */
320
  public function getUpdate() {
321
  $update = parent::getUpdate();
322
  if ( isset($update) ) {
323
- /** @var Puc_v4p2_Plugin_Update $update */
324
  $update->filename = $this->pluginFile;
325
  }
326
  return $update;
@@ -328,7 +364,8 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
328
 
329
  /**
330
  * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
331
- * the new link will appear after the "Visit plugin site" link.
 
332
  *
333
  * You can change the link text by using the "puc_manual_check_link-$slug" filter.
334
  * Returning an empty string from the filter will disable the link.
@@ -365,6 +402,81 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
365
  return $pluginMeta;
366
  }
367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  /**
369
  * Check for updates when the user clicks the "Check for updates" link.
370
  * @see self::addCheckForUpdatesLink()
@@ -381,6 +493,34 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
381
  if ( $shouldCheck ) {
382
  $update = $this->checkForUpdates();
383
  $status = ($update === null) ? 'no_update' : 'update_available';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  wp_redirect(add_query_arg(
385
  array(
386
  'puc_update_check_result' => $status,
@@ -399,22 +539,69 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
399
  */
400
  public function displayManualCheckResult() {
401
  if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->slug) ) {
402
- $status = strval($_GET['puc_update_check_result']);
403
- $title = $this->getPluginTitle();
 
 
 
404
  if ( $status == 'no_update' ) {
405
  $message = sprintf(_x('The %s plugin is up to date.', 'the plugin title', 'plugin-update-checker'), $title);
406
  } else if ( $status == 'update_available' ) {
407
  $message = sprintf(_x('A new version of the %s plugin is available.', 'the plugin title', 'plugin-update-checker'), $title);
 
 
 
 
 
 
408
  } else {
409
  $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));
 
410
  }
411
  printf(
412
- '<div class="updated notice is-dismissible"><p>%s</p></div>',
413
- apply_filters($this->getUniqueName('manual_check_message'), $message, $status)
 
 
414
  );
415
  }
416
  }
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  /**
419
  * Get the translated plugin title.
420
  *
@@ -488,6 +675,13 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
488
  return $this->pluginAbsolutePath;
489
  }
490
 
 
 
 
 
 
 
 
491
  /**
492
  * Register a callback for filtering query arguments.
493
  *
@@ -537,6 +731,10 @@ if ( !class_exists('Puc_v4p2_Plugin_UpdateChecker', false) ):
537
  public function addResultFilter($callback) {
538
  $this->addFilter('request_info_result', $callback, 10, 2);
539
  }
 
 
 
 
540
  }
541
 
542
  endif;
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Plugin_UpdateChecker', false) ):
3
 
4
  /**
5
  * A custom plugin update checker.
8
  * @copyright 2016
9
  * @access public
10
  */
11
+ class Puc_v4p4_Plugin_UpdateChecker extends Puc_v4p4_UpdateChecker {
12
  protected $updateTransient = 'update_plugins';
13
  protected $translationType = 'plugin';
14
 
17
  public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
18
 
19
  private $cachedInstalledVersion = null;
20
+ private $manualCheckErrorTransient = '';
21
 
22
  /**
23
  * Class constructor.
58
  $this->muPluginFile = $this->pluginFile;
59
  }
60
 
61
+ //To prevent a crash during plugin uninstallation, remove updater hooks when the user removes the plugin.
62
+ //Details: https://github.com/YahnisElsts/plugin-update-checker/issues/138#issuecomment-335590964
63
+ add_action('uninstall_' . $this->pluginFile, array($this, 'removeHooks'));
64
+
65
+ $this->manualCheckErrorTransient = $this->getUniqueName('manual_check_errors');
66
+
67
  parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
68
  }
69
 
71
  * Create an instance of the scheduler.
72
  *
73
  * @param int $checkPeriod
74
+ * @return Puc_v4p4_Scheduler
75
  */
76
  protected function createScheduler($checkPeriod) {
77
+ $scheduler = new Puc_v4p4_Scheduler($this, $checkPeriod, array('load-plugins.php'));
78
  register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
79
  return $scheduler;
80
  }
89
  //Override requests for plugin information
90
  add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
91
 
92
+ add_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10, 3);
93
  add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
94
  add_action('admin_init', array($this, 'handleManualCheck'));
95
  add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
101
  parent::installHooks();
102
  }
103
 
104
+ /**
105
+ * Remove update checker hooks.
106
+ *
107
+ * The intent is to prevent a fatal error that can happen if the plugin has an uninstall
108
+ * hook. During uninstallation, WP includes the main plugin file (which creates a PUC instance),
109
+ * the uninstall hook runs, WP deletes the plugin files and then updates some transients.
110
+ * If PUC hooks are still around at this time, they could throw an error while trying to
111
+ * autoload classes from files that no longer exist.
112
+ *
113
+ * The "site_transient_{$transient}" filter is the main problem here, but let's also remove
114
+ * most other PUC hooks to be safe.
115
+ *
116
+ * @internal
117
+ */
118
+ public function removeHooks() {
119
+ parent::removeHooks();
120
+
121
+ remove_filter('plugins_api', array($this, 'injectInfo'), 20);
122
+
123
+ remove_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10);
124
+ remove_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10);
125
+ remove_action('admin_init', array($this, 'handleManualCheck'));
126
+ remove_action('all_admin_notices', array($this, 'displayManualCheckResult'));
127
+
128
+ remove_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
129
+ remove_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
130
+ }
131
+
132
  /**
133
  * Retrieve plugin info from the configured API endpoint.
134
  *
135
  * @uses wp_remote_get()
136
  *
137
  * @param array $queryArgs Additional query arguments to append to the request. Optional.
138
+ * @return Puc_v4p4_Plugin_Info
139
  */
140
  public function requestInfo($queryArgs = array()) {
141
+ list($pluginInfo, $result) = $this->requestMetadata('Puc_v4p4_Plugin_Info', 'request_info', $queryArgs);
142
 
143
  if ( $pluginInfo !== null ) {
144
+ /** @var Puc_v4p4_Plugin_Info $pluginInfo */
145
  $pluginInfo->filename = $this->pluginFile;
146
  $pluginInfo->slug = $this->slug;
147
  }
155
  *
156
  * @uses PluginUpdateChecker::requestInfo()
157
  *
158
+ * @return Puc_v4p4_Update|null An instance of Plugin_Update, or NULL when no updates are available.
159
  */
160
  public function requestUpdate() {
161
  //For the sake of simplicity, this function just calls requestInfo()
164
  if ( $pluginInfo === null ){
165
  return null;
166
  }
167
+ $update = Puc_v4p4_Plugin_Update::fromPluginInfo($pluginInfo);
168
 
169
  $update = $this->filterUpdateResult($update);
170
 
351
  * Uses cached update data. To retrieve update information straight from
352
  * the metadata URL, call requestUpdate() instead.
353
  *
354
+ * @return Puc_v4p4_Plugin_Update|null
355
  */
356
  public function getUpdate() {
357
  $update = parent::getUpdate();
358
  if ( isset($update) ) {
359
+ /** @var Puc_v4p4_Plugin_Update $update */
360
  $update->filename = $this->pluginFile;
361
  }
362
  return $update;
364
 
365
  /**
366
  * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
367
+ * the new link will appear after the "Visit plugin site" link if present, otherwise
368
+ * after the "View plugin details" link.
369
  *
370
  * You can change the link text by using the "puc_manual_check_link-$slug" filter.
371
  * Returning an empty string from the filter will disable the link.
402
  return $pluginMeta;
403
  }
404
 
405
+ /**
406
+ * Add a "View Details" link to the plugin row in the "Plugins" page. By default,
407
+ * the new link will appear before the "Visit plugin site" link (if present).
408
+ *
409
+ * You can change the link text by using the "puc_view_details_link-$slug" filter.
410
+ * Returning an empty string from the filter will disable the link.
411
+ *
412
+ * You can change the position of the link using the
413
+ * "puc_view_details_link_position-$slug" filter.
414
+ * Returning 'before' or 'after' will place the link immediately before/after the
415
+ * "Visit plugin site" link
416
+ * Returning 'append' places the link after any existing links at the time of the hook.
417
+ * Returning 'replace' replaces the "Visit plugin site" link
418
+ * Returning anything else disables the link when there is a "Visit plugin site" link.
419
+ *
420
+ * If there is no "Visit plugin site" link 'append' is always used!
421
+ *
422
+ * @param array $pluginMeta Array of meta links.
423
+ * @param string $pluginFile
424
+ * @param array $pluginData Array of plugin header data.
425
+ * @return array
426
+ */
427
+ public function addViewDetailsLink($pluginMeta, $pluginFile, $pluginData = array()) {
428
+ $isRelevant = ($pluginFile == $this->pluginFile)
429
+ || (!empty($this->muPluginFile) && $pluginFile == $this->muPluginFile);
430
+
431
+ if ( $isRelevant && $this->userCanInstallUpdates() && !isset($pluginData['slug']) ) {
432
+ $linkText = apply_filters($this->getUniqueName('view_details_link'), __('View details'));
433
+ if ( !empty($linkText) ) {
434
+ $viewDetailsLinkPosition = 'append';
435
+
436
+ //Find the "Visit plugin site" link (if present).
437
+ $visitPluginSiteLinkIndex = count($pluginMeta) - 1;
438
+ if ( $pluginData['PluginURI'] ) {
439
+ $escapedPluginUri = esc_url($pluginData['PluginURI']);
440
+ foreach ($pluginMeta as $linkIndex => $existingLink) {
441
+ if ( strpos($existingLink, $escapedPluginUri) !== false ) {
442
+ $visitPluginSiteLinkIndex = $linkIndex;
443
+ $viewDetailsLinkPosition = apply_filters(
444
+ $this->getUniqueName('view_details_link_position'),
445
+ 'before'
446
+ );
447
+ break;
448
+ }
449
+ }
450
+ }
451
+
452
+ $viewDetailsLink = sprintf('<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
453
+ esc_url(network_admin_url('plugin-install.php?tab=plugin-information&plugin=' . urlencode($this->slug) .
454
+ '&TB_iframe=true&width=600&height=550')),
455
+ esc_attr(sprintf(__('More information about %s'), $pluginData['Name'])),
456
+ esc_attr($pluginData['Name']),
457
+ $linkText
458
+ );
459
+ switch ($viewDetailsLinkPosition) {
460
+ case 'before':
461
+ array_splice($pluginMeta, $visitPluginSiteLinkIndex, 0, $viewDetailsLink);
462
+ break;
463
+ case 'after':
464
+ array_splice($pluginMeta, $visitPluginSiteLinkIndex + 1, 0, $viewDetailsLink);
465
+ break;
466
+ case 'replace':
467
+ $pluginMeta[$visitPluginSiteLinkIndex] = $viewDetailsLink;
468
+ break;
469
+ case 'append':
470
+ default:
471
+ $pluginMeta[] = $viewDetailsLink;
472
+ break;
473
+ }
474
+ }
475
+ }
476
+ return $pluginMeta;
477
+ }
478
+
479
+
480
  /**
481
  * Check for updates when the user clicks the "Check for updates" link.
482
  * @see self::addCheckForUpdatesLink()
493
  if ( $shouldCheck ) {
494
  $update = $this->checkForUpdates();
495
  $status = ($update === null) ? 'no_update' : 'update_available';
496
+
497
+ if ( ($update === null) && !empty($this->lastRequestApiErrors) ) {
498
+ //Some errors are not critical. For example, if PUC tries to retrieve the readme.txt
499
+ //file from GitHub and gets a 404, that's an API error, but it doesn't prevent updates
500
+ //from working. Maybe the plugin simply doesn't have a readme.
501
+ //Let's only show important errors.
502
+ $foundCriticalErrors = false;
503
+ $questionableErrorCodes = array(
504
+ 'puc-github-http-error',
505
+ 'puc-gitlab-http-error',
506
+ 'puc-bitbucket-http-error',
507
+ );
508
+
509
+ foreach ($this->lastRequestApiErrors as $item) {
510
+ $wpError = $item['error'];
511
+ /** @var WP_Error $wpError */
512
+ if ( !in_array($wpError->get_error_code(), $questionableErrorCodes) ) {
513
+ $foundCriticalErrors = true;
514
+ break;
515
+ }
516
+ }
517
+
518
+ if ( $foundCriticalErrors ) {
519
+ $status = 'error';
520
+ set_site_transient($this->manualCheckErrorTransient, $this->lastRequestApiErrors, 60);
521
+ }
522
+ }
523
+
524
  wp_redirect(add_query_arg(
525
  array(
526
  'puc_update_check_result' => $status,
539
  */
540
  public function displayManualCheckResult() {
541
  if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->slug) ) {
542
+ $status = strval($_GET['puc_update_check_result']);
543
+ $title = $this->getPluginTitle();
544
+ $noticeClass = 'updated notice-success';
545
+ $details = '';
546
+
547
  if ( $status == 'no_update' ) {
548
  $message = sprintf(_x('The %s plugin is up to date.', 'the plugin title', 'plugin-update-checker'), $title);
549
  } else if ( $status == 'update_available' ) {
550
  $message = sprintf(_x('A new version of the %s plugin is available.', 'the plugin title', 'plugin-update-checker'), $title);
551
+ } else if ( $status === 'error' ) {
552
+ $message = sprintf(_x('Could not determine if updates are available for %s.', 'the plugin title', 'plugin-update-checker'), $title);
553
+ $noticeClass = 'error notice-error';
554
+
555
+ $details = $this->formatManualCheckErrors(get_site_transient($this->manualCheckErrorTransient));
556
+ delete_site_transient($this->manualCheckErrorTransient);
557
  } else {
558
  $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));
559
+ $noticeClass = 'error notice-error';
560
  }
561
  printf(
562
+ '<div class="notice %s is-dismissible"><p>%s</p>%s</div>',
563
+ $noticeClass,
564
+ apply_filters($this->getUniqueName('manual_check_message'), $message, $status),
565
+ $details
566
  );
567
  }
568
  }
569
 
570
+ /**
571
+ * Format the list of errors that were thrown during an update check.
572
+ *
573
+ * @param array $errors
574
+ * @return string
575
+ */
576
+ protected function formatManualCheckErrors($errors) {
577
+ if ( empty($errors) ) {
578
+ return '';
579
+ }
580
+ $output = '';
581
+
582
+ $showAsList = count($errors) > 1;
583
+ if ( $showAsList ) {
584
+ $output .= '<ol>';
585
+ $formatString = '<li>%1$s <code>%2$s</code></li>';
586
+ } else {
587
+ $formatString = '<p>%1$s <code>%2$s</code></p>';
588
+ }
589
+ foreach ($errors as $item) {
590
+ $wpError = $item['error'];
591
+ /** @var WP_Error $wpError */
592
+ $output .= sprintf(
593
+ $formatString,
594
+ $wpError->get_error_message(),
595
+ $wpError->get_error_code()
596
+ );
597
+ }
598
+ if ( $showAsList ) {
599
+ $output .= '</ol>';
600
+ }
601
+
602
+ return $output;
603
+ }
604
+
605
  /**
606
  * Get the translated plugin title.
607
  *
675
  return $this->pluginAbsolutePath;
676
  }
677
 
678
+ /**
679
+ * @return string
680
+ */
681
+ public function getAbsoluteDirectoryPath() {
682
+ return dirname($this->pluginAbsolutePath);
683
+ }
684
+
685
  /**
686
  * Register a callback for filtering query arguments.
687
  *
731
  public function addResultFilter($callback) {
732
  $this->addFilter('request_info_result', $callback, 10, 2);
733
  }
734
+
735
+ protected function createDebugBarExtension() {
736
+ return new Puc_v4p4_DebugBar_PluginExtension($this);
737
+ }
738
  }
739
 
740
  endif;
admin/update-checker/Puc/{v4p2 → v4p4}/Scheduler.php RENAMED
@@ -1,11 +1,11 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Scheduler', false) ):
3
 
4
  /**
5
  * The scheduler decides when and how often to check for updates.
6
- * It calls @see Puc_v4p2_UpdateChecker::checkForUpdates() to perform the actual checks.
7
  */
8
- class Puc_v4p2_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;
@@ -13,7 +13,7 @@ if ( !class_exists('Puc_v4p2_Scheduler', false) ):
13
  protected $hourlyCheckHooks = array('load-update.php');
14
 
15
  /**
16
- * @var Puc_v4p2_UpdateChecker
17
  */
18
  protected $updateChecker;
19
 
@@ -22,7 +22,7 @@ if ( !class_exists('Puc_v4p2_Scheduler', false) ):
22
  /**
23
  * Scheduler constructor.
24
  *
25
- * @param Puc_v4p2_UpdateChecker $updateChecker
26
  * @param int $checkPeriod How often to check for updates (in hours).
27
  * @param array $hourlyHooks
28
  */
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Scheduler', false) ):
3
 
4
  /**
5
  * The scheduler decides when and how often to check for updates.
6
+ * It calls @see Puc_v4p4_UpdateChecker::checkForUpdates() to perform the actual checks.
7
  */
8
+ class Puc_v4p4_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;
13
  protected $hourlyCheckHooks = array('load-update.php');
14
 
15
  /**
16
+ * @var Puc_v4p4_UpdateChecker
17
  */
18
  protected $updateChecker;
19
 
22
  /**
23
  * Scheduler constructor.
24
  *
25
+ * @param Puc_v4p4_UpdateChecker $updateChecker
26
  * @param int $checkPeriod How often to check for updates (in hours).
27
  * @param array $hourlyHooks
28
  */
admin/update-checker/Puc/{v4p2 → v4p4}/StateStore.php RENAMED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_StateStore', false) ):
4
 
5
- class Puc_v4p2_StateStore {
6
  /**
7
  * @var int Last update check timestamp.
8
  */
@@ -14,7 +14,7 @@ if ( !class_exists('Puc_v4p2_StateStore', false) ):
14
  protected $checkedVersion = '';
15
 
16
  /**
17
- * @var Puc_v4p2_Update|null Cached update.
18
  */
19
  protected $update = null;
20
 
@@ -65,7 +65,7 @@ if ( !class_exists('Puc_v4p2_StateStore', false) ):
65
  }
66
 
67
  /**
68
- * @return null|Puc_v4p2_Update
69
  */
70
  public function getUpdate() {
71
  $this->lazyLoad();
@@ -73,10 +73,10 @@ if ( !class_exists('Puc_v4p2_StateStore', false) ):
73
  }
74
 
75
  /**
76
- * @param Puc_v4p2_Update|null $update
77
  * @return $this
78
  */
79
- public function setUpdate(Puc_v4p2_Update $update = null) {
80
  $this->lazyLoad();
81
  $this->update = $update;
82
  return $this;
@@ -138,7 +138,7 @@ if ( !class_exists('Puc_v4p2_StateStore', false) ):
138
  $updateClass = get_class($this->update);
139
  $state->updateClass = $updateClass;
140
  $prefix = $this->getLibPrefix();
141
- if ( Puc_v4p2_Utils::startsWith($updateClass, $prefix) ) {
142
  $state->updateBaseClass = substr($updateClass, strlen($prefix));
143
  }
144
  }
@@ -169,8 +169,8 @@ if ( !class_exists('Puc_v4p2_StateStore', false) ):
169
  return;
170
  }
171
 
172
- $this->lastCheck = intval(Puc_v4p2_Utils::get($state, 'lastCheck', 0));
173
- $this->checkedVersion = Puc_v4p2_Utils::get($state, 'checkedVersion', '');
174
  $this->update = null;
175
 
176
  if ( isset($state->update) ) {
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_StateStore', false) ):
4
 
5
+ class Puc_v4p4_StateStore {
6
  /**
7
  * @var int Last update check timestamp.
8
  */
14
  protected $checkedVersion = '';
15
 
16
  /**
17
+ * @var Puc_v4p4_Update|null Cached update.
18
  */
19
  protected $update = null;
20
 
65
  }
66
 
67
  /**
68
+ * @return null|Puc_v4p4_Update
69
  */
70
  public function getUpdate() {
71
  $this->lazyLoad();
73
  }
74
 
75
  /**
76
+ * @param Puc_v4p4_Update|null $update
77
  * @return $this
78
  */
79
+ public function setUpdate(Puc_v4p4_Update $update = null) {
80
  $this->lazyLoad();
81
  $this->update = $update;
82
  return $this;
138
  $updateClass = get_class($this->update);
139
  $state->updateClass = $updateClass;
140
  $prefix = $this->getLibPrefix();
141
+ if ( Puc_v4p4_Utils::startsWith($updateClass, $prefix) ) {
142
  $state->updateBaseClass = substr($updateClass, strlen($prefix));
143
  }
144
  }
169
  return;
170
  }
171
 
172
+ $this->lastCheck = intval(Puc_v4p4_Utils::get($state, 'lastCheck', 0));
173
+ $this->checkedVersion = Puc_v4p4_Utils::get($state, 'checkedVersion', '');
174
  $this->update = null;
175
 
176
  if ( isset($state->update) ) {
admin/update-checker/Puc/{v4p2 → v4p4}/Theme/Update.php RENAMED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_Theme_Update', false) ):
4
 
5
- class Puc_v4p2_Theme_Update extends Puc_v4p2_Update {
6
  public $details_url = '';
7
 
8
  protected static $extraFields = array('details_url');
@@ -44,8 +44,8 @@ if ( !class_exists('Puc_v4p2_Theme_Update', false) ):
44
  /**
45
  * Create a new instance by copying the necessary fields from another object.
46
  *
47
- * @param StdClass|Puc_v4p2_Theme_Update $object The source object.
48
- * @return Puc_v4p2_Theme_Update The new copy.
49
  */
50
  public static function fromObject($object) {
51
  $update = new self();
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_Theme_Update', false) ):
4
 
5
+ class Puc_v4p4_Theme_Update extends Puc_v4p4_Update {
6
  public $details_url = '';
7
 
8
  protected static $extraFields = array('details_url');
44
  /**
45
  * Create a new instance by copying the necessary fields from another object.
46
  *
47
+ * @param StdClass|Puc_v4p4_Theme_Update $object The source object.
48
+ * @return Puc_v4p4_Theme_Update The new copy.
49
  */
50
  public static function fromObject($object) {
51
  $update = new self();
admin/update-checker/Puc/{v4p2 → v4p4}/Theme/UpdateChecker.php RENAMED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_Theme_UpdateChecker', false) ):
4
 
5
- class Puc_v4p2_Theme_UpdateChecker extends Puc_v4p2_UpdateChecker {
6
  protected $filterSuffix = 'theme';
7
  protected $updateTransient = 'update_themes';
8
  protected $translationType = 'theme';
@@ -45,13 +45,13 @@ if ( !class_exists('Puc_v4p2_Theme_UpdateChecker', false) ):
45
  /**
46
  * Retrieve the latest update (if any) from the configured API endpoint.
47
  *
48
- * @return Puc_v4p2_Update|null An instance of Update, or NULL when no updates are available.
49
  */
50
  public function requestUpdate() {
51
- list($themeUpdate, $result) = $this->requestMetadata('Puc_v4p2_Theme_Update', 'request_update');
52
 
53
  if ( $themeUpdate !== null ) {
54
- /** @var Puc_v4p2_Theme_Update $themeUpdate */
55
  $themeUpdate->slug = $this->slug;
56
  }
57
 
@@ -72,14 +72,24 @@ if ( !class_exists('Puc_v4p2_Theme_UpdateChecker', false) ):
72
  return $this->theme->get('Version');
73
  }
74
 
 
 
 
 
 
 
 
 
 
 
75
  /**
76
  * Create an instance of the scheduler.
77
  *
78
  * @param int $checkPeriod
79
- * @return Puc_v4p2_Scheduler
80
  */
81
  protected function createScheduler($checkPeriod) {
82
- return new Puc_v4p2_Scheduler($this, $checkPeriod, array('load-themes.php'));
83
  }
84
 
85
  /**
@@ -92,6 +102,10 @@ if ( !class_exists('Puc_v4p2_Theme_UpdateChecker', false) ):
92
  return $this->upgraderStatus->isThemeBeingUpgraded($this->stylesheet, $upgrader);
93
  }
94
 
 
 
 
 
95
  /**
96
  * Register a callback for filtering query arguments.
97
  *
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_Theme_UpdateChecker', false) ):
4
 
5
+ class Puc_v4p4_Theme_UpdateChecker extends Puc_v4p4_UpdateChecker {
6
  protected $filterSuffix = 'theme';
7
  protected $updateTransient = 'update_themes';
8
  protected $translationType = 'theme';
45
  /**
46
  * Retrieve the latest update (if any) from the configured API endpoint.
47
  *
48
+ * @return Puc_v4p4_Update|null An instance of Update, or NULL when no updates are available.
49
  */
50
  public function requestUpdate() {
51
+ list($themeUpdate, $result) = $this->requestMetadata('Puc_v4p4_Theme_Update', 'request_update');
52
 
53
  if ( $themeUpdate !== null ) {
54
+ /** @var Puc_v4p4_Theme_Update $themeUpdate */
55
  $themeUpdate->slug = $this->slug;
56
  }
57
 
72
  return $this->theme->get('Version');
73
  }
74
 
75
+ /**
76
+ * @return string
77
+ */
78
+ public function getAbsoluteDirectoryPath() {
79
+ if ( method_exists($this->theme, 'get_stylesheet_directory') ) {
80
+ return $this->theme->get_stylesheet_directory(); //Available since WP 3.4.
81
+ }
82
+ return get_theme_root($this->stylesheet) . '/' . $this->stylesheet;
83
+ }
84
+
85
  /**
86
  * Create an instance of the scheduler.
87
  *
88
  * @param int $checkPeriod
89
+ * @return Puc_v4p4_Scheduler
90
  */
91
  protected function createScheduler($checkPeriod) {
92
+ return new Puc_v4p4_Scheduler($this, $checkPeriod, array('load-themes.php'));
93
  }
94
 
95
  /**
102
  return $this->upgraderStatus->isThemeBeingUpgraded($this->stylesheet, $upgrader);
103
  }
104
 
105
+ protected function createDebugBarExtension() {
106
+ return new Puc_v4p4_DebugBar_Extension($this, 'Puc_v4p4_DebugBar_ThemePanel');
107
+ }
108
+
109
  /**
110
  * Register a callback for filtering query arguments.
111
  *
admin/update-checker/Puc/{v4p2 → v4p4}/Update.php RENAMED
@@ -1,5 +1,5 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Update', false) ):
3
 
4
  /**
5
  * A simple container class for holding information about an available update.
@@ -7,7 +7,7 @@ if ( !class_exists('Puc_v4p2_Update', false) ):
7
  * @author Janis Elsts
8
  * @access public
9
  */
10
- abstract class Puc_v4p2_Update extends Puc_v4p2_Metadata {
11
  public $slug;
12
  public $version;
13
  public $download_url;
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Update', false) ):
3
 
4
  /**
5
  * A simple container class for holding information about an available update.
7
  * @author Janis Elsts
8
  * @access public
9
  */
10
+ abstract class Puc_v4p4_Update extends Puc_v4p4_Metadata {
11
  public $slug;
12
  public $version;
13
  public $download_url;
admin/update-checker/Puc/{v4p2 → v4p4}/UpdateChecker.php RENAMED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
4
 
5
- abstract class Puc_v4p2_UpdateChecker {
6
  protected $filterSuffix = '';
7
  protected $updateTransient = '';
8
  protected $translationType = ''; //"plugin" or "theme".
@@ -36,20 +36,25 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
36
  public $slug = '';
37
 
38
  /**
39
- * @var Puc_v4p2_Scheduler
40
  */
41
  public $scheduler;
42
 
43
  /**
44
- * @var Puc_v4p2_UpgraderStatus
45
  */
46
  protected $upgraderStatus;
47
 
48
  /**
49
- * @var Puc_v4p2_StateStore
50
  */
51
  protected $updateState;
52
 
 
 
 
 
 
53
  public function __construct($metadataUrl, $directoryName, $slug = null, $checkPeriod = 12, $optionName = '') {
54
  $this->debugMode = (bool)(constant('WP_DEBUG'));
55
  $this->metadataUrl = $metadataUrl;
@@ -68,8 +73,8 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
68
  }
69
 
70
  $this->scheduler = $this->createScheduler($checkPeriod);
71
- $this->upgraderStatus = new Puc_v4p2_UpgraderStatus();
72
- $this->updateState = new Puc_v4p2_StateStore($this->optionName);
73
 
74
  if ( did_action('init') ) {
75
  $this->loadTextDomain();
@@ -123,6 +128,31 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
123
 
124
  //Allow HTTP requests to the metadata URL even if it's on a local host.
125
  add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  }
127
 
128
  /**
@@ -169,14 +199,14 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
169
  * and substitute their own scheduler.
170
  *
171
  * @param int $checkPeriod
172
- * @return Puc_v4p2_Scheduler
173
  */
174
  abstract protected function createScheduler($checkPeriod);
175
 
176
  /**
177
  * Check for updates. The results are stored in the DB option specified in $optionName.
178
  *
179
- * @return Puc_v4p2_Update|null
180
  */
181
  public function checkForUpdates() {
182
  $installedVersion = $this->getInstalledVersion();
@@ -189,6 +219,10 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
189
  return null;
190
  }
191
 
 
 
 
 
192
  $state = $this->updateState;
193
  $state->setLastCheckToNow()
194
  ->setCheckedVersion($installedVersion)
@@ -197,13 +231,16 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
197
  $state->setUpdate($this->requestUpdate());
198
  $state->save();
199
 
 
 
 
200
  return $this->getUpdate();
201
  }
202
 
203
  /**
204
  * Load the update checker state from the DB.
205
  *
206
- * @return Puc_v4p2_StateStore
207
  */
208
  public function getUpdateState() {
209
  return $this->updateState->lazyLoad();
@@ -228,7 +265,7 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
228
  * Uses cached update data. To retrieve update information straight from
229
  * the metadata URL, call requestUpdate() instead.
230
  *
231
- * @return Puc_v4p2_Update|null
232
  */
233
  public function getUpdate() {
234
  $update = $this->updateState->getUpdate();
@@ -249,16 +286,16 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
249
  *
250
  * Subclasses should run the update through filterUpdateResult before returning it.
251
  *
252
- * @return Puc_v4p2_Update An instance of Update, or NULL when no updates are available.
253
  */
254
  abstract public function requestUpdate();
255
 
256
  /**
257
  * Filter the result of a requestUpdate() call.
258
  *
259
- * @param Puc_v4p2_Update|null $update
260
  * @param array|WP_Error|null $httpResult The value returned by wp_remote_get(), if any.
261
- * @return Puc_v4p2_Update
262
  */
263
  protected function filterUpdateResult($update, $httpResult = null) {
264
  //Let plugins/themes modify the update.
@@ -279,6 +316,13 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
279
  */
280
  abstract public function getInstalledVersion();
281
 
 
 
 
 
 
 
 
282
  /**
283
  * Trigger a PHP error, but only when $debugMode is enabled.
284
  *
@@ -308,6 +352,34 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
308
  return $name . '-' . $this->slug;
309
  }
310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  /* -------------------------------------------------------------------
312
  * PUC filters and filter utilities
313
  * -------------------------------------------------------------------
@@ -422,7 +494,7 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
422
  * @param string $metaClass Parse the JSON as an instance of this class. It must have a static fromJson method.
423
  * @param string $filterRoot
424
  * @param array $queryArgs Additional query arguments.
425
- * @return array [Puc_v4p2_Metadata|null, array|WP_Error] A metadata instance and the value returned by wp_remote_get().
426
  */
427
  protected function requestMetadata($metaClass, $filterRoot, $queryArgs = array()) {
428
  //Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).
@@ -461,6 +533,7 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
461
  if ( !is_wp_error($status) ){
462
  $metadata = call_user_func(array($metaClass, 'fromJson'), $result['body']);
463
  } else {
 
464
  $this->triggerError(
465
  sprintf('The URL %s does not point to a valid metadata file. ', $url)
466
  . $status->get_error_message(),
@@ -544,6 +617,9 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
544
  * @return array
545
  */
546
  protected function getInstalledTranslations() {
 
 
 
547
  $installedTranslations = wp_get_installed_translations($this->translationType . 's');
548
  if ( isset($installedTranslations[$this->directoryName]) ) {
549
  $installedTranslations = $installedTranslations[$this->directoryName];
@@ -787,6 +863,34 @@ if ( !class_exists('Puc_v4p2_UpdateChecker', false) ):
787
  * @return array Format: ['HeaderKey' => 'Header Name']
788
  */
789
  abstract protected function getHeaderNames();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
790
  }
791
 
792
  endif;
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_UpdateChecker', false) ):
4
 
5
+ abstract class Puc_v4p4_UpdateChecker {
6
  protected $filterSuffix = '';
7
  protected $updateTransient = '';
8
  protected $translationType = ''; //"plugin" or "theme".
36
  public $slug = '';
37
 
38
  /**
39
+ * @var Puc_v4p4_Scheduler
40
  */
41
  public $scheduler;
42
 
43
  /**
44
+ * @var Puc_v4p4_UpgraderStatus
45
  */
46
  protected $upgraderStatus;
47
 
48
  /**
49
+ * @var Puc_v4p4_StateStore
50
  */
51
  protected $updateState;
52
 
53
+ /**
54
+ * @var array List of API errors triggered during the last checkForUpdates() call.
55
+ */
56
+ protected $lastRequestApiErrors = array();
57
+
58
  public function __construct($metadataUrl, $directoryName, $slug = null, $checkPeriod = 12, $optionName = '') {
59
  $this->debugMode = (bool)(constant('WP_DEBUG'));
60
  $this->metadataUrl = $metadataUrl;
73
  }
74
 
75
  $this->scheduler = $this->createScheduler($checkPeriod);
76
+ $this->upgraderStatus = new Puc_v4p4_UpgraderStatus();
77
+ $this->updateState = new Puc_v4p4_StateStore($this->optionName);
78
 
79
  if ( did_action('init') ) {
80
  $this->loadTextDomain();
128
 
129
  //Allow HTTP requests to the metadata URL even if it's on a local host.
130
  add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
131
+
132
+ //DebugBar integration.
133
+ if ( did_action('plugins_loaded') ) {
134
+ $this->maybeInitDebugBar();
135
+ } else {
136
+ add_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Remove hooks that were added by this update checker instance.
142
+ */
143
+ protected function removeHooks() {
144
+ remove_filter('site_transient_' . $this->updateTransient, array($this,'injectUpdate'));
145
+ remove_filter('site_transient_' . $this->updateTransient, array($this, 'injectTranslationUpdates'));
146
+ remove_action(
147
+ 'delete_site_transient_' . $this->updateTransient,
148
+ array($this, 'clearCachedTranslationUpdates')
149
+ );
150
+
151
+ remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10);
152
+ remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10);
153
+ remove_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
154
+
155
+ remove_action('init', array($this, 'loadTextDomain'));
156
  }
157
 
158
  /**
199
  * and substitute their own scheduler.
200
  *
201
  * @param int $checkPeriod
202
+ * @return Puc_v4p4_Scheduler
203
  */
204
  abstract protected function createScheduler($checkPeriod);
205
 
206
  /**
207
  * Check for updates. The results are stored in the DB option specified in $optionName.
208
  *
209
+ * @return Puc_v4p4_Update|null
210
  */
211
  public function checkForUpdates() {
212
  $installedVersion = $this->getInstalledVersion();
219
  return null;
220
  }
221
 
222
+ //Start collecting API errors.
223
+ $this->lastRequestApiErrors = array();
224
+ add_action('puc_api_error', array($this, 'collectApiErrors'), 10, 4);
225
+
226
  $state = $this->updateState;
227
  $state->setLastCheckToNow()
228
  ->setCheckedVersion($installedVersion)
231
  $state->setUpdate($this->requestUpdate());
232
  $state->save();
233
 
234
+ //Stop collecting API errors.
235
+ remove_action('puc_api_error', array($this, 'collectApiErrors'), 10);
236
+
237
  return $this->getUpdate();
238
  }
239
 
240
  /**
241
  * Load the update checker state from the DB.
242
  *
243
+ * @return Puc_v4p4_StateStore
244
  */
245
  public function getUpdateState() {
246
  return $this->updateState->lazyLoad();
265
  * Uses cached update data. To retrieve update information straight from
266
  * the metadata URL, call requestUpdate() instead.
267
  *
268
+ * @return Puc_v4p4_Update|null
269
  */
270
  public function getUpdate() {
271
  $update = $this->updateState->getUpdate();
286
  *
287
  * Subclasses should run the update through filterUpdateResult before returning it.
288
  *
289
+ * @return Puc_v4p4_Update An instance of Update, or NULL when no updates are available.
290
  */
291
  abstract public function requestUpdate();
292
 
293
  /**
294
  * Filter the result of a requestUpdate() call.
295
  *
296
+ * @param Puc_v4p4_Update|null $update
297
  * @param array|WP_Error|null $httpResult The value returned by wp_remote_get(), if any.
298
+ * @return Puc_v4p4_Update
299
  */
300
  protected function filterUpdateResult($update, $httpResult = null) {
301
  //Let plugins/themes modify the update.
316
  */
317
  abstract public function getInstalledVersion();
318
 
319
+ /**
320
+ * Get the full path of the plugin or theme directory.
321
+ *
322
+ * @return string
323
+ */
324
+ abstract public function getAbsoluteDirectoryPath();
325
+
326
  /**
327
  * Trigger a PHP error, but only when $debugMode is enabled.
328
  *
352
  return $name . '-' . $this->slug;
353
  }
354
 
355
+ /**
356
+ * Store API errors that are generated when checking for updates.
357
+ *
358
+ * @internal
359
+ * @param WP_Error $error
360
+ * @param array|null $httpResponse
361
+ * @param string|null $url
362
+ * @param string|null $slug
363
+ */
364
+ public function collectApiErrors($error, $httpResponse = null, $url = null, $slug = null) {
365
+ if ( isset($slug) && ($slug !== $this->slug) ) {
366
+ return;
367
+ }
368
+
369
+ $this->lastRequestApiErrors[] = array(
370
+ 'error' => $error,
371
+ 'httpResponse' => $httpResponse,
372
+ 'url' => $url,
373
+ );
374
+ }
375
+
376
+ /**
377
+ * @return array
378
+ */
379
+ public function getLastRequestApiErrors() {
380
+ return $this->lastRequestApiErrors;
381
+ }
382
+
383
  /* -------------------------------------------------------------------
384
  * PUC filters and filter utilities
385
  * -------------------------------------------------------------------
494
  * @param string $metaClass Parse the JSON as an instance of this class. It must have a static fromJson method.
495
  * @param string $filterRoot
496
  * @param array $queryArgs Additional query arguments.
497
+ * @return array [Puc_v4p4_Metadata|null, array|WP_Error] A metadata instance and the value returned by wp_remote_get().
498
  */
499
  protected function requestMetadata($metaClass, $filterRoot, $queryArgs = array()) {
500
  //Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).
533
  if ( !is_wp_error($status) ){
534
  $metadata = call_user_func(array($metaClass, 'fromJson'), $result['body']);
535
  } else {
536
+ do_action('puc_api_error', $status, $result, $url, $this->slug);
537
  $this->triggerError(
538
  sprintf('The URL %s does not point to a valid metadata file. ', $url)
539
  . $status->get_error_message(),
617
  * @return array
618
  */
619
  protected function getInstalledTranslations() {
620
+ if ( !function_exists('wp_get_installed_translations') ) {
621
+ return array();
622
+ }
623
  $installedTranslations = wp_get_installed_translations($this->translationType . 's');
624
  if ( isset($installedTranslations[$this->directoryName]) ) {
625
  $installedTranslations = $installedTranslations[$this->directoryName];
863
  * @return array Format: ['HeaderKey' => 'Header Name']
864
  */
865
  abstract protected function getHeaderNames();
866
+
867
+ /* -------------------------------------------------------------------
868
+ * DebugBar integration
869
+ * -------------------------------------------------------------------
870
+ */
871
+
872
+ /**
873
+ * Initialize the update checker Debug Bar plugin/add-on thingy.
874
+ */
875
+ public function maybeInitDebugBar() {
876
+ if ( class_exists('Debug_Bar', false) && file_exists(dirname(__FILE__ . '/DebugBar')) ) {
877
+ $this->createDebugBarExtension();
878
+ }
879
+ }
880
+
881
+ protected function createDebugBarExtension() {
882
+ return new Puc_v4p4_DebugBar_Extension($this);
883
+ }
884
+
885
+ /**
886
+ * Display additional configuration details in the Debug Bar panel.
887
+ *
888
+ * @param Puc_v4p4_DebugBar_Panel $panel
889
+ */
890
+ public function onDisplayConfiguration($panel) {
891
+ //Do nothing. Subclasses can use this to add additional info to the panel.
892
+ }
893
+
894
  }
895
 
896
  endif;
admin/update-checker/Puc/{v4p2 → v4p4}/UpgraderStatus.php RENAMED
@@ -1,5 +1,5 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_UpgraderStatus', false) ):
3
 
4
  /**
5
  * A utility class that helps figure out which plugin or theme WordPress is upgrading.
@@ -8,7 +8,7 @@ if ( !class_exists('Puc_v4p2_UpgraderStatus', false) ):
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_v4p2_UpgraderStatus {
12
  private $currentType = null; //"plugin" or "theme".
13
  private $currentId = null; //Plugin basename or theme directory name.
14
 
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_UpgraderStatus', false) ):
3
 
4
  /**
5
  * A utility class that helps figure out which plugin or theme WordPress is upgrading.
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_v4p4_UpgraderStatus {
12
  private $currentType = null; //"plugin" or "theme".
13
  private $currentId = null; //Plugin basename or theme directory name.
14
 
admin/update-checker/Puc/{v4p2 → v4p4}/Utils.php RENAMED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_Utils', false) ):
4
 
5
- class Puc_v4p2_Utils {
6
  /**
7
  * Get a value from a nested array or object based on a path.
8
  *
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_Utils', false) ):
4
 
5
+ class Puc_v4p4_Utils {
6
  /**
7
  * Get a value from a nested array or object based on a path.
8
  *
admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/Api.php RENAMED
@@ -1,8 +1,9 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Vcs_Api') ):
3
 
4
- abstract class Puc_v4p2_Vcs_Api {
5
  protected $tagNameProperty = 'name';
 
6
 
7
  /**
8
  * @var string
@@ -21,7 +22,12 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
21
  protected $httpFilterName = '';
22
 
23
  /**
24
- * Puc_v4p2_Vcs_Api constructor.
 
 
 
 
 
25
  *
26
  * @param string $repositoryUrl
27
  * @param array|string|null $credentials
@@ -42,7 +48,7 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
42
  * Figure out which reference (i.e tag or branch) contains the latest version.
43
  *
44
  * @param string $configBranch Start looking in this branch.
45
- * @return null|Puc_v4p2_Vcs_Reference
46
  */
47
  abstract public function chooseReference($configBranch);
48
 
@@ -54,7 +60,7 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
54
  * @return array Parsed readme.
55
  */
56
  public function getRemoteReadme($ref = 'master') {
57
- $fileContents = $this->getRemoteFile('readme.txt', $ref);
58
  if ( empty($fileContents) ) {
59
  return array();
60
  }
@@ -63,11 +69,43 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
63
  return $parser->parse_readme_contents($fileContents);
64
  }
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  /**
67
  * Get a branch.
68
  *
69
  * @param string $branchName
70
- * @return Puc_v4p2_Vcs_Reference|null
71
  */
72
  abstract public function getBranch($branchName);
73
 
@@ -75,7 +113,7 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
75
  * Get a specific tag.
76
  *
77
  * @param string $tagName
78
- * @return Puc_v4p2_Vcs_Reference|null
79
  */
80
  abstract public function getTag($tagName);
81
 
@@ -83,7 +121,7 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
83
  * Get the tag that looks like the highest version number.
84
  * (Implementations should skip pre-release versions if possible.)
85
  *
86
- * @return Puc_v4p2_Vcs_Reference|null
87
  */
88
  abstract public function getLatestTag();
89
 
@@ -196,7 +234,10 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
196
  * @param string $directory
197
  * @return string|null
198
  */
199
- protected function findChangelogName($directory) {
 
 
 
200
  if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
201
  return null;
202
  }
@@ -238,6 +279,24 @@ if ( !class_exists('Puc_v4p2_Vcs_Api') ):
238
  public function setHttpFilterName($filterName) {
239
  $this->httpFilterName = $filterName;
240
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
 
243
  endif;
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Vcs_Api') ):
3
 
4
+ abstract class Puc_v4p4_Vcs_Api {
5
  protected $tagNameProperty = 'name';
6
+ protected $slug = '';
7
 
8
  /**
9
  * @var string
22
  protected $httpFilterName = '';
23
 
24
  /**
25
+ * @var string|null
26
+ */
27
+ protected $localDirectory = null;
28
+
29
+ /**
30
+ * Puc_v4p4_Vcs_Api constructor.
31
  *
32
  * @param string $repositoryUrl
33
  * @param array|string|null $credentials
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_v4p4_Vcs_Reference
52
  */
53
  abstract public function chooseReference($configBranch);
54
 
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
  }
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_v4p4_Vcs_Reference|null
109
  */
110
  abstract public function getBranch($branchName);
111
 
113
  * Get a specific tag.
114
  *
115
  * @param string $tagName
116
+ * @return Puc_v4p4_Vcs_Reference|null
117
  */
118
  abstract public function getTag($tagName);
119
 
121
  * Get the tag that looks like the highest version number.
122
  * (Implementations should skip pre-release versions if possible.)
123
  *
124
+ * @return Puc_v4p4_Vcs_Reference|null
125
  */
126
  abstract public function getLatestTag();
127
 
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
  }
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;
admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/BaseChecker.php RENAMED
@@ -1,7 +1,7 @@
1
  <?php
2
- if ( !interface_exists('Puc_v4p2_Vcs_BaseChecker', false) ):
3
 
4
- interface Puc_v4p2_Vcs_BaseChecker {
5
  /**
6
  * Set the repository branch to use for updates. Defaults to 'master'.
7
  *
@@ -17,6 +17,11 @@ if ( !interface_exists('Puc_v4p2_Vcs_BaseChecker', false) ):
17
  * @return $this
18
  */
19
  public function setAuthentication($credentials);
 
 
 
 
 
20
  }
21
 
22
  endif;
1
  <?php
2
+ if ( !interface_exists('Puc_v4p4_Vcs_BaseChecker', false) ):
3
 
4
+ interface Puc_v4p4_Vcs_BaseChecker {
5
  /**
6
  * Set the repository branch to use for updates. Defaults to 'master'.
7
  *
17
  * @return $this
18
  */
19
  public function setAuthentication($credentials);
20
+
21
+ /**
22
+ * @return Puc_v4p4_Vcs_Api
23
+ */
24
+ public function getVcsApi();
25
  }
26
 
27
  endif;
admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/BitBucketApi.php RENAMED
@@ -1,9 +1,9 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
3
 
4
- class Puc_v4p2_Vcs_BitBucketApi extends Puc_v4p2_Vcs_Api {
5
  /**
6
- * @var Puc_v4p2_OAuthSignature
7
  */
8
  private $oauth = null;
9
 
@@ -33,7 +33,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
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_v4p2_Vcs_Reference
37
  */
38
  public function chooseReference($configBranch) {
39
  $updateSource = null;
@@ -59,7 +59,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
59
  return null;
60
  }
61
 
62
- return new Puc_v4p2_Vcs_Reference(array(
63
  'name' => $branch->name,
64
  'updated' => $branch->target->date,
65
  'downloadUrl' => $this->getDownloadUrl($branch->name),
@@ -70,7 +70,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
70
  * Get a specific tag.
71
  *
72
  * @param string $tagName
73
- * @return Puc_v4p2_Vcs_Reference|null
74
  */
75
  public function getTag($tagName) {
76
  $tag = $this->api('/refs/tags/' . $tagName);
@@ -78,7 +78,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
78
  return null;
79
  }
80
 
81
- return new Puc_v4p2_Vcs_Reference(array(
82
  'name' => $tag->name,
83
  'version' => ltrim($tag->name, 'v'),
84
  'updated' => $tag->target->date,
@@ -89,7 +89,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
89
  /**
90
  * Get the tag that looks like the highest version number.
91
  *
92
- * @return Puc_v4p2_Vcs_Reference|null
93
  */
94
  public function getLatestTag() {
95
  $tags = $this->api('/refs/tags?sort=-target.date');
@@ -103,7 +103,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
103
  //Return the first result.
104
  if ( !empty($versionTags) ) {
105
  $tag = $versionTags[0];
106
- return new Puc_v4p2_Vcs_Reference(array(
107
  'name' => $tag->name,
108
  'version' => ltrim($tag->name, 'v'),
109
  'updated' => $tag->target->date,
@@ -117,7 +117,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
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_v4p2_Vcs_Reference
121
  */
122
  protected function getStableTag($branch) {
123
  $remoteReadme = $this->getRemoteReadme($branch);
@@ -194,6 +194,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
194
  $this->repository,
195
  ltrim($url, '/')
196
  ));
 
197
 
198
  if ( $this->oauth ) {
199
  $url = $this->oauth->sign($url,'GET');
@@ -205,6 +206,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
205
  }
206
  $response = wp_remote_get($url, $options);
207
  if ( is_wp_error($response) ) {
 
208
  return $response;
209
  }
210
 
@@ -215,10 +217,13 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
215
  return $document;
216
  }
217
 
218
- return new WP_Error(
219
  'puc-bitbucket-http-error',
220
- 'BitBucket API error. HTTP status: ' . $code
221
  );
 
 
 
222
  }
223
 
224
  /**
@@ -228,7 +233,7 @@ if ( !class_exists('Puc_v4p2_Vcs_BitBucketApi', false) ):
228
  parent::setAuthentication($credentials);
229
 
230
  if ( !empty($credentials) && !empty($credentials['consumer_key']) ) {
231
- $this->oauth = new Puc_v4p2_OAuthSignature(
232
  $credentials['consumer_key'],
233
  $credentials['consumer_secret']
234
  );
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Vcs_BitBucketApi', false) ):
3
 
4
+ class Puc_v4p4_Vcs_BitBucketApi extends Puc_v4p4_Vcs_Api {
5
  /**
6
+ * @var Puc_v4p4_OAuthSignature
7
  */
8
  private $oauth = null;
9
 
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_v4p4_Vcs_Reference
37
  */
38
  public function chooseReference($configBranch) {
39
  $updateSource = null;
59
  return null;
60
  }
61
 
62
+ return new Puc_v4p4_Vcs_Reference(array(
63
  'name' => $branch->name,
64
  'updated' => $branch->target->date,
65
  'downloadUrl' => $this->getDownloadUrl($branch->name),
70
  * Get a specific tag.
71
  *
72
  * @param string $tagName
73
+ * @return Puc_v4p4_Vcs_Reference|null
74
  */
75
  public function getTag($tagName) {
76
  $tag = $this->api('/refs/tags/' . $tagName);
78
  return null;
79
  }
80
 
81
+ return new Puc_v4p4_Vcs_Reference(array(
82
  'name' => $tag->name,
83
  'version' => ltrim($tag->name, 'v'),
84
  'updated' => $tag->target->date,
89
  /**
90
  * Get the tag that looks like the highest version number.
91
  *
92
+ * @return Puc_v4p4_Vcs_Reference|null
93
  */
94
  public function getLatestTag() {
95
  $tags = $this->api('/refs/tags?sort=-target.date');
103
  //Return the first result.
104
  if ( !empty($versionTags) ) {
105
  $tag = $versionTags[0];
106
+ return new Puc_v4p4_Vcs_Reference(array(
107
  'name' => $tag->name,
108
  'version' => ltrim($tag->name, 'v'),
109
  'updated' => $tag->target->date,
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_v4p4_Vcs_Reference
121
  */
122
  protected function getStableTag($branch) {
123
  $remoteReadme = $this->getRemoteReadme($branch);
194
  $this->repository,
195
  ltrim($url, '/')
196
  ));
197
+ $baseUrl = $url;
198
 
199
  if ( $this->oauth ) {
200
  $url = $this->oauth->sign($url,'GET');
206
  }
207
  $response = wp_remote_get($url, $options);
208
  if ( is_wp_error($response) ) {
209
+ do_action('puc_api_error', $response, null, $url, $this->slug);
210
  return $response;
211
  }
212
 
217
  return $document;
218
  }
219
 
220
+ $error = new WP_Error(
221
  'puc-bitbucket-http-error',
222
+ sprintf('BitBucket API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
223
  );
224
+ do_action('puc_api_error', $error, $response, $url, $this->slug);
225
+
226
+ return $error;
227
  }
228
 
229
  /**
233
  parent::setAuthentication($credentials);
234
 
235
  if ( !empty($credentials) && !empty($credentials['consumer_key']) ) {
236
+ $this->oauth = new Puc_v4p4_OAuthSignature(
237
  $credentials['consumer_key'],
238
  $credentials['consumer_secret']
239
  );
admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/GitHubApi.php RENAMED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
4
 
5
- class Puc_v4p2_Vcs_GitHubApi extends Puc_v4p2_Vcs_Api {
6
  /**
7
  * @var string GitHub username.
8
  */
@@ -22,6 +22,21 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
22
  */
23
  protected $accessToken;
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  public function __construct($repositoryUrl, $accessToken = null) {
26
  $path = @parse_url($repositoryUrl, PHP_URL_PATH);
27
  if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
@@ -37,7 +52,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
37
  /**
38
  * Get the latest release from GitHub.
39
  *
40
- * @return Puc_v4p2_Vcs_Reference|null
41
  */
42
  public function getLatestRelease() {
43
  $release = $this->api('/repos/:user/:repo/releases/latest');
@@ -45,21 +60,42 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
45
  return null;
46
  }
47
 
48
- $reference = new Puc_v4p2_Vcs_Reference(array(
49
- 'name' => $release->tag_name,
50
- 'version' => ltrim($release->tag_name, 'v'), //Remove the "v" prefix from "v1.2.3".
51
  'downloadUrl' => $this->signDownloadUrl($release->zipball_url),
52
- 'updated' => $release->created_at,
53
  'apiResponse' => $release,
54
  ));
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  if ( !empty($release->body) ) {
57
  /** @noinspection PhpUndefinedClassInspection */
58
  $reference->changelog = Parsedown::instance()->text($release->body);
59
  }
60
- if ( isset($release->assets[0]) ) {
61
- $reference->downloadCount = $release->assets[0]->download_count;
62
- }
63
 
64
  return $reference;
65
  }
@@ -67,7 +103,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
67
  /**
68
  * Get the tag that looks like the highest version number.
69
  *
70
- * @return Puc_v4p2_Vcs_Reference|null
71
  */
72
  public function getLatestTag() {
73
  $tags = $this->api('/repos/:user/:repo/tags');
@@ -82,9 +118,9 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
82
  }
83
 
84
  $tag = $versionTags[0];
85
- return new Puc_v4p2_Vcs_Reference(array(
86
- 'name' => $tag->name,
87
- 'version' => ltrim($tag->name, 'v'),
88
  'downloadUrl' => $this->signDownloadUrl($tag->zipball_url),
89
  'apiResponse' => $tag,
90
  ));
@@ -94,7 +130,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
94
  * Get a branch by name.
95
  *
96
  * @param string $branchName
97
- * @return null|Puc_v4p2_Vcs_Reference
98
  */
99
  public function getBranch($branchName) {
100
  $branch = $this->api('/repos/:user/:repo/branches/' . $branchName);
@@ -102,8 +138,8 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
102
  return null;
103
  }
104
 
105
- $reference = new Puc_v4p2_Vcs_Reference(array(
106
- 'name' => $branch->name,
107
  'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
108
  'apiResponse' => $branch,
109
  ));
@@ -127,7 +163,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
127
  '/repos/:user/:repo/commits',
128
  array(
129
  'path' => $filename,
130
- 'sha' => $ref,
131
  )
132
  );
133
  if ( !is_wp_error($commits) && is_array($commits) && isset($commits[0]) ) {
@@ -158,6 +194,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
158
  * @return mixed|WP_Error
159
  */
160
  protected function api($url, $queryParams = array()) {
 
161
  $url = $this->buildApiUrl($url, $queryParams);
162
 
163
  $options = array('timeout' => 10);
@@ -166,6 +203,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
166
  }
167
  $response = wp_remote_get($url, $options);
168
  if ( is_wp_error($response) ) {
 
169
  return $response;
170
  }
171
 
@@ -176,10 +214,13 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
176
  return $document;
177
  }
178
 
179
- return new WP_Error(
180
  'puc-github-http-error',
181
- 'GitHub API error. HTTP status: ' . $code
182
  );
 
 
 
183
  }
184
 
185
  /**
@@ -249,7 +290,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
249
  * Get a specific tag.
250
  *
251
  * @param string $tagName
252
- * @return Puc_v4p2_Vcs_Reference|null
253
  */
254
  public function getTag($tagName) {
255
  //The current GitHub update checker doesn't use getTag, so I didn't bother to implement it.
@@ -265,7 +306,7 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
265
  * Figure out which reference (i.e tag or branch) contains the latest version.
266
  *
267
  * @param string $configBranch Start looking in this branch.
268
- * @return null|Puc_v4p2_Vcs_Reference
269
  */
270
  public function chooseReference($configBranch) {
271
  $updateSource = null;
@@ -297,6 +338,76 @@ if ( !class_exists('Puc_v4p2_Vcs_GitHubApi', false) ):
297
  return add_query_arg('access_token', $this->credentials, $url);
298
  }
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  }
301
 
302
  endif;
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_Vcs_GitHubApi', false) ):
4
 
5
+ class Puc_v4p4_Vcs_GitHubApi extends Puc_v4p4_Vcs_Api {
6
  /**
7
  * @var string GitHub username.
8
  */
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
  public function __construct($repositoryUrl, $accessToken = null) {
41
  $path = @parse_url($repositoryUrl, PHP_URL_PATH);
42
  if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
52
  /**
53
  * Get the latest release from GitHub.
54
  *
55
+ * @return Puc_v4p4_Vcs_Reference|null
56
  */
57
  public function getLatestRelease() {
58
  $release = $this->api('/repos/:user/:repo/releases/latest');
60
  return null;
61
  }
62
 
63
+ $reference = new Puc_v4p4_Vcs_Reference(array(
64
+ 'name' => $release->tag_name,
65
+ 'version' => ltrim($release->tag_name, 'v'), //Remove the "v" prefix from "v1.2.3".
66
  'downloadUrl' => $this->signDownloadUrl($release->zipball_url),
67
+ 'updated' => $release->created_at,
68
  'apiResponse' => $release,
69
  ));
70
 
71
+ if ( isset($release->assets[0]) ) {
72
+ $reference->downloadCount = $release->assets[0]->download_count;
73
+ }
74
+
75
+ if ( $this->releaseAssetsEnabled && isset($release->assets, $release->assets[0]) ) {
76
+ //Use the first release asset that matches the specified regular expression.
77
+ $matchingAssets = array_filter($release->assets, array($this, 'matchesAssetFilter'));
78
+ if ( !empty($matchingAssets) ) {
79
+ if ( $this->isAuthenticationEnabled() ) {
80
+ /**
81
+ * Keep in mind that we'll need to add an "Accept" header to download this asset.
82
+ * @see setReleaseDownloadHeader()
83
+ */
84
+ $reference->downloadUrl = $this->signDownloadUrl($matchingAssets[0]->url);
85
+ } else {
86
+ //It seems that browser_download_url only works for public repositories.
87
+ //Using an access_token doesn't help. Maybe OAuth would work?
88
+ $reference->downloadUrl = $matchingAssets[0]->browser_download_url;
89
+ }
90
+
91
+ $reference->downloadCount = $matchingAssets[0]->download_count;
92
+ }
93
+ }
94
+
95
  if ( !empty($release->body) ) {
96
  /** @noinspection PhpUndefinedClassInspection */
97
  $reference->changelog = Parsedown::instance()->text($release->body);
98
  }
 
 
 
99
 
100
  return $reference;
101
  }
103
  /**
104
  * Get the tag that looks like the highest version number.
105
  *
106
+ * @return Puc_v4p4_Vcs_Reference|null
107
  */
108
  public function getLatestTag() {
109
  $tags = $this->api('/repos/:user/:repo/tags');
118
  }
119
 
120
  $tag = $versionTags[0];
121
+ return new Puc_v4p4_Vcs_Reference(array(
122
+ 'name' => $tag->name,
123
+ 'version' => ltrim($tag->name, 'v'),
124
  'downloadUrl' => $this->signDownloadUrl($tag->zipball_url),
125
  'apiResponse' => $tag,
126
  ));
130
  * Get a branch by name.
131
  *
132
  * @param string $branchName
133
+ * @return null|Puc_v4p4_Vcs_Reference
134
  */
135
  public function getBranch($branchName) {
136
  $branch = $this->api('/repos/:user/:repo/branches/' . $branchName);
138
  return null;
139
  }
140
 
141
+ $reference = new Puc_v4p4_Vcs_Reference(array(
142
+ 'name' => $branch->name,
143
  'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
144
  'apiResponse' => $branch,
145
  ));
163
  '/repos/:user/:repo/commits',
164
  array(
165
  'path' => $filename,
166
+ 'sha' => $ref,
167
  )
168
  );
169
  if ( !is_wp_error($commits) && is_array($commits) && isset($commits[0]) ) {
194
  * @return mixed|WP_Error
195
  */
196
  protected function api($url, $queryParams = array()) {
197
+ $baseUrl = $url;
198
  $url = $this->buildApiUrl($url, $queryParams);
199
 
200
  $options = array('timeout' => 10);
203
  }
204
  $response = wp_remote_get($url, $options);
205
  if ( is_wp_error($response) ) {
206
+ do_action('puc_api_error', $response, null, $url, $this->slug);
207
  return $response;
208
  }
209
 
214
  return $document;
215
  }
216
 
217
+ $error = new WP_Error(
218
  'puc-github-http-error',
219
+ sprintf('GitHub API error. Base URL: "%s", HTTP status code: %d.', $baseUrl, $code)
220
  );
221
+ do_action('puc_api_error', $error, $response, $url, $this->slug);
222
+
223
+ return $error;
224
  }
225
 
226
  /**
290
  * Get a specific tag.
291
  *
292
  * @param string $tagName
293
+ * @return Puc_v4p4_Vcs_Reference|null
294
  */
295
  public function getTag($tagName) {
296
  //The current GitHub update checker doesn't use getTag, so I didn't bother to implement it.
306
  * Figure out which reference (i.e tag or branch) contains the latest version.
307
  *
308
  * @param string $configBranch Start looking in this branch.
309
+ * @return null|Puc_v4p4_Vcs_Reference
310
  */
311
  public function chooseReference($configBranch) {
312
  $updateSource = null;
338
  return add_query_arg('access_token', $this->credentials, $url);
339
  }
340
 
341
+ /**
342
+ * Enable updating via release assets.
343
+ *
344
+ * If the latest release contains no usable assets, the update checker
345
+ * will fall back to using the automatically generated ZIP archive.
346
+ *
347
+ * Private repositories will only work with WordPress 3.7 or later.
348
+ *
349
+ * @param string|null $fileNameRegex Optional. Use only those assets where the file name matches this regex.
350
+ */
351
+ public function enableReleaseAssets($fileNameRegex = null) {
352
+ $this->releaseAssetsEnabled = true;
353
+ $this->assetFilterRegex = $fileNameRegex;
354
+ $this->assetApiBaseUrl = sprintf(
355
+ '//api.github.com/repos/%1$s/%2$s/releases/assets/',
356
+ $this->userName,
357
+ $this->repositoryName
358
+ );
359
+
360
+ //Optimization: Instead of filtering all HTTP requests, let's do it only when
361
+ //WordPress is about to download an update.
362
+ add_filter('upgrader_pre_download', array($this, 'addHttpRequestFilter'), 10, 1); //WP 3.7+
363
+ }
364
+
365
+ /**
366
+ * Does this asset match the file name regex?
367
+ *
368
+ * @param stdClass $releaseAsset
369
+ * @return bool
370
+ */
371
+ protected function matchesAssetFilter($releaseAsset) {
372
+ if ( $this->assetFilterRegex === null ) {
373
+ //The default is to accept all assets.
374
+ return true;
375
+ }
376
+ return isset($releaseAsset->name) && preg_match($this->assetFilterRegex, $releaseAsset->name);
377
+ }
378
+
379
+ /**
380
+ * @internal
381
+ * @param bool $result
382
+ * @return bool
383
+ */
384
+ public function addHttpRequestFilter($result) {
385
+ static $filterAdded = false;
386
+ if ( $this->releaseAssetsEnabled && !$filterAdded && $this->isAuthenticationEnabled() ) {
387
+ add_filter('http_request_args', array($this, 'setReleaseDownloadHeader'), 10, 2);
388
+ $filterAdded = true;
389
+ }
390
+ return $result;
391
+ }
392
+
393
+ /**
394
+ * Set the HTTP header that's necessary to download private release assets.
395
+ *
396
+ * See GitHub docs:
397
+ * @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
398
+ *
399
+ * @internal
400
+ * @param array $requestArgs
401
+ * @param string $url
402
+ * @return array
403
+ */
404
+ public function setReleaseDownloadHeader($requestArgs, $url = '') {
405
+ //Is WordPress trying to download one of our assets?
406
+ if ( strpos($url, $this->assetApiBaseUrl) !== false ) {
407
+ $requestArgs['headers']['accept'] = 'application/octet-stream';
408
+ }
409
+ return $requestArgs;
410
+ }
411
  }
412
 
413
  endif;
admin/update-checker/Puc/v4p4/Vcs/GitLabApi.php ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ if ( !class_exists('Puc_v4p4_Vcs_GitLabApi', false) ):
4
+
5
+ class Puc_v4p4_Vcs_GitLabApi extends Puc_v4p4_Vcs_Api {
6
+ /**
7
+ * @var string GitLab username.
8
+ */
9
+ protected $userName;
10
+
11
+ /**
12
+ * @var string GitLab server host.
13
+ */
14
+ private $repositoryHost;
15
+
16
+ /**
17
+ * @var string GitLab repository name.
18
+ */
19
+ protected $repositoryName;
20
+
21
+ /**
22
+ * @var string GitLab authentication token. Optional.
23
+ */
24
+ protected $accessToken;
25
+
26
+ public function __construct($repositoryUrl, $accessToken = null) {
27
+ //Parse the repository host to support custom hosts.
28
+ $this->repositoryHost = @parse_url($repositoryUrl, PHP_URL_HOST);
29
+
30
+ //Find the repository information
31
+ $path = @parse_url($repositoryUrl, PHP_URL_PATH);
32
+ if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
33
+ $this->userName = $matches['username'];
34
+ $this->repositoryName = $matches['repository'];
35
+ } else {
36
+ //This is not a traditional url, it could be gitlab is in a deeper subdirectory.
37
+ //Get the path segments.
38
+ $segments = explode('/', untrailingslashit(ltrim($path, '/')));
39
+
40
+ //We need at least /user-name/repository-name/
41
+ if ( count($segments) < 2 ) {
42
+ throw new InvalidArgumentException('Invalid GitLab repository URL: "' . $repositoryUrl . '"');
43
+ }
44
+
45
+ //Get the username and repository name.
46
+ $usernameRepo = array_splice($segments, -2, 2);
47
+ $this->userName = $usernameRepo[0];
48
+ $this->repositoryName = $usernameRepo[1];
49
+
50
+ //Append the remaining segments to the host.
51
+ $this->repositoryHost = trailingslashit($this->repositoryHost) . implode('/', $segments);
52
+ }
53
+
54
+ parent::__construct($repositoryUrl, $accessToken);
55
+ }
56
+
57
+ /**
58
+ * Get the latest release from GitLab.
59
+ *
60
+ * @return Puc_v4p4_Vcs_Reference|null
61
+ */
62
+ public function getLatestRelease() {
63
+ return $this->getLatestTag();
64
+ }
65
+
66
+ /**
67
+ * Get the tag that looks like the highest version number.
68
+ *
69
+ * @return Puc_v4p4_Vcs_Reference|null
70
+ */
71
+ public function getLatestTag() {
72
+ $tags = $this->api('/:user/:repo/repository/tags');
73
+ if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
74
+ return null;
75
+ }
76
+
77
+ $versionTags = $this->sortTagsByVersion($tags);
78
+ if ( empty($versionTags) ) {
79
+ return null;
80
+ }
81
+
82
+ $tag = $versionTags[0];
83
+ return new Puc_v4p4_Vcs_Reference(array(
84
+ 'name' => $tag->name,
85
+ 'version' => ltrim($tag->name, 'v'),
86
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($tag->name),
87
+ 'apiResponse' => $tag
88
+ ));
89
+ }
90
+
91
+ /**
92
+ * Get a branch by name.
93
+ *
94
+ * @param string $branchName
95
+ * @return null|Puc_v4p4_Vcs_Reference
96
+ */
97
+ public function getBranch($branchName) {
98
+ $branch = $this->api('/:user/:repo/repository/branches/' . $branchName);
99
+ if ( is_wp_error($branch) || empty($branch) ) {
100
+ return null;
101
+ }
102
+
103
+ $reference = new Puc_v4p4_Vcs_Reference(array(
104
+ 'name' => $branch->name,
105
+ 'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
106
+ 'apiResponse' => $branch,
107
+ ));
108
+
109
+ if ( isset($branch->commit, $branch->commit->committed_date) ) {
110
+ $reference->updated = $branch->commit->committed_date;
111
+ }
112
+
113
+ return $reference;
114
+ }
115
+
116
+ /**
117
+ * Get the timestamp of the latest commit that changed the specified branch or tag.
118
+ *
119
+ * @param string $ref Reference name (e.g. branch or tag).
120
+ * @return string|null
121
+ */
122
+ public function getLatestCommitTime($ref) {
123
+ $commits = $this->api('/:user/:repo/repository/commits/', array('ref_name' => $ref));
124
+ if ( is_wp_error($commits) || !is_array($commits) || !isset($commits[0]) ) {
125
+ return null;
126
+ }
127
+
128
+ return $commits[0]->committed_date;
129
+ }
130
+
131
+ /**
132
+ * Perform a GitLab API request.
133
+ *
134
+ * @param string $url
135
+ * @param array $queryParams
136
+ * @return mixed|WP_Error
137
+ */
138
+ protected function api($url, $queryParams = array()) {
139
+ $baseUrl = $url;
140
+ $url = $this->buildApiUrl($url, $queryParams);
141
+
142
+ $options = array('timeout' => 10);
143
+ if ( !empty($this->httpFilterName) ) {
144
+ $options = apply_filters($this->httpFilterName, $options);
145
+ }
146
+
147
+ $response = wp_remote_get($url, $options);
148
+ if ( is_wp_error($response) ) {
149
+ do_action('puc_api_error', $response, null, $url, $this->slug);
150
+ return $response;
151
+ }
152
+
153
+ $code = wp_remote_retrieve_response_code($response);
154
+ $body = wp_remote_retrieve_body($response);
155
+ if ( $code === 200 ) {
156
+ return json_decode($body);
157
+ }
158
+
159
+ $error = new WP_Error(
160
+ 'puc-gitlab-http-error',
161
+ sprintf('GitLab API error. URL: "%s", HTTP status code: %d.', $baseUrl, $code)
162
+ );
163
+ do_action('puc_api_error', $error, $response, $url, $this->slug);
164
+
165
+ return $error;
166
+ }
167
+
168
+ /**
169
+ * Build a fully qualified URL for an API request.
170
+ *
171
+ * @param string $url
172
+ * @param array $queryParams
173
+ * @return string
174
+ */
175
+ protected function buildApiUrl($url, $queryParams) {
176
+ $variables = array(
177
+ 'user' => $this->userName,
178
+ 'repo' => $this->repositoryName
179
+ );
180
+
181
+ foreach ($variables as $name => $value) {
182
+ $url = str_replace("/:{$name}", urlencode('/' . $value), $url);
183
+ }
184
+
185
+ $url = substr($url, 3);
186
+ $url = sprintf('https://%1$s/api/v4/projects/%2$s', $this->repositoryHost, $url);
187
+
188
+ if ( !empty($this->accessToken) ) {
189
+ $queryParams['private_token'] = $this->accessToken;
190
+ }
191
+
192
+ if ( !empty($queryParams) ) {
193
+ $url = add_query_arg($queryParams, $url);
194
+ }
195
+
196
+ return $url;
197
+ }
198
+
199
+ /**
200
+ * Get the contents of a file from a specific branch or tag.
201
+ *
202
+ * @param string $path File name.
203
+ * @param string $ref
204
+ * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
205
+ */
206
+ public function getRemoteFile($path, $ref = 'master') {
207
+ $response = $this->api('/:user/:repo/repository/files/' . $path, array('ref' => $ref));
208
+ if ( is_wp_error($response) || !isset($response->content) || $response->encoding !== 'base64' ) {
209
+ return null;
210
+ }
211
+
212
+ return base64_decode($response->content);
213
+ }
214
+
215
+ /**
216
+ * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
217
+ *
218
+ * @param string $ref
219
+ * @return string
220
+ */
221
+ public function buildArchiveDownloadUrl($ref = 'master') {
222
+ $url = sprintf(
223
+ 'https://%1$s/%2$s/%3$s/repository/%4$s/archive.zip',
224
+ $this->repositoryHost,
225
+ urlencode($this->userName),
226
+ urlencode($this->repositoryName),
227
+ urlencode($ref)
228
+ );
229
+
230
+ if ( !empty($this->accessToken) ) {
231
+ $url = add_query_arg('private_token', $this->accessToken, $url);
232
+ }
233
+
234
+ return $url;
235
+ }
236
+
237
+ /**
238
+ * Get a specific tag.
239
+ *
240
+ * @param string $tagName
241
+ * @return Puc_v4p4_Vcs_Reference|null
242
+ */
243
+ public function getTag($tagName) {
244
+ throw new LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
245
+ }
246
+
247
+ /**
248
+ * Figure out which reference (i.e tag or branch) contains the latest version.
249
+ *
250
+ * @param string $configBranch Start looking in this branch.
251
+ * @return null|Puc_v4p4_Vcs_Reference
252
+ */
253
+ public function chooseReference($configBranch) {
254
+ $updateSource = null;
255
+
256
+ // GitLab doesn't handle releases the same as GitHub so just use the latest tag
257
+ if ( $configBranch === 'master' ) {
258
+ $updateSource = $this->getLatestTag();
259
+ }
260
+
261
+ if ( empty($updateSource) ) {
262
+ $updateSource = $this->getBranch($configBranch);
263
+ }
264
+
265
+ return $updateSource;
266
+ }
267
+
268
+ public function setAuthentication($credentials) {
269
+ parent::setAuthentication($credentials);
270
+ $this->accessToken = is_string($credentials) ? $credentials : null;
271
+ }
272
+ }
273
+
274
+ endif;
admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/PluginUpdateChecker.php RENAMED
@@ -1,21 +1,21 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
3
 
4
- class Puc_v4p2_Vcs_PluginUpdateChecker extends Puc_v4p2_Plugin_UpdateChecker implements Puc_v4p2_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_v4p2_Vcs_Api Repository API client.
12
  */
13
  protected $api = null;
14
 
15
  /**
16
- * Puc_v4p2_Vcs_PluginUpdateChecker constructor.
17
  *
18
- * @param Puc_v4p2_Vcs_Api $api
19
  * @param string $pluginFile
20
  * @param string $slug
21
  * @param int $checkPeriod
@@ -27,6 +27,8 @@ if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
27
  $this->api->setHttpFilterName($this->getUniqueName('request_info_options'));
28
 
29
  parent::__construct($api->getRepositoryUrl(), $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);
 
 
30
  }
31
 
32
  public function requestInfo($unusedParameter = null) {
@@ -37,8 +39,9 @@ if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
37
  }
38
 
39
  $api = $this->api;
 
40
 
41
- $info = new Puc_v4p2_Plugin_Info();
42
  $info->filename = $this->pluginFile;
43
  $info->slug = $this->slug;
44
 
@@ -60,6 +63,16 @@ if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
60
  }
61
  } else {
62
  //There's probably a network problem or an authentication error.
 
 
 
 
 
 
 
 
 
 
63
  return null;
64
  }
65
 
@@ -104,18 +117,18 @@ if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
104
  * @return bool
105
  */
106
  protected function readmeTxtExistsLocally() {
107
- $pluginDirectory = dirname($this->pluginAbsolutePath);
108
- if ( empty($this->pluginAbsolutePath) || !is_dir($pluginDirectory) || ($pluginDirectory === '.') ) {
109
  return false;
110
  }
111
- return is_file($pluginDirectory . '/readme.txt');
112
  }
113
 
114
  /**
115
  * Copy plugin metadata from a file header to a Plugin Info object.
116
  *
117
  * @param array $fileHeader
118
- * @param Puc_v4p2_Plugin_Info $pluginInfo
119
  */
120
  protected function setInfoFromHeader($fileHeader, $pluginInfo) {
121
  $headerToPropertyMap = array(
@@ -146,7 +159,7 @@ if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
146
  * Copy plugin metadata from the remote readme.txt file.
147
  *
148
  * @param string $ref GitHub tag or branch where to look for the readme.
149
- * @param Puc_v4p2_Plugin_Info $pluginInfo
150
  */
151
  protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
152
  $readme = $this->api->getRemoteReadme($ref);
@@ -179,6 +192,10 @@ if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
179
  return $this;
180
  }
181
 
 
 
 
 
182
  public function getUpdate() {
183
  $update = parent::getUpdate();
184
 
@@ -188,6 +205,13 @@ if ( !class_exists('Puc_v4p2_Vcs_PluginUpdateChecker') ):
188
 
189
  return $update;
190
  }
 
 
 
 
 
 
 
191
  }
192
 
193
  endif;
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Vcs_PluginUpdateChecker') ):
3
 
4
+ class Puc_v4p4_Vcs_PluginUpdateChecker extends Puc_v4p4_Plugin_UpdateChecker implements Puc_v4p4_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_v4p4_Vcs_Api Repository API client.
12
  */
13
  protected $api = null;
14
 
15
  /**
16
+ * Puc_v4p4_Vcs_PluginUpdateChecker constructor.
17
  *
18
+ * @param Puc_v4p4_Vcs_Api $api
19
  * @param string $pluginFile
20
  * @param string $slug
21
  * @param int $checkPeriod
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) {
39
  }
40
 
41
  $api = $this->api;
42
+ $api->setLocalDirectory($this->getAbsoluteDirectoryPath());
43
 
44
+ $info = new Puc_v4p4_Plugin_Info();
45
  $info->filename = $this->pluginFile;
46
  $info->slug = $this->slug;
47
 
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
 
117
  * @return bool
118
  */
119
  protected function readmeTxtExistsLocally() {
120
+ $pluginDirectory = $this->getAbsoluteDirectoryPath();
121
+ if ( empty($pluginDirectory) || !is_dir($pluginDirectory) || ($pluginDirectory === '.') ) {
122
  return false;
123
  }
124
+ return is_file($pluginDirectory . '/' . $this->api->getLocalReadmeName());
125
  }
126
 
127
  /**
128
  * Copy plugin metadata from a file header to a Plugin Info object.
129
  *
130
  * @param array $fileHeader
131
+ * @param Puc_v4p4_Plugin_Info $pluginInfo
132
  */
133
  protected function setInfoFromHeader($fileHeader, $pluginInfo) {
134
  $headerToPropertyMap = array(
159
  * Copy plugin metadata from the remote readme.txt file.
160
  *
161
  * @param string $ref GitHub tag or branch where to look for the readme.
162
+ * @param Puc_v4p4_Plugin_Info $pluginInfo
163
  */
164
  protected function setInfoFromRemoteReadme($ref, $pluginInfo) {
165
  $readme = $this->api->getRemoteReadme($ref);
192
  return $this;
193
  }
194
 
195
+ public function getVcsApi() {
196
+ return $this->api;
197
+ }
198
+
199
  public function getUpdate() {
200
  $update = parent::getUpdate();
201
 
205
 
206
  return $update;
207
  }
208
+
209
+ public function onDisplayConfiguration($panel) {
210
+ parent::onDisplayConfiguration($panel);
211
+ $panel->row('Branch', $this->branch);
212
+ $panel->row('Authentication enabled', $this->api->isAuthenticationEnabled() ? 'Yes' : 'No');
213
+ $panel->row('API client', get_class($this->api));
214
+ }
215
  }
216
 
217
  endif;
admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/Reference.php RENAMED
@@ -1,5 +1,5 @@
1
  <?php
2
- if ( !class_exists('Puc_v4p2_Vcs_Reference', false) ):
3
 
4
  /**
5
  * This class represents a VCS branch or tag. It's intended as a read only, short-lived container
@@ -13,7 +13,7 @@ if ( !class_exists('Puc_v4p2_Vcs_Reference', false) ):
13
  * @property string|null $changelog
14
  * @property int|null $downloadCount
15
  */
16
- class Puc_v4p2_Vcs_Reference {
17
  private $properties = array();
18
 
19
  public function __construct($properties = array()) {
1
  <?php
2
+ if ( !class_exists('Puc_v4p4_Vcs_Reference', false) ):
3
 
4
  /**
5
  * This class represents a VCS branch or tag. It's intended as a read only, short-lived container
13
  * @property string|null $changelog
14
  * @property int|null $downloadCount
15
  */
16
+ class Puc_v4p4_Vcs_Reference {
17
  private $properties = array();
18
 
19
  public function __construct($properties = array()) {
admin/update-checker/Puc/{v4p2 → v4p4}/Vcs/ThemeUpdateChecker.php RENAMED
@@ -1,22 +1,22 @@
1
  <?php
2
 
3
- if ( !class_exists('Puc_v4p2_Vcs_ThemeUpdateChecker', false) ):
4
 
5
- class Puc_v4p2_Vcs_ThemeUpdateChecker extends Puc_v4p2_Theme_UpdateChecker implements Puc_v4p2_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_v4p2_Vcs_Api Repository API client.
13
  */
14
  protected $api = null;
15
 
16
  /**
17
- * Puc_v4p2_Vcs_ThemeUpdateChecker constructor.
18
  *
19
- * @param Puc_v4p2_Vcs_Api $api
20
  * @param null $stylesheet
21
  * @param null $customSlug
22
  * @param int $checkPeriod
@@ -27,12 +27,15 @@ if ( !class_exists('Puc_v4p2_Vcs_ThemeUpdateChecker', false) ):
27
  $this->api->setHttpFilterName($this->getUniqueName('request_update_options'));
28
 
29
  parent::__construct($api->getRepositoryUrl(), $stylesheet, $customSlug, $checkPeriod, $optionName);
 
 
30
  }
31
 
32
  public function requestUpdate() {
33
  $api = $this->api;
 
34
 
35
- $update = new Puc_v4p2_Theme_Update();
36
  $update->slug = $this->slug;
37
 
38
  //Figure out which reference (tag or branch) we'll use to get the latest version of the theme.
@@ -41,19 +44,29 @@ if ( !class_exists('Puc_v4p2_Vcs_ThemeUpdateChecker', false) ):
41
  $ref = $updateSource->name;
42
  $update->download_url = $updateSource->downloadUrl;
43
  } else {
 
 
 
 
 
 
 
 
 
 
44
  $ref = $this->branch;
45
  }
46
 
47
  //Get headers from the main stylesheet in this branch/tag. Its "Version" header and other metadata
48
  //are what the WordPress install will actually see after upgrading, so they take precedence over releases/tags.
49
  $remoteHeader = $this->getFileHeader($api->getRemoteFile('style.css', $ref));
50
- $update->version = Puc_v4p2_Utils::findNotEmpty(array(
51
  $remoteHeader['Version'],
52
- Puc_v4p2_Utils::get($updateSource, 'version'),
53
  ));
54
 
55
  //The details URL defaults to the Theme URI header or the repository URL.
56
- $update->details_url = Puc_v4p2_Utils::findNotEmpty(array(
57
  $remoteHeader['ThemeURI'],
58
  $this->theme->get('ThemeURI'),
59
  $this->metadataUrl,
@@ -80,6 +93,10 @@ if ( !class_exists('Puc_v4p2_Vcs_ThemeUpdateChecker', false) ):
80
  return $this;
81
  }
82
 
 
 
 
 
83
  public function getUpdate() {
84
  $update = parent::getUpdate();
85
 
@@ -89,6 +106,13 @@ if ( !class_exists('Puc_v4p2_Vcs_ThemeUpdateChecker', false) ):
89
 
90
  return $update;
91
  }
 
 
 
 
 
 
 
92
  }
93
 
94
  endif;
1
  <?php
2
 
3
+ if ( !class_exists('Puc_v4p4_Vcs_ThemeUpdateChecker', false) ):
4
 
5
+ class Puc_v4p4_Vcs_ThemeUpdateChecker extends Puc_v4p4_Theme_UpdateChecker implements Puc_v4p4_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_v4p4_Vcs_Api Repository API client.
13
  */
14
  protected $api = null;
15
 
16
  /**
17
+ * Puc_v4p4_Vcs_ThemeUpdateChecker constructor.
18
  *
19
+ * @param Puc_v4p4_Vcs_Api $api
20
  * @param null $stylesheet
21
  * @param null $customSlug
22
  * @param int $checkPeriod
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->getAbsoluteDirectoryPath());
37
 
38
+ $update = new Puc_v4p4_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.
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->getFileHeader($api->getRemoteFile('style.css', $ref));
63
+ $update->version = Puc_v4p4_Utils::findNotEmpty(array(
64
  $remoteHeader['Version'],
65
+ Puc_v4p4_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_v4p4_Utils::findNotEmpty(array(
70
  $remoteHeader['ThemeURI'],
71
  $this->theme->get('ThemeURI'),
72
  $this->metadataUrl,
93
  return $this;
94
  }
95
 
96
+ public function getVcsApi() {
97
+ return $this->api;
98
+ }
99
+
100
  public function getUpdate() {
101
  $update = parent::getUpdate();
102
 
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;
admin/update-checker/license.txt CHANGED
@@ -1,7 +1,7 @@
1
- Copyright (c) 2014 Jānis Elsts
2
 
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
 
5
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
 
7
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ Copyright (c) 2017 Jānis Elsts
2
 
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
 
5
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
 
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
admin/update-checker/plugin-update-checker.php CHANGED
@@ -1,22 +1,24 @@
1
  <?php
2
  /**
3
- * Plugin Update Checker Library 4.2
4
  * http://w-shadow.com/
5
  *
6
  * Copyright 2017 Janis Elsts
7
  * Released under the MIT license. See license.txt for details.
8
  */
9
 
 
10
  require dirname(__FILE__) . '/Puc/v4/Factory.php';
11
- require dirname(__FILE__) . '/Puc/v4p2/Autoloader.php';
12
- new Puc_v4p2_Autoloader();
13
 
14
  //Register classes defined in this file with the factory.
15
- Puc_v4_Factory::addVersion('Plugin_UpdateChecker', 'Puc_v4p2_Plugin_UpdateChecker', '4.2');
16
- Puc_v4_Factory::addVersion('Theme_UpdateChecker', 'Puc_v4p2_Theme_UpdateChecker', '4.2');
17
 
18
- Puc_v4_Factory::addVersion('Vcs_PluginUpdateChecker', 'Puc_v4p2_Vcs_PluginUpdateChecker', '4.2');
19
- Puc_v4_Factory::addVersion('Vcs_ThemeUpdateChecker', 'Puc_v4p2_Vcs_ThemeUpdateChecker', '4.2');
20
 
21
- Puc_v4_Factory::addVersion('GitHubApi', 'Puc_v4p2_Vcs_GitHubApi', '4.2');
22
- Puc_v4_Factory::addVersion('BitBucketApi', 'Puc_v4p2_Vcs_BitBucketApi', '4.2');
 
1
  <?php
2
  /**
3
+ * Plugin Update Checker Library 4.4
4
  * http://w-shadow.com/
5
  *
6
  * Copyright 2017 Janis Elsts
7
  * Released under the MIT license. See license.txt for details.
8
  */
9
 
10
+ require dirname(__FILE__) . '/Puc/v4p4/Factory.php';
11
  require dirname(__FILE__) . '/Puc/v4/Factory.php';
12
+ require dirname(__FILE__) . '/Puc/v4p4/Autoloader.php';
13
+ new Puc_v4p4_Autoloader();
14
 
15
  //Register classes defined in this file with the factory.
16
+ Puc_v4_Factory::addVersion('Plugin_UpdateChecker', 'Puc_v4p4_Plugin_UpdateChecker', '4.4');
17
+ Puc_v4_Factory::addVersion('Theme_UpdateChecker', 'Puc_v4p4_Theme_UpdateChecker', '4.4');
18
 
19
+ Puc_v4_Factory::addVersion('Vcs_PluginUpdateChecker', 'Puc_v4p4_Vcs_PluginUpdateChecker', '4.4');
20
+ Puc_v4_Factory::addVersion('Vcs_ThemeUpdateChecker', 'Puc_v4p4_Vcs_ThemeUpdateChecker', '4.4');
21
 
22
+ Puc_v4_Factory::addVersion('GitHubApi', 'Puc_v4p4_Vcs_GitHubApi', '4.4');
23
+ Puc_v4_Factory::addVersion('BitBucketApi', 'Puc_v4p4_Vcs_BitBucketApi', '4.4');
24
+ Puc_v4_Factory::addVersion('GitLabApi', 'Puc_v4p4_Vcs_GitLabApi', '4.4');
admin/update-checker/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
+ }
admin/view/addons.php CHANGED
@@ -90,7 +90,7 @@ if ( !is_array( $list_addons ) ) {
90
  <div class="plugin-description"><p><?php echo $a_addon['description'] ?></p></div>
91
  <?php if ( ( is_plugin_active( $a_addon[ 'slug' ] . '/index.php' ) || is_plugin_active( $a_addon[ 'slug' ] . '/' . $a_addon[ 'slug' ] . '.php' ) ) ): ?>
92
  <div class="active second">
93
- License Key: <input type="text" name="licenses[<?php echo $a_addon['slug'] ?>]" value="<?php echo !empty( wp_slimstat::$settings[ 'addon_licenses' ][ $a_addon[ 'slug' ] ] ) ? wp_slimstat::$settings[ 'addon_licenses' ][ $a_addon[ 'slug' ] ] : '' ?>" size="50">
94
  </div>
95
  <?php endif; ?>
96
  </td>
90
  <div class="plugin-description"><p><?php echo $a_addon['description'] ?></p></div>
91
  <?php if ( ( is_plugin_active( $a_addon[ 'slug' ] . '/index.php' ) || is_plugin_active( $a_addon[ 'slug' ] . '/' . $a_addon[ 'slug' ] . '.php' ) ) ): ?>
92
  <div class="active second">
93
+ License Key <input type="text" name="licenses[<?php echo $a_addon['slug'] ?>]" value="<?php echo !empty( wp_slimstat::$settings[ 'addon_licenses' ][ $a_addon[ 'slug' ] ] ) ? wp_slimstat::$settings[ 'addon_licenses' ][ $a_addon[ 'slug' ] ] : '' ?>" size="50">
94
  </div>
95
  <?php endif; ?>
96
  </td>
admin/view/wp-slimstat-reports.php CHANGED
@@ -1871,8 +1871,8 @@ class wp_slimstat_reports {
1871
  var dataProvider = {
1872
  map: "worldLow",
1873
  getAreasFromMap: true,
1874
- areas:[ <?php echo implode(',', $data_areas) ?> ],
1875
- images: [ <?php echo implode(',', $data_points) ?> ]
1876
  };
1877
 
1878
  // Create AmMap object
1871
  var dataProvider = {
1872
  map: "worldLow",
1873
  getAreasFromMap: true,
1874
+ areas:[ <?php echo implode( ',', $data_areas ) ?> ],
1875
+ images: [ <?php if ( !empty( $data_points ) ) echo implode( ',', $data_points ) ?> ]
1876
  };
1877
 
1878
  // Create AmMap object
admin/wp-slimstat-admin.php CHANGED
@@ -11,8 +11,8 @@ class wp_slimstat_admin {
11
  * Init -- Sets things up.
12
  */
13
  public static function init() {
14
- //self::$admin_notice = "";
15
- //self::$admin_notice .= '<br/><br/><a id="slimstat-hide-admin-notice" href="#" class="button-secondary">Got it, thanks</a>';
16
 
17
  // Load language files
18
  load_plugin_textdomain( 'wp-slimstat', WP_PLUGIN_DIR .'/wp-slimstat/languages', '/wp-slimstat/languages' );
11
  * Init -- Sets things up.
12
  */
13
  public static function init() {
14
+ self::$admin_notice = "We just launched our <a href='https://www.wp-slimstat.com' target='_blank'>redesigned website</a>, to promote our plugin in a cleaner and simpler way. And we are also looking for <strong>sponsors</strong>: if you can donate your time, your talent or your money to support the development of Slimstat, please <a href='http://support.wp-slimstat.com/' target='_blank'>contact our support team</a> today to learn more.";
15
+ self::$admin_notice .= '<br/><br/><a id="slimstat-hide-admin-notice" href="#" class="button-secondary">Got it, thanks</a>';
16
 
17
  // Load language files
18
  load_plugin_textdomain( 'wp-slimstat', WP_PLUGIN_DIR .'/wp-slimstat/languages', '/wp-slimstat/languages' );
readme.txt CHANGED
@@ -5,7 +5,7 @@ Tags: analytics, statistics, counter, tracking, reports, wassup, geolocation, on
5
  Text Domain: wp-slimstat
6
  Requires at least: 3.8
7
  Tested up to: 4.9
8
- Stable tag: 4.7.5.2
9
 
10
  == Description ==
11
  The leading web analytics plugin for WordPress. Track returning customers and registered users, monitor Javascript events, detect intrusions, analyze email campaigns. Thousands of WordPress sites are already using it.
@@ -71,6 +71,11 @@ Our knowledge base is available on our [support center](http://docs.wp-slimstat.
71
  5. **Responsive layout** - Keep an eye on your reports on the go
72
 
73
  == Changelog ==
 
 
 
 
 
74
  = 4.7.5.2 =
75
  * [Update] You can now customize the amount of dots displayed on the World Map, under Slimstat > Settings > Reports > Access Log and World Map. Thank you, [service4](https://wordpress.org/support/topic/new-geolocation-map-with-cities/).
76
  * [Fix] A dependency error was being highlighted for one of our premium add-ons under certain circumstances. Thank you, Peter.
5
  Text Domain: wp-slimstat
6
  Requires at least: 3.8
7
  Tested up to: 4.9
8
+ Stable tag: 4.7.5.3
9
 
10
  == Description ==
11
  The leading web analytics plugin for WordPress. Track returning customers and registered users, monitor Javascript events, detect intrusions, analyze email campaigns. Thousands of WordPress sites are already using it.
71
  5. **Responsive layout** - Keep an eye on your reports on the go
72
 
73
  == Changelog ==
74
+ = 4.7.5.3 =
75
+ * [New] For those who can't manage to automatically update their local copy of our premium add-ons, we've added a link to manually download the zip file. You'll find it under the Plugins screen, once you've entered and saved the corresponding license key under Slimstat > Add-ons.
76
+ * [Update] The [Update Checker](https://github.com/YahnisElsts/plugin-update-checker) library has been updated to version 4.4.
77
+ * [Fix] The world map was not being displayed correctly if no data points were available for the selected time range (thank you, Michel).
78
+
79
  = 4.7.5.2 =
80
  * [Update] You can now customize the amount of dots displayed on the World Map, under Slimstat > Settings > Reports > Access Log and World Map. Thank you, [service4](https://wordpress.org/support/topic/new-geolocation-map-with-cities/).
81
  * [Fix] A dependency error was being highlighted for one of our premium add-ons under certain circumstances. Thank you, Peter.
wp-slimstat.php CHANGED
@@ -3,7 +3,7 @@
3
  Plugin Name: Slimstat Analytics
4
  Plugin URI: http://wordpress.org/plugins/wp-slimstat/
5
  Description: The leading web analytics plugin for WordPress
6
- Version: 4.7.5.2
7
  Author: Jason Crouse
8
  Author URI: http://www.wp-slimstat.com/
9
  Text Domain: wp-slimstat
@@ -15,7 +15,7 @@ if ( !empty( wp_slimstat::$settings ) ) {
15
  }
16
 
17
  class wp_slimstat {
18
- public static $version = '4.7.5.2';
19
  public static $settings = array();
20
 
21
  public static $wpdb = '';
@@ -1789,16 +1789,42 @@ class wp_slimstat {
1789
  }
1790
 
1791
  $update_checker_objects = array();
 
 
 
 
1792
  foreach ( self::$update_checker as $a_slug ) {
1793
  $a_clean_slug = str_replace( array( 'wp_slimstat_', '_' ), array( '', '-' ), $a_slug );
 
1794
  if ( !empty( self::$settings[ 'addon_licenses' ][ 'wp-slimstat-' . $a_clean_slug ] ) ) {
 
 
 
 
 
 
1795
 
1796
- // This is only included if add-ons are installed
1797
- include_once( plugin_dir_path( __FILE__ ) . 'admin/update-checker/plugin-update-checker.php' );
1798
 
1799
- $update_checker_objects[] = Puc_v4_Factory::buildUpdateChecker( 'http://www.wp-slimstat.com/update-checker/?slug=' . $a_clean_slug . '&key=' . urlencode( self::$settings[ 'addon_licenses' ][ 'wp-slimstat-' . $a_clean_slug ] ), dirname( dirname( __FILE__ ) ) . '/wp-slimstat-' . $a_clean_slug . '/index.php', 'wp-slimstat-' . $a_clean_slug );
 
 
 
 
 
 
 
 
 
 
 
 
 
1800
  }
1801
  }
 
 
1802
  }
1803
 
1804
  public static function register_widget() {
3
  Plugin Name: Slimstat Analytics
4
  Plugin URI: http://wordpress.org/plugins/wp-slimstat/
5
  Description: The leading web analytics plugin for WordPress
6
+ Version: 4.7.5.3
7
  Author: Jason Crouse
8
  Author URI: http://www.wp-slimstat.com/
9
  Text Domain: wp-slimstat
15
  }
16
 
17
  class wp_slimstat {
18
+ public static $version = '4.7.5.3';
19
  public static $settings = array();
20
 
21
  public static $wpdb = '';
1789
  }
1790
 
1791
  $update_checker_objects = array();
1792
+
1793
+ // This is only included if add-ons are installed
1794
+ include_once( plugin_dir_path( __FILE__ ) . 'admin/update-checker/plugin-update-checker.php' );
1795
+
1796
  foreach ( self::$update_checker as $a_slug ) {
1797
  $a_clean_slug = str_replace( array( 'wp_slimstat_', '_' ), array( '', '-' ), $a_slug );
1798
+
1799
  if ( !empty( self::$settings[ 'addon_licenses' ][ 'wp-slimstat-' . $a_clean_slug ] ) ) {
1800
+ $update_checker_objects[ $a_clean_slug ] = Puc_v4_Factory::buildUpdateChecker( 'http://www.wp-slimstat.com/update-checker/?slug=' . $a_clean_slug . '&key=' . urlencode( self::$settings[ 'addon_licenses' ][ 'wp-slimstat-' . $a_clean_slug ] ), dirname( dirname( __FILE__ ) ) . '/wp-slimstat-' . $a_clean_slug . '/index.php', 'wp-slimstat-' . $a_clean_slug );
1801
+
1802
+ add_filter( "plugin_action_links_wp-slimstat-{$a_clean_slug}/index.php", array( __CLASS__, 'add_plugin_manual_download_link' ), 10, 2 );
1803
+ }
1804
+ }
1805
+ }
1806
 
1807
+ public static function add_plugin_manual_download_link( $_links = array(), $_plugin_file = '' ) {
1808
+ $a_clean_slug = str_replace( array( 'wp-slimstat-', '/index.php' ), array( '', '' ), $_plugin_file );
1809
 
1810
+ if ( false !== ( $download_url = get_transient( 'wp-slimstat-download-link-' . $a_clean_slug ) ) ) {
1811
+ $_links[] = '<a href="' . $download_url . '">Download ZIP</a>';
1812
+ }
1813
+ else {
1814
+ $url = 'http://www.wp-slimstat.com/update-checker/?slug=' . $a_clean_slug . '&key=' . urlencode( self::$settings[ 'addon_licenses' ][ 'wp-slimstat-' . $a_clean_slug ] );
1815
+ $response = wp_safe_remote_get( $url, array( 'timeout' => 300, 'user-agent' => 'Slimstat Analytics/' . self::$version . '; ' . home_url() ) );
1816
+
1817
+ if ( !is_wp_error( $response ) && 200 == wp_remote_retrieve_response_code( $response ) ) {
1818
+ $data = @json_decode( $response[ 'body' ] );
1819
+
1820
+ if ( is_object( $data ) ) {
1821
+ $_links[] = '<a href="' . $data->download_url . '">Download ZIP</a>';
1822
+ set_transient( 'wp-slimstat-download-link-' . $a_clean_slug, $data->download_url, 172800 ); // 48 hours
1823
+ }
1824
  }
1825
  }
1826
+
1827
+ return $_links;
1828
  }
1829
 
1830
  public static function register_widget() {