Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
CSS_Proxy
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 2
272
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 handle_css_proxy
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
210
1<?php
2
3namespace Automattic\Jetpack_Boost\Modules\Optimizations\Critical_CSS;
4
5use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State;
6
7/**
8 * Add an ajax endpoint to proxy external CSS files.
9 */
10class CSS_Proxy {
11    const NONCE_ACTION = 'jb-generate-proxy-nonce';
12
13    public static function init() {
14        $instance = new self();
15
16        if ( is_admin() ) {
17            add_action( 'wp_ajax_boost_proxy_css', array( $instance, 'handle_css_proxy' ) );
18        }
19    }
20
21    /**
22     * AJAX handler to handle proxying of external CSS resources.
23     *
24     * @return void
25     */
26    public function handle_css_proxy() {
27
28        // Verify valid nonce.
29        if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), self::NONCE_ACTION ) ) {
30            wp_die( '', 400 );
31        }
32
33        // Make sure currently logged in as admin.
34        if ( ! current_user_can( 'manage_options' ) ) {
35            wp_die( '', 400 );
36        }
37
38        // Reject any request made when not generating.
39        if ( ! ( new Critical_CSS_State() )->is_requesting() ) {
40            wp_die( '', 400 );
41        }
42
43        // Validate URL and fetch.
44        $proxy_url = filter_var( wp_unslash( $_POST['proxy_url'] ?? '' ), FILTER_VALIDATE_URL );
45        if ( ! wp_http_validate_url( $proxy_url ) ) {
46            die( 'Invalid URL' );
47        }
48
49        $url_path = wp_parse_url( $proxy_url, PHP_URL_PATH );
50        if ( ! $url_path || substr( strtolower( $url_path ), -4 ) !== '.css' ) {
51            wp_die( 'Invalid CSS file URL', 400 );
52        }
53
54        $cache_key = 'jb_css_proxy_' . md5( $proxy_url );
55        $response  = get_transient( $cache_key );
56
57        if ( is_array( $response ) && isset( $response['error'] ) ) {
58            wp_die( esc_html( $response['error'] ), 400 );
59        }
60
61        $css = '';
62        if ( false === $response ) {
63            $response     = wp_safe_remote_get( $proxy_url );
64            $content_type = wp_remote_retrieve_header( $response, 'content-type' );
65            if ( strpos( $content_type, 'text/css' ) === false ) {
66                set_transient( $cache_key, array( 'error' => 'Invalid content type. Expected CSS.' ), HOUR_IN_SECONDS );
67                wp_die( 'Invalid content type. Expected CSS.', 400 );
68            }
69            $css = wp_remote_retrieve_body( $response );
70            set_transient( $cache_key, $css, HOUR_IN_SECONDS );
71        }
72
73        if ( is_wp_error( $response ) ) {
74            // TODO: Nicer error handling.
75            die( 'error' );
76        }
77
78        if ( $css ) {
79            header( 'Content-type: text/css' );
80            // Outputting proxied CSS contents unescaped.
81            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
82            echo wp_strip_all_tags( $css );
83            die( 0 );
84        }
85    }
86}