PageRenderTime 60ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/core/xpdo/cache/xpdocachemanager.class.php

http://github.com/modxcms/revolution
PHP | 1087 lines | 883 code | 22 blank | 182 comment | 127 complexity | 65d0660671be436c1b4cb8474f61e3b9 MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /*
  3. * Copyright 2010-2015 by MODX, LLC.
  4. *
  5. * This file is part of xPDO.
  6. *
  7. * xPDO is free software; you can redistribute it and/or modify it under the
  8. * terms of the GNU General Public License as published by the Free Software
  9. * Foundation; either version 2 of the License, or (at your option) any later
  10. * version.
  11. *
  12. * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
  13. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  14. * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License along with
  17. * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
  18. * Suite 330, Boston, MA 02111-1307 USA
  19. */
  20. /**
  21. * Classes implementing a default cache implementation for xPDO.
  22. *
  23. * @package xpdo
  24. * @subpackage cache
  25. */
  26. /**
  27. * The default cache manager implementation for xPDO.
  28. *
  29. * @package xpdo
  30. * @subpackage cache
  31. */
  32. class xPDOCacheManager {
  33. const CACHE_PHP = 0;
  34. const CACHE_JSON = 1;
  35. const CACHE_SERIALIZE = 2;
  36. const CACHE_DIR = 'objects/';
  37. const LOG_DIR = 'logs/';
  38. protected $xpdo= null;
  39. protected $caches= array();
  40. protected $options= array();
  41. protected $_umask= null;
  42. public function __construct(& $xpdo, $options = array()) {
  43. $this->xpdo= & $xpdo;
  44. $this->options= $options;
  45. $this->_umask= umask();
  46. }
  47. /**
  48. * Get an instance of a provider which implements the xPDOCache interface.
  49. */
  50. public function & getCacheProvider($key = '', $options = array()) {
  51. $objCache = null;
  52. if (empty($key)) {
  53. $key = $this->getOption(xPDO::OPT_CACHE_KEY, $options, 'default');
  54. }
  55. $objCacheClass= 'xPDOFileCache';
  56. if (!isset($this->caches[$key]) || !is_object($this->caches[$key])) {
  57. if ($cacheClass = $this->getOption($key . '_' . xPDO::OPT_CACHE_HANDLER, $options, $this->getOption(xPDO::OPT_CACHE_HANDLER, $options))) {
  58. $cacheClass = $this->xpdo->loadClass($cacheClass, XPDO_CORE_PATH, false, true);
  59. if ($cacheClass) {
  60. $objCacheClass= $cacheClass;
  61. }
  62. }
  63. $options[xPDO::OPT_CACHE_KEY]= $key;
  64. $this->caches[$key] = new $objCacheClass($this->xpdo, $options);
  65. if (empty($this->caches[$key]) || !$this->caches[$key]->isInitialized()) {
  66. $this->caches[$key] = new xPDOFileCache($this->xpdo, $options);
  67. }
  68. $objCache = $this->caches[$key];
  69. $objCacheClass= get_class($objCache);
  70. } else {
  71. $objCache =& $this->caches[$key];
  72. $objCacheClass= get_class($objCache);
  73. }
  74. if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Returning {$objCacheClass}:{$key} cache provider from available providers: " . print_r(array_keys($this->caches), 1));
  75. return $objCache;
  76. }
  77. /**
  78. * Get an option from supplied options, the cacheManager options, or xpdo itself.
  79. *
  80. * @param string $key Unique identifier for the option.
  81. * @param array $options A set of explicit options to override those from xPDO or the
  82. * xPDOCacheManager implementation.
  83. * @param mixed $default An optional default value to return if no value is found.
  84. * @return mixed The value of the option.
  85. */
  86. public function getOption($key, $options = array(), $default = null) {
  87. $option = $default;
  88. if (is_array($key)) {
  89. if (!is_array($option)) {
  90. $default= $option;
  91. $option= array();
  92. }
  93. foreach ($key as $k) {
  94. $option[$k]= $this->getOption($k, $options, $default);
  95. }
  96. } elseif (is_string($key) && !empty($key)) {
  97. if (is_array($options) && !empty($options) && array_key_exists($key, $options)) {
  98. $option = $options[$key];
  99. } elseif (is_array($this->options) && !empty($this->options) && array_key_exists($key, $this->options)) {
  100. $option = $this->options[$key];
  101. } else {
  102. $option = $this->xpdo->getOption($key, null, $default);
  103. }
  104. }
  105. return $option;
  106. }
  107. /**
  108. * Get default folder permissions based on umask
  109. *
  110. * @return integer The default folder permissions.
  111. */
  112. public function getFolderPermissions() {
  113. $perms = 0777;
  114. $perms = $perms & (0777 - $this->_umask);
  115. return $perms;
  116. }
  117. /**
  118. * Get default file permissions based on umask
  119. *
  120. * @return integer The default file permissions.
  121. */
  122. public function getFilePermissions() {
  123. $perms = 0666;
  124. $perms = $perms & (0777 - $this->_umask);
  125. return $perms;
  126. }
  127. /**
  128. * Get the absolute path to a writable directory for storing files.
  129. *
  130. * @access public
  131. * @return string The absolute path of the xPDO cache directory.
  132. */
  133. public function getCachePath() {
  134. $cachePath= false;
  135. if (empty($this->xpdo->cachePath)) {
  136. if (!isset ($this->xpdo->config['cache_path'])) {
  137. while (true) {
  138. if (!empty ($_ENV['TMP'])) {
  139. if ($cachePath= strtr($_ENV['TMP'], '\\', '/'))
  140. break;
  141. }
  142. if (!empty ($_ENV['TMPDIR'])) {
  143. if ($cachePath= strtr($_ENV['TMPDIR'], '\\', '/'))
  144. break;
  145. }
  146. if (!empty ($_ENV['TEMP'])) {
  147. if ($cachePath= strtr($_ENV['TEMP'], '\\', '/'))
  148. break;
  149. }
  150. if ($temp_file= @ tempnam(md5(uniqid(rand(), TRUE)), '')) {
  151. $cachePath= strtr(dirname($temp_file), '\\', '/');
  152. @ unlink($temp_file);
  153. }
  154. break;
  155. }
  156. if ($cachePath) {
  157. if ($cachePath[strlen($cachePath) - 1] != '/') $cachePath .= '/';
  158. $cachePath .= '.xpdo-cache';
  159. }
  160. }
  161. else {
  162. $cachePath= strtr($this->xpdo->config['cache_path'], '\\', '/');
  163. }
  164. } else {
  165. $cachePath= $this->xpdo->cachePath;
  166. }
  167. if ($cachePath) {
  168. $perms = $this->getOption('new_folder_permissions', null, $this->getFolderPermissions());
  169. if (is_string($perms)) $perms = octdec($perms);
  170. if (@ $this->writeTree($cachePath, $perms)) {
  171. if ($cachePath[strlen($cachePath) - 1] != '/') $cachePath .= '/';
  172. if (!is_writeable($cachePath)) {
  173. @ chmod($cachePath, $perms);
  174. }
  175. } else {
  176. $cachePath= false;
  177. }
  178. }
  179. return $cachePath;
  180. }
  181. /**
  182. * Writes a file to the filesystem.
  183. *
  184. * @access public
  185. * @param string $filename The absolute path to the location the file will
  186. * be written in.
  187. * @param string $content The content of the newly written file.
  188. * @param string $mode The php file mode to write in. Defaults to 'wb'. Note that this method always
  189. * uses a (with b or t if specified) to open the file and that any mode except a means existing file
  190. * contents will be overwritten.
  191. * @param array $options An array of options for the function.
  192. * @return int|bool Returns the number of bytes written to the file or false on failure.
  193. */
  194. public function writeFile($filename, $content, $mode= 'wb', $options= array()) {
  195. $written= false;
  196. if (!is_array($options)) {
  197. $options = is_scalar($options) && !is_bool($options) ? array('new_folder_permissions' => $options) : array();
  198. }
  199. $dirname= dirname($filename);
  200. if (!file_exists($dirname)) {
  201. $this->writeTree($dirname, $options);
  202. }
  203. $mode = str_replace('+', '', $mode);
  204. switch ($mode[0]) {
  205. case 'a':
  206. $append = true;
  207. break;
  208. default:
  209. $append = false;
  210. break;
  211. }
  212. $fmode = (strlen($mode) > 1 && in_array($mode[1], array('b', 't'))) ? "a{$mode[1]}" : 'a';
  213. $file= @fopen($filename, $fmode);
  214. if ($file) {
  215. if ($append === true) {
  216. $written= fwrite($file, $content);
  217. } else {
  218. $locked = false;
  219. $attempt = 1;
  220. $attempts = (integer) $this->getOption(xPDO::OPT_CACHE_ATTEMPTS, $options, 1);
  221. $attemptDelay = (integer) $this->getOption(xPDO::OPT_CACHE_ATTEMPT_DELAY, $options, 1000);
  222. while (!$locked && ($attempts === 0 || $attempt <= $attempts)) {
  223. if ($this->getOption('use_flock', $options, true)) {
  224. $locked = flock($file, LOCK_EX | LOCK_NB);
  225. } else {
  226. $lockFile = $this->lockFile($filename, $options);
  227. $locked = $lockFile != false;
  228. }
  229. if (!$locked && $attemptDelay > 0 && ($attempts === 0 || $attempt < $attempts)) {
  230. usleep($attemptDelay);
  231. }
  232. $attempt++;
  233. }
  234. if ($locked) {
  235. fseek($file, 0);
  236. ftruncate($file, 0);
  237. $written= fwrite($file, $content);
  238. if ($this->getOption('use_flock', $options, true)) {
  239. flock($file, LOCK_UN);
  240. } else {
  241. $this->unlockFile($filename, $options);
  242. }
  243. }
  244. }
  245. @fclose($file);
  246. if ($written !== false && $fileMode = $this->getOption('new_file_permissions', $options, false)) {
  247. if (is_string($fileMode)) $fileMode = octdec($fileMode);
  248. @ chmod($filename, $fileMode);
  249. }
  250. }
  251. return ($written !== false);
  252. }
  253. /**
  254. * Add an exclusive lock to a file for atomic write operations in multi-threaded environments.
  255. *
  256. * xPDO::OPT_USE_FLOCK must be set to false (or 0) or xPDO will assume flock is reliable.
  257. *
  258. * @param string $file The name of the file to lock.
  259. * @param array $options An array of options for the process.
  260. * @return boolean True only if the current process obtained an exclusive lock for writing.
  261. */
  262. public function lockFile($file, array $options = array()) {
  263. $locked = false;
  264. $lockDir = $this->getOption('lock_dir', $options, $this->getCachePath() . 'locks' . DIRECTORY_SEPARATOR);
  265. if ($this->writeTree($lockDir, $options)) {
  266. $lockFile = $this->lockFileName($file, $options);
  267. if (!file_exists($lockFile)) {
  268. $myPID = (XPDO_CLI_MODE || !isset($_SERVER['SERVER_ADDR']) ? gethostname() : $_SERVER['SERVER_ADDR']) . '.' . getmypid();
  269. $myPID .= mt_rand();
  270. $tmpLockFile = "{$lockFile}.{$myPID}";
  271. if (file_put_contents($tmpLockFile, $myPID)) {
  272. if (link($tmpLockFile, $lockFile)) {
  273. $locked = true;
  274. }
  275. @unlink($tmpLockFile);
  276. }
  277. }
  278. } else {
  279. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "The lock_dir at {$lockDir} is not writable and could not be created");
  280. }
  281. if (!$locked) $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Attempt to lock file {$file} failed");
  282. return $locked;
  283. }
  284. /**
  285. * Release an exclusive lock on a file created by lockFile().
  286. *
  287. * @param string $file The name of the file to unlock.
  288. * @param array $options An array of options for the process.
  289. */
  290. public function unlockFile($file, array $options = array()) {
  291. @unlink($this->lockFileName($file, $options));
  292. }
  293. /**
  294. * Get an absolute path to a lock file for a specified file path.
  295. *
  296. * @param string $file The absolute path to get the lock filename for.
  297. * @param array $options An array of options for the process.
  298. * @return string The absolute path for the lock file
  299. */
  300. protected function lockFileName($file, array $options = array()) {
  301. $lockDir = $this->getOption('lock_dir', $options, $this->getCachePath() . 'locks' . DIRECTORY_SEPARATOR);
  302. return $lockDir . preg_replace('/\W/', '_', $file) . $this->getOption(xPDO::OPT_LOCKFILE_EXTENSION, $options, '.lock');
  303. }
  304. /**
  305. * Recursively writes a directory tree of files to the filesystem
  306. *
  307. * @access public
  308. * @param string $dirname The directory to write
  309. * @param array $options An array of options for the function. Can also be a value representing
  310. * a permissions mode to write new directories with, though this is deprecated.
  311. * @return boolean Returns true if the directory was successfully written.
  312. */
  313. public function writeTree($dirname, $options= array()) {
  314. $written= false;
  315. if (!empty ($dirname)) {
  316. if (!is_array($options)) $options = is_scalar($options) && !is_bool($options) ? array('new_folder_permissions' => $options) : array();
  317. $mode = $this->getOption('new_folder_permissions', $options, $this->getFolderPermissions());
  318. if (is_string($mode)) $mode = octdec($mode);
  319. $dirname= strtr(trim($dirname), '\\', '/');
  320. if ($dirname[strlen($dirname) - 1] == '/') $dirname = substr($dirname, 0, strlen($dirname) - 1);
  321. if (is_dir($dirname) || (is_writable(dirname($dirname)) && @mkdir($dirname, $mode))) {
  322. $written= true;
  323. } elseif (!$this->writeTree(dirname($dirname), $options)) {
  324. $written= false;
  325. } else {
  326. $written= @ mkdir($dirname, $mode);
  327. }
  328. if ($written) {
  329. @ chmod($dirname, $mode);
  330. }
  331. }
  332. return $written;
  333. }
  334. /**
  335. * Copies a file from a source file to a target directory.
  336. *
  337. * @access public
  338. * @param string $source The absolute path of the source file.
  339. * @param string $target The absolute path of the target destination
  340. * directory.
  341. * @param array $options An array of options for this function.
  342. * @return boolean|array Returns true if the copy operation was successful, or a single element
  343. * array with filename as key and stat results of the successfully copied file as a result.
  344. */
  345. public function copyFile($source, $target, $options = array()) {
  346. $copied= false;
  347. if (!is_array($options)) $options = is_scalar($options) && !is_bool($options) ? array('new_file_permissions' => $options) : array();
  348. if (func_num_args() === 4) $options['new_folder_permissions'] = func_get_arg(3);
  349. if ($this->writeTree(dirname($target), $options)) {
  350. $existed= file_exists($target);
  351. if ($existed && $this->getOption('copy_newer_only', $options, false) && (($ttime = filemtime($target)) > ($stime = filemtime($source)))) {
  352. $this->xpdo->log(xPDO::LOG_LEVEL_INFO, "xPDOCacheManager->copyFile(): Skipping copy of newer file {$target} ({$ttime}) from {$source} ({$stime})");
  353. } else {
  354. $copied= copy($source, $target);
  355. }
  356. if ($copied) {
  357. if (!$this->getOption('copy_preserve_permissions', $options, false)) {
  358. $fileMode = $this->getOption('new_file_permissions', $options, $this->getFilePermissions());
  359. if (is_string($fileMode)) $fileMode = octdec($fileMode);
  360. @ chmod($target, $fileMode);
  361. }
  362. if ($this->getOption('copy_preserve_filemtime', $options, true)) @ touch($target, filemtime($source));
  363. if ($this->getOption('copy_return_file_stat', $options, false)) {
  364. $stat = stat($target);
  365. if (is_array($stat)) {
  366. $stat['overwritten']= $existed;
  367. $copied = array($target => $stat);
  368. }
  369. }
  370. }
  371. }
  372. if (!$copied) {
  373. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOCacheManager->copyFile(): Could not copy file {$source} to {$target}");
  374. }
  375. return $copied;
  376. }
  377. /**
  378. * Recursively copies a directory tree from a source directory to a target
  379. * directory.
  380. *
  381. * @access public
  382. * @param string $source The absolute path of the source directory.
  383. * @param string $target The absolute path of the target destination directory.
  384. * @param array $options An array of options for this function.
  385. * @return array|boolean Returns an array of all files and folders that were copied or false.
  386. */
  387. public function copyTree($source, $target, $options= array()) {
  388. $copied= false;
  389. $source= strtr($source, '\\', '/');
  390. $target= strtr($target, '\\', '/');
  391. if ($source[strlen($source) - 1] == '/') $source = substr($source, 0, strlen($source) - 1);
  392. if ($target[strlen($target) - 1] == '/') $target = substr($target, 0, strlen($target) - 1);
  393. if (is_dir($source . '/')) {
  394. if (!is_array($options)) $options = is_scalar($options) && !is_bool($options) ? array('new_folder_permissions' => $options) : array();
  395. if (func_num_args() === 4) $options['new_file_permissions'] = func_get_arg(3);
  396. if (!is_dir($target . '/')) {
  397. $this->writeTree($target . '/', $options);
  398. }
  399. if (is_dir($target)) {
  400. if (!is_writable($target)) {
  401. $dirMode = $this->getOption('new_folder_permissions', $options, $this->getFolderPermissions());
  402. if (is_string($dirMode)) $dirMode = octdec($dirMode);
  403. if (! @ chmod($target, $dirMode)) {
  404. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "{$target} is not writable and permissions could not be modified.");
  405. }
  406. }
  407. if ($handle= @ opendir($source)) {
  408. $excludeItems = $this->getOption('copy_exclude_items', $options, array('.', '..','.svn','.svn/','.svn\\'));
  409. $excludePatterns = $this->getOption('copy_exclude_patterns', $options);
  410. $copiedFiles = array();
  411. $error = false;
  412. while (false !== ($item= readdir($handle))) {
  413. $copied = false;
  414. if (is_array($excludeItems) && !empty($excludeItems) && in_array($item, $excludeItems)) continue;
  415. if (is_array($excludePatterns) && !empty($excludePatterns) && $this->matches($item, $excludePatterns)) continue;
  416. $from= $source . '/' . $item;
  417. $to= $target . '/' . $item;
  418. if (is_dir($from)) {
  419. if (!($copied= $this->copyTree($from, $to, $options))) {
  420. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy directory {$from} to {$to}");
  421. $error = true;
  422. } else {
  423. $copiedFiles = array_merge($copiedFiles, $copied);
  424. }
  425. } elseif (is_file($from)) {
  426. if (!$copied= $this->copyFile($from, $to, $options)) {
  427. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy file {$from} to {$to}; could not create directory.");
  428. $error = true;
  429. } else {
  430. $copiedFiles[] = $to;
  431. }
  432. } else {
  433. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy {$from} to {$to}");
  434. }
  435. }
  436. @ closedir($handle);
  437. if (!$error) $copiedFiles[] = $target;
  438. $copied = $copiedFiles;
  439. } else {
  440. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not read source directory {$source}");
  441. }
  442. } else {
  443. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not create target directory {$target}");
  444. }
  445. } else {
  446. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Source directory {$source} does not exist.");
  447. }
  448. return $copied;
  449. }
  450. /**
  451. * Recursively deletes a directory tree of files.
  452. *
  453. * @access public
  454. * @param string $dirname An absolute path to the source directory to delete.
  455. * @param array $options An array of options for this function.
  456. * @return boolean Returns true if the deletion was successful.
  457. */
  458. public function deleteTree($dirname, $options= array('deleteTop' => false, 'skipDirs' => false, 'extensions' => array('.cache.php'))) {
  459. $result= false;
  460. if (is_dir($dirname)) { /* Operate on dirs only */
  461. if (substr($dirname, -1) != '/') {
  462. $dirname .= '/';
  463. }
  464. $result= array ();
  465. if (!is_array($options)) {
  466. $numArgs = func_num_args();
  467. $options = array(
  468. 'deleteTop' => is_scalar($options) ? (boolean) $options : false
  469. ,'skipDirs' => $numArgs > 2 ? func_get_arg(2) : false
  470. ,'extensions' => $numArgs > 3 ? func_get_arg(3) : array('.cache.php')
  471. );
  472. }
  473. $hasMore= true;
  474. if ($handle= opendir($dirname)) {
  475. $limit= 4;
  476. $extensions= $this->getOption('extensions', $options, array('.cache.php'));
  477. $excludeItems = $this->getOption('delete_exclude_items', $options, array('.', '..','.svn','.svn/','.svn\\'));
  478. $excludePatterns = $this->getOption('delete_exclude_patterns', $options);
  479. while ($hasMore && $limit--) {
  480. if (!$handle) {
  481. $handle= opendir($dirname);
  482. }
  483. $hasMore= false;
  484. while (false !== ($file= @ readdir($handle))) {
  485. if (is_array($excludeItems) && !empty($excludeItems) && in_array($file, $excludeItems)) continue;
  486. if (is_array($excludePatterns) && !empty($excludePatterns) && $this->matches($file, $excludePatterns)) continue;
  487. if ($file != '.' && $file != '..') { /* Ignore . and .. */
  488. $path= $dirname . $file;
  489. if (is_dir($path)) {
  490. $suboptions = array_merge($options, array('deleteTop' => !$this->getOption('skipDirs', $options, false)));
  491. if ($subresult= $this->deleteTree($path, $suboptions)) {
  492. $result= array_merge($result, $subresult);
  493. }
  494. }
  495. elseif (is_file($path)) {
  496. if (is_array($extensions) && !empty($extensions) && !$this->endsWith($file, $extensions)) continue;
  497. if (unlink($path)) {
  498. array_push($result, $path);
  499. } else {
  500. $hasMore= true;
  501. }
  502. }
  503. }
  504. }
  505. closedir($handle);
  506. }
  507. if ($this->getOption('deleteTop', $options, false)) {
  508. if (@ rmdir($dirname)) {
  509. array_push($result, $dirname);
  510. }
  511. }
  512. }
  513. } else {
  514. $result= false; /* return false if attempting to operate on a file */
  515. }
  516. return $result;
  517. }
  518. /**
  519. * Sees if a string ends with a specific pattern or set of patterns.
  520. *
  521. * @access public
  522. * @param string $string The string to check.
  523. * @param string|array $pattern The pattern or an array of patterns to check against.
  524. * @return boolean True if the string ends with the pattern or any of the patterns provided.
  525. */
  526. public function endsWith($string, $pattern) {
  527. $matched= false;
  528. if (is_string($string) && ($stringLen= strlen($string))) {
  529. if (is_array($pattern)) {
  530. foreach ($pattern as $subPattern) {
  531. if (is_string($subPattern) && $this->endsWith($string, $subPattern)) {
  532. $matched= true;
  533. break;
  534. }
  535. }
  536. } elseif (is_string($pattern)) {
  537. if (($patternLen= strlen($pattern)) && $stringLen >= $patternLen) {
  538. $matched= (substr($string, -$patternLen) === $pattern);
  539. }
  540. }
  541. }
  542. return $matched;
  543. }
  544. /**
  545. * Sees if a string matches a specific pattern or set of patterns.
  546. *
  547. * @access public
  548. * @param string $string The string to check.
  549. * @param string|array $pattern The pattern or an array of patterns to check against.
  550. * @return boolean True if the string matched the pattern or any of the patterns provided.
  551. */
  552. public function matches($string, $pattern) {
  553. $matched= false;
  554. if (is_string($string) && ($stringLen= strlen($string))) {
  555. if (is_array($pattern)) {
  556. foreach ($pattern as $subPattern) {
  557. if (is_string($subPattern) && $this->matches($string, $subPattern)) {
  558. $matched= true;
  559. break;
  560. }
  561. }
  562. } elseif (is_string($pattern)) {
  563. $matched= preg_match($pattern, $string);
  564. }
  565. }
  566. return $matched;
  567. }
  568. /**
  569. * Generate a PHP executable representation of an xPDOObject.
  570. *
  571. * @todo Complete $generateRelated functionality.
  572. * @todo Add stdObject support.
  573. *
  574. * @access public
  575. * @param xPDOObject $obj An xPDOObject to generate the cache file for
  576. * @param string $objName The name of the xPDOObject
  577. * @param boolean $generateObjVars If true, will also generate maps for all
  578. * object variables. Defaults to false.
  579. * @param boolean $generateRelated If true, will also generate maps for all
  580. * related objects. Defaults to false.
  581. * @param string $objRef The reference to the xPDO instance, in string
  582. * format.
  583. * @param boolean $format The format to cache in. Defaults to
  584. * xPDOCacheManager::CACHE_PHP, which is set to cache in executable PHP format.
  585. * @return string The source map file, in string format.
  586. */
  587. public function generateObject($obj, $objName, $generateObjVars= false, $generateRelated= false, $objRef= 'this->xpdo', $format= xPDOCacheManager::CACHE_PHP) {
  588. $source= false;
  589. if (is_object($obj) && $obj instanceof xPDOObject) {
  590. $className= $obj->_class;
  591. $source= "\${$objName}= \${$objRef}->newObject('{$className}');\n";
  592. $source .= "\${$objName}->fromArray(" . var_export($obj->toArray('', true), true) . ", '', true, true);\n";
  593. if ($generateObjVars && $objectVars= get_object_vars($obj)) {
  594. foreach ($objectVars as $vk => $vv) {
  595. if ($vk === 'modx') {
  596. $source .= "\${$objName}->{$vk}= & \${$objRef};\n";
  597. }
  598. elseif ($vk === 'xpdo') {
  599. $source .= "\${$objName}->{$vk}= & \${$objRef};\n";
  600. }
  601. elseif (!is_resource($vv)) {
  602. $source .= "\${$objName}->{$vk}= " . var_export($vv, true) . ";\n";
  603. }
  604. }
  605. }
  606. if ($generateRelated && !empty ($obj->_relatedObjects)) {
  607. foreach ($obj->_relatedObjects as $className => $fk) {
  608. foreach ($fk as $key => $relObj) {} /* TODO: complete $generateRelated functionality */
  609. }
  610. }
  611. }
  612. return $source;
  613. }
  614. /**
  615. * Add a key-value pair to a cache provider if it does not already exist.
  616. *
  617. * @param string $key A unique key identifying the item being stored.
  618. * @param mixed & $var A reference to the PHP variable representing the item.
  619. * @param integer $lifetime Seconds the item will be valid in cache.
  620. * @param array $options Additional options for the cache add operation.
  621. */
  622. public function add($key, & $var, $lifetime= 0, $options= array()) {
  623. $return= false;
  624. if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options))) {
  625. $value= null;
  626. if (is_object($var) && $var instanceof xPDOObject) {
  627. $value= $var->toArray('', true);
  628. } else {
  629. $value= $var;
  630. }
  631. $return= $cache->add($key, $value, $lifetime, $options);
  632. }
  633. return $return;
  634. }
  635. /**
  636. * Replace a key-value pair in in a cache provider.
  637. *
  638. * @access public
  639. * @param string $key A unique key identifying the item being replaced.
  640. * @param mixed & $var A reference to the PHP variable representing the item.
  641. * @param integer $lifetime Seconds the item will be valid in objcache.
  642. * @param array $options Additional options for the cache replace operation.
  643. * @return boolean True if the replace was successful.
  644. */
  645. public function replace($key, & $var, $lifetime= 0, $options= array()) {
  646. $return= false;
  647. if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
  648. $value= null;
  649. if (is_object($var) && $var instanceof xPDOObject) {
  650. $value= $var->toArray('', true);
  651. } else {
  652. $value= $var;
  653. }
  654. $return= $cache->replace($key, $value, $lifetime, $options);
  655. }
  656. return $return;
  657. }
  658. /**
  659. * Set a key-value pair in a cache provider.
  660. *
  661. * @access public
  662. * @param string $key A unique key identifying the item being set.
  663. * @param mixed & $var A reference to the PHP variable representing the item.
  664. * @param integer $lifetime Seconds the item will be valid in objcache.
  665. * @param array $options Additional options for the cache set operation.
  666. * @return boolean True if the set was successful
  667. */
  668. public function set($key, & $var, $lifetime= 0, $options= array()) {
  669. $return= false;
  670. if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
  671. $value= null;
  672. if (is_object($var) && $var instanceof xPDOObject) {
  673. $value= $var->toArray('', true);
  674. } else {
  675. $value= $var;
  676. }
  677. $return= $cache->set($key, $value, $lifetime, $options);
  678. } else {
  679. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No cache implementation found.');
  680. }
  681. return $return;
  682. }
  683. /**
  684. * Delete a key-value pair from a cache provider.
  685. *
  686. * @access public
  687. * @param string $key A unique key identifying the item being deleted.
  688. * @param array $options Additional options for the cache deletion.
  689. * @return boolean True if the deletion was successful.
  690. */
  691. public function delete($key, $options = array()) {
  692. $return= false;
  693. if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
  694. $return= $cache->delete($key, $options);
  695. }
  696. return $return;
  697. }
  698. /**
  699. * Get a value from a cache provider by key.
  700. *
  701. * @access public
  702. * @param string $key A unique key identifying the item being retrieved.
  703. * @param array $options Additional options for the cache retrieval.
  704. * @return mixed The value of the object cache key
  705. */
  706. public function get($key, $options = array()) {
  707. $return= false;
  708. if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
  709. $return= $cache->get($key, $options);
  710. }
  711. return $return;
  712. }
  713. /**
  714. * Flush the contents of a cache provider.
  715. *
  716. * @access public
  717. * @param array $options Additional options for the cache flush.
  718. * @return boolean True if the flush was successful.
  719. */
  720. public function clean($options = array()) {
  721. $return= false;
  722. if ($cache = $this->getCacheProvider($this->getOption(xPDO::OPT_CACHE_KEY, $options), $options)) {
  723. $return= $cache->flush($options);
  724. }
  725. return $return;
  726. }
  727. /**
  728. * Refresh specific or all cache providers.
  729. *
  730. * The default behavior is to call clean() with the provided options
  731. *
  732. * @param array $providers An associative array with keys representing the cache provider key
  733. * and the value an array of options.
  734. * @param array &$results An associative array for collecting results for each provider.
  735. * @return array An array of results for each provider that is refreshed.
  736. */
  737. public function refresh(array $providers = array(), array &$results = array()) {
  738. if (empty($providers)) {
  739. foreach ($this->caches as $cacheKey => $cache) {
  740. $providers[$cacheKey] = array();
  741. }
  742. }
  743. foreach ($providers as $key => $options) {
  744. if (array_key_exists($key, $this->caches) && !array_key_exists($key, $results)) {
  745. $results[$key] = $this->clean(array_merge($options, array(xPDO::OPT_CACHE_KEY => $key)));
  746. }
  747. }
  748. return (array_search(false, $results, true) === false);
  749. }
  750. /**
  751. * Escapes all single quotes in a string
  752. *
  753. * @access public
  754. * @param string $s The string to escape single quotes in.
  755. * @return string The string with single quotes escaped.
  756. */
  757. public function escapeSingleQuotes($s) {
  758. $q1= array (
  759. "\\",
  760. "'"
  761. );
  762. $q2= array (
  763. "\\\\",
  764. "\\'"
  765. );
  766. return str_replace($q1, $q2, $s);
  767. }
  768. }
  769. /**
  770. * An abstract class that defines the methods a cache provider must implement.
  771. *
  772. * @package xpdo
  773. * @subpackage cache
  774. */
  775. abstract class xPDOCache {
  776. public $xpdo= null;
  777. protected $options= array();
  778. protected $key= '';
  779. protected $initialized= false;
  780. public function __construct(& $xpdo, $options = array()) {
  781. $this->xpdo= & $xpdo;
  782. $this->options= $options;
  783. $this->key = $this->getOption(xPDO::OPT_CACHE_KEY, $options, 'default');
  784. }
  785. /**
  786. * Indicates if this xPDOCache instance has been properly initialized.
  787. *
  788. * @return boolean true if the implementation was initialized successfully.
  789. */
  790. public function isInitialized() {
  791. return (boolean) $this->initialized;
  792. }
  793. /**
  794. * Get an option from supplied options, the cache options, or the xpdo config.
  795. *
  796. * @param string $key Unique identifier for the option.
  797. * @param array $options A set of explicit options to override those from xPDO or the xPDOCache
  798. * implementation.
  799. * @param mixed $default An optional default value to return if no value is found.
  800. * @return mixed The value of the option.
  801. */
  802. public function getOption($key, $options = array(), $default = null) {
  803. $option = $default;
  804. if (is_array($key)) {
  805. if (!is_array($option)) {
  806. $default= $option;
  807. $option= array();
  808. }
  809. foreach ($key as $k) {
  810. $option[$k]= $this->getOption($k, $options, $default);
  811. }
  812. } elseif (is_string($key) && !empty($key)) {
  813. if (is_array($options) && !empty($options) && array_key_exists($key, $options)) {
  814. $option = $options[$key];
  815. } elseif (is_array($this->options) && !empty($this->options) && array_key_exists($key, $this->options)) {
  816. $option = $this->options[$key];
  817. } else {
  818. $option = $this->xpdo->cacheManager->getOption($key, null, $default);
  819. }
  820. }
  821. return $option;
  822. }
  823. /**
  824. * Get the actual cache key the implementation will use.
  825. *
  826. * @param string $key The identifier the application uses.
  827. * @param array $options Additional options for the operation.
  828. * @return string The identifier with any implementation specific prefixes or other
  829. * transformations applied.
  830. */
  831. public function getCacheKey($key, $options = array()) {
  832. $prefix = $this->getOption('cache_prefix', $options);
  833. if (!empty($prefix)) $key = $prefix . $key;
  834. return $this->key . '/' . $key;
  835. }
  836. /**
  837. * Adds a value to the cache.
  838. *
  839. * @access public
  840. * @param string $key A unique key identifying the item being set.
  841. * @param string $var A reference to the PHP variable representing the item.
  842. * @param integer $expire The amount of seconds for the variable to expire in.
  843. * @param array $options Additional options for the operation.
  844. * @return boolean True if successful
  845. */
  846. abstract public function add($key, $var, $expire= 0, $options= array());
  847. /**
  848. * Sets a value in the cache.
  849. *
  850. * @access public
  851. * @param string $key A unique key identifying the item being set.
  852. * @param string $var A reference to the PHP variable representing the item.
  853. * @param integer $expire The amount of seconds for the variable to expire in.
  854. * @param array $options Additional options for the operation.
  855. * @return boolean True if successful
  856. */
  857. abstract public function set($key, $var, $expire= 0, $options= array());
  858. /**
  859. * Replaces a value in the cache.
  860. *
  861. * @access public
  862. * @param string $key A unique key identifying the item being set.
  863. * @param string $var A reference to the PHP variable representing the item.
  864. * @param integer $expire The amount of seconds for the variable to expire in.
  865. * @param array $options Additional options for the operation.
  866. * @return boolean True if successful
  867. */
  868. abstract public function replace($key, $var, $expire= 0, $options= array());
  869. /**
  870. * Deletes a value from the cache.
  871. *
  872. * @access public
  873. * @param string $key A unique key identifying the item being deleted.
  874. * @param array $options Additional options for the operation.
  875. * @return boolean True if successful
  876. */
  877. abstract public function delete($key, $options= array());
  878. /**
  879. * Gets a value from the cache.
  880. *
  881. * @access public
  882. * @param string $key A unique key identifying the item to fetch.
  883. * @param array $options Additional options for the operation.
  884. * @return mixed The value retrieved from the cache.
  885. */
  886. public function get($key, $options= array()) {}
  887. /**
  888. * Flush all values from the cache.
  889. *
  890. * @access public
  891. * @param array $options Additional options for the operation.
  892. * @return boolean True if successful.
  893. */
  894. abstract public function flush($options= array());
  895. }
  896. /**
  897. * A simple file-based caching implementation using executable PHP.
  898. *
  899. * This can be used to relieve database loads, though the overall performance is
  900. * about the same as without the file-based cache. For maximum performance and
  901. * scalability, use a server with memcached and the PHP memcache extension
  902. * configured.
  903. *
  904. * @package xpdo
  905. * @subpackage cache
  906. */
  907. class xPDOFileCache extends xPDOCache {
  908. public function __construct(& $xpdo, $options = array()) {
  909. parent :: __construct($xpdo, $options);
  910. $this->initialized = true;
  911. }
  912. public function getCacheKey($key, $options = array()) {
  913. $cachePath = $this->getOption('cache_path', $options);
  914. $cacheExt = $this->getOption('cache_ext', $options, '.cache.php');
  915. $key = parent :: getCacheKey($key, $options);
  916. return $cachePath . $key . $cacheExt;
  917. }
  918. public function add($key, $var, $expire= 0, $options= array()) {
  919. $added= false;
  920. if (!file_exists($this->getCacheKey($key, $options))) {
  921. if ($expire === true)
  922. $expire= 0;
  923. $added= $this->set($key, $var, $expire, $options);
  924. }
  925. return $added;
  926. }
  927. public function set($key, $var, $expire= 0, $options= array()) {
  928. $set= false;
  929. if ($var !== null) {
  930. if ($expire === true)
  931. $expire= 0;
  932. $expirationTS= $expire ? time() + $expire : 0;
  933. $expireContent= '';
  934. if ($expirationTS) {
  935. $expireContent= 'if(time() > ' . $expirationTS . '){return null;}';
  936. }
  937. $fileName= $this->getCacheKey($key, $options);
  938. $format = (integer) $this->getOption(xPDO::OPT_CACHE_FORMAT, $options, xPDOCacheManager::CACHE_PHP);
  939. switch ($format) {
  940. case xPDOCacheManager::CACHE_SERIALIZE:
  941. $content= serialize(array('expires' => $expirationTS, 'content' => $var));
  942. break;
  943. case xPDOCacheManager::CACHE_JSON:
  944. $content= $this->xpdo->toJSON(array('expires' => $expirationTS, 'content' => $var));
  945. break;
  946. case xPDOCacheManager::CACHE_PHP:
  947. default:
  948. $content= '<?php ' . $expireContent . ' return ' . var_export($var, true) . ';';
  949. break;
  950. }
  951. $folderMode = $this->getOption('new_cache_folder_permissions', $options, false);
  952. if ($folderMode) $options['new_folder_permissions'] = $folderMode;
  953. $fileMode = $this->getOption('new_cache_file_permissions', $options, false);
  954. if ($fileMode) $options['new_file_permissions'] = $fileMode;
  955. $set= $this->xpdo->cacheManager->writeFile($fileName, $content, 'wb', $options);
  956. }
  957. return $set;
  958. }
  959. public function replace($key, $var, $expire= 0, $options= array()) {
  960. $replaced= false;
  961. if (file_exists($this->getCacheKey($key, $options))) {
  962. if ($expire === true)
  963. $expire= 0;
  964. $replaced= $this->set($key, $var, $expire, $options);
  965. }
  966. return $replaced;
  967. }
  968. public function delete($key, $options= array()) {
  969. $deleted= false;
  970. if ($this->getOption('multiple_object_delete', $options, true)) {
  971. $cacheKey= $this->getCacheKey($key, array_merge($options, array('cache_ext' => '')));
  972. if (file_exists($cacheKey) && is_dir($cacheKey)) {
  973. $results = $this->xpdo->cacheManager->deleteTree($cacheKey, array_merge(array('deleteTop' => false, 'skipDirs' => false, 'extensions' => array('.cache.php')), $options));
  974. if ($results !== false) {
  975. $deleted = true;
  976. }
  977. }
  978. }
  979. $cacheKey= $this->getCacheKey($key, $options);
  980. if (file_exists($cacheKey)) {
  981. $deleted= @ unlink($cacheKey);
  982. }
  983. return $deleted;
  984. }
  985. public function get($key, $options= array()) {
  986. $value= null;
  987. $cacheKey= $this->getCacheKey($key, $options);
  988. if (file_exists($cacheKey)) {
  989. if ($file = @fopen($cacheKey, 'rb')) {
  990. $format = (integer) $this->getOption(xPDO::OPT_CACHE_FORMAT, $options, xPDOCacheManager::CACHE_PHP);
  991. if (flock($file, LOCK_SH)) {
  992. switch ($format) {
  993. case xPDOCacheManager::CACHE_PHP:
  994. if (!filesize($cacheKey)) {
  995. $value= false;
  996. break;
  997. }
  998. $value= @include $cacheKey;
  999. break;
  1000. case xPDOCacheManager::CACHE_JSON:
  1001. $payload = stream_get_contents($file);
  1002. if ($payload !== false) {
  1003. $payload = $this->xpdo->fromJSON($payload);
  1004. if (is_array($payload) && isset($payload['expires']) && (empty($payload['expires']) || time() < $payload['expires'])) {
  1005. if (array_key_exists('content', $payload)) {
  1006. $value= $payload['content'];
  1007. }
  1008. }
  1009. }
  1010. break;
  1011. case xPDOCacheManager::CACHE_SERIALIZE:
  1012. $payload = stream_get_contents($file);
  1013. if ($payload !== false) {
  1014. $payload = unserialize($payload);
  1015. if (is_array($payload) && isset($payload['expires']) && (empty($payload['expires']) || time() < $payload['expires'])) {
  1016. if (array_key_exists('content', $payload)) {
  1017. $value= $payload['content'];
  1018. }
  1019. }
  1020. }
  1021. break;
  1022. }
  1023. flock($file, LOCK_UN);
  1024. if ($value === null && $this->getOption('removeIfEmpty', $options, true)) {
  1025. fclose($file);
  1026. @ unlink($cacheKey);
  1027. return $value;
  1028. }
  1029. }
  1030. @fclose($file);
  1031. }
  1032. }
  1033. return $value;
  1034. }
  1035. public function flush($options= array()) {
  1036. $cacheKey= $this->getCacheKey('', array_merge($options, array('cache_ext' => '')));
  1037. $results = $this->xpdo->cacheManager->deleteTree($cacheKey, array_merge(array('deleteTop' => false, 'skipDirs' => false, 'extensions' => array('.cache.php')), $options));
  1038. return ($results !== false);
  1039. }
  1040. }