| @@ -40,8 +40,17 @@ | ||
| 40 | 40 | */ |
| 41 | 41 | private $ob_started = false; |
| 42 | 42 | |
| 43 | 43 | /** |
| 44 | + * Nesting level of the buffer opened by this class. | |
| 45 | + * | |
| 46 | + * Recorded so shutdown can tell whether the buffer on top is still ours. | |
| 47 | + * | |
| 48 | + * @var int | |
| 49 | + */ | |
| 50 | + private $ob_level = 0; | |
| 51 | + | |
| 52 | + /** | |
| 44 | 53 | * Constructor |
| 45 | 54 | * |
| 46 | 55 | * @param Vigilante_Settings $settings Settings instance. |
| 47 | 56 | */ |
| @@ -68,10 +77,13 @@ | ||
| 68 | 77 | add_filter( 'wp_get_attachment_url', array( $this, 'fix_url_scheme' ), 10, 1 ); |
| 69 | 78 | add_filter( 'the_content', array( $this, 'fix_content_urls' ), 999 ); |
| 70 | 79 | add_filter( 'widget_text', array( $this, 'fix_content_urls' ), 999 ); |
| 71 | 80 | |
| 72 | - // The rewriters above only cover same-domain URLs; external | |
| 73 | - // http:// references need the browser-side CSP directive. | |
| 81 | + } | |
| 82 | + | |
| 83 | + // Its own setting since 2.9.8: the rewriters above are same-domain and | |
| 84 | + // harmless, this one governs everybody else's resources too. | |
| 85 | + if ( ! empty( $this->options['upgrade_insecure_requests'] ) ) { | |
| 74 | 86 | add_action( 'send_headers', array( $this, 'emit_upgrade_insecure_requests' ) ); |
| 75 | 87 | } |
| 76 | 88 | } |
| 77 | 89 | |
| @@ -83,8 +95,22 @@ | ||
| 83 | 95 | if ( is_ssl() ) { |
| 84 | 96 | return; |
| 85 | 97 | } |
| 86 | 98 | |
| 99 | + /* | |
| 100 | + * Only redirect when the site itself declares HTTPS. A site whose home | |
| 101 | + * URL is still http:// has not moved to HTTPS, and sending every request | |
| 102 | + * to an address that may not answer takes it offline outright. Read from | |
| 103 | + * the home option (which honours the WP_HOME constant through the | |
| 104 | + * option_home filter) rather than home_url(), so the answer is the | |
| 105 | + * address the site declares and not one derived from the current | |
| 106 | + * request. A site already on HTTPS has an https home URL and keeps | |
| 107 | + * redirecting exactly as before. | |
| 108 | + */ | |
| 109 | + if ( 0 !== strpos( (string) get_option( 'home' ), 'https://' ) ) { | |
| 110 | + return; | |
| 111 | + } | |
| 112 | + | |
| 87 | 113 | // Skip CLI |
| 88 | 114 | if ( defined( 'WP_CLI' ) && WP_CLI ) { |
| 89 | 115 | return; |
| 90 | 116 | } |
| @@ -141,13 +167,46 @@ | ||
| 141 | 167 | if ( ! is_ssl() ) { |
| 142 | 168 | return; |
| 143 | 169 | } |
| 144 | 170 | |
| 171 | + // The rewriter only touches complete HTML documents (fix_output_buffer() | |
| 172 | + // bails on anything without <html or <!DOCTYPE), so buffering the admin, | |
| 173 | + // AJAX and REST responses pays for a buffer and a callback that can never | |
| 174 | + // do any work. On a WooCommerce site the cart-fragments endpoint alone is | |
| 175 | + // dozens of those per visitor. | |
| 176 | + if ( is_admin() || wp_doing_ajax() || $this->is_rest_request() ) { | |
| 177 | + return; | |
| 178 | + } | |
| 179 | + | |
| 145 | 180 | ob_start( array( $this, 'fix_output_buffer' ) ); |
| 146 | 181 | $this->ob_started = true; |
| 182 | + $this->ob_level = ob_get_level(); | |
| 147 | 183 | } |
| 148 | 184 | |
| 149 | 185 | /** |
| 186 | + * Whether the current request is a REST API request. | |
| 187 | + * | |
| 188 | + * REST_REQUEST is only defined once the request is being served, which is | |
| 189 | + * after wp_loaded, so the REST route prefix is checked as well. | |
| 190 | + * | |
| 191 | + * @return bool | |
| 192 | + */ | |
| 193 | + private function is_rest_request() { | |
| 194 | + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { | |
| 195 | + return true; | |
| 196 | + } | |
| 197 | + | |
| 198 | + if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { | |
| 199 | + return false; | |
| 200 | + } | |
| 201 | + | |
| 202 | + $path = wp_parse_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH ); | |
| 203 | + $prefix = '/' . trim( rest_get_url_prefix(), '/' ) . '/'; | |
| 204 | + | |
| 205 | + return is_string( $path ) && 0 === strpos( $path, $prefix ); | |
| 206 | + } | |
| 207 | + | |
| 208 | + /** | |
| 150 | 209 | * Explicitly close the output buffer on shutdown. |
| 151 | 210 | * |
| 152 | 211 | * Ensures the buffer opened by start_output_buffer() is always |
| 153 | 212 | * properly closed within the same logical flow. |
| @@ -152,9 +211,14 @@ | ||
| 152 | 211 | * Ensures the buffer opened by start_output_buffer() is always |
| 153 | 212 | * properly closed within the same logical flow. |
| 154 | 213 | */ |
| 155 | 214 | public function end_output_buffer() { |
| 156 | - if ( $this->ob_started && ob_get_level() > 0 ) { | |
| 215 | + // Only flush when the buffer on top is exactly the one we opened. Testing | |
| 216 | + // for "is there any buffer at all" would close somebody else's buffer when | |
| 217 | + // another plugin opened one after ours and had not closed it yet, leaving | |
| 218 | + // ours open on top of that. When the levels do not match, doing nothing is | |
| 219 | + // the safe move: PHP flushes what is left at the end of the request. | |
| 220 | + if ( $this->ob_started && ob_get_level() === $this->ob_level ) { | |
| 157 | 221 | ob_end_flush(); |
| 158 | 222 | $this->ob_started = false; |
| 159 | 223 | } |
| 160 | 224 | } |
| @@ -245,31 +309,8 @@ | ||
| 245 | 309 | return $content; |
| 246 | 310 | } |
| 247 | 311 | |
| 248 | 312 | return $this->replace_http_with_https( $content ); |
| 249 | - } | |
| 250 | - | |
| 251 | - /** | |
| 252 | - * Update WordPress site URLs to HTTPS | |
| 253 | - * | |
| 254 | - * @return bool | |
| 255 | - */ | |
| 256 | - public function update_site_urls() { | |
| 257 | - $siteurl = get_option( 'siteurl' ); | |
| 258 | - $home = get_option( 'home' ); | |
| 259 | - $updated = false; | |
| 260 | - | |
| 261 | - if ( strpos( $siteurl, 'http://' ) === 0 ) { | |
| 262 | - update_option( 'siteurl', str_replace( 'http://', 'https://', $siteurl ) ); | |
| 263 | - $updated = true; | |
| 264 | - } | |
| 265 | - | |
| 266 | - if ( strpos( $home, 'http://' ) === 0 ) { | |
| 267 | - update_option( 'home', str_replace( 'http://', 'https://', $home ) ); | |
| 268 | - $updated = true; | |
| 269 | - } | |
| 270 | - | |
| 271 | - return $updated; | |
| 272 | 313 | } |
| 273 | 314 | |
| 274 | 315 | /** |
| 275 | 316 | * Check if site is properly configured for HTTPS |