summaryrefslogtreecommitdiffstats
path: root/admin/survey/minify/lib/Minify/JS/ClosureCompiler.php
blob: 05e2e334c6b6c47c398fe3e370c16f654b00b110 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
<?php
/**
 * Class Minify_JS_ClosureCompiler
 * @package Minify
 */

/**
 * Minify Javascript using Google's Closure Compiler API
 *
 * @link http://code.google.com/closure/compiler/
 * @package Minify
 * @author Stephen Clay <steve@mrclay.org>
 *
 * @todo can use a stream wrapper to unit test this?
 */
class Minify_JS_ClosureCompiler
{

    /**
     * @var string The option key for the maximum POST byte size
     */
    const OPTION_MAX_BYTES = 'maxBytes';

    /**
     * @var string The option key for additional params. @see __construct
     */
    const OPTION_ADDITIONAL_OPTIONS = 'additionalParams';

    /**
     * @var string The option key for the fallback Minifier
     */
    const OPTION_FALLBACK_FUNCTION = 'fallbackFunc';

    /**
     * @var string The option key for the service URL
     */
    const OPTION_COMPILER_URL = 'compilerUrl';

    /**
     * @var int The default maximum POST byte size according to https://developers.google.com/closure/compiler/docs/api-ref
     */
    const DEFAULT_MAX_BYTES = 200000;

    /**
     * @var string[] $DEFAULT_OPTIONS The default options to pass to the compiler service
     *
     * @note This would be a constant if PHP allowed it
     */
    private static $DEFAULT_OPTIONS = array(
        'output_format' => 'text',
        'compilation_level' => 'SIMPLE_OPTIMIZATIONS'
    );

    /**
     * @var string $url URL of compiler server. defaults to Google's
     */
    protected $serviceUrl = 'https://closure-compiler.appspot.com/compile';

    /**
     * @var int $maxBytes The maximum JS size that can be sent to the compiler server in bytes
     */
    protected $maxBytes = self::DEFAULT_MAX_BYTES;

    /**
     * @var string[] $additionalOptions Additional options to pass to the compiler service
     */
    protected $additionalOptions = array();

    /**
     * @var callable Function to minify JS if service fails. Default is JSMin
     */
    protected $fallbackMinifier = array('JSMin\\JSMin', 'minify');

    /**
     * Minify JavaScript code via HTTP request to a Closure Compiler API
     *
     * @param string $js input code
     * @param array $options Options passed to __construct(). @see __construct
     *
     * @return string
     */
    public static function minify($js, array $options = array())
    {
        $obj = new self($options);

        return $obj->min($js);
    }

    /**
     * @param array $options Options with keys available below:
     *
     *  fallbackFunc     : (callable) function to minify if service unavailable. Default is JSMin.
     *
     *  compilerUrl      : (string) URL to closure compiler server
     *
     *  maxBytes         : (int) The maximum amount of bytes to be sent as js_code in the POST request.
     *                     Defaults to 200000.
     *
     *  additionalParams : (string[]) Additional parameters to pass to the compiler server. Can be anything named
     *                     in https://developers.google.com/closure/compiler/docs/api-ref except for js_code and
     *                     output_info
     */
    public function __construct(array $options = array())
    {
        if (isset($options[self::OPTION_FALLBACK_FUNCTION])) {
            $this->fallbackMinifier = $options[self::OPTION_FALLBACK_FUNCTION];
        }
        if (isset($options[self::OPTION_COMPILER_URL])) {
            $this->serviceUrl = $options[self::OPTION_COMPILER_URL];
        }
        if (isset($options[self::OPTION_ADDITIONAL_OPTIONS]) && is_array($options[self::OPTION_ADDITIONAL_OPTIONS])) {
            $this->additionalOptions = $options[self::OPTION_ADDITIONAL_OPTIONS];
        }
        if (isset($options[self::OPTION_MAX_BYTES])) {
            $this->maxBytes = (int) $options[self::OPTION_MAX_BYTES];
        }
    }

    /**
     * Call the service to perform the minification
     *
     * @param string $js JavaScript code
     * @return string
     * @throws Minify_JS_ClosureCompiler_Exception
     */
    public function min($js)
    {
        $postBody = $this->buildPostBody($js);

        if ($this->maxBytes > 0) {
            $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
                ? mb_strlen($postBody, '8bit')
                : strlen($postBody);
            if ($bytes > $this->maxBytes) {
                throw new Minify_JS_ClosureCompiler_Exception(
                    'POST content larger than ' . $this->maxBytes . ' bytes'
                );
            }
        }

        $response = $this->getResponse($postBody);

        if (preg_match('/^Error\(\d\d?\):/', $response)) {
            if (is_callable($this->fallbackMinifier)) {
                // use fallback
                $response = "/* Received errors from Closure Compiler API:\n$response"
                          . "\n(Using fallback minifier)\n*/\n";
                $response .= call_user_func($this->fallbackMinifier, $js);
            } else {
                throw new Minify_JS_ClosureCompiler_Exception($response);
            }
        }

        if ($response === '') {
            $errors = $this->getResponse($this->buildPostBody($js, true));
            throw new Minify_JS_ClosureCompiler_Exception($errors);
        }

        return $response;
    }

    /**
     * Get the response for a given POST body
     *
     * @param string $postBody
     * @return string
     * @throws Minify_JS_ClosureCompiler_Exception
     */
    protected function getResponse($postBody)
    {
        $allowUrlFopen = preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));

        if ($allowUrlFopen) {
            $contents = file_get_contents($this->serviceUrl, false, stream_context_create(array(
                'http' => array(
                    'method' => 'POST',
                    'compilation_level' => 'SIMPLE',
                    'output_format' => 'text',
                    'output_info' => 'compiled_code',
                    'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close\r\n",
                    'content' => $postBody,
                    'max_redirects' => 0,
                    'timeout' => 15,
                )
            )));
        } elseif (defined('CURLOPT_POST')) {
            $ch = curl_init($this->serviceUrl);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded'));
            curl_setopt($ch, CURLOPT_POSTFIELDS, $postBody);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
            $contents = curl_exec($ch);
            curl_close($ch);
        } else {
            throw new Minify_JS_ClosureCompiler_Exception(
               "Could not make HTTP request: allow_url_open is false and cURL not available"
            );
        }

        if (false === $contents) {
            throw new Minify_JS_ClosureCompiler_Exception(
               "No HTTP response from server"
            );
        }

        return trim($contents);
    }

    /**
     * Build a POST request body
     *
     * @param string $js JavaScript code
     * @param bool $returnErrors
     * @return string
     */
    protected function buildPostBody($js, $returnErrors = false)
    {
        return http_build_query(
            array_merge(
                self::$DEFAULT_OPTIONS,
                $this->additionalOptions,
                array(
                    'js_code' => $js,
                    'output_info' => ($returnErrors ? 'errors' : 'compiled_code')
                )
            ),
            null,
            '&'
        );
    }
}

class Minify_JS_ClosureCompiler_Exception extends Exception
{
}