PluginProbe ʕ •ᴥ•ʔ
SiteOrigin CSS / trunk
SiteOrigin CSS vtrunk
1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.10 1.5.11 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0
so-css / inc / installer / github-updater / updater.php
so-css / inc / installer / github-updater Last commit date
Parsedown 1 month ago LICENSE 1 month ago README.md 1 month ago updater.php 1 month ago
updater.php
428 lines
1 <?php
2 /**
3 * SiteOrigin Updater
4 *
5 * A utility for updating SiteOrigin plugins from GitHub.
6 *
7 * License: GPLv3
8 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
9 */
10
11 class SiteOrigin_Updater {
12 private $file;
13 private $plugin_slug;
14 private $owner;
15 private $actual_repo_name;
16 private $updates_branch = 'master';
17
18 public function __construct( $file, $plugin_slug, $repo_name_with_owner, $updates_branch = 'master' ) {
19 $this->file = $file;
20 $this->plugin_slug = $plugin_slug;
21 $this->updates_branch = $updates_branch;
22
23 if (
24 empty( $repo_name_with_owner ) ||
25 strpos( $repo_name_with_owner, '/' ) === false
26 ) {
27 throw new InvalidArgumentException(
28 'SiteOrigin_Updater: Repository identifier (argument 3) must be a non-empty string in "owner/repository-name" format. E.g., "my-github-username/my-plugin-repo". Provided: ' . var_export($repo_name_with_owner, true)
29 );
30 }
31
32 list($this->owner, $this->actual_repo_name) = explode('/', $repo_name_with_owner, 2);
33
34 if (
35 empty( $this->owner ) ||
36 empty( $this->actual_repo_name )
37 ) {
38 throw new InvalidArgumentException(
39 'SiteOrigin_Updater: Owner and repository name segments cannot be empty in "owner/repository-name" format. Provided: "' . $repo_name_with_owner . '"'
40 );
41 }
42
43 // Check if WordPress 5.8+ is available for Update URI system.
44 global $wp_version;
45 if (
46 version_compare( $wp_version, '5.8', '>=' ) &&
47 $this->has_update_uri()
48 ) {
49 // Use modern WordPress 5.8+ Update URI system (avoids jQuery selector bug).
50 add_filter( 'update_plugins_github.com', array( $this, 'check_for_github_update' ), 10, 4 );
51 } else {
52 // Fall back to legacy system for older WordPress versions.
53 add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_update' ), 15 );
54 }
55
56 add_filter( 'plugins_api', array( $this, 'plugin_api_call' ), 10, 3 );
57 }
58
59 public function check_for_update( $transient ) {
60 $all_headers = $this->get_plugin_headers();
61 $current_version = $this->get_current_version();
62
63 if (
64 ! empty( $all_headers ) &&
65 version_compare( $current_version, $all_headers['Version'], '<' )
66 ) {
67 // There is a newer version available on GitHub (comparing local version to remote plugin header).
68 $update = $this->get_plugin_data();
69 $update->new_version = $all_headers['Version'];
70 $update->stable_version = $all_headers['Version'];
71
72 // Prevent potential warnings if we're the first thing to add data here.
73 if ( ! is_object( $transient ) ) {
74 $transient = new stdClass();
75 }
76 if ( ! isset( $transient->response ) ) {
77 $transient->response = array();
78 }
79
80 $transient->response[ $update->slug ] = $update;
81 }
82
83 return $transient;
84 }
85
86 public function get_plugin_headers() {
87 static $all_headers = array();
88
89 if ( ! empty( $all_headers ) ) {
90 return $all_headers;
91 }
92
93 // Fetch the remote plugin file from GitHub to read its header.
94 $response = wp_remote_get( 'https://raw.githubusercontent.com/' . $this->owner . '/' . $this->actual_repo_name . '/' . $this->updates_branch . '/' . $this->plugin_slug . '.php' );
95
96 if (
97 is_wp_error( $response ) ||
98 empty( $response['body'] )
99 ) {
100 return false;
101 }
102
103 $file_data = $response['body'];
104 $all_headers = array(
105 'Name' => 'Plugin Name',
106 'PluginURI' => 'Plugin URI',
107 'Version' => 'Version',
108 'Description' => 'Description',
109 'Author' => 'Author',
110 'AuthorURI' => 'Author URI',
111 );
112
113 foreach ( $all_headers as $field => $regex ) {
114 if (
115 preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) &&
116 ! empty( $match[1] )
117 ) {
118 $all_headers[ $field ] = _cleanup_header_comment( $match[1] );
119 } else {
120 $all_headers[ $field ] = '';
121 }
122 }
123
124 return $all_headers;
125 }
126
127 /**
128 * Get and parse a markdown file from GitHub.
129 *
130 * @param string $file
131 *
132 * @return bool|string
133 */
134 public function get_github_markdown( $file = 'readme.md' ) {
135 $response = wp_remote_get( 'https://raw.githubusercontent.com/' . $this->owner . '/' . $this->actual_repo_name . '/' . $this->updates_branch . '/' . urlencode( $file ) );
136
137 if (
138 is_wp_error( $response ) ||
139 empty( $response['body'] )
140 ) {
141 return false;
142 }
143
144 if ( ! class_exists( 'Parsedown' ) ) {
145 $parsedown_path = dirname( __FILE__ ) . '/Parsedown/Parsedown.php';
146 if ( file_exists( $parsedown_path ) ) {
147 require_once $parsedown_path;
148 } else {
149 // Fallback: return raw markdown if Parsedown not found.
150 return $response['body'];
151 }
152 }
153
154 if ( class_exists( 'Parsedown' ) ) {
155 $parsedown = new Parsedown();
156 return $parsedown->parse( $response['body'] );
157 }
158
159 // Fallback: return raw markdown.
160 return $response['body'];
161 }
162
163 private function get_plugin_data() {
164 $headers = $this->get_plugin_headers();
165 if ( empty( $headers ) ) {
166 return array();
167 }
168 $data = new stdClass();
169 $data->slug = plugin_basename( $this->file );
170 $data->id = $this->plugin_slug;
171 $data->plugin_name = $headers['Name'];
172 $data->name = $headers['Name'];
173 $data->version = $headers['Version'];
174 $data->author = $headers['Author'];
175 $data->url = $headers['PluginURI'];
176 $data->homepage = $headers['PluginURI'];
177 $data->download_link = 'https://github.com/' . $this->owner . '/' . $this->actual_repo_name . '/archive/' . $this->updates_branch . '.zip';
178 $data->package = 'https://github.com/' . $this->owner . '/' . $this->actual_repo_name . '/archive/' . $this->updates_branch . '.zip';
179
180 // Add required fields for modal display.
181 $data->short_description = ! empty( $headers['Description'] ) ? $headers['Description'] : 'A WordPress plugin by SiteOrigin.';
182 $data->requires = '4.7';
183 $data->tested = '6.8.1';
184 $data->requires_php = '7.0.0';
185 $data->last_updated = date( 'Y-m-d' );
186 $data->added = date( 'Y-m-d' );
187
188 // Set icon - use conventional naming with PNG preference.
189 $icon_png_path = plugin_dir_path( $this->file ) . 'img/icon.png';
190
191 if ( file_exists( $icon_png_path ) ) {
192 $data->icons = array(
193 '1x' => plugin_dir_url( $this->file ) . 'img/icon.png',
194 '2x' => plugin_dir_url( $this->file ) . 'img/icon.png',
195 'default' => plugin_dir_url( $this->file ) . 'img/icon.png',
196 );
197 } else {
198 // Fallback to WordPress.org geopattern icon.
199 $data->icons = array(
200 'default' => "https://s.w.org/plugins/geopattern-icon/{$data->slug}.svg",
201 );
202 }
203
204 // Set banner - only if a proper banner exists.
205 $banner_path = plugin_dir_path( $this->file ) . 'img/banner.png';
206 if ( file_exists( $banner_path ) ) {
207 $data->banners = array(
208 'low' => plugin_dir_url( $this->file ) . 'img/banner.png',
209 'high' => plugin_dir_url( $this->file ) . 'img/banner.png',
210 );
211 }
212
213 $data->sections = array(
214 'description' => $this->get_github_markdown( 'readme.md' ),
215 'changelog' => $this->get_github_markdown( 'changelog.md' ),
216 );
217 return $data;
218 }
219
220 /**
221 * Add all the plugin details to the Plugin API call.
222 *
223 * @return stdClass
224 */
225 public function plugin_api_call( $def, $action, $args ) {
226 if ( $action !== 'plugin_information' ) {
227 return $def;
228 }
229
230 // Handle both legacy slug format and modern dirname format.
231 $plugin_basename = plugin_basename( $this->file );
232 $plugin_dirname = dirname( $plugin_basename );
233
234 if ( ! isset( $args->slug ) ) {
235 return $def;
236 }
237
238 // Check if this is our plugin (support both slug formats).
239 if (
240 $args->slug !== $plugin_basename &&
241 $args->slug !== $plugin_dirname
242 ) {
243 return $def;
244 }
245
246 // Get plugin data using the appropriate method.
247 global $wp_version;
248 if (
249 version_compare( $wp_version, '5.8', '>=' ) &&
250 $this->has_update_uri()
251 ) {
252 // Use GitHub API for modern system.
253 $data = $this->get_github_plugin_data();
254 } else {
255 // Use legacy method.
256 $data = $this->get_plugin_data();
257 }
258
259 return empty( $data ) ? $def : $data;
260 }
261
262 /**
263 * Get plugin data from GitHub API (for WordPress 5.8+ system).
264 */
265 private function get_github_plugin_data() {
266 // Get plugin headers from local file.
267 $plugin_data = get_file_data( $this->file, array(
268 'Name' => 'Plugin Name',
269 'PluginURI' => 'Plugin URI',
270 'Version' => 'Version',
271 'Description' => 'Description',
272 'Author' => 'Author',
273 'AuthorURI' => 'Author URI',
274 ) );
275
276 // Get latest release info from GitHub Releases API.
277 $response = wp_remote_get( 'https://api.github.com/repos/' . $this->owner . '/' . $this->actual_repo_name . '/releases/latest' );
278
279 if ( is_wp_error( $response ) ) {
280 return false;
281 }
282
283 $release_data = json_decode( wp_remote_retrieve_body( $response ), true );
284
285 $data = new stdClass();
286 $data->slug = dirname( plugin_basename( $this->file ) );
287 $data->plugin_name = $plugin_data['Name'];
288 $data->name = $plugin_data['Name'];
289 $data->version = ! empty( $release_data['tag_name'] ) ? ltrim( $release_data['tag_name'], 'v' ) : $plugin_data['Version'];
290 $data->author = $plugin_data['Author'];
291 $data->homepage = $plugin_data['PluginURI'];
292 $data->download_link = ! empty( $release_data ) ? $this->get_github_download_url( $release_data ) : '';
293 $data->requires = '4.7';
294 $data->tested = '6.8.1';
295 $data->requires_php = '7.0.0';
296
297 // Add required fields for modal display.
298 $data->short_description = ! empty( $plugin_data['Description'] ) ? $plugin_data['Description'] : 'A WordPress plugin by SiteOrigin.';
299 $data->last_updated = ! empty( $release_data['published_at'] ) ? date( 'Y-m-d', strtotime( $release_data['published_at'] ) ) : date( 'Y-m-d' );
300 $data->added = date( 'Y-m-d' );
301
302 // Set icon - use conventional naming with PNG preference.
303 $icon_png_path = plugin_dir_path( $this->file ) . 'img/icon.png';
304
305 if ( file_exists( $icon_png_path ) ) {
306 $data->icons = array(
307 '1x' => plugin_dir_url( $this->file ) . 'img/icon.png',
308 '2x' => plugin_dir_url( $this->file ) . 'img/icon.png',
309 'default' => plugin_dir_url( $this->file ) . 'img/icon.png',
310 );
311 } else {
312 // Fallback to WordPress.org geopattern icon.
313 $data->icons = array(
314 'default' => "https://s.w.org/plugins/geopattern-icon/{$data->slug}.svg",
315 );
316 }
317
318 // Set banner - only if a proper banner exists.
319 $banner_path = plugin_dir_path( $this->file ) . 'img/banner.png';
320 if ( file_exists( $banner_path ) ) {
321 $data->banners = array(
322 'low' => plugin_dir_url( $this->file ) . 'img/banner.png',
323 'high' => plugin_dir_url( $this->file ) . 'img/banner.png',
324 );
325 }
326
327 $data->sections = array(
328 'description' => $this->get_github_markdown( 'readme.md' ),
329 'changelog' => $this->get_github_markdown( 'changelog.md' ),
330 );
331
332 return $data;
333 }
334
335 private function get_current_version() {
336 $plugin_data = get_file_data( $this->file, array( 'Version' => 'Version' ) );
337 return isset( $plugin_data['Version'] ) ? $plugin_data['Version'] : '0.0.0';
338 }
339
340 /**
341 * Check if the plugin has an Update URI header pointing to GitHub.
342 */
343 private function has_update_uri() {
344 $plugin_data = get_file_data( $this->file, array( 'UpdateURI' => 'Update URI' ) );
345 return (
346 ! empty( $plugin_data['UpdateURI'] ) &&
347 strpos( $plugin_data['UpdateURI'], 'github.com' ) !== false
348 );
349 }
350
351 /**
352 * WordPress 5.8+ Update URI system - check for GitHub updates.
353 */
354 public function check_for_github_update( $update, $plugin_data, $plugin_file, $locales ) {
355 // Only handle our plugin.
356 if ( plugin_basename( $this->file ) !== $plugin_file ) {
357 return $update;
358 }
359
360 // Skip if update already found.
361 if ( ! empty( $update ) ) {
362 return $update;
363 }
364
365 // Get latest release from GitHub Releases API.
366 $response = wp_remote_get( 'https://api.github.com/repos/' . $this->owner . '/' . $this->actual_repo_name . '/releases/latest' );
367
368 if (
369 is_wp_error( $response ) ||
370 wp_remote_retrieve_response_code( $response ) !== 200
371 ) {
372 return false;
373 }
374
375 $release_data = json_decode( wp_remote_retrieve_body( $response ), true );
376
377 if ( empty( $release_data['tag_name'] ) ) {
378 return false;
379 }
380
381 $new_version = ltrim( $release_data['tag_name'], 'v' );
382 $current_version = $plugin_data['Version'];
383
384 // Check if update is available (comparing local version to GitHub release tag).
385 if ( version_compare( $current_version, $new_version, '<' ) ) {
386 // Get icon data for the update notification.
387 $icon_png_path = plugin_dir_path( $this->file ) . 'img/icon.png';
388 $icons = array();
389
390 if ( file_exists( $icon_png_path ) ) {
391 $icons = array(
392 '1x' => plugin_dir_url( $this->file ) . 'img/icon.png',
393 '2x' => plugin_dir_url( $this->file ) . 'img/icon.png',
394 'default' => plugin_dir_url( $this->file ) . 'img/icon.png',
395 );
396 }
397
398 return array(
399 'slug' => dirname( plugin_basename( $this->file ) ),
400 'version' => $new_version,
401 'url' => $release_data['html_url'],
402 'package' => $this->get_github_download_url( $release_data ),
403 'icons' => $icons,
404 'tested' => '6.8.1',
405 );
406 }
407
408 return false;
409 }
410
411 /**
412 * Get download URL from GitHub release data.
413 */
414 private function get_github_download_url( $release_data ) {
415 // Look for ZIP asset first.
416 if ( ! empty( $release_data['assets'] ) ) {
417 foreach ( $release_data['assets'] as $asset ) {
418 if ( strpos( $asset['name'], '.zip' ) !== false ) {
419 return $asset['browser_download_url'];
420 }
421 }
422 }
423
424 // Fallback to source ZIP.
425 return $release_data['zipball_url'];
426 }
427 }
428