├── LICENSE ├── Lite.php ├── README.md ├── composer.json └── languages └── git-updater-lite.pot /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andy Fragen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Lite.php: -------------------------------------------------------------------------------- 1 | slug = basename( dirname( $file_path ) ); 48 | 49 | if ( str_ends_with( $file_path, 'functions.php' ) ) { 50 | $this->file = $this->slug . '/style.css'; 51 | $file_path = dirname( $file_path ) . '/style.css'; 52 | } else { 53 | $this->file = $this->slug . '/' . basename( $file_path ); 54 | } 55 | 56 | $file_data = get_file_data( 57 | $file_path, 58 | array( 59 | 'Version' => 'Version', 60 | 'UpdateURI' => 'Update URI', 61 | ) 62 | ); 63 | $this->local_version = $file_data['Version']; 64 | $this->update_server = $this->check_update_uri( $file_data['UpdateURI'] ); 65 | } 66 | 67 | /** 68 | * Ensure properly formatted Update URI. 69 | * 70 | * @param string $updateUri Data from Update URI header. 71 | * 72 | * @return string|\WP_Error 73 | */ 74 | private function check_update_uri( $updateUri ) { 75 | if ( filter_var( $updateUri, FILTER_VALIDATE_URL ) 76 | && null === parse_url( $updateUri, PHP_URL_PATH ) // null means no path is present. 77 | ) { 78 | $updateUri = untrailingslashit( trim( $updateUri ) ); 79 | } else { 80 | return new \WP_Error( 'invalid_header_data', 'Invalid data from Update URI header', $updateUri ); 81 | } 82 | 83 | return $updateUri; 84 | } 85 | 86 | /** 87 | * Get API data. 88 | * 89 | * @global string $pagenow Current page. 90 | * @return void|\WP_Error 91 | */ 92 | public function run() { 93 | global $pagenow; 94 | 95 | // Needed for mu-plugin. 96 | if ( ! isset( $pagenow ) ) { 97 | $php_self = isset( $_SERVER['PHP_SELF'] ) ? sanitize_url( wp_unslash( $_SERVER['PHP_SELF'] ) ) : null; 98 | if ( null !== $php_self ) { 99 | // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 100 | $pagenow = basename( $php_self ); 101 | } 102 | } 103 | 104 | // Only run on the following pages. 105 | $pages = array( 'update-core.php', 'update.php', 'plugins.php', 'themes.php' ); 106 | $view_details = array( 'plugin-install.php', 'theme-install.php' ); 107 | $autoupdate_pages = array( 'admin-ajax.php', 'index.php', 'wp-cron.php' ); 108 | if ( ! in_array( $pagenow, array_merge( $pages, $view_details, $autoupdate_pages ), true ) ) { 109 | return; 110 | } 111 | 112 | if ( empty( $this->update_server ) || is_wp_error( $this->update_server ) ) { 113 | return new \WP_Error( 'invalid_domain', 'Invalid update server domain', $this->update_server ); 114 | } 115 | $url = "$this->update_server/wp-json/git-updater/v1/update-api/?slug=$this->slug"; 116 | $response = get_site_transient( "git-updater-lite_{$this->file}" ); 117 | if ( ! $response ) { 118 | $response = wp_remote_post( $url ); 119 | if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) === 404 ) { 120 | return $response; 121 | } 122 | 123 | $this->api_data = (object) json_decode( wp_remote_retrieve_body( $response ), true ); 124 | if ( null === $this->api_data || empty( (array) $this->api_data ) || property_exists( $this->api_data, 'error' ) ) { 125 | return new \WP_Error( 'non_json_api_response', 'Poorly formed JSON', $response ); 126 | } 127 | $this->api_data->file = $this->file; 128 | 129 | /* 130 | * Set transient for 5 minutes as AWS sets 5 minute timeout 131 | * for release asset redirect. 132 | * 133 | * Set limited timeout so wp_remote_post() not hit as frequently. 134 | * wp_remote_post() for plugin/theme check can run on every pageload 135 | * for certain pages. 136 | */ 137 | set_site_transient( "git-updater-lite_{$this->file}", $this->api_data, 5 * \MINUTE_IN_SECONDS ); 138 | } else { 139 | if ( property_exists( $response, 'error' ) ) { 140 | return new \WP_Error( 'repo-no-exist', 'Specified repo does not exist' ); 141 | } 142 | $this->api_data = $response; 143 | } 144 | 145 | $this->load_hooks(); 146 | } 147 | 148 | /** 149 | * Load hooks. 150 | * 151 | * @return void 152 | */ 153 | public function load_hooks() { 154 | $type = $this->api_data->type; 155 | add_filter( 'upgrader_source_selection', array( $this, 'upgrader_source_selection' ), 10, 4 ); 156 | add_filter( "{$type}s_api", array( $this, 'repo_api_details' ), 99, 3 ); 157 | add_filter( "site_transient_update_{$type}s", array( $this, 'update_site_transient' ), 15, 1 ); 158 | if ( ! is_multisite() ) { 159 | add_filter( 'wp_prepare_themes_for_js', array( $this, 'customize_theme_update_html' ) ); 160 | } 161 | 162 | // Load hook for adding authentication headers for download packages. 163 | add_filter( 164 | 'upgrader_pre_download', 165 | function () { 166 | add_filter( 'http_request_args', array( $this, 'add_auth_header' ), 15, 2 ); 167 | return false; // upgrader_pre_download filter default return value. 168 | } 169 | ); 170 | } 171 | 172 | /** 173 | * Correctly rename dependency for activation. 174 | * 175 | * @param string $source Path fo $source. 176 | * @param string $remote_source Path of $remote_source. 177 | * @param \Plugin_Upgrader|\Theme_Upgrader $upgrader An Upgrader object. 178 | * @param array $hook_extra Array of hook data. 179 | * 180 | * @throws \TypeError If the type of $upgrader is not correct. 181 | * 182 | * @return string|\WP_Error 183 | */ 184 | public function upgrader_source_selection( string $source, string $remote_source, $upgrader, $hook_extra = null ) { 185 | global $wp_filesystem; 186 | 187 | $new_source = $source; 188 | 189 | // Exit if installing. 190 | if ( isset( $hook_extra['action'] ) && 'install' === $hook_extra['action'] ) { 191 | return $source; 192 | } 193 | 194 | // TODO: add type hint for $upgrader, PHP 8 minimum due to `|`. 195 | if ( ! $upgrader instanceof \Plugin_Upgrader && ! $upgrader instanceof \Theme_Upgrader ) { 196 | throw new \TypeError( __METHOD__ . '(): Argument #3 ($upgrader) must be of type Plugin_Upgrader|Theme_Upgrader, ' . esc_attr( gettype( $upgrader ) ) . ' given.' ); 197 | } 198 | 199 | // Rename plugins. 200 | if ( $upgrader instanceof \Plugin_Upgrader ) { 201 | if ( isset( $hook_extra['plugin'] ) ) { 202 | $slug = dirname( $hook_extra['plugin'] ); 203 | $new_source = trailingslashit( $remote_source ) . $slug; 204 | } 205 | } 206 | 207 | // Rename themes. 208 | if ( $upgrader instanceof \Theme_Upgrader ) { 209 | if ( isset( $hook_extra['theme'] ) ) { 210 | $slug = $hook_extra['theme']; 211 | $new_source = trailingslashit( $remote_source ) . $slug; 212 | } 213 | } 214 | 215 | if ( basename( $source ) === $slug ) { 216 | return $source; 217 | } 218 | 219 | if ( trailingslashit( strtolower( $source ) ) !== trailingslashit( strtolower( $new_source ) ) ) { 220 | $wp_filesystem->move( $source, $new_source, true ); 221 | } 222 | 223 | return trailingslashit( $new_source ); 224 | } 225 | 226 | /** 227 | * Put changelog in plugins_api, return WP.org data as appropriate 228 | * 229 | * @param bool $result Default false. 230 | * @param string $action The type of information being requested from the Plugin Installation API. 231 | * @param \stdClass $response Repo API arguments. 232 | * 233 | * @return \stdClass|bool 234 | */ 235 | public function repo_api_details( $result, string $action, \stdClass $response ) { 236 | if ( "{$this->api_data->type}_information" !== $action ) { 237 | return $result; 238 | } 239 | 240 | // Exit if not our repo. 241 | if ( $response->slug !== $this->api_data->slug ) { 242 | return $result; 243 | } 244 | 245 | return $this->api_data; 246 | } 247 | 248 | /** 249 | * Hook into site_transient_update_{plugins|themes} to update from GitHub. 250 | * 251 | * @param \stdClass $transient Plugin|Theme update transient. 252 | * 253 | * @return \stdClass 254 | */ 255 | public function update_site_transient( $transient ) { 256 | // needed to fix PHP 7.4 warning. 257 | if ( ! is_object( $transient ) ) { 258 | $transient = new \stdClass(); 259 | } 260 | 261 | $response = array( 262 | 'slug' => $this->api_data->slug, 263 | $this->api_data->type => 'theme' === $this->api_data->type ? $this->api_data->slug : $this->api_data->file, 264 | 'url' => isset( $this->api_data->url ) ? $this->api_data->url : $this->api_data->slug, 265 | 'icons' => (array) $this->api_data->icons, 266 | 'banners' => $this->api_data->banners, 267 | 'branch' => $this->api_data->branch, 268 | 'type' => "{$this->api_data->git}-{$this->api_data->type}", 269 | 'update-supported' => true, 270 | 'requires' => $this->api_data->requires, 271 | 'requires_php' => $this->api_data->requires_php, 272 | 'new_version' => $this->api_data->version, 273 | 'package' => $this->api_data->download_link, 274 | 'tested' => $this->api_data->tested, 275 | ); 276 | if ( 'theme' === $this->api_data->type ) { 277 | $response['theme_uri'] = $response['url']; 278 | } 279 | 280 | if ( version_compare( $this->api_data->version, $this->local_version, '>' ) ) { 281 | $response = 'plugin' === $this->api_data->type ? (object) $response : $response; 282 | $key = 'plugin' === $this->api_data->type ? $this->api_data->file : $this->api_data->slug; 283 | $transient->response[ $key ] = $response; 284 | } else { 285 | $response = 'plugin' === $this->api_data->type ? (object) $response : $response; 286 | 287 | // Add repo without update to $transient->no_update for 'View details' link. 288 | $transient->no_update[ $this->api_data->file ] = $response; 289 | } 290 | 291 | return $transient; 292 | } 293 | 294 | /** 295 | * Add auth header for download package. 296 | * 297 | * @param array $args Array of http args. 298 | * @param string $url Download URL. 299 | * 300 | * @return array 301 | */ 302 | public function add_auth_header( $args, $url ) { 303 | if ( property_exists( $this->api_data, 'auth_header' ) 304 | && str_contains( $url, $this->api_data->slug ) 305 | ) { 306 | $args = array_merge( $args, $this->api_data->auth_header ); 307 | } 308 | return $args; 309 | } 310 | 311 | 312 | /** 313 | * Call theme messaging for single site installation. 314 | * 315 | * @author Seth Carstens 316 | * 317 | * @param array $prepared_themes Array of prepared themes. 318 | * 319 | * @return array 320 | */ 321 | public function customize_theme_update_html( $prepared_themes ) { 322 | $theme = $this->api_data; 323 | 324 | if ( 'theme' !== $theme->type ) { 325 | return $prepared_themes; 326 | } 327 | 328 | if ( ! empty( $prepared_themes[ $theme->slug ]['hasUpdate'] ) ) { 329 | $prepared_themes[ $theme->slug ]['update'] = $this->append_theme_actions_content( $theme ); 330 | } else { 331 | $prepared_themes[ $theme->slug ]['description'] .= $this->append_theme_actions_content( $theme ); 332 | } 333 | 334 | return $prepared_themes; 335 | } 336 | 337 | /** 338 | * Create theme update messaging for single site installation. 339 | * 340 | * @author Seth Carstens 341 | * 342 | * @access protected 343 | * @codeCoverageIgnore 344 | * 345 | * @param \stdClass $theme Theme object. 346 | * 347 | * @return string (content buffer) 348 | */ 349 | protected function append_theme_actions_content( $theme ) { 350 | $details_url = esc_attr( 351 | add_query_arg( 352 | array( 353 | 'tab' => 'theme-information', 354 | 'theme' => $theme->slug, 355 | 'TB_iframe' => 'true', 356 | 'width' => 270, 357 | 'height' => 400, 358 | ), 359 | self_admin_url( 'theme-install.php' ) 360 | ) 361 | ); 362 | $nonced_update_url = wp_nonce_url( 363 | esc_attr( 364 | add_query_arg( 365 | array( 366 | 'action' => 'upgrade-theme', 367 | 'theme' => rawurlencode( $theme->slug ), 368 | ), 369 | self_admin_url( 'update.php' ) 370 | ) 371 | ), 372 | 'upgrade-theme_' . $theme->slug 373 | ); 374 | 375 | $current = get_site_transient( 'update_themes' ); 376 | 377 | /** 378 | * Display theme update links. 379 | */ 380 | ob_start(); 381 | if ( isset( $current->response[ $theme->slug ] ) ) { 382 | ?> 383 |

384 | 385 | name ) 390 | ); 391 | printf( 392 | ' ', 393 | esc_url( $details_url ), 394 | esc_attr( $theme->name ) 395 | ); 396 | if ( ! empty( $current->response[ $theme->slug ]['package'] ) ) { 397 | printf( 398 | /* translators: 1: version number, 2: closing anchor tag, 3: update URL */ 399 | esc_html__( 'View version %1$s details%2$s or %3$supdate now%2$s.', 'git-updater-lite' ), 400 | $theme->remote_version = isset( $theme->remote_version ) ? esc_attr( $theme->remote_version ) : null, 401 | '', 402 | sprintf( 403 | /* translators: %s: theme name */ 404 | '', 405 | esc_attr( $theme->name ) 406 | ) 407 | ); 408 | } else { 409 | printf( 410 | /* translators: 1: version number, 2: closing anchor tag, 3: update URL */ 411 | esc_html__( 'View version %1$s details%2$s.', 'git-updater-lite' ), 412 | $theme->remote_version = isset( $theme->remote_version ) ? esc_attr( $theme->remote_version ) : null, 413 | '' 414 | ); 415 | printf( 416 | /* translators: %s: opening/closing paragraph and italic tags */ 417 | esc_html__( '%1$sAutomatic update is unavailable for this theme.%2$s', 'git-updater-lite' ), 418 | '

', 419 | '

' 420 | ); 421 | } 422 | ?> 423 | 424 |

425 | 8 | * License: MIT 9 | 10 | A simple standalone library to enable automatic updates to your git hosted WordPress plugins or themes. 11 | 12 | ## Description 13 | 14 | **This is version 2.x and contains a breaking change from 1.5.x.** 15 | 16 | This library was designed to be added to your git hosted plugin or theme to enable standalone updates. 17 | 18 | You must have a publicly reachable site that will be used for dynamically retrieving the update API data. 19 | 20 | * [Git Updater](https://git-updater.com) is required on a site where all of the release versions of your plugins and themes are installed. 21 | * All of your plugins/themes **must** be integrated with Git Updater. 22 | * You must be using Git Updater v12.9.0 or better. 23 | 24 | Git Updater is capable of returning a [REST endpoint](https://git-updater.com/knowledge-base/remote-management-restful-endpoints/#articleTOC_3/) containing the `plugins_api()` or `themes_api()` data for your plugin/theme. You will pass this endpoint during the integration. 25 | 26 | The REST endpoint format is as follows. 27 | 28 | * plugins - `https://my-site.com/wp-json/git-updater/v1/update-api/?slug=my-plugin` 29 | * themes - `https://my-site.com/wp-json/git-updater/v1/update-api/?slug=my-theme` 30 | 31 | ## Installation 32 | 33 | Add via composer. `composer require afragen/git-updater-lite:^2` 34 | 35 | * Add the `Update URI: ` header to your plugin or theme headers. Where `` is the domain to the update server, eg `https://git-updater.com`. 36 | 37 | * Add the following code to your plugin file or theme's functions.php file. 38 | 39 | ```php 40 | require_once __DIR__ . '/vendor/afragen/git-updater-lite/Lite.php'; 41 | ( new \Fragen\Git_Updater\Lite( __FILE__ ) )->run(); 42 | ``` 43 | 44 | An example integrated plugin is here, https://github.com/afragen/test-plugin-gu-lite 45 | 46 | ```php 47 | run(); 69 | ``` 70 | 71 | FWIW, I test by decreasing the version number locally to see an update. 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "afragen/git-updater-lite", 3 | "description": "A simple class to integrate with Git Updater for standalone plugin/theme updates.", 4 | "version": "2.3.1", 5 | "type": "library", 6 | "keywords": [ 7 | "wordpress", 8 | "plugin", 9 | "theme", 10 | "updater" 11 | ], 12 | "license": "MIT", 13 | "repositories": [ 14 | { 15 | "type": "vcs", 16 | "url": "https://github.com/afragen/git-updater-lite" 17 | } 18 | ], 19 | "authors": [ 20 | { 21 | "name": "Andy Fragen", 22 | "email": "andy@thefragens.com", 23 | "homepage": "https://github.com/afragen/git-updater-lite", 24 | "role": "Developer" 25 | } 26 | ], 27 | "prefer-stable": true, 28 | "require": { 29 | "php": ">=7.4" 30 | }, 31 | "autoload": { 32 | "classmap": [ 33 | "Lite.php" 34 | ] 35 | }, 36 | "require-dev": { 37 | "squizlabs/php_codesniffer": "3.10.3", 38 | "wp-coding-standards/wpcs": "~3.1.0", 39 | "yoast/phpunit-polyfills": "^1.1.0" 40 | }, 41 | "config": { 42 | "allow-plugins": { 43 | "dealerdirect/phpcodesniffer-composer-installer": true 44 | } 45 | }, 46 | "scripts": { 47 | "wpcs": [ 48 | "vendor/bin/phpcbf .; vendor/bin/phpcs ." 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /languages/git-updater-lite.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "Report-Msgid-Bugs-To: https://github.com/afragen/git-updater-lite/issues\n" 5 | "Last-Translator: FULL NAME \n" 6 | "Language-Team: LANGUAGE \n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "POT-Creation-Date: 2025-02-24T21:37:03+00:00\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "X-Generator: WP-CLI 2.11.0\n" 13 | "X-Domain: git-updater-lite\n" 14 | 15 | #. translators: %s: theme name 16 | #: Lite.php:388 17 | msgid "There is a new version of %s available." 18 | msgstr "" 19 | 20 | #. translators: 1: version number, 2: closing anchor tag, 3: update URL 21 | #: Lite.php:399 22 | msgid "View version %1$s details%2$s or %3$supdate now%2$s." 23 | msgstr "" 24 | 25 | #. translators: %s: theme name 26 | #: Lite.php:404 27 | msgid "Update %s now" 28 | msgstr "" 29 | 30 | #. translators: 1: version number, 2: closing anchor tag, 3: update URL 31 | #: Lite.php:411 32 | msgid "View version %1$s details%2$s." 33 | msgstr "" 34 | 35 | #. translators: %s: opening/closing paragraph and italic tags 36 | #: Lite.php:417 37 | msgid "%1$sAutomatic update is unavailable for this theme.%2$s" 38 | msgstr "" 39 | --------------------------------------------------------------------------------