Issues (5)

src/AbstractCachingProxy.php (4 issues)

1
<?php
2
/*------------------------------------------------------------------------------
3
4
   Project  : CachingProxy
5
   Filename : src/AbstractCachingProxy.php
6
   Autor    : (c) Sebastian Krüger <[email protected]>
7
   Date     : 15.09.2013
8
9
   For the full copyright and license information, please view the LICENSE
10
   file that was distributed with this source code.
11
12
   Description: Basisklasse die einen Mechanismus zu Cachen von Dateien auf dem
13
                Server implemeniert. Anwendung ist später für CSS und Javscript
14
                Dateien vorgesehen
15
16
                Die zu Cachenden Dateien werden zu einer gesammten Datei zusammengefasst
17
                Externe Dateien werden nicht zusammengefasst, sondern vorerst einfach nur
18
                als Einbindung ausgegeben. Falls vorhanden wird die minifizierte Version der
19
                Datei vorgezogen. Zu guter letzt werden die Dateien auch noch per GZ gepackt
20
                um statische gepackte Dateien anbieten zu können
21
22
  ----------------------------------------------------------------------------*/
23
24
namespace secra\CachingProxy;
25
26
abstract class AbstractCachingProxy
27
{
28
    protected $internfilelist = array();      // array with files that should be cached later
29
    private $externfilelist = array();        // array with extern files
30
31
    protected $docrootpath = null;            // webserver document root path
32
    protected $cachepath = null;              // absolut path on webserver were cached files should be placed
33
    protected $relcachepath = null;           // relative cachepath for scripttags in html
34
35
    // In debugmode every file will be include in a single tag without modification
36
    protected $debugmode = false;
37
38
    /**
39
     * Implement later to set the document rootpath and cachepath
40
     *
41
     * @param  string $webserverRootPath     absolut path to webserver root
42
     * @param  string $cachePath             path to cachefile location based on webserver root path
43
     *
44
     * @return AbstractCachingProxy|null     objectinstance
45
    */
46
    public function __construct($webserverRootPath, $cachePath)
47
    {
48
        $this->setWebserverRootPath($webserverRootPath);
49
        $this->setCachepath($cachePath);
50
    }
51
52
    /**
53
     * Implement later html code return
54
     *
55
     * Implement this to get the specific html head code
56
     *
57
     * @codeCoverageIgnore
58
     *
59
     * @return string   the html scripttag code
60
    */
61
    abstract public function getIncludeHtml();
62
63
    /**
64
     * Delivers extension for cached files
65
     *
66
     * @codeCoverageIgnore
67
     *
68
     * @return string   file extension
69
     *
70
     */
71
    abstract protected function getCacheFileExtension();
72
73
    /**
74
     * Add files to proxy
75
     *
76
     * Add intern, project relative files or extern files, on different domain
77
     * to the filelist
78
     *
79
     * @param  string $filename     the filepath/URL to script
80
     *
81
     * @return boolean             false on error
82
     */
83
    public function addFile($filename)
84
    {
85
        // Fügt eine Datei zur Cacheliste hinzu, es wird hier schon nach internen oder
86
        // Externen Dateien unterschieden beginnen z.B. mit http, https, ftp und dann ://
87
        // or protocoll less // links
88
        if (!preg_match("#^[a-z]{3,5}://#i", $filename) && !preg_match("#^//#i", $filename)) {
89
            // intern files, work for the cache
90
            $absolutfilename = self::makeAbsolutPath($filename);
0 ignored issues
show
Bug Best Practice introduced by
The method secra\CachingProxy\Abstr...roxy::makeAbsolutPath() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

90
            /** @scrutinizer ignore-call */ 
91
            $absolutfilename = self::makeAbsolutPath($filename);
Loading history...
91
            if (!file_exists($absolutfilename)) {
92
                // the file did't exist
93
                return false;
94
            }
95
96
            // Falls möglich Minifizierte Version der Datei benutzen
97
            $minfilename = self::makeMinifiPath($absolutfilename);
0 ignored issues
show
Bug Best Practice introduced by
The method secra\CachingProxy\Abstr...Proxy::makeMinifiPath() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

97
            /** @scrutinizer ignore-call */ 
98
            $minfilename = self::makeMinifiPath($absolutfilename);
Loading history...
98
99
            if (file_exists($minfilename)) {
100
                // Es scheint jemand die Datei gepackt zu haben
101
                $absolutfilename = $minfilename;
102
            }
103
104
            $this->internfilelist[] = $absolutfilename;
105
        } else {
106
            // Ist wohl eine Externe Pfadangabe, kann nicht gechached werden
107
            $this->externfilelist[] = $filename;
108
        }
109
        return true;
110
    }
111
112
    /**
113
     * Return list of all files can be include
114
     *
115
     * First deliver all intern then all extern files
116
     * the user can decide by himself, what he would like to do with the list
117
     *
118
     * @return string[]       list of files
119
     */
120
    public function getIncludeFileset()
121
    {
122
        // Return list of all files that can include
123
        // first all intern then all extern files
124
        // the user can decide by himself, what he would like to do with the list
125
126
        // Exclude double files from filelist
127
        $this->internfilelist = array_unique($this->internfilelist);
128
        $this->externfilelist = array_unique($this->externfilelist);
129
130
        $returnfilelist = array();
131
132
        // generate cachefile
133
        // the return will not be used in debugmode, but generate the files anyway
134
        $oneModifiedCacheFile = $this->getCacheFile();
135
136
        if ($this->debugmode===false) {
137
            // put intern files into the cached version
138
            if ($oneModifiedCacheFile!=null) {
139
                // only replace the intern file list, if theres intern files and the modified cache file exits
140
                $returnfilelist[] = $oneModifiedCacheFile;
141
            }
142
        } else {
143
            // we are in debugmode!
144
            // only put the internfiles to the list of returned files
145
            foreach ($this->internfilelist as $file) {
146
                // strip the absolut dir for inclusion and put it to the list
147
                // use the $ as reg_exp separater because don't expect it in path
148
                $returnfilelist[] = "/".preg_replace("$^".($this->docrootpath)."$", "", $file);
149
            }
150
        }
151
152
        // extern files will only add to the list
153
        foreach ($this->externfilelist as $file) {
154
            $returnfilelist[] = $file;
155
        }
156
157
        return $returnfilelist;
158
    }
159
160
    /**
161
     * Sweet as simple ... activate the debugmode
162
     *
163
     * @return null
164
     */
165
    public function enableDebugmode()
166
    {
167
        // sweet as simple ... activate the debugmode
168
        $this->debugmode=true;
169
        return null;
170
    }
171
172
    /**
173
     * belive it or not ... deactivate the debugmode
174
     *
175
     * @return null
176
     */
177
    public function disableDebugmode()
178
    {
179
        $this->debugmode=false;
180
        return null;
181
    }
182
183
    /**
184
     * Set the relative Cachepath
185
     * use simple $_SERVER["DOCUMENT_ROOT"] to set this value
186
     *
187
     * Set absolut webserver rootpath
188
     *
189
     * @param string $documentRootPath     path to webserverroot
190
     *
191
     * @return boolean     false on error
192
     */
193
    protected function setWebserverRootPath($documentRootPath)
194
    {
195
        // Reset the documentrootpath
196
        $this->docrootpath = null;
197
198
        // Add trailing slash if not there
199
        if (!preg_match("#/$#", $documentRootPath)) {
200
            $documentRootPath .= "/";
201
        }
202
203
        if (file_exists($documentRootPath)) {
204
            $this->docrootpath = $documentRootPath;
205
            return true;
206
        } else {
207
            return false;
208
        }
209
    }
210
211
    /**
212
     * Set path to cachefile folder
213
     *
214
     * The cachingpath must be set relativ from docroot of project
215
     *
216
     * @param string $cachepath     path to cachefilefolder
217
     *
218
     * @return boolean              true if cachefolder exist, false if not
219
     */
220
    protected function setCachepath($cachepath)
221
    {
222
223
        // try to make cachepath absolut
224
        $absolutcachepath = self::makeAbsolutPath($cachepath);
0 ignored issues
show
Bug Best Practice introduced by
The method secra\CachingProxy\Abstr...roxy::makeAbsolutPath() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

224
        /** @scrutinizer ignore-call */ 
225
        $absolutcachepath = self::makeAbsolutPath($cachepath);
Loading history...
225
226
        // check if path exist could be false/null because the use of makeAbsolutPath()!!
227
        if (is_dir($absolutcachepath)) {
228
229
            // extra check on trailing slash of $absolutcachepath because of realpath function use
230
            if (!preg_match("#/$#", $absolutcachepath)) {
231
                $absolutcachepath .= "/";
232
            }
233
234
            $this->cachepath=$absolutcachepath;
235
236
            // check if path has trailing slash, if not add now
237
            if (!preg_match("#/$#", $cachepath)) {
238
                $cachepath .= "/";
239
            }
240
241
            // check if path begin with slash, if not add it now because
242
            // later every intern files will include absolut to the webserver
243
            // root path, also importent, if mode rewrite is in use on the server
244
            if (!preg_match("#^/#", $cachepath)) {
245
                $cachepath = "/".$cachepath;
246
            }
247
248
            $this->relcachepath=$cachepath;
249
            return true;
250
        } else {
251
            // folder did't exist
252
            // TODO: throw next time an error!
253
            return false;
254
        }
255
    }
256
257
    /**
258
     *
259
     * Predefined Function to do something content specific work in
260
     * concrete classes right befor the minify process
261
     * default do nothing and return the sting one2one
262
     *
263
     * @param string $filecontent     filecontent to process
264
     * @param string $filepath        path to the file to convert it later
265
     *
266
     * @return string     modified filecontent
267
     */
268
    protected function modifyFilecontent($filecontent, $filepath)
269
    {
270
        // default -> return content one2one and ignore the $filepath
271
        return $filecontent;
272
    }
273
274
    /**
275
     * Relative to absolut path
276
     *
277
     * Convert relativ path webserver root path to
278
     * absolut path from root in file system
279
     *
280
     * @param string $path     relative path to webserver root
281
     *
282
     * @return string         absolut path to webserver root
283
     */
284
    private function makeAbsolutPath($path)
285
    {
286
        // Note, if file/folder don't exists, realpath will return false
287
        return realpath($this->docrootpath.$path);
288
    }
289
290
    /**
291
     * Add .min in file path
292
     *
293
     * Check if minified version of file exits
294
     * is exits use it
295
     *
296
     * @param string $path    path to notminified version of file
297
     *
298
     * @return string       path to minified version of file
299
     */
300
    private function makeMinifiPath($path)
301
    {
302
        // check if there's a minified version of file, if yes there min version will be used
303
304
        // split path at the dots
305
        $splitpath = explode(".", $path);
306
307
        $newfragments = array();
308
309
        for ($i=0; $i<count($splitpath); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
310
            if ($i==(count($splitpath)-1)) {
311
                // insert "min" bevor last element (file ending)
312
                $newfragments[]="min";
313
            }
314
            $newfragments[]=$splitpath[$i];
315
        }
316
317
        // now put the puzzelpices together
318
        return implode(".", $newfragments);
319
    }
320
321
    /**
322
     * Return one cached file
323
     *
324
     * Check if cached and gzipped version of file exits
325
     * if not convert all intern files to one file and
326
     * write it to one cache
327
     *
328
     * @return string|false|null       path to cached file or null if no intern files and false on error
329
     */
330
    private function getCacheFile()
331
    {
332
        // ask if there file signature match with, requested files in the file list
333
        $cachefilesignature = $this->calculateFileSignature();
334
335
        // connect the path, related to document root
336
        $cachefile = $cachefilesignature.$this->getCacheFileExtension();
337
338
        $absolutcachepath = $this->cachepath.$cachefile;
339
340
        // set return value null in case there are no internfiles
341
        $returnfile = null;
342
343
        if ($cachefilesignature!=null) {
344
            // The cachefilesignature has to be different from null to start
345
            if (!file_exists($absolutcachepath)) {
346
                // the file has never been written, write now -> the hard way!
347
                // put files together
348
                foreach ($this->internfilelist as $file) {
349
                    // read content of current file
350
                    // if overwritten, modfiy the content and put the files together in one string
351
                    $filecontent = $this->modifyFilecontent(file_get_contents($file), $file);
352
353
                    // to be safe, add new line
354
                    $filecontent .= "\n";
355
356
                    // append content while writing and look file on other access tries!
357
                    if (file_put_contents($absolutcachepath, $filecontent, FILE_APPEND | LOCK_EX)===false) {
358
                        // TODO: this error check won't work well, maybe better use other function to write the file
359
                        return false;
360
                    }
361
                }
362
                // short delay to be safe
363
                usleep(5000);
364
365
                // now make the gzip version, once we on the way
366
                file_put_contents($absolutcachepath.".gz", gzencode(file_get_contents($absolutcachepath), 9));
367
            }
368
369
            // Files in Cachefolder still exits, assume we created it at another run
370
            // don't create them once again only build the path an return it
371
            $returnfile = $this->relcachepath.$cachefile;
372
        }
373
374
        return $returnfile;
375
    }
376
377
    /**
378
     * Calculate a signature to detect file modification
379
     *
380
     * Calculate a signature of all intern files
381
     * base parameters are filename and file modfied date
382
     * hash function is md5
383
     *
384
     * @return string|null       signature
385
     */
386
    private function calculateFileSignature()
387
    {
388
        // create a signature with all files in intern array structure
389
        // to make it unified, every filname and filechange date will calculate
390
        // together and return as md5 sum
391
        $tempstingbase = "";
392
393
        foreach ($this->internfilelist as $file) {
394
            $tempstingbase .= $file."->";
395
            $tempstingbase .= "(".filemtime($file).") ";
396
        }
397
398
        // when there are no intern files, function return null
399
        $signature=null;
400
401
        if (count($this->internfilelist)>0) {
402
            // there are intern files - simple md5 should do, no secure risk
403
            $signature = md5($tempstingbase);
404
        }
405
406
        return $signature;
407
    }
408
409
    // TODO: build cleanup function for old cached files
410
}