summaryrefslogtreecommitdiffstats
path: root/admin/survey/minify/lib/Minify/Source/Factory.php
blob: a8174177ddfeb12fe09de75a543cccc895a430aa (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
<?php

class Minify_Source_Factory
{

    /**
     * @var array
     */
    protected $options;

    /**
     * @var callable[]
     */
    protected $handlers = array();

    /**
     * @var Minify_Env
     */
    protected $env;

    /**
     * @param Minify_Env            $env
     * @param array                 $options
     *
     *   noMinPattern        : Pattern matched against basename of the filepath (if present). If the pattern
     *                         matches, Minify will try to avoid re-compressing the resource.
     *
     *   fileChecker         : Callable responsible for verifying the existence of the file.
     *
     *   resolveDocRoot      : If true, a leading "//" will be replaced with the document root.
     *
     *   checkAllowDirs      : If true, the filepath will be verified to be within one of the directories
     *                         specified by allowDirs.
     *
     *   allowDirs           : Directory paths in which sources can be served.
     *
     *   uploaderHoursBehind : How many hours behind are the file modification times of uploaded files?
     *                         If you upload files from Windows to a non-Windows server, Windows may report
     *                         incorrect mtimes for the files. Immediately after modifying and uploading a
     *                         file, use the touch command to update the mtime on the server. If the mtime
     *                         jumps ahead by a number of hours, set this variable to that number. If the mtime
     *                         moves back, this should not be needed.
     *
     * @param Minify_CacheInterface $cache Optional cache for handling .less files.
     *
     */
    public function __construct(Minify_Env $env, array $options = array(), Minify_CacheInterface $cache = null)
    {
        $this->env = $env;
        $this->options = array_merge(array(
            'noMinPattern' => '@[-\\.]min\\.(?:[a-zA-Z]+)$@i', // matched against basename
            'fileChecker' => array($this, 'checkIsFile'),
            'resolveDocRoot' => true,
            'checkAllowDirs' => true,
            'allowDirs' => array('//'),
            'uploaderHoursBehind' => 0,
        ), $options);

        // resolve // in allowDirs
        $docRoot = $env->getDocRoot();
        foreach ($this->options['allowDirs'] as $i => $dir) {
            if (0 === strpos($dir, '//')) {
                $this->options['allowDirs'][$i] = $docRoot . substr($dir, 1);
            }
        }

        if ($this->options['fileChecker'] && !is_callable($this->options['fileChecker'])) {
            throw new InvalidArgumentException("fileChecker option is not callable");
        }

        $this->setHandler('~\.less$~i', function ($spec) use ($cache) {
            return new Minify_LessCssSource($spec, $cache);
        });

        $this->setHandler('~\.scss~i', function ($spec) use ($cache) {
            return new Minify_ScssCssSource($spec, $cache);
        });

        $this->setHandler('~\.(js|css)$~i', function ($spec) {
            return new Minify_Source($spec);
        });
    }

    /**
     * @param string   $basenamePattern A pattern tested against basename. E.g. "~\.css$~"
     * @param callable $handler         Function that recieves a $spec array and returns a Minify_SourceInterface
     */
    public function setHandler($basenamePattern, $handler)
    {
        $this->handlers[$basenamePattern] = $handler;
    }

    /**
     * @param string $file
     * @return string
     *
     * @throws Minify_Source_FactoryException
     */
    public function checkIsFile($file)
    {
        $realpath = realpath($file);
        if (!$realpath) {
            throw new Minify_Source_FactoryException("File failed realpath(): $file");
        }

        $basename = basename($file);
        if (0 === strpos($basename, '.')) {
            throw new Minify_Source_FactoryException("Filename starts with period (may be hidden): $basename");
        }

        if (!is_file($realpath) || !is_readable($realpath)) {
            throw new Minify_Source_FactoryException("Not a file or isn't readable: $file");
        }

        return $realpath;
    }

    /**
     * @param mixed $spec
     *
     * @return Minify_SourceInterface
     *
     * @throws Minify_Source_FactoryException
     */
    public function makeSource($spec)
    {
        if (is_string($spec)) {
            $spec = array(
                'filepath' => $spec,
            );
        } elseif ($spec instanceof Minify_SourceInterface) {
            return $spec;
        }

        $source = null;

        if (empty($spec['filepath'])) {
            // not much we can check
            return new Minify_Source($spec);
        }

        if ($this->options['resolveDocRoot'] && 0 === strpos($spec['filepath'], '//')) {
            $spec['filepath'] = $this->env->getDocRoot() . substr($spec['filepath'], 1);
        }

        if (!empty($this->options['fileChecker'])) {
            $spec['filepath'] = call_user_func($this->options['fileChecker'], $spec['filepath']);
        }

        if ($this->options['checkAllowDirs']) {
            $allowDirs = (array)$this->options['allowDirs'];
            $inAllowedDir = false;
            $filePath = $this->env->normalizePath($spec['filepath']);
            foreach ($allowDirs as $allowDir) {
                if (strpos($filePath, $this->env->normalizePath($allowDir)) === 0) {
                    $inAllowedDir = true;
                }
            }

            if (!$inAllowedDir) {
                $allowDirsStr = implode(';', $allowDirs);
                throw new Minify_Source_FactoryException("File '{$spec['filepath']}' is outside \$allowDirs "
                    . "($allowDirsStr). If the path is resolved via an alias/symlink, look into the "
                    . "\$min_symlinks option.");
            }
        }

        $basename = basename($spec['filepath']);

        if ($this->options['noMinPattern'] && preg_match($this->options['noMinPattern'], $basename)) {
            if (preg_match('~\.(css|less)$~i', $basename)) {
                $spec['minifyOptions']['compress'] = false;
            // we still want URI rewriting to work for CSS
            } else {
                $spec['minifier'] = 'Minify::nullMinifier';
            }
        }

        $hoursBehind = $this->options['uploaderHoursBehind'];
        if ($hoursBehind != 0) {
            $spec['uploaderHoursBehind'] = $hoursBehind;
        }

        foreach ($this->handlers as $basenamePattern => $handler) {
            if (preg_match($basenamePattern, $basename)) {
                $source = call_user_func($handler, $spec);
                break;
            }
        }

        if (!$source) {
            throw new Minify_Source_FactoryException("Handler not found for file: $basename");
        }

        return $source;
    }
}