1<?php
2/**
3 *
4 * Interface for writing files, retrieving files and checking caches
5 *
6 */
7namespace CssCrush;
8
9class IO
10{
11    protected $process;
12
13    public function __construct(Process $process)
14    {
15        $this->process = $process;
16    }
17
18    public function init()
19    {
20        $this->process->cacheFile = "{$this->process->output->dir}/.csscrush";
21    }
22
23    public function getOutputDir()
24    {
25        $outputDir = $this->process->options->output_dir;
26
27        return $outputDir ? $outputDir : $this->process->input->dir;
28    }
29
30    public function getOutputFilename()
31    {
32        $options = $this->process->options;
33
34        $inputBasename = basename($this->process->input->filename, '.css');
35        $outputBasename = $inputBasename;
36
37        if (! empty($options->output_file)) {
38            $outputBasename = basename($options->output_file, '.css');
39        }
40
41        if ($this->process->input->dir === $this->getOutputDir() && $inputBasename === $outputBasename) {
42            $outputBasename .= '.crush';
43        }
44
45        return "$outputBasename.css";
46    }
47
48    public function getOutputUrl()
49    {
50        $process = $this->process;
51        $options = $process->options;
52        $filename = $process->output->filename;
53
54        $url = $process->output->dirUrl . '/' . $filename;
55
56        // Make URL relative if the input path was relative.
57        $input_path = new Url($process->input->raw);
58        if ($input_path->isRelative) {
59            $url = Util::getLinkBetweenPaths(Crush::$config->scriptDir, $process->output->dir) . $filename;
60        }
61
62        // Optional query-string timestamp.
63        if ($options->versioning !== false) {
64            $url .= '?';
65            if (isset($process->cacheData[$filename]['datem_sum'])) {
66                $url .= $process->cacheData[$filename]['datem_sum'];
67            }
68            else {
69                $url .= time();
70            }
71        }
72
73        return $url;
74    }
75
76    public function validateCache()
77    {
78        $process = $this->process;
79        $options = $process->options;
80        $input = $process->input;
81
82        $dir = $this->getOutputDir();
83        $filename = $this->getOutputFilename();
84        $path = "$dir/$filename";
85
86        if (! file_exists($path)) {
87            debug('No file cached.');
88
89            return false;
90        }
91
92        if (! isset($process->cacheData[$filename])) {
93            debug('Cached file exists but is not registered.');
94
95            return false;
96        }
97
98        $data =& $process->cacheData[$filename];
99
100        // Make stack of file mtimes starting with the input file.
101        $file_sums = array($input->mtime);
102        foreach ($data['imports'] as $import_file) {
103
104            // Check if this is docroot relative or input dir relative.
105            $root = strpos($import_file, '/') === 0 ? $process->docRoot : $input->dir;
106            $import_filepath = realpath($root) . "/$import_file";
107
108            if (file_exists($import_filepath)) {
109                $file_sums[] = filemtime($import_filepath);
110            }
111            else {
112                // File has been moved, remove old file and skip to compile.
113                debug('Recompiling - an import file has been moved.');
114
115                return false;
116            }
117        }
118
119        $files_changed = $data['datem_sum'] != array_sum($file_sums);
120        if ($files_changed) {
121            debug('Files have been modified. Recompiling.');
122        }
123
124        // Compare runtime options and cached options for differences.
125        // Cast because the cached options may be a \stdClass if an IO adapter has been used.
126        $options_changed = false;
127        $cached_options = (array) $data['options'];
128        $active_options = $options->get();
129        foreach ($cached_options as $key => &$value) {
130            if (isset($active_options[$key]) && $active_options[$key] !== $value) {
131                debug('Options have been changed. Recompiling.');
132                $options_changed = true;
133                break;
134            }
135        }
136
137        if (! $options_changed && ! $files_changed) {
138            debug("Files and options have not been modified, returning cached file.");
139
140            return true;
141        }
142        else {
143            $data['datem_sum'] = array_sum($file_sums);
144
145            return false;
146        }
147    }
148
149    public function getCacheData()
150    {
151        $process = $this->process;
152
153        if (file_exists($process->cacheFile) && $process->cacheData) {
154
155            // Already loaded and config file exists in the current directory
156            return;
157        }
158
159        $cache_data_exists = file_exists($process->cacheFile);
160        $cache_data_file_is_writable = $cache_data_exists ? is_writable($process->cacheFile) : false;
161        $cache_data = array();
162
163        if (
164            $cache_data_exists &&
165            $cache_data_file_is_writable &&
166            $cache_data = json_decode(file_get_contents($process->cacheFile), true)
167        ) {
168            // Successfully loaded config file.
169            debug('Cache data loaded.');
170        }
171        else {
172            // Config file may exist but not be writable (may not be visible in some ftp situations?)
173            if ($cache_data_exists) {
174                if (! @unlink($process->cacheFile)) {
175                    notice('Could not delete cache data file.');
176                }
177            }
178            else {
179                debug('Creating cache data file.');
180            }
181            Util::filePutContents($process->cacheFile, json_encode(array()), __METHOD__);
182        }
183
184        return $cache_data;
185    }
186
187    public function saveCacheData()
188    {
189        $process = $this->process;
190
191        debug('Saving config.');
192
193        $flags = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
194        Util::filePutContents($process->cacheFile, json_encode($process->cacheData, $flags), __METHOD__);
195    }
196
197    public function write(StringObject $string)
198    {
199        $process = $this->process;
200
201        $dir = $this->getOutputDir();
202        $filename = $this->getOutputFilename();
203        $sourcemapFilename = "$filename.map";
204
205        if ($process->sourceMap) {
206            $string->append($process->newline . "/*# sourceMappingURL=$sourcemapFilename */");
207        }
208
209        if (Util::filePutContents("$dir/$filename", $string, __METHOD__)) {
210
211            $jsonFlags = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
212
213            if ($process->sourceMap) {
214                Util::filePutContents("$dir/$sourcemapFilename",
215                    json_encode($process->sourceMap, $jsonFlags), __METHOD__);
216            }
217
218            if ($process->options->stat_dump) {
219                $statFile = is_string($process->options->stat_dump) ?
220                    $process->options->stat_dump : "$dir/$filename.json";
221
222                $GLOBALS['CSSCRUSH_STAT_FILE'] = $statFile;
223                Util::filePutContents($statFile, json_encode(csscrush_stat(), $jsonFlags), __METHOD__);
224            }
225
226            return true;
227        }
228
229        return false;
230    }
231}
232