1: <?php
2:
3: /*
4: * The MIT License
5: *
6: * Copyright 2015 LUDATO.
7: *
8: * Permission is hereby granted, free of charge, to any person obtaining a copy
9: * of this software and associated documentation files (the "Software"), to deal
10: * in the Software without restriction, including without limitation the rights
11: * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12: * copies of the Software, and to permit persons to whom the Software is
13: * furnished to do so, subject to the following conditions:
14: *
15: * The above copyright notice and this permission notice shall be included in
16: * all copies or substantial portions of the Software.
17: *
18: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20: * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21: * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22: * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23: * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24: * THE SOFTWARE.
25: */
26:
27: namespace Ludato;
28:
29: /**
30: * LUDATO HyperCache
31: * @author David Kostal
32: * @license http://opensource.org/licenses/MIT MIT
33: */
34: Class HyperCache {
35:
36: private $fullDirectory;
37: private $fullFilename;
38: private $cacheDirectory;
39: private $pagePath;
40: private $fullPrependPath;
41: private $fullAppendPath;
42: private $dev;
43:
44: /**
45: * @var string Valid PHP code (without opening tags) to be prepended to cache
46: */
47: public $prepend;
48:
49: /**
50: * @var string Valid PHP code (without opening tags) to be appended to cache
51: */
52: public $append;
53:
54: /**
55: *
56: * @var boolean Determines if prepended code should be eval()'ed when generating cache (alternative - put code before startCache())
57: */
58: public $evalPrepend;
59:
60: /**
61: *
62: * @var boolean Determines if appended code should be eval()'ed when generating cache (alternative - put code after saveCache())
63: */
64: public $evalAppend;
65:
66: /**
67: * Saving params and configuring
68: * Note: Paths are saved url-encoded
69: * @param string $directory Path to directory for caching
70: * @param string|null $page Full page name, including extension (if you want it determined automatically using PHP_SELF, use NULL)
71: * @param string $param Parameters of caching (eg. for details.php it would be ID). You can combine whatever you want here
72: * @param boolean $dev Determines if development texts are shown
73: */
74: function __construct($directory, $page = NULL, $param = "default", $dev = FALSE) {
75: mb_internal_encoding("UTF-8");
76: if ($page === NULL) {
77: $pathinfo = pathinfo($_SERVER['PHP_SELF']);
78: $page = $pathinfo['basename'];
79: }
80: /*
81: if (mb_strpos($directory, "./") === 1) {
82: $directory = "./" . $directory;
83: } */
84: if (mb_substr($directory, -1) === DIRECTORY_SEPARATOR) {
85: $directory = rtrim($directory, DIRECTORY_SEPARATOR);
86: }
87: $this->fullDirectory = urlencode($directory) . DIRECTORY_SEPARATOR . urlencode($page) . DIRECTORY_SEPARATOR . urlencode($param) . DIRECTORY_SEPARATOR;
88:
89: $this->cacheDirectory = urlencode($directory);
90: $this->pagePath = urlencode($directory) . DIRECTORY_SEPARATOR . urlencode($page) . DIRECTORY_SEPARATOR;
91:
92: if ($dev) {
93: $this->dev = TRUE;
94: } else {
95: $this->dev = FALSE;
96: }
97:
98: $this->fullFilename = $this->fullDirectory . "cached";
99: $this->fullPrependPath = $this->fullDirectory . "prepend" . ".php";
100: $this->fullAppendPath = $this->fullDirectory . "append" . ".php";
101: }
102:
103: /**
104: * Starts caching
105: * @return void
106: */
107: function startCache() {
108: if ($this->evalPrepend) {
109: eval($this->prepend);
110: }
111: flush();
112: ob_start();
113: }
114:
115: /**
116: * Saves cached file and flushes the buffer (shows output)
117: * @return boolean
118: */
119: function saveCache() {
120: if (is_writable($this->fullDirectory)) {
121: $dirmade = TRUE;
122: } else {
123: $dirmade = @mkdir($this->fullDirectory, 0777, TRUE);
124: }
125:
126: if (!$dirmade) {
127: throw new \Exception("Caching directory not writeable", 0);
128: }
129:
130: if (!is_file($this->cacheDirectory . ".htaccess")) {
131: $hw = fopen($this->cacheDirectory . ".htaccess", "w");
132: $htaccess = <<<EOT
133: Order deny,allow
134: Deny from all
135: EOT;
136: fputs($hw, $htaccess, strlen($htaccess));
137: fclose($hw);
138: }
139:
140: $page = ob_get_contents();
141: ob_end_clean();
142: //$time = time();
143: @unlink($this->fullFilename);
144: @unlink($this->fullPrependPath);
145: @unlink($this->fullAppendPath);
146: //chmod($file, 0777);
147:
148: $fw = fopen($this->fullFilename, "w");
149: fputs($fw, $page, strlen($page));
150: fclose($fw);
151:
152: if ($this->prepend) {
153: $fwp = fopen($this->fullPrependPath, "w");
154: $prepend = "<?php " . $this->prepend . "?>";
155: fputs($fwp, $prepend, strlen($prepend));
156: fclose($fwp);
157: }
158:
159: if ($this->append) {
160: $fwp = fopen($this->fullAppendPath, "w");
161: $append = "<?php " . $this->append . "?>";
162: fputs($fwp, $append, strlen($append));
163: fclose($fwp);
164: }
165:
166: echo $page;
167: if ($this->dev) {
168: echo 'cache generated';
169: }
170: if ($this->evalAppend) {
171: eval($this->append);
172: }
173: return TRUE;
174: }
175:
176: /**
177: * Loads cached file
178: * @return void
179: */
180: function getCache() {
181: if ($this->dev) {
182: $time_pre = microtime(true);
183: }
184:
185: if (is_file($this->fullPrependPath)) {
186: require $this->fullPrependPath;
187: }
188:
189: readfile($this->fullFilename);
190:
191: if (is_file($this->fullAppendPath)) {
192: require $this->fullAppendPath;
193: }
194:
195: if ($this->dev) {
196: $time_post = microtime(true);
197: $exec_time = ($time_post - $time_pre) * 1000;
198: echo "Loaded from cache ({$this->fullFilename} in {$exec_time} ms)"; //DEBUG!
199: }
200: }
201:
202: /**
203: * Shows cache and if file is cached, otherwise starts caching and executing the rest of the file
204: * @return void
205: */
206: function autoLoadCache() {
207: if ($this->isCached()) {
208: $this->getCache();
209: die();
210: } else {
211: $this->startCache();
212: }
213: }
214:
215: /**
216: * Automaically ending and saving cache
217: * @return void
218: */
219: function autoEndCache() {
220: try {
221: $this->saveCache();
222: } catch (\Exception $e) {
223: echo "\n" . $e . "\n";
224: die();
225: }
226: }
227:
228: /**
229: * Returns if file is cached
230: * @return boolean Returns true if cached, else returns false.
231: */
232: function isCached() {
233: if (is_readable($this->fullFilename)) {
234: return TRUE;
235: } else {
236: return FALSE;
237: }
238: }
239:
240: /**
241: * Deletes cache for current page and params (eg. You have CMS and publish a new article. You need to purge just article_list.php, not article_details.php. If user publishes comment, purge just article_details.php with param of article name/ID)
242: * @return void
243: */
244: function purgeCurrent() {
245: $this->recursiveDelete($this->fullDirectory);
246: }
247:
248: /**
249: * Purges all files specified cacing directory
250: * @return void
251: */
252: function purgeAll() {
253: $this->recursiveDelete($this->cacheDirectory);
254: }
255:
256: /**
257: * Purges file for current page (eg. You have CMS and publish a new article. You need to purge just article_list.php, not article_details.php. If user publishes comment, purge just article_details.php with param of article name/ID)
258: * @return void
259: */
260: function purgePage() {
261: $this->recursiveDelete($this->pagePath);
262: }
263:
264: /**
265: * Purge cache from specified directory, for specific page (and params)
266: * @param string $directory Caching directory
267: * @param string $page Page
268: * @param string $param Params (opt.)
269: * @deprecated 0.1.0
270: */
271: function purgeCustom($directory, $page, $param = NULL) {
272: if (!(mb_substr($directory, -1) === DIRECTORY_SEPARATOR)) {
273: $directory = $directory . DIRECTORY_SEPARATOR;
274: }
275: if ($param !== NULL) {
276:
277: if (!(mb_substr($param, -1) === DIRECTORY_SEPARATOR)) {
278: $param = $param . DIRECTORY_SEPARATOR;
279: }
280:
281: $dir = urlencode($directory) . urlencode($page) . DIRECTORY_SEPARATOR . urlencode($param);
282: } else {
283: $dir = urlencode($directory) . urlencode($page) . DIRECTORY_SEPARATOR;
284: }
285: $this->recursiveDelete($dir);
286: }
287:
288: /**
289: * Delete a file or recursively delete a directory
290: * @param string $str Path to file or directory
291: * @return void
292: */
293: private function recursiveDelete($str) {
294: try {
295: if (is_file($str)) {
296: return @unlink($str);
297: } elseif (is_dir($str)) {
298: $scan = glob(rtrim($str, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '*');
299: foreach ($scan as $path) {
300: $this->recursiveDelete($path);
301: }
302: return @rmdir($str);
303: } else {
304: throw new \Exception("Invalid path", 0);
305: }
306: } catch (\Exception $e) {
307: echo $e;
308: echo "Internal HyperCache error";
309: die();
310: }
311: }
312:
313: }
314:
315: /**
316: * @todo finih this
317: * Sanitizes folder/file name
318: * @param string $str Input string
319: * @return string Sanitized string
320: */
321: /*
322: function cache_filename_sanitize($str) {
323: preg_replace("/[\/]")
324: }
325: */
326: