├── .gitignore ├── readme.md ├── readme-orig.md ├── readme.txt └── github-plugin-updater.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Stor* 2 | .svn 3 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | This plugin has been rolled into the afragen/github-updater repository. This repository is no longer actively updated. -------------------------------------------------------------------------------- /readme-orig.md: -------------------------------------------------------------------------------- 1 | This is still a working draft.... 2 | 3 | How does this plugin work? 4 | ========================== 5 | 6 | The challenge I faced when updating a plugin from GitHub was the naming of the zipball archive. When extracted the foldername is not of your choice. 7 | But I do like to think that you should be able to dictate the foldername of your plugin. So after some research I came up with the solution to download 8 | the zipball, open it with the native php ZipArchive (WordPress uses this as well), rename the folder, save the zipball and offer that as the download. 9 | To achieve this I had to let de update-url point to wp-admin where I supply the name of the github repo and the cookie required to login to wp-admin. 10 | The admin_init hook will then take over and sets headers equal to that of a zipball from GitHub and sends the zipball, then cleans it up. 11 | 12 | Now, though my research was pretty extensive (for my doing at least), I am not completely sure this is the best approach. So I any feedback is most welcome. 13 | 14 | Other than that, this structure will allows a pretty native WordPress plugin update experience. We should be able to: 15 | 16 | * Supply extra information before updating 17 | * The plugin author can support this plugin directly or the end-user can add any repo to the functions.php 18 | * Adding a GUI is also very possible and if this plugin is good in what it claims to do, it will be added 19 | 20 | Again, any feedback and testing on various platforms/ configurations is very welcome. -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === GitHub Plugin Updater === 2 | Contributors: codepress,davidmosterd,tschutter 3 | Tags: github,update,plugin,repository 4 | Requires at least: 3.4 5 | Tested up to: 3.5 6 | Stable tag: 1.0 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Update plugins that are hosted on GitHub. 11 | 12 | == Description == 13 | 14 | Updates plugins that are hosted on GitHub. This is a beta release, so you might not use it production environments. 15 | 16 | Example usage: 17 | 18 | ` 19 | function my_github_plugin_updater() { 20 | 21 | if ( ! function_exists( 'github_plugin_updater_register' ) ) 22 | return false; 23 | 24 | github_plugin_updater_register( array( 25 | 'owner' => 'codepress', 26 | 'repo' => 'github-plugin-updater', 27 | 'slug' => 'github-plugin-updater/github-plugin-updater.php', // defaults to the repo value ('repo/repo.php') 28 | ) ); 29 | } 30 | add_action( 'plugins_loaded', 'my_github_plugin_updater' ); 31 | ` 32 | 33 | Currently we are working on the following features: 34 | 35 | * Add a GUI to add plugins by their GitHub url 36 | * Add a debug mode which forces to reset the plugin transients 37 | * Add a description to your plugin for users to read before updating 38 | 39 | == Installation == 40 | 41 | 1. Upload github-plugin-updater to the /wp-content/plugins/ directory 42 | 2. Activate GitHub Plugin Updater through the 'Plugins' menu in WordPress 43 | 3. Configure the plugins you want to update via GitHub 44 | 45 | == Frequently Asked Questions == 46 | 47 | = I have an idea for a great way to improve this plugin = 48 | 49 | Great! We'd love to hear from you. The plugin is hosted on [GitHub](https://github.com/codepress/github-plugin-updater) for issues or pull requests. 50 | 51 | = Why not get the zipball directly from GitHub? = 52 | 53 | The zipball from GitHub might is not likely packaged similar to the the slug of your plugin. It's stored as a 54 | temporary file, opened by the ZipArchive class and modified to match the plugin slug path before being send to WordPress. 55 | This seems closest to the process of WordPress updating a plugin. If you know a neater way, let us know. 56 | 57 | = Can I integrate this plugin with my own plugin? = 58 | 59 | You can, should take little work. However, it may not be wise. We feel that separating the updater from the plugin itself is 60 | safest. Any bug found in this plugin might affect your plugin and in the worst case your plugin won't be able to update. 61 | Separating the two will allow us to fix any bugs in this plugin and keep the update process working just fine. 62 | 63 | == Changelog == 64 | 65 | = 1.0 = 66 | 67 | * Initial release. -------------------------------------------------------------------------------- /github-plugin-updater.php: -------------------------------------------------------------------------------- 1 | null, 49 | 'owner' => null, 50 | 'slug' => null, 51 | 'access_token' => null, 52 | 'http_args' => array(), 53 | ); 54 | $this->config = (object) array_merge( $defaults, $config ); 55 | 56 | // default slug equals the repo name 57 | if ( empty( $this->config->slug ) ) 58 | $this->config->slug = $this->config->repo . '/' . $this->config->repo . '.php'; 59 | 60 | add_filter( 'http_request_args', array( $this, 'add_http_args' ), 10, 2 ); 61 | add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'update_available' ) ); 62 | 63 | if ( isset( $_GET['get-zipball'] ) && $_GET['get-zipball'] == $this->config->slug ) 64 | add_action( 'admin_init', array( $this, 'get_zipball' ) ); 65 | } 66 | 67 | /** 68 | * Call the GitHub API and return a json decoded body. 69 | * 70 | * @since 1.0 71 | * @param string $url 72 | * @see http://developer.github.com/v3/ 73 | * @return boolean|object 74 | */ 75 | protected function api( $url ) { 76 | 77 | add_filter( 'http_request_args', array( $this, 'add_http_args' ), 10, 2 ); 78 | 79 | $response = wp_remote_get( $this->get_api_url( $url ) ); 80 | 81 | remove_filter( 'http_request_args', array( $this, 'add_http_args' ) ); 82 | 83 | if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != '200' ) 84 | return false; 85 | 86 | return json_decode( wp_remote_retrieve_body( $response ) ); 87 | } 88 | 89 | /** 90 | * Return API url. 91 | * 92 | * @todo Maybe allow a filter to add or modify segments. 93 | * @since 1.0 94 | * @param string $endpoint 95 | * @return string 96 | */ 97 | protected function get_api_url( $endpoint ) { 98 | $segments = array( 99 | 'owner' => $this->config->owner, 100 | 'repo' => $this->config->repo, 101 | 'archive_format' => 'zipball', 102 | ); 103 | 104 | foreach ( $segments as $segment => $value ) { 105 | $endpoint = str_replace( '/:' . $segment, '/' . $value, $endpoint ); 106 | } 107 | 108 | if ( ! empty( $this->config->access_token ) ) 109 | $endpoint = add_query_arg( 'access_token', $this->config->access_token ); 110 | 111 | return 'https://api.github.com' . $endpoint; 112 | } 113 | 114 | /** 115 | * Reads the remote plugin file. 116 | * 117 | * Uses a transient to limit the calls to the API. 118 | * 119 | * @since 1.0 120 | */ 121 | protected function get_remote_info() { 122 | $remote = get_site_transient( __CLASS__ . ':remote' . $this->config->repo ); 123 | 124 | if ( ! $remote ) { 125 | $remote = $this->api( '/repos/:owner/:repo/contents/' . basename( $this->config->slug ) ); 126 | 127 | if ( $remote ) 128 | set_site_transient( __CLASS__ . ':remote' . $this->config->repo, $remote, 60 * 60 ); 129 | } 130 | 131 | return $remote; 132 | } 133 | 134 | /** 135 | * Retrieves the local version from the file header of the plugin 136 | * 137 | * @since 1.0 138 | * @return string|boolean 139 | */ 140 | protected function get_local_version() { 141 | $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $this->config->slug ); 142 | 143 | if ( ! empty( $data['Version'] ) ) 144 | return $data['Version']; 145 | 146 | return false; 147 | } 148 | 149 | /** 150 | * Retrieves the remote version from the file header of the plugin 151 | * 152 | * @since 1.0 153 | * @return string|boolean 154 | */ 155 | protected function get_remote_version() { 156 | $response = $this->get_remote_info(); 157 | 158 | if ( ! $response ) 159 | return false; 160 | 161 | preg_match( '#^\s*Version\:\s*(.*)$#im', base64_decode( $response->content ), $matches ); 162 | 163 | if ( ! empty( $matches[1] ) ) 164 | return $matches[1]; 165 | 166 | return false; 167 | } 168 | 169 | /** 170 | * Hooks into pre_set_site_transient_update_plugins to update from GitHub. 171 | * 172 | * @since 1.0 173 | * @todo fill url with value from remote repostory 174 | * @param $transient 175 | * @return $transient If all goes well, an updated one. 176 | */ 177 | public function update_available( $transient ) { 178 | 179 | if ( empty( $transient->checked ) ) 180 | return $transient; 181 | 182 | $local_version = $this->get_local_version(); 183 | $remote_version = $this->get_remote_version(); 184 | 185 | if ( $local_version && $remote_version && version_compare( $remote_version, $local_version, '>' ) ) { 186 | $plugin = array( 187 | 'slug' => dirname( $this->config->slug ), 188 | 'new_version' => $remote_version, 189 | 'url' => null, 190 | 'package' => admin_url( '?get-zipball=' . $this->config->slug ), 191 | ); 192 | 193 | $transient->response[ $this->config->slug ] = (object) $plugin; 194 | } 195 | 196 | return $transient; 197 | } 198 | 199 | /** 200 | * Allows to change any args used on downloading the zipball 201 | * 202 | * @since 1.0 203 | * @param type $args 204 | * @return type 205 | */ 206 | public function add_http_args( $args, $url ) { 207 | 208 | if ( 0 === strpos( $url, $this->get_api_url( '/repos/:owner/:repo/:archive_format' ) ) ) { 209 | foreach( $this->config->http_args as $name => $value ) { 210 | $args[ $name ] = $value; 211 | } 212 | } 213 | 214 | if ( admin_url( '?get-zipball=' . $this->config->slug ) == $url ) { 215 | $cookie = new WP_Http_Cookie( array( 216 | 'name' => AUTH_COOKIE, 217 | 'path' => ADMIN_COOKIE_PATH, 218 | 'value' => $_COOKIE[ AUTH_COOKIE ], 219 | 'expires' => 300, 220 | 'domain' => defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : $_SERVER['HTTP_HOST'], 221 | ) ); 222 | 223 | $args['cookies'][] = $cookie; 224 | } 225 | 226 | return $args; 227 | } 228 | 229 | /** 230 | * GitHub decides on the root directory in the zipball, but we might disagree. 231 | * 232 | * @since 1.0 233 | */ 234 | public function get_zipball() { 235 | 236 | add_filter( 'http_request_args', array( $this, 'add_http_args' ), 10, 2 ); 237 | 238 | $zipball = download_url( $this->get_api_url( '/repos/:owner/:repo/:archive_format' ) ); 239 | 240 | remove_filter( 'http_request_args', array( $this, 'add_http_args' ) ); 241 | 242 | if ( is_wp_error( $zipball ) ) 243 | $this->return_404(); 244 | 245 | $z = new ZipArchive(); 246 | 247 | if ( true === $z->open( $zipball ) ) { 248 | $length = strlen( $z->getNameIndex( 0 ) ); 249 | $status = true; 250 | 251 | for ( $i=0; $i<$z->numFiles; $i++ ) { 252 | $name = $z->getNameIndex( $i ); 253 | 254 | if ( ! $name ) 255 | $status = false; 256 | 257 | $newname = substr_replace( $name, $this->config->repo, 0, $length - 1 ); 258 | 259 | if ( ! $z->renameName( $name, $newname ) ) 260 | $status = false; 261 | } 262 | 263 | $z->close(); 264 | 265 | if ( $status ) { 266 | header( 'Content-Disposition: attachment; filename=' . $this->config->repo . '.zip' ); 267 | header( 'Content-Type: application/zip' ); 268 | header( 'Content-Length: ' . filesize( $zipball ) ); 269 | 270 | ob_clean(); 271 | flush(); 272 | readfile( $zipball ); 273 | unlink( $zipball ); 274 | exit; 275 | } 276 | } 277 | 278 | unlink( $zipball ); 279 | 280 | $this->return_404(); 281 | } 282 | 283 | /** 284 | * Getting the zipball has failed. All hail the zipball. 285 | * 286 | * @since 1.0 287 | */ 288 | protected function return_404() { 289 | header( 'HTTP/1.1 404 Not Found', true, 404 ); 290 | exit; 291 | } 292 | } 293 | 294 | endif; --------------------------------------------------------------------------------