-
Notifications
You must be signed in to change notification settings - Fork 13
/
class.magic-min.php
1027 lines (877 loc) · 41.4 KB
/
class.magic-min.php
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/*------------------------------------------------------------------------------
** File: class.magic-min.php
** Class: MagicMin
** Description: Javascript and CSS minification/merging class to simplify movement from development to production versions of files
** Dependencies: jShrink (https://github.com/tedious/JShrink)
** Version: 3.0.4
** Created: 01-Jun-2013
** Updated: 21-Mar-2015
** Author: Bennett Stone
** Homepage: www.phpdevtips.com
**------------------------------------------------------------------------------
** COPYRIGHT (c) 2015 BENNETT STONE
**
** The source code included in this package is free software; you can
** redistribute it and/or modify it under the terms of the GNU General Public
** License as published by the Free Software Foundation. This license can be
** read at:
**
** http://www.opensource.org/licenses/gpl-license.php
**
** This program is distributed in the hope that it will be useful, but WITHOUT
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
** FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
**------------------------------------------------------------------------------
**
** Usage:
** $min = new Minifier();
** <script src="<?php $min->minify( '[source filename]', '[output filename (optional)]', '[version (optional)]' ); ?>"></script>
** <link rel="stylesheet" media="all" href="<?php $min->minify( 'css/copy-from.css', 'css/test-prod.minified.css', '1.8' ); ?>">
**
** Usage example for merge and minify:
** $min->merge( 'output filename and location', 'directory', 'type (js or css)', array( of items to exclude ) );
** $min->merge( 'js/one-file-merged.js', 'js', 'js', array( 'js/inline-edit.js', 'js/autogrow.js' ) );
**
** Normalized output example using merge and minify:
** <script src="<?php $min->merge( 'js/production.min.js', 'js', 'js' ); ?>"></script>
**
** Adding gzip, base64 image encoding, or returning rather than echo:
** $vars = array(
** 'echo' => false,
** 'encode' => true,
** 'timer' => true,
** 'gzip' => true
** );
** $minified = new Minifier( $vars );
**
** Using JShrink for js minification as opposed to google closure (default set to google closure)
** $vars = array(
** 'closure' => false,
** 'gzip' => true,
** 'encode' => true
** );
** $minified = new Minifier( $vars );
**
** NEW as of 3.0.0: Output sha1 hashed time-based filenames for new files to break caching without version numbers
** $vars = array(
** 'closure' => false,
** 'gzip' => true,
** 'encode' => true,
** 'remove_comments' => true,
** 'hashed_filenames' => true,
** 'output_log' => true
** );
** $minified = new Minifier( $vars );
**
**------------------------------------------------------------------------------ */
class Minifier {
public $content;
public $output_file;
public $extension;
private $type;
//Max image size for inclusion
const IMAGE_MAX_SIZE = 5;
//For script execution time (src: http://bit.ly/18O3VWw)
private $mtime;
//Sum of output messages
private static $messages = array();
//array of settings to add-to/adjust
private $settings = array();
//List of available config keys that can be set via init
private $config_keys = array(
'echo' => true, //Return or echo the values
'encode' => false, //base64 images from CSS and include as part of the file?
'timer' => true, //Ouput script execution time
'gzip' => false, //Output as php with gzip?
'closure' => true, //Use google closure (utilizes cURL)
'remove_comments' => true, //Remove comments,
'hashed_filenames' => false, //Generate hashbased filenames to break caches,
'output_log' => false, //Output logs automatically at end of file output,
'force_rebuild' => false, //Brute force rebuild of minified assets- USE SPARINGLY!
);
/**
* Construct function
* @access public
* @param array $vars
* @return mixed
*/
public function __construct( $vars = array() )
{
$this->mtime = microtime( true );
foreach( $this->config_keys as $key => $default )
{
if( isset( $vars[$key] ) )
{
self::$messages[]['Minifier Log'] = $key .': '. $vars[$key];
$this->settings[$key] = $vars[$key];
}
else
{
self::$messages[]['Minifier Log'] = $key .': '. $default;
$this->settings[$key] = $default;
}
}
} //end __construct()
/**
* Private function to strip directory names from TOC output
* Used for make_min()
*
* @access private
* @param array $input
* @return array $output
*/
private function strip_directory( $input )
{
return basename( $input );
}
/**
* Private function used to automatically add URI schemes to
* assets in order for them to have their contents retrieved
* ONLY WORKS to add schemes to URI's prefixed with "//" to
* handle protocol irrelevant loading when using either http or https
*
* For example, the following file WOULD work:
* //ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js
*
* While, the following would NOT work:
* ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js
*
* @access private
* @param string $uri
* @param string $scheme
* @return string
*/
private function add_uri_scheme( $uri, $scheme = 'http:' )
{
if( substr( $uri, 0, 2 ) == '//' )
{
return parse_url( $uri, PHP_URL_SCHEME ) === null ? $scheme . $uri : $uri;
}
else
{
return $uri;
}
}
/**
* Private function to determine if files are local or remote
* Used for merge_images() and minify() to determine if filemtime can be used
*
* @access private
* @param string $file
* @return bool
*/
private function remote_file( $file )
{
//It is a remote file
if( substr( $file, 0, 4 ) == 'http' )
{
return true;
}
//Local file
else
{
return false;
}
}
/**
* Internal function to output everything as a gmdate
* Prevents issues with servertime vs. PHP time settings by
* turning all timestamps into gmt
* @access private
* @param int time
* @return int
*/
private function gmstamp( $time = '' )
{
$time = !empty( $time ) ? $time : time();
return gmdate( 'U', $time );
}
/**
* Function to create or retrieve stored data for cachefiles
* that are generated using hashed filenames
* Cachedfiles store:
** orig filename
** hashed filename
** timestamp
** regenerated timestamp (if the file has been regenerated)
*
* Both minify() and merge() run processes to determine filenames and timestamps
* through this function to unify the placement of hashed_filename checks, and files
* are created/checked using this function
*
* @access private
* @param string $source_file- always the same- name of NONhashed minified file
* @param string $reference_file - mainly used to determine accurate file extensions
* @param bool $regen (wipe to recreate contents of cachefile, defaults to false)
* @return object (for less-array-ey bracket retrieval)
*/
private function minified_filedata( $source_file, $reference_files = array(), $regen = false )
{
if( $this->settings['hashed_filenames'] && is_dir( dirname( $source_file ) ) && is_writable( dirname( $source_file ) ) )
{
//Reference filename to create
$cache_refname = sha1( $source_file ) .'.txt';
$checkfile = dirname( $source_file ) . DIRECTORY_SEPARATOR . $cache_refname;
if( file_exists( $checkfile ) && !$regen )
{
$data = file_get_contents( $checkfile );
return (object)unserialize( $data );
}
else
{
$new_ext = strtolower( pathinfo( $source_file, PATHINFO_EXTENSION ) );
if( $new_ext == 'php' )
{
$source_file = rtrim( strtolower( $source_file ), '.php' );
$new_ext = strtolower( pathinfo( $source_file, PATHINFO_EXTENSION ) ) .'.php';
}
$time = $this->gmstamp();
$data = array(
'files' => $reference_files,
'references' => dirname( $source_file ) . DIRECTORY_SEPARATOR. sha1( $time ) . '.'. $new_ext,
'filemtime' => $time,
'generated' => $time
);
//If we need to regen, just wipe the contents of the file
if( $regen === true )
{
$data['regenerated'] = $this->gmstamp();
}
$handle = fopen( $checkfile, 'w' ) or error_log( 'Cannot open file: '.$checkfile );
fwrite( $handle, serialize( $data ) );
fclose( $handle );
return (object)$data;
}
}
else
{
if( file_exists( $source_file ) )
{
$data = array(
'files' => $reference_files,
'references' => $source_file,
'filemtime' => $this->gmstamp( filemtime( $source_file ) ),
'generated' => $this->gmstamp( filemtime( $source_file ) )
);
}
else
{
$time = $this->gmstamp();
$data = array(
'files' => $reference_files,
'references' => $source_file,
'filemtime' => $time,
'generated' => $time
);
}
return (object)$data;
}
}
/**
* Function to seek out and replace image references within CSS with base64_encoded data streams
* Used in minify_contents function IF global for $this->encode
* This function will retrieve the contents of local OR remote images, and is based on
* Matthias Mullie <[email protected]>'s function, "importFiles" from the JavaScript and CSS minifier
* http://www.phpclasses.org/package/7519-PHP-Optimize-JavaScript-and-CSS-files.html
*
* @access private
* @param string $source_file (used for location)
* @param string $contents
* @return string $updated_style
*/
private function merge_images( $source_file, $contents )
{
$this->directory = dirname( $source_file ) . DIRECTORY_SEPARATOR;
if( preg_match_all( '/url\((["\']?)((?!["\']?data:).*?\.(gif|png|jpg|jpeg))\\1\)/i', $contents, $this->matches, PREG_SET_ORDER ) )
{
$this->find = array();
$this->replace = array();
foreach( $this->matches as $this->graphic )
{
$this->extension = pathinfo( $this->graphic[2], PATHINFO_EXTENSION );
$this->image_file = '';
//See if the file is remote or local
if( $this->remote_file( $this->graphic[2] ) )
{
//It's remote, and CURL is pretty fast
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $this->graphic[2] );
curl_setopt( $ch, CURLOPT_NOBODY, 1 );
curl_setopt( $ch, CURLOPT_FAILONERROR, 1 );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
//And it WAS remote, and it DOES exist
if( curl_exec( $ch ) !== FALSE )
{
//Get the image file
$cd = curl_init( $this->graphic[2] );
curl_setopt( $cd, CURLOPT_HEADER, 0 );
curl_setopt( $cd, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $cd, CURLOPT_BINARYTRANSFER, 1 );
$this->image_file = curl_exec( $cd );
//Get the remote filesize
$this->filesize = curl_getinfo( $cd, CURLINFO_CONTENT_LENGTH_DOWNLOAD );
curl_close( $cd );
if( $this->filesize <= Minifier::IMAGE_MAX_SIZE * 1024 )
{
//Assign the find and replace
$this->find[] = $this->graphic[0];
$this->replace[] = 'url(data:'.$this->extension.';base64,'.base64_encode( $this->image_file ).')';
}
} //End file exists
curl_close( $ch );
} //End remote file
elseif( file_exists( $this->directory . $this->graphic[2] ) )
{
//File DOES exist locally, get the contents
//Check the filesize
$this->filesize = filesize( $this->directory . $this->graphic[2] );
if( $this->filesize <= Minifier::IMAGE_MAX_SIZE * 1024 )
{
//File is within the filesize requirements so add it
$this->image_file = file_get_contents( $this->directory . $this->graphic[2] );
//Assign the find and replace
$this->find[] = $this->graphic[0];
$this->replace[] = 'url(data:'.$this->extension.';base64,'.base64_encode( $this->image_file ).')';
}
} //End local file
}
//Log the number of replacements to the console
self::$messages[]['Minifier Log: merge_images'] = count( $this->replace ) .' files base64_encoded into ' . $source_file;
//Find and replace all the images with the base64 data
$this->updated_style = str_replace( $this->find, $this->replace, $contents );
return $this->updated_style;
} //End if( regex for images)
else
{
//No images found in the sheet, just return the contents
return $contents;
}
} //end merge_images()
/**
* Private function to handle minification of file contents
* Supports CSS and JS files
*
* @access private
* @param string $src_file
* @param bool $run_minification (default true)
* @return string $content
*/
private function minify_contents( $src_file, $run_minification = true )
{
$this->source = @file_get_contents( $src_file );
//Log the error and continue if we can't get the file contents
if( !$this->source )
{
self::$messages[]['Minifier ERROR'] = 'Unable to retrieve the contents of '. $src_file . '. Skipping at '. __LINE__ .' in '. basename( __FILE__ );
//This will cause potential js errors, but allow the script to continue processing while notifying the user via console
$this->source = '';
}
$this->type = strtolower( pathinfo( $src_file, PATHINFO_EXTENSION ) );
$this->output = '';
/**
* If the filename indicates that the contents are already minified, we'll just return the contents
* If the switch is flipped (useful for loading things such as jquery via google cdn)
*/
if( preg_match( '/\.min\./i', $src_file ) || $run_minification === false )
{
return $this->source;
}
else
{
if( !empty( $this->type ) && $this->type == 'css' )
{
$this->content = $this->source;
//If the param is set to merge images into the css before minifying...
if( $this->settings['encode'] )
{
$this->content = $this->merge_images( $src_file, $this->content );
}
/* remove comments */
if( $this->settings['remove_comments'] )
{
$this->content = preg_replace( '!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $this->content );
}
/* remove tabs, spaces, newlines, etc. */
$this->content = preg_replace( '/(\s\s+|\t|\n)/', ' ', $this->content );
/* remove other spaces before/after ; */
$this->content = preg_replace( array('(( )+{)','({( )+)'), '{', $this->content );
$this->content = preg_replace( array('(( )+})','(}( )+)','(;( )*})'), '}', $this->content );
$this->content = preg_replace( array('(;( )+)','(( )+;)'), ';', $this->content );
/* remove spaces after : */
$this->content = preg_replace( '(:( )*)', ':', $this->content );
} //end $this->type == 'css'
if( !empty( $this->type ) && $this->type == 'js' )
{
$this->content = $this->source;
/**
* Migrated preg_replace and str_replace custom minification to use google closure API
* OR jShrink on 15-Jun-2013 due to js minification irregularities with most regex's:
* https://github.com/tedious/JShrink
* https://developers.google.com/closure/compiler/
* https://developers.google.com/closure/compiler/docs/api-ref
* Accomodates lack of local file for JShrink by getting contents from github
* and writing to a local file for the class (just in case)
* If bool is passed for 'closure' => true during class initiation, cURL request processes
*/
if( $this->settings['closure'] )
{
//Build the data array
$data = array(
'compilation_level' => 'SIMPLE_OPTIMIZATIONS',
'output_format' => 'json',
'output_info' => 'compiled_code',
'js_code' => urlencode( $this->content )
);
//Compile it into a post compatible format
$fields_string = '';
foreach( $data as $key => $value )
{
$fields_string .= $key . '=' . $value . '&';
}
rtrim( $fields_string, '&' );
//Initiate and execute the curl request
$h = curl_init();
curl_setopt( $h, CURLOPT_URL, 'http://closure-compiler.appspot.com/compile' );
curl_setopt( $h, CURLOPT_POST, true );
curl_setopt( $h, CURLOPT_POSTFIELDS, $fields_string );
curl_setopt( $h, CURLOPT_HEADER, false );
curl_setopt( $h, CURLOPT_RETURNTRANSFER, 1 );
$result = curl_exec( $h );
$result_raw = json_decode( $result, true );
//If we've made too many requests, or passed bad data, our js will be broken
if( isset( $result_raw['serverErrors'] ) && !empty( $result_raw['serverErrors'] ) )
{
$e_code = $result_raw['serverErrors'][0]['code'];
$e_message = $result_raw['serverErrors'][0]['error'];
self::$messages[]['Minifier ERROR'] = $e_code . ': '. $e_message . ' File: '. basename( $src_file ) . '. Returning unminified contents.';
}
else
{
$this->content = $result_raw['compiledCode'];
}
//close connection
curl_close( $h );
} //end if( $this->settings['closure'] )
else
{
//Not using google closure, default to JShrink but make sure the file exists
if( !file_exists( dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'jShrink.php' ) )
{
self::$messages[]['Minifier Log'] = 'jShrink does not exist locally. Retrieving...';
$this->handle = fopen( dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'jShrink.php', 'w' );
$this->jshrink = file_get_contents( 'https://raw.github.com/tedivm/JShrink/master/src/JShrink/Minifier.php' );
fwrite( $this->handle, $this->jshrink );
fclose( $this->handle );
}
//Include jShrink
require_once( dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'jShrink.php' );
//Minify the javascript
$this->content = JShrink\Minifier::minify( $this->content, array( 'flaggedComments' => $this->settings['remove_comments'] ) );
} //end if( !$this->settings['closure'] )
} //end $this->type == 'js'
//Add to the output and return it
$this->output .= $this->content;
return $this->output;
}
} //end minify_contents()
/**
* Private function to create file with, or without minified contents
*
* @access private
* @param string path to, and name of source file
* @param string path to, and name of new minified file
* @param bool $do_minify (default is true) (used for remote files)
* @return string new filename/location (same as path to variable)
*/
private function make_min( $src_file, $new_file, $do_minify = true )
{
self::$messages[]['Minifier note'] = 'Writing new file to '. dirname( $new_file );
//Make sure the directory exists and is writable
if( !is_dir( dirname( $new_file ) ) || !is_writeable( dirname( $new_file ) ) )
{
self::$messages[]['Minifier ERROR'] = dirname( $new_file ) . ' is not writable. Cannot create minified file.';
trigger_error( dirname( $new_file ) . ' is not writable. Cannot create minified file.' );
return false;
}
//Output gzip data as needed, but default to none
//Lengthy line usage is intentional to provide cleanly formatted fwrite contents
$this->prequel = '';
if( $this->settings['gzip'] )
{
$this->prequel = '<?php' . PHP_EOL;
$this->prequel .= 'if( extension_loaded( "zlib" ) )' . PHP_EOL;
$this->prequel .= '{' . PHP_EOL;
$this->prequel .= ' ob_start( "ob_gzhandler" );' . PHP_EOL;
$this->prequel .= '}' . PHP_EOL;
$this->prequel .= 'else' . PHP_EOL;
$this->prequel .= '{' . PHP_EOL;
$this->prequel .= ' ob_start();' . PHP_EOL;
$this->prequel .= '}' . PHP_EOL;
//Get the actual file type for header
$this->extension = strtolower( pathinfo( $new_file, PATHINFO_EXTENSION ) );
/**
* If gzip is enabled, the .php extension is added automatically
* and must be accounted for to prevent files from being recreated
*/
if( $this->extension != 'php' )
{
$new_file = $new_file. '.php';
}
//Close out the php row so we can continue with normal content
$offset = 60 * 60 * 24 * 31;
$this->prequel .= 'header( \'Content-Encoding: gzip\' );' . PHP_EOL;
$this->prequel .= 'header( \'Cache-Control: max-age=' . $offset.'\' );' . PHP_EOL;
$this->prequel .= 'header( \'Expires: ' . gmdate( "D, d M Y H:i:s", time() + $offset ) . ' GMT\' );' . PHP_EOL;
$this->prequel .= 'header( \'Last-Modified: ' . gmdate( "D, d M Y H:i:s", filemtime( __FILE__ ) ) . ' GMT\' );' . PHP_EOL;
//Add the header content type output for correct rendering
if( $this->extension == 'css' || ( strpos( $new_file, '.css' ) !== false ) )
{
$this->prequel .= 'header( \'Content-type: text/css; charset: UTF-8\' );' . PHP_EOL;
}
if( $this->extension == 'js' || ( strpos( $new_file, '.js' ) !== false ) )
{
$this->prequel .= 'header( \'Content-type: application/javascript; charset: UTF-8\' );' . PHP_EOL;
}
//Close out the php tag that gets written to the file
$this->prequel .= '?>' . PHP_EOL;
} //End if( $this->gzip )
//Single files
if( !is_array( $src_file ) )
{
$this->filetag = '/**' . PHP_EOL;
$this->filetag .= ' * Filename: '. basename( $src_file ) . PHP_EOL;
$this->filetag .= ' * Generated by MagicMin '.date('Y-m-d'). ' at '. date('h:i:s A') . PHP_EOL;
$this->filetag .= ' */' . PHP_EOL;
$this->content = $this->prequel . $this->filetag . $this->minify_contents( $src_file, $do_minify );
}
else
{
//Strip the directory names from the $src_file array for security
$filenames = array_map( array( $this, 'strip_directory' ), $src_file );
//Make a temporary var to store the data and write a TOC
$this->compiled = '/**' . PHP_EOL;
$this->compiled .= ' * Table of contents: ' . PHP_EOL;
$this->compiled .= ' * '. implode( PHP_EOL. ' * ', $filenames ) . PHP_EOL;
$this->compiled .= ' * Generated by MagicMin: ' . date( 'Y-m-d h:i:s' ). PHP_EOL;
$this->compiled .= ' */' . PHP_EOL;
//Loop through an array of files to write to the new file
foreach( $src_file as $this->new_file )
{
/**
* It's relatively safe to assume that remote files being retrieved
* already have minified contents (ie. Google CDN hosted jquery)
* so prevent re-minification, but default to $do_minify = true;
*/
$do_minify = true;
if( $this->remote_file( $this->new_file ) )
{
//Remote files should not have compressed content
$do_minify = false;
}
$this->compiled .= $this->minify_contents( $this->new_file, $do_minify );
}
//Write the temporary contents to the full contents
$this->content = trim( $this->prequel . $this->compiled );
//Remove the temporary data
unset( $this->compiled );
} //End $src_file is_array
//If the file already exists, open it and empty it
if( file_exists( $new_file ) && is_writeable( $new_file ) )
{
$f = fopen( $new_file, 'w' );
fclose( $f );
}
//Create the new file
$this->handle = fopen( $new_file, 'w' );
//Log any error messages from the new file creation
if( !$this->handle )
{
self::$messages[]['Minifier ERROR'] = 'Unable to open file: '.$new_file;
trigger_error( 'Unable to open file: '.$new_file );
return false;
}
else
{
//Write the minified contents to it
fwrite( $this->handle, $this->content );
fclose( $this->handle );
//Make sure this filemtime syncs up with everything else magicmin does
touch( $new_file, $this->gmstamp() );
//Log to the console
self::$messages[]['Minifier Log: New file'] = 'Successfully created '. $new_file;
//Return filename and location
return $new_file;
}
} //end make_min()
/**
* Get contents of JS or CSS script, create minified version
* Idea and partial adaptation from: http://davidwalsh.name/php-cache-function
* Dependent on "make_min" function
*
* Example usage:
* <script src="<?php $min->minify( 'js/script.dev.js', 'js/script.js', '1.3' ); ?>"></script>
* <link rel="stylesheet" href="<?php $min->minify( 'css/style.css', 'css/styles.min.css', '1.8' ); ?>" />
* $min->minify( 'source file', 'output file', 'version' );
*
* @access public
* @param string $src_file (filename and location for original file)
* @param string $file (filename and location for output file. Empty defaults to [filename].min.[extension])
* @param string $version
* @return string $output_file (includes provided location)
*/
public function minify( $src_file, $file = '', $version = '' )
{
//Handle double slash prefixed remote filenames, as well as checking relative filenames
$src_file = $this->add_uri_scheme( $src_file );
//Since the $file (output) filename is optional, if empty, just add .min.[ext]
if( empty( $file ) )
{
//Get the pathinfo
$ext = pathinfo( $src_file );
//Create a new filename
$file = $ext['dirname'] . DIRECTORY_SEPARATOR . $ext['filename'] . '.min.' . $ext['extension'];
}
//If we have gzip enabled, we must account for the .php extension
if( $this->settings['gzip'] && ( strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ) != '.php' ) )
{
$file .= '.php';
}
$minfile = $this->minified_filedata( $src_file, array( $file ) );
$file = $minfile->references;
//The source file is remote, and we can't check for an updated version anyway
if( $this->remote_file( $src_file ) && file_exists( $file ) )
{
$this->output_file = $file;
}
//The local version doesn't exist, but we don't need to minify
elseif( $this->remote_file( $src_file ) && !file_exists( $file ) )
{
$this->output_file = $this->make_min( $src_file, $file, false );
//Add the filename to the output log
self::$messages[]['Minifier Log: minify'] = 'Retrieving contents of '.$src_file .' to add to '.$file;
}
//The file already exists and doesn't need to be recreated
elseif( ( $this->settings['force_rebuild'] === true ) || ( file_exists( $file ) && file_exists( $src_file ) ) && ( $this->gmstamp( filemtime( $src_file ) ) < $minfile->filemtime ) )
{
//No change, so the output is the same as the input
$this->output_file = $file;
}
//The file exists, but the development version is newer
elseif( ( $this->settings['force_rebuild'] === true ) || ( file_exists( $file ) && file_exists( $src_file ) ) && ( $this->gmstamp( filemtime( $src_file ) ) > $minfile->filemtime ) )
{
//Remove the file so we can do a clean recreate
chmod( $file, 0777 );
unlink( $file );
//Regen cacheref
$minfile = $this->minified_filedata( $src_file, array( $file ), true );
$file = $minfile->references;
//Make the cached version
$this->output_file = $this->make_min( $src_file, $file );
//Add to the console.log output
self::$messages[]['Minifier Log: minify'] = 'Made new version of '.$src_file.' into '.$file;
}
//The minified file doesn't exist, make one
else
{
//Make the cached version
$this->output_file = $this->make_min( $src_file, $file );
//Add to the console.log output if desired
self::$messages[]['Minifier Log: minify'] = 'Made new version of '.$src_file.' into '.$file;
}
//Add the ? params if they exist
if( !empty( $version ) )
{
$this->output_file .= '?v='. $version;
}
//Return the output filename or echo
if( $this->settings['echo'] )
{
echo $this->output_file;
}
else
{
return $this->output_file;
}
} //end minify()
/**
* Get the contents of js or css files, minify, and merge into a single file
*
* Example usage:
* <?php
* require_once( 'class.magic-min.php' );
* $min = new Minifier();
* ?>
* <script src="<?php $min->merge( '[output folder]/[output filename.js]', '[directory]', '[type(js or css)]', array( '[filetoignore]', '[filetoignore]' ) ); ?>"></script>
* <script src="<?php $min->merge( 'js/one-file-merged.js', 'js', 'js', array( 'js/inline-edit.js', 'js/autogrow.js' ) ); ?>"></script>
*
* @access public
* @param string $output_filename
* @param string $directory to loop through
* @param mixed $list_or_type (css, js, selective - default is js)
**** $list_or_type will also accept "selective" array which overrides glob and only includes specified files
**** $list_or_type array passed files are included in order, and no other files will be included
**** files must all be the same type in order to prevent eronious output contents (js and css do not mix)
* @param array $exclude files to exclude
* @param array $order to specify output order
* @return string new filenae
*/
public function merge( $output_filename, $directory, $list_or_type = 'js', $exclude = array(), $order = array() )
{
/**
* Added selective inclusion to override glob and exclusion 13-Jun-2013 ala Ray Beriau
* This assumes the user has passed an array of filenames, in order rather than a file type
* By doing so, we'll set the directory to indicate no contents, and priorize directly into $order
*/
if( is_array( $list_or_type ) && !empty( $list_or_type ) )
{
//Direct the directory to be an empty array
$this->directory = array();
//Utilize the $order variable
$order = $list_or_type;
}
else
{
//Open the directory for looping and seek out files of appropriate type
$this->directory = glob( $directory .'/*.'.$list_or_type );
}
/**
* Reassign the $output_filename if gzip is enabled as we must account for the .php
* extension in order to prevent the file from being recreated
*/
if( $this->settings['gzip'] && ( strtolower( pathinfo( $output_filename, PATHINFO_EXTENSION ) ) != '.php' ) )
{
$output_filename .= '.php';
}
$reference_files = ( is_array( $list_or_type ) && !empty( $list_or_type ) ) ? $list_or_type : array( $output_filename );
$minfile = $this->minified_filedata( $output_filename, $reference_files );
$minified_name = $minfile->references;
$minified_filelist = !empty( $minfile->files ) ? $minfile->files : array();
//Create a bool to determine if a new file needs to be created
$this->create_new = false;
//Allow brute force rebuild from construct param
if( $this->settings['force_rebuild'] === true )
{
self::$messages[]['Minifier Log: Force Rebuild'] = 'Force rebuild set to true';
$this->create_new = true;
}
//Start the array of files to add to the cache
$this->compilation = array();
//Determine if a specific order is needed, if so remove only those files from glob seek
if( !empty( $order ) )
{
self::$messages[]['Minifier Log: Merge order'] = 'Order specified with '. count( $order ) .' files';
foreach( $order as $this->file )
{
//Handle protocol irrelevant URIs such as '//ajax.google...'
$this->file = $this->add_uri_scheme( $this->file );
//Check each file for modification greater than the output file if it exists
if( file_exists( $minified_name ) && ( $this->file != $output_filename ) && ( !$this->remote_file( $this->file ) ) && ( $this->gmstamp( filemtime( $this->file ) ) > $minfile->filemtime ) || !in_array( $this->file, $minified_filelist ) )
{
self::$messages[]['Minifier Log: New File Flagged'] = 'Flagged for update by '. $this->file;
$this->create_new = true;
}
//Add the specified files to the beginning of the use array passed to $this->make_min
$this->compilation[] = $this->file;
}
//Now remove the same files from the glob directory
$this->directory = array_diff( $this->directory, $this->compilation );
} //End !empty( $order )
//Loop through the directory grabbing files along the way
foreach( $this->directory as $this->file )
{
//Handle protocol irrelevant URIs such as '//ajax.google...'
$this->file = $this->add_uri_scheme( $this->file );
//Make sure we didn't want to exclude this file before adding it
if( !in_array( $this->file, $exclude ) && ( $this->file != $minified_name ) )
{
//Check each file for modification greater than the output file if it exists
if( file_exists( $minified_name ) && ( !$this->remote_file( $this->file ) ) && ( $this->gmstamp( filemtime( $this->file ) ) > $minfile->filemtime ) || !in_array( $this->file, $minified_filelist ) )
{
self::$messages[]['Minifier Log: New File Flagged'] = 'Flagged for update by '. $this->file;
$this->create_new = true;
}
$this->compilation[] = $this->file;
}
} //End foreach( $this->directory )
//Check to see that we have the same number of files passed to the function as were stored
if( count( $minified_filelist ) != count( $this->compilation ) )
{
$this->create_new = true;
}
//Only recreate the file as needed
if( file_exists( $minified_name ) && $this->create_new )
{
self::$messages[]['Minifier Log: Creating new'] = $minified_name .' exists but flagged for new';
//Remove the file so we can do a clean recreate
chmod( $minified_name, 0777 );
unlink( $minified_name );
//Regen cacheref
$minfile = $this->minified_filedata( $output_filename, $this->compilation, true );
$output_filename = $minfile->references;
//Group and minify the contents
$this->compressed = $this->make_min( $this->compilation, $output_filename );
}
elseif( !file_exists( $minified_name ) )
{
//Regen cacheref
$minfile = $this->minified_filedata( $output_filename, $this->compilation, true );
$output_filename = $minfile->references;
//Group and minify the contents
$this->compressed = $this->make_min( $this->compilation, $output_filename );
}
else
{
$this->compressed = $minified_name;
}
//Echo or return
if( $this->settings['echo'] )
{
echo $this->compressed;
}
else
{
return $this->compressed;
}
} //end merge()
/**
* Output any return data to the javascript console/source of page
* Usage (assuming minifier is initiated as $minifier):
* <?php $minifier->logs(); ?>
*
* @param none
* @return string
*/
public function logs()
{
//Add the timer the console.log output if desired
if( $this->settings['timer'] )
{
self::$messages[]['Minifier Log: timer'] = 'MagicMin processed and loaded in '. ( microtime( true ) - $this->mtime ) .' seconds';
}
if( !empty( self::$messages ) )
{
echo PHP_EOL . '<script>' . PHP_EOL;
foreach( self::$messages as $this->data )
{