├── composer.json ├── readme.md └── update-blocker.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "rarst/update-blocker", 3 | "description": "Lightweight generic blocker of updates from official WordPress repositories", 4 | "keywords" : ["wordpress"], 5 | "type" : "wordpress-plugin", 6 | "homepage" : "https://github.com/Rarst/update-blocker", 7 | "license" : "MIT", 8 | "authors" : [ 9 | { 10 | "name" : "Andrey Savchenko", 11 | "email" : "contact@rarst.net", 12 | "homepage": "http://www.Rarst.net/" 13 | } 14 | ], 15 | "support" : { 16 | "issues": "https://github.com/Rarst/update-blocker/issues" 17 | }, 18 | "require" : { 19 | "composer/installers": "~1.0" 20 | } 21 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Update Blocker — for WP repositories 2 | 3 | Update Blocker is a lightweight generic blocker of plugin, theme, and core updates from official WordPress repositories. 4 | 5 | It was created as shared reusable plugin for the sake of no longer reinventing that particular wheel. 6 | 7 | ### Goals 8 | 9 | - single main file 10 | - `mu-plugins`–friendly 11 | - no hard dependencies 12 | 13 | ### Not goals 14 | 15 | - interface 16 | - elaborate API 17 | - as–library use 18 | - unreasonable compat 19 | 20 | ## Installation 21 | 22 | ### Plugin 23 | 24 | 1. [Download ZIP](https://github.com/Rarst/update-blocker/archive/master.zip). 25 | 2. Unpack files from inside into `wp-content/plugins/update-blocker`. 26 | 27 | ### MU-plugin 28 | 29 | 1. [Download `update-blocker.php`](https://raw.githubusercontent.com/Rarst/update-blocker/master/update-blocker.php). 30 | 2. Place into `wp-content/mu-plugins`. 31 | 3. Edit settings for blocks in the file. 32 | 33 | ### Composer 34 | 35 | Create project in the `wp-content/plugins`: 36 | 37 | ``` 38 | composer create-project rarst/update-blocker:~1.0 39 | ``` 40 | 41 | Or require in `composer.json` of site project: 42 | 43 | ```json 44 | { 45 | "require": { 46 | "rarst/update-blocker": "~1.0" 47 | } 48 | } 49 | ``` 50 | 51 | Requiring on plugin/theme level is not implemented, use `suggest`: 52 | 53 | ```json 54 | { 55 | "suggest": { 56 | "rarst/update-blocker": "Prevents invalid updates from official repositories" 57 | } 58 | } 59 | ``` 60 | 61 | ## Configuration 62 | 63 | Plugin's settings have following structure: 64 | 65 | ```php 66 | array( 67 | 'all' => false, 68 | 'files' => array( '.git', '.svn', '.hg' ), 69 | 'plugins' => array( 'update-blocker/update-blocker.php' ), 70 | 'themes' => array(), 71 | 'core' => false, 72 | ) 73 | ``` 74 | 75 | - `all` — boolean, disables updates completely 76 | - `files` — array of plugin/theme root–relative files to detect for block 77 | - `plugins` — array of plugin base names (`folder-name/plugin-name.php`) to block 78 | - `themes` — array of theme slugs (`theme-name`) to block 79 | - `core` — boolean, disables core updates 80 | 81 | Settings pass through `update_blocker_blocked` filter. 82 | 83 | Processed data passes through `update_blocker_plugins` and `update_blocker_themes` filters during update checks. 84 | 85 | ### Plugin opt–in 86 | 87 | ```php 88 | add_filter( 'update_blocker_blocked', function( $blocked ) { 89 | $blocked['plugins'][] = plugin_basename( __FILE__ ); // or just folder-name/plugin-name.php string 90 | return $blocked; 91 | } ); 92 | ``` 93 | 94 | ### Theme opt–in 95 | 96 | ```php 97 | add_filter( 'update_blocker_blocked', function( $blocked ) { 98 | $blocked['themes'][] = 'theme-name'; 99 | return $blocked; 100 | } ); 101 | ``` 102 | 103 | ### Core opt–in 104 | 105 | ```php 106 | add_filter( 'update_blocker_blocked', function ( $blocked ) { 107 | $blocked['core'] = true; 108 | return $blocked; 109 | } ); 110 | ``` 111 | 112 | ## License 113 | 114 | - MIT -------------------------------------------------------------------------------- /update-blocker.php: -------------------------------------------------------------------------------- 1 | false, 35 | 'files' => array( '.git', '.svn', '.hg' ), 36 | 'plugins' => array( 'update-blocker/update-blocker.php' ), 37 | 'themes' => array(), 38 | 'core' => false, 39 | ) ); 40 | 41 | /** 42 | * Main and only plugin's class. 43 | */ 44 | class Plugin { 45 | 46 | /** @var object $blocked */ 47 | public $blocked; 48 | 49 | /** @var object|boolean $api */ 50 | public $api; 51 | 52 | /** 53 | * @param array $blocked Configuration to use. 54 | */ 55 | public function __construct( $blocked = array() ) { 56 | register_activation_hook( __FILE__, array( $this, 'delete_update_transients' ) ); 57 | register_deactivation_hook( __FILE__, array( $this, 'delete_update_transients' ) ); 58 | 59 | $defaults = array( 'all' => false, 'core' => false ) + array_fill_keys( array( 'files', 'plugins', 'themes' ), array() ); 60 | $this->blocked = array_merge( $defaults, $blocked ); 61 | 62 | add_action( 'init', array( $this, 'init' ) ); 63 | } 64 | 65 | /** 66 | * Init actions. 67 | */ 68 | public function init() { 69 | $this->blocked = (object) apply_filters( 'update_blocker_blocked', $this->blocked ); 70 | 71 | if ( $this->blocked->all ) { 72 | add_filter( 'pre_http_request', array( $this, 'pre_http_request' ), 10, 3 ); 73 | } else { 74 | add_filter( 'http_request_args', array( $this, 'http_request_args' ), 10, 2 ); 75 | } 76 | 77 | if ( $this->blocked->all || $this->blocked->core ) { 78 | add_filter( 'pre_site_transient_update_core', array( $this, 'return_empty_core_update' ) ); 79 | } 80 | } 81 | 82 | /** 83 | * Delete update transients for plugins and themes. 84 | * 85 | * Performed on activation/deactivation to reset state. 86 | */ 87 | public function delete_update_transients() { 88 | delete_site_transient( 'update_plugins' ); 89 | delete_site_transient( 'update_themes' ); 90 | } 91 | 92 | /** 93 | * Block HTTP requests to plugin/theme API endpoints. 94 | * 95 | * Used for complete updates block. 96 | * 97 | * @param boolean $false Pass through kill request booleans. 98 | * @param array $request_args Request arguments. 99 | * @param string $url Request URL. 100 | * 101 | * @return boolean|null 102 | */ 103 | public function pre_http_request( $false, $request_args, $url ) { 104 | $api = $this->get_api( $url ); 105 | 106 | return empty( $api ) ? $false : null; 107 | } 108 | 109 | /** 110 | * Filter blocked plugins and themes out of update request. 111 | * 112 | * @param array $request_args Request arguments. 113 | * @param string $url Request URL. 114 | * 115 | * @return array 116 | */ 117 | public function http_request_args( $request_args, $url ) { 118 | 119 | $this->api = $this->get_api( $url ); 120 | 121 | if ( empty( $this->api ) ) { 122 | return $request_args; 123 | } 124 | 125 | $data = $this->decode( $request_args['body'][ $this->api->type ] ); 126 | 127 | if ( $this->api->is_plugin ) { 128 | $data = $this->filter_plugins( $data ); 129 | } elseif ( $this->api->is_theme ) { 130 | $data = $this->filter_themes( $data ); 131 | } 132 | 133 | $data = apply_filters( 'update_blocker_' . $this->api->type, $data ); 134 | 135 | $request_args['body'][ $this->api->type ] = $this->encode( $data ); 136 | 137 | return $request_args; 138 | } 139 | 140 | /** 141 | * Determine API context for a given endpoint URL. 142 | * 143 | * @param string $url API URL. 144 | * 145 | * @return object|boolean 146 | */ 147 | public function get_api( $url ) { 148 | /* @see https://github.com/cftp/external-update-api/blob/master/external-update-api/euapi.php#L45 */ 149 | static $regex = '#://api\.wordpress\.org/(?Pplugins|themes)/update-check/(?P[0-9.]+)/#'; 150 | $match = preg_match( $regex, $url, $api ); 151 | 152 | if ( $match ) { 153 | $api['is_serial'] = ( 1.0 == (float) $api['version'] ); 154 | $api['is_plugin'] = ( 'plugins' === $api['type'] ); 155 | $api['is_theme'] = ( 'themes' === $api['type'] ); 156 | 157 | return (object) $api; 158 | } 159 | 160 | return false; 161 | } 162 | 163 | /** 164 | * Decode API request data, conditionally on API version. 165 | * 166 | * @param string $data Serialized data or JSON. 167 | * 168 | * @return array 169 | */ 170 | public function decode( $data ) { 171 | return $this->api->is_serial ? (array) unserialize( $data ) : json_decode( $data, true ); 172 | } 173 | 174 | /** 175 | * Encode API request data, conditionally on API version. 176 | * 177 | * @param array $data Data array to encode. 178 | * 179 | * @return string Serialized or JSON. 180 | */ 181 | public function encode( $data ) { 182 | if ( $this->api->is_serial ) { 183 | return serialize( $this->api->is_plugin ? (object) $data : $data ); 184 | } 185 | 186 | return json_encode( $data ); 187 | } 188 | 189 | /** 190 | * Filter disabled plugins out of data set. 191 | * 192 | * @param array $data Data set. 193 | * 194 | * @return array 195 | */ 196 | public function filter_plugins( $data ) { 197 | 198 | foreach ( $data['plugins'] as $file => $plugin ) { 199 | $path = trailingslashit( WP_PLUGIN_DIR . '/' . dirname( $file ) ); // TODO files without dir? 200 | 201 | if ( in_array( $file, $this->blocked->plugins, true ) || $this->has_blocked_file( $path ) ) { 202 | unset( $data['plugins'][ $file ] ); 203 | unset( $data['active'][ array_search( $file, $data['active'] ) ] ); 204 | } 205 | } 206 | 207 | return $data; 208 | } 209 | 210 | /** 211 | * Filter disabled themes out of data set. 212 | * 213 | * @param array $data Data set. 214 | * 215 | * @return array 216 | */ 217 | public function filter_themes( $data ) { 218 | 219 | foreach ( $data['themes'] as $slug => $theme ) { 220 | 221 | $path = trailingslashit( wp_get_theme( $slug )->get_stylesheet_directory() ); 222 | 223 | if ( in_array( $slug, $this->blocked->themes, true ) || $this->has_blocked_file( $path ) ) { 224 | unset( $data['themes'][ $slug ] ); 225 | } 226 | } 227 | 228 | return $data; 229 | } 230 | 231 | /** 232 | * Determine if path location has any of blocked files from configuration. 233 | * 234 | * @param string $path Filesystem directory path. 235 | * 236 | * @return bool 237 | */ 238 | public function has_blocked_file( $path ) { 239 | 240 | foreach ( $this->blocked->files as $file ) { 241 | if ( file_exists( $path . $file ) ) { 242 | return true; 243 | } 244 | } 245 | 246 | return false; 247 | } 248 | 249 | /** 250 | * Returns faked empty results to short circuit core update check. 251 | * 252 | * @return object 253 | */ 254 | public function return_empty_core_update() { 255 | 256 | global $wp_version; 257 | 258 | return (object) array( 259 | 'updates' => array(), 260 | 'version_checked' => $wp_version, 261 | 'last_checked' => time(), 262 | ); 263 | } 264 | } 265 | --------------------------------------------------------------------------------