├── 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 | '