├── external-update-api.php ├── external-update-api ├── euapi.php ├── handler-files.php ├── handler-github.php ├── handler.php ├── info.php ├── item-plugin.php ├── item-theme.php ├── item.php └── update.php ├── github-oauth-connector.php └── readme.md /external-update-api.php: -------------------------------------------------------------------------------- 1 | plugins|themes)/update-check/(?P[0-9\.]+)/#', $url, $matches ) ) { 48 | 49 | switch ( $matches['type'] ) { 50 | 51 | case 'plugins': 52 | return $this->plugin_request( $args, floatval( $matches['version'] ) ); 53 | break; 54 | 55 | case 'themes': 56 | return $this->theme_request( $args, floatval( $matches['version'] ) ); 57 | break; 58 | 59 | } 60 | 61 | } 62 | 63 | $query = parse_url( $url, PHP_URL_QUERY ); 64 | 65 | if ( empty( $query ) ) { 66 | return $args; 67 | } 68 | 69 | parse_str( $query, $query ); 70 | 71 | if ( !isset( $query['_euapi_type'] ) or !isset( $query['_euapi_file'] ) ) { 72 | return $args; 73 | } 74 | 75 | if ( !( $handler = $this->get_handler( $query['_euapi_type'], $query['_euapi_file'] ) ) ) { 76 | return $args; 77 | } 78 | 79 | $args = array_merge( $args, $handler->config['http'] ); 80 | 81 | return $args; 82 | 83 | } 84 | 85 | /** 86 | * Filters the arguments for HTTP requests to the plugin update check API. 87 | * 88 | * Here we loop over each plugin in the update check request and remove ones for which we're 89 | * handling or excluding updates. 90 | * 91 | * @author John Blackbourn 92 | * @param array $args HTTP request arguments. 93 | * @param float $version The API request version number. 94 | * @return array Updated array of arguments. 95 | */ 96 | protected function plugin_request( array $args, $version ) { 97 | 98 | switch ( $version ) { 99 | 100 | case 1.0: 101 | _doing_it_wrong( __METHOD__, sprintf( __( 'External Update API is not compatible with version %s of the WordPress Plugin API. Please update to the latest version of WordPress.', 'euapi' ), $version ), 0.4 ); 102 | return $args; 103 | break; 104 | 105 | case 1.1: 106 | default: 107 | $plugins = json_decode( $args['body']['plugins'] ); 108 | break; 109 | 110 | } 111 | 112 | if ( ! is_object( $plugins ) or empty( $plugins->plugins ) ) { 113 | return $args; 114 | } 115 | 116 | foreach ( $plugins->plugins as $plugin => $data ) { 117 | 118 | if ( !is_object( $data ) ) { 119 | continue; 120 | } 121 | 122 | $data = get_object_vars( $data ); 123 | $item = new EUAPI_Item_Plugin( $plugin, $data ); 124 | $handler = $this->get_handler( 'plugin', $plugin, $item ); 125 | 126 | if ( null === $handler ) { 127 | continue; 128 | } 129 | 130 | if ( is_a( $handler, 'EUAPI_Handler' ) ) { 131 | $handler->item = $item; 132 | } 133 | 134 | unset( $plugins->plugins->{$plugin} ); 135 | 136 | } 137 | 138 | $args['body']['plugins'] = json_encode( $plugins ); 139 | 140 | return $args; 141 | 142 | } 143 | 144 | /** 145 | * Filters the arguments for HTTP requests to the theme update check API. 146 | * 147 | * Here we loop over each theme in the update check request and remove ones for which we're 148 | * handling or excluding updates. 149 | * 150 | * @author John Blackbourn 151 | * @param array $args HTTP request arguments. 152 | * @param float $version The API request version number. 153 | * @return array Updated array of arguments. 154 | */ 155 | protected function theme_request( array $args, $version ) { 156 | 157 | switch ( $version ) { 158 | 159 | case 1.0: 160 | _doing_it_wrong( __METHOD__, sprintf( __( 'External Update API is not compatible with version %s of the WordPress Theme API. Please update to the latest version of WordPress.', 'euapi' ), $version ), 0.4 ); 161 | return $args; 162 | break; 163 | 164 | case 1.1: 165 | default: 166 | $themes = json_decode( $args['body']['themes'] ); 167 | break; 168 | 169 | } 170 | 171 | if ( ! is_object( $themes ) or empty( $themes->themes ) ) { 172 | return $args; 173 | } 174 | 175 | foreach ( $themes->themes as $theme => $data ) { 176 | 177 | if ( !is_object( $data ) ) { 178 | continue; 179 | } 180 | 181 | $data = get_object_vars( $data ); 182 | 183 | if ( !isset( $data['ThemeURI'] ) ) { 184 | # ThemeURI is missing from $data by default for some reason 185 | $data['ThemeURI'] = wp_get_theme( $data['Template'] )->get( 'ThemeURI' ); 186 | } 187 | 188 | $item = new EUAPI_Item_Theme( $theme, $data ); 189 | $handler = $this->get_handler( 'theme', $theme, $item ); 190 | 191 | if ( null === $handler ) { 192 | continue; 193 | } 194 | 195 | if ( is_a( $handler, 'EUAPI_Handler' ) ) { 196 | $handler->item = $item; 197 | } 198 | 199 | unset( $themes->themes->{$theme} ); 200 | 201 | } 202 | 203 | $args['body']['themes'] = json_encode( $themes ); 204 | 205 | return $args; 206 | 207 | } 208 | 209 | /** 210 | * Called immediately before the plugin update check results are saved in a transient. 211 | * 212 | * We use this to fire off update checks to each of the plugins we're handling updates 213 | * for, and populate the results in the update check object. 214 | * 215 | * @author John Blackbourn 216 | * @param object $update The plugin update check object. 217 | * @return object The updated update check object. 218 | */ 219 | public function filter_update_plugins( $update ) { 220 | if ( !isset( $this->handlers['plugin'] ) ) { 221 | return $update; 222 | } 223 | return self::check( $update, $this->handlers['plugin'] ); 224 | } 225 | 226 | /** 227 | * Called immediately before the theme update check results are saved in a transient. 228 | * 229 | * We use this to fire off update checks to each of the themes we're handling updates 230 | * for, and populate the results in the update check object. 231 | * 232 | * @author John Blackbourn 233 | * @param object $update Theme update check object. 234 | * @return object Updated update check object. 235 | */ 236 | public function filter_update_themes( $update ) { 237 | if ( !isset( $this->handlers['theme'] ) ) { 238 | return $update; 239 | } 240 | return self::check( $update, $this->handlers['theme'] ); 241 | } 242 | 243 | /** 244 | * Fire off update checks for each of the handlers specified and populate the results in 245 | * the update check object. 246 | * 247 | * @author John Blackbourn 248 | * @param object $update Update check object. 249 | * @param array $handlers Handlers that we're interested in. 250 | * @return object Updated update check object. 251 | */ 252 | public static function check( $update, array $handlers ) { 253 | 254 | if ( empty( $update->checked ) ) { 255 | return $update; 256 | } 257 | 258 | foreach ( array_filter( $handlers ) as $handler ) { 259 | 260 | $handler_update = $handler->get_update(); 261 | 262 | if ( $handler_update->get_new_version() and 1 === version_compare( $handler_update->get_new_version(), $handler->get_current_version() ) ) { 263 | if ( 'plugin' == $handler->get_type() ) { 264 | $update->response[ $handler->get_file() ] = (object) $handler_update->get_data_to_store(); 265 | } else { 266 | $update->response[ $handler->get_file() ] = $handler_update->get_data_to_store(); 267 | } 268 | } 269 | 270 | } 271 | 272 | return $update; 273 | 274 | } 275 | 276 | /** 277 | * Get the update handler for the given item, if one is present. 278 | * 279 | * @author John Blackbourn 280 | * @param string $type Handler type (either 'plugin' or 'theme'). 281 | * @param string $file Item base file name. 282 | * @param EUAPI_Item|null $item Item object for the plugin/theme. Optional. 283 | * @return EUAPI_Handler|null Update handler object, or null if no update handler is present. 284 | */ 285 | public function get_handler( $type, $file, $item = null ) { 286 | 287 | if ( isset( $this->handlers[$type] ) and array_key_exists( $file, $this->handlers[$type] ) ) { 288 | return $this->handlers[$type][$file]; 289 | } 290 | 291 | if ( !$item ) { 292 | $item = self::populate_item( $type, $file ); 293 | } 294 | 295 | if ( ! is_a( $item, 'EUAPI_Item' ) ) { 296 | $handler = null; 297 | } else { 298 | $handler = apply_filters( "euapi_{$type}_handler", null, $item ); 299 | } 300 | 301 | $this->handlers[$type][$file] = $handler; 302 | 303 | return $handler; 304 | 305 | } 306 | 307 | /** 308 | * Returns the item data for a given item, typically by reading the item file header 309 | * and populating its data. 310 | * 311 | * @author John Blackbourn 312 | * @param string $type Handler type (either 'plugin' or 'theme'). 313 | * @param string $file Item base file name. 314 | * @return EUAPI_Item|null Item object or null on failure. 315 | */ 316 | protected static function populate_item( $type, $file ) { 317 | 318 | switch ( $type ) { 319 | 320 | case 'plugin': 321 | if ( $data = self::get_plugin_data( $file ) ) { 322 | return new EUAPI_Item_Plugin( $file, $data ); 323 | } 324 | break; 325 | 326 | case 'theme': 327 | if ( $data = self::get_theme_data( $file ) ) { 328 | return new EUAPI_Item_Theme( $file, $data ); 329 | } 330 | break; 331 | 332 | } 333 | 334 | return null; 335 | 336 | } 337 | 338 | /** 339 | * Get data for a plugin by reading its file header. 340 | * 341 | * @param string $file Plugin base file name. 342 | * @return array|false Array of plugin data, or false on failure. 343 | */ 344 | public static function get_plugin_data( $file ) { 345 | 346 | require_once ABSPATH . '/wp-admin/includes/plugin.php'; 347 | 348 | if ( file_exists( $plugin = WP_PLUGIN_DIR . '/' . $file ) ) { 349 | return get_plugin_data( $plugin ); 350 | } 351 | 352 | return false; 353 | 354 | } 355 | 356 | /** 357 | * Get data for a theme by reading its file header. 358 | * 359 | * @param string $file Theme directory name. 360 | * @return array|false Array of theme data, or false on failure. 361 | */ 362 | public static function get_theme_data( $file ) { 363 | 364 | $theme = wp_get_theme( $file ); 365 | 366 | if ( !$theme->exists() ) { 367 | return false; 368 | } 369 | 370 | $data = array( 371 | 'Name' => '', 372 | 'ThemeURI' => '', 373 | 'Description' => '', 374 | 'Author' => '', 375 | 'AuthorURI' => '', 376 | 'Version' => '', 377 | 'Template' => '', 378 | 'Status' => '', 379 | 'Tags' => '', 380 | 'TextDomain' => '', 381 | 'DomainPath' => '', 382 | ); 383 | 384 | foreach ( $data as $k => $v ) { 385 | $data[$k] = $theme->get( $k ); 386 | } 387 | 388 | return $data; 389 | 390 | } 391 | 392 | /** 393 | * Before the Plugin API performs an action, this short-circuit callback is fired, allowing us to override the 394 | * API method for a given action. 395 | * 396 | * Here, we override the action which fetches plugin information from the wp.org API 397 | * and return our own plugin information if necessary. 398 | * 399 | * @param bool|object $default Default return value for this request. Usually boolean false. 400 | * @param string $action API function being performed. 401 | * @param object $plugin Plugin Info API object. 402 | * @return bool|WP_Error|EUAPI_Info EUAPI Info object, WP_Error object on failure, $default if we're not interfering. 403 | */ 404 | public function filter_plugins_api( $default, $action, $plugin ) { 405 | 406 | if ( 'plugin_information' != $action ) { 407 | return $default; 408 | } 409 | if ( false === strpos( $plugin->slug, '/' ) ) { 410 | return $default; 411 | } 412 | 413 | $handler = $this->get_handler( 'plugin', $plugin->slug ); 414 | 415 | if ( ! is_a( $handler, 'EUAPI_Handler' ) ) { 416 | return $default; 417 | } 418 | 419 | return $handler->get_info(); 420 | 421 | } 422 | 423 | /** 424 | * Before the Theme API performs an action, this short-circuit callback is fired, allowing us to override the 425 | * API method for a given action. 426 | * 427 | * Here, we override the action which fetches theme information from the wp.org API 428 | * and return our own theme information if necessary. 429 | * 430 | * @param bool|object $default Default return value for this request. Usually boolean false. 431 | * @param string $action API function being performed. 432 | * @param object $theme Theme Info API object. 433 | * @return bool|WP_Error|EUAPI_Info EUAPI Info object, WP_Error object on failure, $default if we're not interfering. 434 | */ 435 | public function filter_themes_api( $default, $action, $theme ) { 436 | 437 | if ( 'theme_information' != $action ) { 438 | return $default; 439 | } 440 | 441 | $handler = $this->get_handler( 'theme', $theme->slug ); 442 | 443 | if ( ! is_a( $handler, 'EUAPI_Handler' ) ) { 444 | return $default; 445 | } 446 | 447 | return $handler->get_info(); 448 | 449 | } 450 | 451 | /** 452 | * Fetch the contents of a URL. 453 | * 454 | * @author John Blackbourn 455 | * @param string $url URL to fetch. 456 | * @param array $args Array of arguments passed to wp_remote_get(). 457 | * @return WP_Error|string WP_Error object on failure, string contents of URL body on success. 458 | */ 459 | public static function fetch( $url, array $args = array() ) { 460 | 461 | $args = array_merge( array( 462 | 'timeout' => 5 463 | ), $args ); 464 | 465 | $response = wp_remote_get( $url, $args ); 466 | 467 | if ( is_wp_error( $response ) ) { 468 | return $response; 469 | } 470 | 471 | $code = wp_remote_retrieve_response_code( $response ); 472 | $message = wp_remote_retrieve_response_message( $response ); 473 | 474 | if ( 200 != $code ) { 475 | return new WP_Error( 'fetch_failed', esc_html( $code . ' ' . $message ) ); 476 | } 477 | 478 | return wp_remote_retrieve_body( $response ); 479 | 480 | } 481 | 482 | /** 483 | * Parse a plugin or theme file to fetch its header values. 484 | * 485 | * Based on WordPress' `get_file_data()` function. 486 | * 487 | * @param string $content The file content. 488 | * @param array $all_headers The headers to return. 489 | * @return array The header values. 490 | */ 491 | public static function get_content_data( $content, array $all_headers ) { 492 | 493 | // Pull only the first 8kiB of the file in. 494 | if ( function_exists( 'mb_substr' ) ) { 495 | $file_data = mb_substr( $content, 0, 8192 ); 496 | } else { 497 | $file_data = substr( $content, 0, 8192 ); 498 | } 499 | 500 | // Make sure we catch CR-only line endings. 501 | $file_data = str_replace( "\r", "\n", $file_data ); 502 | 503 | foreach ( $all_headers as $field => $regex ) { 504 | if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) { 505 | $all_headers[ $field ] = _cleanup_header_comment( $match[1] ); 506 | } else { 507 | $all_headers[ $field ] = ''; 508 | } 509 | } 510 | 511 | return $all_headers; 512 | } 513 | 514 | /** 515 | * Pre-load our handlers so the plugin/theme update filters can function. 516 | * 517 | * @param bool|WP_Error $default Default return value for the update. Usually boolean true. 518 | * @param array $hook_extra Extra arguments passed to hooked filters. 519 | * @return bool|WP_Error Boolean true or a WP_Error object. 520 | */ 521 | public function filter_upgrader_pre_install( $default, array $hook_extra ) { 522 | 523 | if ( isset( $hook_extra['plugin'] ) ) { 524 | $this->get_handler( 'plugin', $hook_extra['plugin'] ); 525 | } else if ( isset( $hook_extra['theme'] ) ) { 526 | $this->get_handler( 'theme', $hook_extra['theme'] ); 527 | } 528 | 529 | return $default; 530 | 531 | } 532 | 533 | /** 534 | * If we have a handler for this update, do some post-processing after the update. 535 | * 536 | * @param bool|WP_Error $default Default return value for the update. Usually boolean true. 537 | * @param array $hook_extra Extra arguments passed to hooked filters. 538 | * @param array $result Installation result data. 539 | * @return bool|WP_Error Boolean true or a WP_Error object. 540 | */ 541 | public function filter_upgrader_post_install( $default, array $hook_extra, array $result ) { 542 | 543 | global $wp_filesystem; 544 | 545 | if ( isset( $hook_extra['plugin'] ) ) { 546 | $handler = $this->get_handler( 'plugin', $hook_extra['plugin'] ); 547 | } else if ( isset( $hook_extra['theme'] ) ) { 548 | $handler = $this->get_handler( 'theme', $hook_extra['theme'] ); 549 | } else { 550 | return $default; 551 | } 552 | 553 | if ( ! is_a( $handler, 'EUAPI_Handler' ) ) { 554 | return $default; 555 | } 556 | 557 | switch ( $handler->get_type() ) { 558 | 559 | case 'plugin': 560 | $proper_destination = WP_PLUGIN_DIR . '/' . $handler->config['folder_name']; 561 | break; 562 | case 'theme': 563 | $proper_destination = get_theme_root() . '/' . $handler->config['folder_name']; 564 | break; 565 | 566 | } 567 | 568 | // Move 569 | $wp_filesystem->move( $result['destination'], $proper_destination ); 570 | 571 | return $default; 572 | 573 | } 574 | 575 | /** 576 | * Singleton instantiator. 577 | * 578 | * @return EUAPI Our instance of the EUAPI class. 579 | */ 580 | public static function init() { 581 | 582 | static $instance = null; 583 | 584 | if ( !$instance ) 585 | $instance = new EUAPI; 586 | 587 | return $instance; 588 | 589 | } 590 | 591 | /** 592 | * Eat our own dog food. Handle updates to EUAPI through GitHub. 593 | * 594 | * @param EUAPI_Handler|null $handler The handler object for this item, or null if a handler isn't set. 595 | * @param EUAPI_Item $item The item in question. 596 | * @return EUAPI_Handler|null The handler for this item, or null. 597 | */ 598 | public function filter_euapi_plugin_handler( EUAPI_Handler $handler = null, EUAPI_Item $item ) { 599 | 600 | if ( 'https://github.com/cftp/external-update-api' == $item->url ) { 601 | 602 | $handler = new EUAPI_Handler_GitHub( array( 603 | 'type' => $item->type, 604 | 'file' => $item->file, 605 | 'github_url' => $item->url, 606 | 'http' => array( 607 | 'sslverify' => false, 608 | ), 609 | ) ); 610 | 611 | } 612 | 613 | return $handler; 614 | 615 | } 616 | 617 | } 618 | 619 | endif; // endif class exists 620 | -------------------------------------------------------------------------------- /external-update-api/handler-files.php: -------------------------------------------------------------------------------- 1 | array( 29 | 'timeout' => 5, 30 | ), 31 | ); 32 | 33 | // Back-compat with earlier versions where we had these values in the root of the $config array. 34 | if ( isset( $config['sslverify'] ) ) { 35 | $config['http']['sslverify'] = $config['sslverify']; 36 | } 37 | if ( isset( $config['timeout'] ) ) { 38 | $config['http']['timeout'] = $config['timeout']; 39 | } 40 | 41 | parent::__construct( array_merge( $defaults, $config ) ); 42 | 43 | } 44 | 45 | /** 46 | * Returns the URL of the plugin or theme file. 47 | * 48 | * @author John Blackbourn 49 | * @param string $file Optional file name. Defaults to base plugin file or theme stylesheet. 50 | * @return string URL of the plugin file. 51 | */ 52 | abstract public function get_file_url( $file = null ); 53 | 54 | /** 55 | * Fetch the latest version number. Does this by fetching the plugin 56 | * file and then parsing the header to get the version number. 57 | * 58 | * @author John Blackbourn 59 | * @return string|false Version number, or false on failure. 60 | */ 61 | final public function fetch_new_version() { 62 | 63 | $response = EUAPI::fetch( $this->get_file_url(), $this->config['http'] ); 64 | 65 | if ( is_wp_error( $response ) ) { 66 | return false; 67 | } 68 | 69 | $data = EUAPI::get_content_data( $response, array( 70 | 'version' => 'Version' 71 | ) ); 72 | 73 | if ( empty( $data['version'] ) ) { 74 | return false; 75 | } 76 | 77 | return $data['version']; 78 | 79 | } 80 | 81 | /** 82 | * Fetch info about the latest version of the item. 83 | * 84 | * @author John Blackbourn 85 | * @return EUAPI_Info|WP_Error An EUAPI_Info object, or a WP_Error object on failure. 86 | */ 87 | final public function fetch_info() { 88 | 89 | $fields = array( 90 | 'author' => 'Author', 91 | 'description' => 'Description' 92 | ); 93 | 94 | switch ( $this->get_type() ) { 95 | 96 | case 'plugin': 97 | $file = $this->get_file_url(); 98 | $fields['plugin_name'] = 'Plugin Name'; 99 | break; 100 | 101 | case 'theme': 102 | $file = $this->get_file_url( 'style.css' ); 103 | $fields['theme_name'] = 'Theme Name'; 104 | break; 105 | 106 | } 107 | 108 | $response = EUAPI::fetch( $file, $this->config['http'] ); 109 | 110 | if ( is_wp_error( $response ) ) { 111 | return $response; 112 | } 113 | 114 | $data = EUAPI::get_content_data( $response, $fields ); 115 | 116 | $info = array_merge( $data, array( 117 | 118 | 'slug' => $this->get_file(), 119 | 'version' => $this->get_new_version(), 120 | 'homepage' => $this->get_homepage_url(), 121 | 'download_link' => $this->get_package_url(), 122 | # 'requires' => '', 123 | # 'tested' => '', 124 | # 'last_updated' => '', 125 | 'downloaded' => 0, 126 | 'sections' => array( 127 | 'description' => $data['description'], 128 | ), 129 | 130 | ) ); 131 | 132 | return new EUAPI_Info( $info ); 133 | 134 | } 135 | 136 | } 137 | 138 | endif; // endif class exists 139 | -------------------------------------------------------------------------------- /external-update-api/handler-github.php: -------------------------------------------------------------------------------- 1 | null, 38 | ); 39 | 40 | $path = trim( parse_url( $config['github_url'], PHP_URL_PATH ), '/' ); 41 | list( $username, $repo ) = explode( '/', $path, 2 ); 42 | 43 | $defaults['base_url'] = sprintf( 'https://raw.githubusercontent.com/%1$s/%2$s/master', 44 | $username, 45 | $repo 46 | ); 47 | $defaults['package_url'] = sprintf( 'https://api.github.com/repos/%1$s/%2$s/zipball', 48 | $username, 49 | $repo 50 | ); 51 | 52 | parent::__construct( array_merge( $defaults, $config ) ); 53 | 54 | } 55 | 56 | /** 57 | * Returns the URL of the plugin or theme's homepage. 58 | * 59 | * @author John Blackbourn 60 | * @return string URL of the plugin or theme's homepage. 61 | */ 62 | public function get_homepage_url() { 63 | 64 | return $this->config['github_url']; 65 | 66 | } 67 | 68 | /** 69 | * Returns the URL of the plugin or theme file on GitHub, with access token appended if relevant. 70 | * 71 | * @author John Blackbourn 72 | * @param string $file Optional file name. Defaults to base plugin file or theme stylesheet. 73 | * @return string URL of the plugin file. 74 | */ 75 | public function get_file_url( $file = null ) { 76 | 77 | if ( empty( $file ) ) { 78 | $file = $this->config['file_name']; 79 | } 80 | 81 | $url = trailingslashit( $this->config['base_url'] ) . $file; 82 | 83 | if ( !empty( $this->config['access_token'] ) ) { 84 | $url = add_query_arg( array( 85 | 'access_token' => $this->config['access_token'] 86 | ), $url ); 87 | } 88 | 89 | return $url; 90 | } 91 | 92 | /** 93 | * Returns the URL of the plugin or theme's ZIP package on GitHub, with access token appended if relevant. 94 | * 95 | * @author John Blackbourn 96 | * @return string URL of the plugin or theme's ZIP package. 97 | */ 98 | public function get_package_url() { 99 | 100 | $url = $this->config['package_url']; 101 | 102 | if ( !empty( $this->config['access_token'] ) ) { 103 | $url = add_query_arg( array( 104 | 'access_token' => $this->config['access_token'] 105 | ), $url ); 106 | } 107 | 108 | return $url; 109 | 110 | } 111 | 112 | } 113 | 114 | endif; // endif class exists 115 | -------------------------------------------------------------------------------- /external-update-api/handler.php: -------------------------------------------------------------------------------- 1 | config = apply_filters( "euapi_{$config['type']}_handler_config", array_merge( $defaults, $config ) ); 35 | } 36 | 37 | /** 38 | * Return the URL of the item's homepage. 39 | * 40 | * @abstract 41 | * @return string URL of the item's homepage. 42 | */ 43 | abstract public function get_homepage_url(); 44 | 45 | /** 46 | * Return the URL of the item's ZIP package. 47 | * 48 | * @abstract 49 | * @return string URL of the item's ZIP package. 50 | */ 51 | abstract public function get_package_url(); 52 | 53 | /** 54 | * Fetch the latest version number of the item, typically from an external location. 55 | * 56 | * @abstract 57 | * @return string|false Version number, or false on failure. 58 | */ 59 | abstract public function fetch_new_version(); 60 | 61 | /** 62 | * Fetch info about the latest version of the item. 63 | * 64 | * @abstract 65 | * @return EUAPI_Info|WP_Error An EUAPI_Info object, or a WP_Error object on failure. 66 | */ 67 | abstract public function fetch_info(); 68 | 69 | /** 70 | * Fetch the upgrade notice for the item, typically from an external location. 71 | * 72 | * @return string|false Upgrade notice, or false on failure. 73 | */ 74 | public function fetch_upgrade_notice(){ 75 | return false; 76 | } 77 | 78 | /** 79 | * Get the current item's base file name (eg. my-plugin/my-plugin.php or my-theme/style.css). 80 | * 81 | * @author John Blackbourn 82 | * @return string File name 83 | */ 84 | final public function get_file() { 85 | return $this->config['file']; 86 | } 87 | 88 | /** 89 | * Get the current installed version number of the item. 90 | * 91 | * @author John Blackbourn 92 | * @return string|false Version number, or false on failure. 93 | */ 94 | final public function get_current_version() { 95 | 96 | if ( isset( $this->item ) ) { 97 | return $this->item->get_version(); 98 | } else { 99 | return false; 100 | } 101 | 102 | } 103 | 104 | /** 105 | * Get the latest version number of the item. 106 | * 107 | * @author John Blackbourn 108 | * @return string|false Version number, or false on failure. 109 | */ 110 | final public function get_new_version() { 111 | 112 | if ( !isset( $this->new_version ) ) { 113 | $this->new_version = $this->fetch_new_version(); 114 | } 115 | 116 | return $this->new_version; 117 | 118 | } 119 | 120 | /** 121 | * Get the upgrade notice for the item. 122 | * 123 | * @author John Blackbourn 124 | * @return string|false Upgrade notice, or false on failure. 125 | */ 126 | final public function get_upgrade_notice() { 127 | 128 | if ( !isset( $this->upgrade_notice ) ) { 129 | $this->upgrade_notice = $this->fetch_upgrade_notice(); 130 | } 131 | 132 | return $this->upgrade_notice; 133 | 134 | } 135 | 136 | /** 137 | * Get the update object for the item. 138 | * 139 | * @author John Blackbourn 140 | * @return EUAPI_Update Object containing various info about the latest update. 141 | */ 142 | final public function get_update() { 143 | 144 | if ( isset( $this->update ) ) { 145 | return $this->update; 146 | } 147 | 148 | $package = add_query_arg( array( 149 | '_euapi_type' => $this->get_type(), 150 | '_euapi_file' => $this->get_file() 151 | ), $this->get_package_url() ); 152 | 153 | return $this->update = new EUAPI_Update( array( 154 | 'slug' => $this->get_file(), 155 | 'new_version' => $this->get_new_version(), 156 | 'upgrade_notice' => $this->get_upgrade_notice(), 157 | 'url' => $this->get_homepage_url(), 158 | 'package' => $package, 159 | 'config' => $this->get_config(), 160 | ) ); 161 | 162 | } 163 | 164 | /** 165 | * Get the info object for the item. 166 | * 167 | * @author John Blackbourn 168 | * @return EUAPI_Info|WP_Error An EUAPI_Info object, or a WP_Error object on failure. 169 | */ 170 | final public function get_info() { 171 | 172 | if ( !isset( $this->info ) ) { 173 | $this->info = $this->fetch_info(); 174 | } 175 | 176 | return $this->info; 177 | 178 | } 179 | 180 | /** 181 | * Helper function to get the current item config. 182 | * 183 | * @author John Blackbourn 184 | * @return array Config array. 185 | */ 186 | final public function get_config() { 187 | return $this->config; 188 | } 189 | 190 | /** 191 | * Helper function to get the handler type (either 'plugin' or 'theme'). 192 | * 193 | * @author John Blackbourn 194 | * @return string Handler type. 195 | */ 196 | final public function get_type() { 197 | if ( !in_array( $this->config['type'], array( 'plugin', 'theme' ), true ) ) { 198 | return 'plugin'; 199 | } 200 | return $this->config['type']; 201 | } 202 | 203 | } 204 | 205 | endif; // endif class exists 206 | -------------------------------------------------------------------------------- /external-update-api/info.php: -------------------------------------------------------------------------------- 1 | $v ) { 17 | $this->$k = $v; 18 | } 19 | 20 | } 21 | 22 | } 23 | 24 | endif; 25 | -------------------------------------------------------------------------------- /external-update-api/item-plugin.php: -------------------------------------------------------------------------------- 1 | file = $plugin; 18 | $this->url = $data['PluginURI']; 19 | $this->version = $data['Version']; 20 | $this->data = $data; 21 | 22 | } 23 | 24 | } 25 | 26 | endif; 27 | -------------------------------------------------------------------------------- /external-update-api/item-theme.php: -------------------------------------------------------------------------------- 1 | file = $theme; 18 | $this->url = $data['ThemeURI']; 19 | $this->version = $data['Version']; 20 | $this->data = $data; 21 | 22 | } 23 | 24 | } 25 | 26 | endif; 27 | -------------------------------------------------------------------------------- /external-update-api/item.php: -------------------------------------------------------------------------------- 1 | version; 14 | } 15 | 16 | public function get_url() { 17 | return $this->url; 18 | } 19 | 20 | } 21 | 22 | endif; 23 | -------------------------------------------------------------------------------- /external-update-api/update.php: -------------------------------------------------------------------------------- 1 | slug = $args['slug']; 15 | $this->new_version = $args['new_version']; 16 | $this->upgrade_notice = $args['upgrade_notice']; 17 | $this->url = $args['url']; 18 | $this->package = $args['package']; 19 | 20 | } 21 | 22 | public function get_data_to_store() { 23 | return get_object_vars( $this ); 24 | } 25 | 26 | public function get_new_version() { 27 | return $this->new_version; 28 | } 29 | 30 | } 31 | 32 | endif; 33 | -------------------------------------------------------------------------------- /github-oauth-connector.php: -------------------------------------------------------------------------------- 1 | 62 |
63 |

64 |
65 | 'client_id', 95 | 'type' => 'text', 96 | 'description' => '', 97 | ) 98 | ); 99 | add_settings_field( 100 | 'client_secret', __( 'Client Secret', 'github-oauth-connector' ), array( $this, 'input_field' ), 'github-oauth-connector', 'ghupdate_private', 101 | array( 102 | 'id' => 'client_secret', 103 | 'type' => 'text', 104 | 'description' => '', 105 | ) 106 | ); 107 | add_settings_field( 108 | 'access_token', __( 'Access Token', 'github-oauth-connector' ), array( $this, 'token_field' ), 'github-oauth-connector', 'ghupdate_private', 109 | array( 110 | 'id' => 'access_token', 111 | ) 112 | ); 113 | 114 | } 115 | 116 | /** 117 | * Output the description field for the settings screen. 118 | * 119 | * @return null 120 | */ 121 | public function private_description() { 122 | 123 | $name = preg_replace( '|^https?://|', '', home_url() ); 124 | $url = home_url(); 125 | $callback = get_site_url( null, '', 'admin' ); 126 | 127 | ?> 128 |

129 |

130 |
    131 |
  1. Create an application on GitHub using the following values:', 'github-oauth-connector' ), 'https://github.com/settings/applications/new' ); ?> 132 |
      133 |
    • Name: %s', 'github-oauth-connector' ), $name ); ?>
    • 134 |
    • URL: %s', 'github-oauth-connector' ), $url ); ?>
    • 135 |
    • Callback URL: %s', 'github-oauth-connector' ), $callback ); ?>
    • 136 |
    137 |
  2. 138 |
  3. Client ID and a Client Secret. Copy the values into the fields below.', 'github-oauth-connector' ); ?>
  4. 139 |
  5. 140 |
141 | 155 | 156 | 157 | 173 |

174 | 175 | 178 | 179 | 182 | 222 |
223 | 224 |
225 | 226 |

227 |
228 | 229 |
230 |
231 | 237 |
238 |
239 | 240 |
241 | 'repo', 264 | 'client_id' => $gh['client_id'], 265 | 'redirect_uri' => $redirect_uri, 266 | ); 267 | $query = add_query_arg($query_args, $query); 268 | wp_redirect( $query ); 269 | 270 | exit; 271 | 272 | } 273 | 274 | /** 275 | * Callback handler for the OAuth connection response. Saves the access token to the database. 276 | * 277 | * @return null 278 | */ 279 | public function ajax_set_github_oauth_key() { 280 | $gh = get_option('ghupdate'); 281 | 282 | $query = admin_url( 'plugins.php' ); 283 | $query = add_query_arg( array( 'page' => 'github-oauth-connector' ), $query ); 284 | 285 | if ( isset($_GET['code']) ) { 286 | // Receive authorised token 287 | $query = 'https://github.com/login/oauth/access_token'; 288 | $query_args = array( 289 | 'client_id' => $gh['client_id'], 290 | 'client_secret' => $gh['client_secret'], 291 | 'code' => stripslashes( $_GET['code'] ), 292 | ); 293 | $query = add_query_arg( $query_args, $query ); 294 | $response = wp_remote_get( $query, array( 'sslverify' => false ) ); 295 | parse_str( $response['body'] ); // populates $access_token, $token_type 296 | 297 | if ( isset( $access_token ) and !empty( $access_token ) ) { 298 | $gh['access_token'] = $access_token; 299 | update_option( 'euapi_github_access_token', $access_token ); 300 | update_option('ghupdate', $gh ); 301 | 302 | if ( function_exists( 'euapi_flush_transients' ) ) 303 | euapi_flush_transients(); 304 | 305 | $query = add_query_arg( array( 306 | 'page' => 'github-oauth-connector', 307 | 'authorised' => 'true' 308 | ), admin_url( 'plugins.php' ) ); 309 | wp_redirect( $query ); 310 | exit; 311 | } 312 | 313 | } 314 | 315 | $query = add_query_arg( array( 'authorised' => 'false' ), $query ); 316 | wp_redirect($query); 317 | exit; 318 | 319 | } 320 | 321 | } 322 | 323 | add_action('init', create_function('', 'global $GitHub_OAuth_Connector; $GitHub_OAuth_Connector = new GitHub_OAuth_Connector();') ); 324 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # External Update API # 2 | 3 | **Contributors:** codeforthepeople, johnbillion 4 | **Tags:** updates, github 5 | **Requires at least:** 3.7 6 | **Tested up to:** 3.9 7 | **Stable tag:** 0.5 8 | **License:** GPL v2 or later 9 | 10 | Add support for updating themes and plugins via external sources. Includes an update handler for plugins and themes hosted in public or private repos on GitHub. 11 | 12 | ## Description ## 13 | 14 | Add support for updating themes and plugins via external sources instead of the WordPress.org repos. Includes an update handler for plugins and themes hosted on GitHub. 15 | 16 | ## Installation ## 17 | 18 | 1. Download the plugin ZIP file and extract it into your plugins directory, or clone the repo into your plugins directory with `git clone git@github.com:cftp/external-update-api`. 19 | 2. Activate the plugin. 20 | 3. See the Usage section below. 21 | 22 | ### Usage ### 23 | 24 | The plugin comes bundled with an update handler for GitHub. To add a handler for a different external source, see the 'Writing a new Handler' section below. 25 | 26 | You can tell the update API to use a public or private GitHub repo to update a plugin or theme on your site. To do this, hook into the `euapi_plugin_handler` or `euapi_theme_handler` hook, respectively, and return a handler for your plugin or theme. 27 | 28 | Plugin Example: 29 | 30 | ``` 31 | function my_plugin_update_handler( EUAPI_Handler $handler = null, EUAPI_Item_Plugin $item ) { 32 | 33 | if ( 'my-plugin/my-plugin.php' == $item->file ) { 34 | 35 | $handler = new EUAPI_Handler_GitHub( array( 36 | 'type' => $item->type, 37 | 'file' => $item->file, 38 | 'github_url' => 'https://github.com/my-username/my-plugin', 39 | 'http' => array( 40 | 'sslverify' => false, 41 | ), 42 | ) ); 43 | 44 | } 45 | 46 | return $handler; 47 | 48 | } 49 | add_filter( 'euapi_plugin_handler', 'my_plugin_update_handler', 10, 2 ); 50 | ``` 51 | 52 | Theme Example: 53 | 54 | ``` 55 | function my_theme_update_handler( EUAPI_Handler $handler = null, EUAPI_Item_Theme $item ) { 56 | 57 | if ( 'my-theme/style.css' == $item->file ) { 58 | 59 | $handler = new EUAPI_Handler_GitHub( array( 60 | 'type' => $item->type, 61 | 'file' => $item->file, 62 | 'github_url' => 'https://github.com/my-username/my-theme', 63 | 'http' => array( 64 | 'sslverify' => false, 65 | ), 66 | ) ); 67 | 68 | } 69 | 70 | return $handler; 71 | 72 | } 73 | add_filter( 'euapi_theme_handler', 'my_theme_update_handler', 10, 2 ); 74 | ``` 75 | 76 | If your repo is private then you'll need to pass in an additional `access_token` parameter that contains your OAuth access token. 77 | 78 | You can see some more example handlers in our [CFTP Updater repo](https://github.com/cftp/cftp-updater). 79 | 80 | ### Writing a new Handler ### 81 | 82 | To write a new handler, your best bet is to copy the `EUAPI_Handler_GitHub` class included in the plugin and go from there. See the `EUAPI_Handler` class (and, optionally, the `EUAPI_Handler_Files` class) for the abstract methods which must be defined in your class. 83 | 84 | ## Frequently Asked Questions ## 85 | 86 | None yet. 87 | 88 | ## Upgrade Notice ## 89 | 90 | ### 0.5 ### 91 | 92 | * Fix integration with theme updates. 93 | * EUAPI is now a network-only plugin when used on Multisite. 94 | * Eat our own dog food. EUAPI now handles its own updates through GitHub. 95 | * At long-last fix the wonky compatibility with WordPress 3.7+. 96 | * Increase the minimum required WordPress version to 3.7. 97 | 98 | ## Changelog ## 99 | 100 | ### 0.5 ### 101 | 102 | * Fix integration with theme updates. 103 | * EUAPI is now a network-only plugin when used on Multisite. 104 | * Eat our own dog food. EUAPI now handles its own updates through GitHub. 105 | * Add support for an upgrade notice (not used by default). 106 | * Introduce an abstract `EUAPI_Handler_Files` class to simplify extension by other handlers. 107 | * Inline docs improvements. 108 | 109 | ### 0.4 ### 110 | 111 | * At long-last fix the wonky compatibility with WordPress 3.7+. 112 | * Increase the minimum required WordPress version to 3.7. 113 | 114 | ### 0.3.5 ### 115 | 116 | * Support JSON-encoded API requests in addition to serialisation. This is pre-emptive support for WordPress 3.7. 117 | 118 | ### 0.3.4 ### 119 | 120 | * Support the upcoming SSL communication with `api.wordpress.org` 121 | 122 | ### 0.3.3 ### 123 | 124 | * Correct a method name in the `EUAPI_Handler` class. 125 | 126 | ### 0.3.2 ### 127 | 128 | * Change a method name and inline docs to clarify that both plugins and themes are supported. 129 | 130 | ### 0.3.1 ### 131 | 132 | * Prevent false positives when reporting available updates. 133 | * Prevent multiple simultaneous updates breaking due to a variable name clash. 134 | 135 | ### 0.3 ### 136 | 137 | * Allow a handler to return boolean false to prevent update checks being performed altogether. 138 | 139 | ### 0.2.4 ### 140 | 141 | * First public release. 142 | 143 | ## Screenshots ## 144 | 145 | None yet. 146 | --------------------------------------------------------------------------------