1 |
efrain |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
/**
|
|
|
4 |
* Abstract minifier class.
|
|
|
5 |
*
|
|
|
6 |
* Please report bugs on https://github.com/matthiasmullie/minify/issues
|
|
|
7 |
*
|
|
|
8 |
* @author Matthias Mullie <minify@mullie.eu>
|
|
|
9 |
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
|
|
|
10 |
* @license MIT License
|
|
|
11 |
*/
|
|
|
12 |
|
|
|
13 |
namespace MatthiasMullie\Minify;
|
|
|
14 |
|
|
|
15 |
use MatthiasMullie\Minify\Exceptions\IOException;
|
|
|
16 |
use Psr\Cache\CacheItemInterface;
|
|
|
17 |
|
|
|
18 |
/**
|
|
|
19 |
* Abstract minifier class.
|
|
|
20 |
*
|
|
|
21 |
* Please report bugs on https://github.com/matthiasmullie/minify/issues
|
|
|
22 |
*
|
|
|
23 |
* @author Matthias Mullie <minify@mullie.eu>
|
|
|
24 |
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
|
|
|
25 |
* @license MIT License
|
|
|
26 |
*/
|
|
|
27 |
abstract class Minify
|
|
|
28 |
{
|
|
|
29 |
/**
|
|
|
30 |
* The data to be minified.
|
|
|
31 |
*
|
|
|
32 |
* @var string[]
|
|
|
33 |
*/
|
|
|
34 |
protected $data = array();
|
|
|
35 |
|
|
|
36 |
/**
|
|
|
37 |
* Array of patterns to match.
|
|
|
38 |
*
|
|
|
39 |
* @var string[]
|
|
|
40 |
*/
|
|
|
41 |
protected $patterns = array();
|
|
|
42 |
|
|
|
43 |
/**
|
|
|
44 |
* This array will hold content of strings and regular expressions that have
|
|
|
45 |
* been extracted from the JS source code, so we can reliably match "code",
|
|
|
46 |
* without having to worry about potential "code-like" characters inside.
|
|
|
47 |
*
|
|
|
48 |
* @internal
|
|
|
49 |
*
|
|
|
50 |
* @var string[]
|
|
|
51 |
*/
|
|
|
52 |
public $extracted = array();
|
|
|
53 |
|
|
|
54 |
/**
|
|
|
55 |
* Init the minify class - optionally, code may be passed along already.
|
|
|
56 |
*/
|
|
|
57 |
public function __construct(/* $data = null, ... */)
|
|
|
58 |
{
|
|
|
59 |
// it's possible to add the source through the constructor as well ;)
|
|
|
60 |
if (func_num_args()) {
|
|
|
61 |
call_user_func_array(array($this, 'add'), func_get_args());
|
|
|
62 |
}
|
|
|
63 |
}
|
|
|
64 |
|
|
|
65 |
/**
|
|
|
66 |
* Add a file or straight-up code to be minified.
|
|
|
67 |
*
|
|
|
68 |
* @param string|string[] $data
|
|
|
69 |
*
|
|
|
70 |
* @return static
|
|
|
71 |
*/
|
|
|
72 |
public function add($data /* $data = null, ... */)
|
|
|
73 |
{
|
|
|
74 |
// bogus "usage" of parameter $data: scrutinizer warns this variable is
|
|
|
75 |
// not used (we're using func_get_args instead to support overloading),
|
|
|
76 |
// but it still needs to be defined because it makes no sense to have
|
|
|
77 |
// this function without argument :)
|
|
|
78 |
$args = array($data) + func_get_args();
|
|
|
79 |
|
|
|
80 |
// this method can be overloaded
|
|
|
81 |
foreach ($args as $data) {
|
|
|
82 |
if (is_array($data)) {
|
|
|
83 |
call_user_func_array(array($this, 'add'), $data);
|
|
|
84 |
continue;
|
|
|
85 |
}
|
|
|
86 |
|
|
|
87 |
// redefine var
|
|
|
88 |
$data = (string) $data;
|
|
|
89 |
|
|
|
90 |
// load data
|
|
|
91 |
$value = $this->load($data);
|
|
|
92 |
$key = ($data != $value) ? $data : count($this->data);
|
|
|
93 |
|
|
|
94 |
// replace CR linefeeds etc.
|
|
|
95 |
// @see https://github.com/matthiasmullie/minify/pull/139
|
|
|
96 |
$value = str_replace(array("\r\n", "\r"), "\n", $value);
|
|
|
97 |
|
|
|
98 |
// store data
|
|
|
99 |
$this->data[$key] = $value;
|
|
|
100 |
}
|
|
|
101 |
|
|
|
102 |
return $this;
|
|
|
103 |
}
|
|
|
104 |
|
|
|
105 |
/**
|
|
|
106 |
* Add a file to be minified.
|
|
|
107 |
*
|
|
|
108 |
* @param string|string[] $data
|
|
|
109 |
*
|
|
|
110 |
* @return static
|
|
|
111 |
*
|
|
|
112 |
* @throws IOException
|
|
|
113 |
*/
|
|
|
114 |
public function addFile($data /* $data = null, ... */)
|
|
|
115 |
{
|
|
|
116 |
// bogus "usage" of parameter $data: scrutinizer warns this variable is
|
|
|
117 |
// not used (we're using func_get_args instead to support overloading),
|
|
|
118 |
// but it still needs to be defined because it makes no sense to have
|
|
|
119 |
// this function without argument :)
|
|
|
120 |
$args = array($data) + func_get_args();
|
|
|
121 |
|
|
|
122 |
// this method can be overloaded
|
|
|
123 |
foreach ($args as $path) {
|
|
|
124 |
if (is_array($path)) {
|
|
|
125 |
call_user_func_array(array($this, 'addFile'), $path);
|
|
|
126 |
continue;
|
|
|
127 |
}
|
|
|
128 |
|
|
|
129 |
// redefine var
|
|
|
130 |
$path = (string) $path;
|
|
|
131 |
|
|
|
132 |
// check if we can read the file
|
|
|
133 |
if (!$this->canImportFile($path)) {
|
|
|
134 |
throw new IOException('The file "' . $path . '" could not be opened for reading. Check if PHP has enough permissions.');
|
|
|
135 |
}
|
|
|
136 |
|
|
|
137 |
$this->add($path);
|
|
|
138 |
}
|
|
|
139 |
|
|
|
140 |
return $this;
|
|
|
141 |
}
|
|
|
142 |
|
|
|
143 |
/**
|
|
|
144 |
* Minify the data & (optionally) saves it to a file.
|
|
|
145 |
*
|
|
|
146 |
* @param string[optional] $path Path to write the data to
|
|
|
147 |
*
|
|
|
148 |
* @return string The minified data
|
|
|
149 |
*/
|
|
|
150 |
public function minify($path = null)
|
|
|
151 |
{
|
|
|
152 |
$content = $this->execute($path);
|
|
|
153 |
|
|
|
154 |
// save to path
|
|
|
155 |
if ($path !== null) {
|
|
|
156 |
$this->save($content, $path);
|
|
|
157 |
}
|
|
|
158 |
|
|
|
159 |
return $content;
|
|
|
160 |
}
|
|
|
161 |
|
|
|
162 |
/**
|
|
|
163 |
* Minify & gzip the data & (optionally) saves it to a file.
|
|
|
164 |
*
|
|
|
165 |
* @param string[optional] $path Path to write the data to
|
|
|
166 |
* @param int[optional] $level Compression level, from 0 to 9
|
|
|
167 |
*
|
|
|
168 |
* @return string The minified & gzipped data
|
|
|
169 |
*/
|
|
|
170 |
public function gzip($path = null, $level = 9)
|
|
|
171 |
{
|
|
|
172 |
$content = $this->execute($path);
|
|
|
173 |
$content = gzencode($content, $level, FORCE_GZIP);
|
|
|
174 |
|
|
|
175 |
// save to path
|
|
|
176 |
if ($path !== null) {
|
|
|
177 |
$this->save($content, $path);
|
|
|
178 |
}
|
|
|
179 |
|
|
|
180 |
return $content;
|
|
|
181 |
}
|
|
|
182 |
|
|
|
183 |
/**
|
|
|
184 |
* Minify the data & write it to a CacheItemInterface object.
|
|
|
185 |
*
|
|
|
186 |
* @param CacheItemInterface $item Cache item to write the data to
|
|
|
187 |
*
|
|
|
188 |
* @return CacheItemInterface Cache item with the minifier data
|
|
|
189 |
*/
|
|
|
190 |
public function cache(CacheItemInterface $item)
|
|
|
191 |
{
|
|
|
192 |
$content = $this->execute();
|
|
|
193 |
$item->set($content);
|
|
|
194 |
|
|
|
195 |
return $item;
|
|
|
196 |
}
|
|
|
197 |
|
|
|
198 |
/**
|
|
|
199 |
* Minify the data.
|
|
|
200 |
*
|
|
|
201 |
* @param string[optional] $path Path to write the data to
|
|
|
202 |
*
|
|
|
203 |
* @return string The minified data
|
|
|
204 |
*/
|
|
|
205 |
abstract public function execute($path = null);
|
|
|
206 |
|
|
|
207 |
/**
|
|
|
208 |
* Load data.
|
|
|
209 |
*
|
|
|
210 |
* @param string $data Either a path to a file or the content itself
|
|
|
211 |
*
|
|
|
212 |
* @return string
|
|
|
213 |
*/
|
|
|
214 |
protected function load($data)
|
|
|
215 |
{
|
|
|
216 |
// check if the data is a file
|
|
|
217 |
if ($this->canImportFile($data)) {
|
|
|
218 |
$data = file_get_contents($data);
|
|
|
219 |
|
|
|
220 |
// strip BOM, if any
|
|
|
221 |
if (substr($data, 0, 3) == "\xef\xbb\xbf") {
|
|
|
222 |
$data = substr($data, 3);
|
|
|
223 |
}
|
|
|
224 |
}
|
|
|
225 |
|
|
|
226 |
return $data;
|
|
|
227 |
}
|
|
|
228 |
|
|
|
229 |
/**
|
|
|
230 |
* Save to file.
|
|
|
231 |
*
|
|
|
232 |
* @param string $content The minified data
|
|
|
233 |
* @param string $path The path to save the minified data to
|
|
|
234 |
*
|
|
|
235 |
* @throws IOException
|
|
|
236 |
*/
|
|
|
237 |
protected function save($content, $path)
|
|
|
238 |
{
|
|
|
239 |
$handler = $this->openFileForWriting($path);
|
|
|
240 |
|
|
|
241 |
$this->writeToFile($handler, $content);
|
|
|
242 |
|
|
|
243 |
@fclose($handler);
|
|
|
244 |
}
|
|
|
245 |
|
|
|
246 |
/**
|
|
|
247 |
* Register a pattern to execute against the source content.
|
|
|
248 |
*
|
|
|
249 |
* If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work.
|
|
|
250 |
* If you need that functionality, use a callback instead.
|
|
|
251 |
*
|
|
|
252 |
* @param string $pattern PCRE pattern
|
|
|
253 |
* @param string|callable $replacement Replacement value for matched pattern
|
|
|
254 |
*/
|
|
|
255 |
protected function registerPattern($pattern, $replacement = '')
|
|
|
256 |
{
|
|
|
257 |
// study the pattern, we'll execute it more than once
|
|
|
258 |
$pattern .= 'S';
|
|
|
259 |
|
|
|
260 |
$this->patterns[] = array($pattern, $replacement);
|
|
|
261 |
}
|
|
|
262 |
|
|
|
263 |
/**
|
|
|
264 |
* Both JS and CSS use the same form of multi-line comment, so putting the common code here.
|
|
|
265 |
*/
|
|
|
266 |
protected function stripMultilineComments()
|
|
|
267 |
{
|
|
|
268 |
// First extract comments we want to keep, so they can be restored later
|
|
|
269 |
// PHP only supports $this inside anonymous functions since 5.4
|
|
|
270 |
$minifier = $this;
|
|
|
271 |
$callback = function ($match) use ($minifier) {
|
|
|
272 |
$count = count($minifier->extracted);
|
|
|
273 |
$placeholder = '/*'.$count.'*/';
|
|
|
274 |
$minifier->extracted[$placeholder] = $match[0];
|
|
|
275 |
|
|
|
276 |
return $placeholder;
|
|
|
277 |
};
|
|
|
278 |
$this->registerPattern('/
|
|
|
279 |
# optional newline
|
|
|
280 |
\n?
|
|
|
281 |
|
|
|
282 |
# start comment
|
|
|
283 |
\/\*
|
|
|
284 |
|
|
|
285 |
# comment content
|
|
|
286 |
(?:
|
|
|
287 |
# either starts with an !
|
|
|
288 |
!
|
|
|
289 |
|
|
|
|
290 |
# or, after some number of characters which do not end the comment
|
|
|
291 |
(?:(?!\*\/).)*?
|
|
|
292 |
|
|
|
293 |
# there is either a @license or @preserve tag
|
|
|
294 |
@(?:license|preserve)
|
|
|
295 |
)
|
|
|
296 |
|
|
|
297 |
# then match to the end of the comment
|
|
|
298 |
.*?\*\/\n?
|
|
|
299 |
|
|
|
300 |
/ixs', $callback);
|
|
|
301 |
|
|
|
302 |
// Then strip all other comments
|
|
|
303 |
$this->registerPattern('/\/\*.*?\*\//s', '');
|
|
|
304 |
}
|
|
|
305 |
|
|
|
306 |
/**
|
|
|
307 |
* We can't "just" run some regular expressions against JavaScript: it's a
|
|
|
308 |
* complex language. E.g. having an occurrence of // xyz would be a comment,
|
|
|
309 |
* unless it's used within a string. Of you could have something that looks
|
|
|
310 |
* like a 'string', but inside a comment.
|
|
|
311 |
* The only way to accurately replace these pieces is to traverse the JS one
|
|
|
312 |
* character at a time and try to find whatever starts first.
|
|
|
313 |
*
|
|
|
314 |
* @param string $content The content to replace patterns in
|
|
|
315 |
*
|
|
|
316 |
* @return string The (manipulated) content
|
|
|
317 |
*/
|
|
|
318 |
protected function replace($content)
|
|
|
319 |
{
|
|
|
320 |
$contentLength = strlen($content);
|
|
|
321 |
$output = '';
|
|
|
322 |
$processedOffset = 0;
|
|
|
323 |
$positions = array_fill(0, count($this->patterns), -1);
|
|
|
324 |
$matches = array();
|
|
|
325 |
|
|
|
326 |
while ($processedOffset < $contentLength) {
|
|
|
327 |
// find first match for all patterns
|
|
|
328 |
foreach ($this->patterns as $i => $pattern) {
|
|
|
329 |
list($pattern, $replacement) = $pattern;
|
|
|
330 |
|
|
|
331 |
// we can safely ignore patterns for positions we've unset earlier,
|
|
|
332 |
// because we know these won't show up anymore
|
|
|
333 |
if (array_key_exists($i, $positions) == false) {
|
|
|
334 |
continue;
|
|
|
335 |
}
|
|
|
336 |
|
|
|
337 |
// no need to re-run matches that are still in the part of the
|
|
|
338 |
// content that hasn't been processed
|
|
|
339 |
if ($positions[$i] >= $processedOffset) {
|
|
|
340 |
continue;
|
|
|
341 |
}
|
|
|
342 |
|
|
|
343 |
$match = null;
|
|
|
344 |
if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) {
|
|
|
345 |
$matches[$i] = $match;
|
|
|
346 |
|
|
|
347 |
// we'll store the match position as well; that way, we
|
|
|
348 |
// don't have to redo all preg_matches after changing only
|
|
|
349 |
// the first (we'll still know where those others are)
|
|
|
350 |
$positions[$i] = $match[0][1];
|
|
|
351 |
} else {
|
|
|
352 |
// if the pattern couldn't be matched, there's no point in
|
|
|
353 |
// executing it again in later runs on this same content;
|
|
|
354 |
// ignore this one until we reach end of content
|
|
|
355 |
unset($matches[$i], $positions[$i]);
|
|
|
356 |
}
|
|
|
357 |
}
|
|
|
358 |
|
|
|
359 |
// no more matches to find: everything's been processed, break out
|
|
|
360 |
if (!$matches) {
|
|
|
361 |
// output the remaining content
|
|
|
362 |
$output .= substr($content, $processedOffset);
|
|
|
363 |
break;
|
|
|
364 |
}
|
|
|
365 |
|
|
|
366 |
// see which of the patterns actually found the first thing (we'll
|
|
|
367 |
// only want to execute that one, since we're unsure if what the
|
|
|
368 |
// other found was not inside what the first found)
|
|
|
369 |
$matchOffset = min($positions);
|
|
|
370 |
$firstPattern = array_search($matchOffset, $positions);
|
|
|
371 |
$match = $matches[$firstPattern];
|
|
|
372 |
|
|
|
373 |
// execute the pattern that matches earliest in the content string
|
|
|
374 |
list(, $replacement) = $this->patterns[$firstPattern];
|
|
|
375 |
|
|
|
376 |
// add the part of the input between $processedOffset and the first match;
|
|
|
377 |
// that content wasn't matched by anything
|
|
|
378 |
$output .= substr($content, $processedOffset, $matchOffset - $processedOffset);
|
|
|
379 |
// add the replacement for the match
|
|
|
380 |
$output .= $this->executeReplacement($replacement, $match);
|
|
|
381 |
// advance $processedOffset past the match
|
|
|
382 |
$processedOffset = $matchOffset + strlen($match[0][0]);
|
|
|
383 |
}
|
|
|
384 |
|
|
|
385 |
return $output;
|
|
|
386 |
}
|
|
|
387 |
|
|
|
388 |
/**
|
|
|
389 |
* If $replacement is a callback, execute it, passing in the match data.
|
|
|
390 |
* If it's a string, just pass it through.
|
|
|
391 |
*
|
|
|
392 |
* @param string|callable $replacement Replacement value
|
|
|
393 |
* @param array $match Match data, in PREG_OFFSET_CAPTURE form
|
|
|
394 |
*
|
|
|
395 |
* @return string
|
|
|
396 |
*/
|
|
|
397 |
protected function executeReplacement($replacement, $match)
|
|
|
398 |
{
|
|
|
399 |
if (!is_callable($replacement)) {
|
|
|
400 |
return $replacement;
|
|
|
401 |
}
|
|
|
402 |
// convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects
|
|
|
403 |
foreach ($match as &$matchItem) {
|
|
|
404 |
$matchItem = $matchItem[0];
|
|
|
405 |
}
|
|
|
406 |
|
|
|
407 |
return $replacement($match);
|
|
|
408 |
}
|
|
|
409 |
|
|
|
410 |
/**
|
|
|
411 |
* Strings are a pattern we need to match, in order to ignore potential
|
|
|
412 |
* code-like content inside them, but we just want all of the string
|
|
|
413 |
* content to remain untouched.
|
|
|
414 |
*
|
|
|
415 |
* This method will replace all string content with simple STRING#
|
|
|
416 |
* placeholder text, so we've rid all strings from characters that may be
|
|
|
417 |
* misinterpreted. Original string content will be saved in $this->extracted
|
|
|
418 |
* and after doing all other minifying, we can restore the original content
|
|
|
419 |
* via restoreStrings().
|
|
|
420 |
*
|
|
|
421 |
* @param string[optional] $chars
|
|
|
422 |
* @param string[optional] $placeholderPrefix
|
|
|
423 |
*/
|
|
|
424 |
protected function extractStrings($chars = '\'"', $placeholderPrefix = '')
|
|
|
425 |
{
|
|
|
426 |
// PHP only supports $this inside anonymous functions since 5.4
|
|
|
427 |
$minifier = $this;
|
|
|
428 |
$callback = function ($match) use ($minifier, $placeholderPrefix) {
|
|
|
429 |
// check the second index here, because the first always contains a quote
|
|
|
430 |
if ($match[2] === '') {
|
|
|
431 |
/*
|
|
|
432 |
* Empty strings need no placeholder; they can't be confused for
|
|
|
433 |
* anything else anyway.
|
|
|
434 |
* But we still needed to match them, for the extraction routine
|
|
|
435 |
* to skip over this particular string.
|
|
|
436 |
*/
|
|
|
437 |
return $match[0];
|
|
|
438 |
}
|
|
|
439 |
|
|
|
440 |
$count = count($minifier->extracted);
|
|
|
441 |
$placeholder = $match[1] . $placeholderPrefix . $count . $match[1];
|
|
|
442 |
$minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1];
|
|
|
443 |
|
|
|
444 |
return $placeholder;
|
|
|
445 |
};
|
|
|
446 |
|
|
|
447 |
/*
|
|
|
448 |
* The \\ messiness explained:
|
|
|
449 |
* * Don't count ' or " as end-of-string if it's escaped (has backslash
|
|
|
450 |
* in front of it)
|
|
|
451 |
* * Unless... that backslash itself is escaped (another leading slash),
|
|
|
452 |
* in which case it's no longer escaping the ' or "
|
|
|
453 |
* * So there can be either no backslash, or an even number
|
|
|
454 |
* * multiply all of that times 4, to account for the escaping that has
|
|
|
455 |
* to be done to pass the backslash into the PHP string without it being
|
|
|
456 |
* considered as escape-char (times 2) and to get it in the regex,
|
|
|
457 |
* escaped (times 2)
|
|
|
458 |
*/
|
|
|
459 |
$this->registerPattern('/([' . $chars . '])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback);
|
|
|
460 |
}
|
|
|
461 |
|
|
|
462 |
/**
|
|
|
463 |
* This method will restore all extracted data (strings, regexes) that were
|
|
|
464 |
* replaced with placeholder text in extract*(). The original content was
|
|
|
465 |
* saved in $this->extracted.
|
|
|
466 |
*
|
|
|
467 |
* @param string $content
|
|
|
468 |
*
|
|
|
469 |
* @return string
|
|
|
470 |
*/
|
|
|
471 |
protected function restoreExtractedData($content)
|
|
|
472 |
{
|
|
|
473 |
if (!$this->extracted) {
|
|
|
474 |
// nothing was extracted, nothing to restore
|
|
|
475 |
return $content;
|
|
|
476 |
}
|
|
|
477 |
|
|
|
478 |
$content = strtr($content, $this->extracted);
|
|
|
479 |
|
|
|
480 |
$this->extracted = array();
|
|
|
481 |
|
|
|
482 |
return $content;
|
|
|
483 |
}
|
|
|
484 |
|
|
|
485 |
/**
|
|
|
486 |
* Check if the path is a regular file and can be read.
|
|
|
487 |
*
|
|
|
488 |
* @param string $path
|
|
|
489 |
*
|
|
|
490 |
* @return bool
|
|
|
491 |
*/
|
|
|
492 |
protected function canImportFile($path)
|
|
|
493 |
{
|
|
|
494 |
$parsed = parse_url($path);
|
|
|
495 |
if (
|
|
|
496 |
// file is elsewhere
|
|
|
497 |
isset($parsed['host']) ||
|
|
|
498 |
// file responds to queries (may change, or need to bypass cache)
|
|
|
499 |
isset($parsed['query'])
|
|
|
500 |
) {
|
|
|
501 |
return false;
|
|
|
502 |
}
|
|
|
503 |
|
|
|
504 |
return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path);
|
|
|
505 |
}
|
|
|
506 |
|
|
|
507 |
/**
|
|
|
508 |
* Attempts to open file specified by $path for writing.
|
|
|
509 |
*
|
|
|
510 |
* @param string $path The path to the file
|
|
|
511 |
*
|
|
|
512 |
* @return resource Specifier for the target file
|
|
|
513 |
*
|
|
|
514 |
* @throws IOException
|
|
|
515 |
*/
|
|
|
516 |
protected function openFileForWriting($path)
|
|
|
517 |
{
|
|
|
518 |
if ($path === '' || ($handler = @fopen($path, 'w')) === false) {
|
|
|
519 |
throw new IOException('The file "' . $path . '" could not be opened for writing. Check if PHP has enough permissions.');
|
|
|
520 |
}
|
|
|
521 |
|
|
|
522 |
return $handler;
|
|
|
523 |
}
|
|
|
524 |
|
|
|
525 |
/**
|
|
|
526 |
* Attempts to write $content to the file specified by $handler. $path is used for printing exceptions.
|
|
|
527 |
*
|
|
|
528 |
* @param resource $handler The resource to write to
|
|
|
529 |
* @param string $content The content to write
|
|
|
530 |
* @param string $path The path to the file (for exception printing only)
|
|
|
531 |
*
|
|
|
532 |
* @throws IOException
|
|
|
533 |
*/
|
|
|
534 |
protected function writeToFile($handler, $content, $path = '')
|
|
|
535 |
{
|
|
|
536 |
if (
|
|
|
537 |
!is_resource($handler) ||
|
|
|
538 |
($result = @fwrite($handler, $content)) === false ||
|
|
|
539 |
($result < strlen($content))
|
|
|
540 |
) {
|
|
|
541 |
throw new IOException('The file "' . $path . '" could not be written to. Check your disk space and file permissions.');
|
|
|
542 |
}
|
|
|
543 |
}
|
|
|
544 |
|
|
|
545 |
protected static function str_replace_first($search, $replace, $subject)
|
|
|
546 |
{
|
|
|
547 |
$pos = strpos($subject, $search);
|
|
|
548 |
if ($pos !== false) {
|
|
|
549 |
return substr_replace($subject, $replace, $pos, strlen($search));
|
|
|
550 |
}
|
|
|
551 |
|
|
|
552 |
return $subject;
|
|
|
553 |
}
|
|
|
554 |
}
|