Config.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <?php
  2. namespace Grav\Common\Config;
  3. use Grav\Common\File\CompiledYamlFile;
  4. use Grav\Common\Grav;
  5. use Grav\Common\Data\Data;
  6. use RocketTheme\Toolbox\Blueprints\Blueprints;
  7. use RocketTheme\Toolbox\File\PhpFile;
  8. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  9. /**
  10. * The Config class contains configuration information.
  11. *
  12. * @author RocketTheme
  13. * @license MIT
  14. */
  15. class Config extends Data
  16. {
  17. protected $grav;
  18. protected $streams = [
  19. 'system' => [
  20. 'type' => 'ReadOnlyStream',
  21. 'prefixes' => [
  22. '' => ['system'],
  23. ]
  24. ],
  25. 'user' => [
  26. 'type' => 'ReadOnlyStream',
  27. 'prefixes' => [
  28. '' => ['user'],
  29. ]
  30. ],
  31. 'blueprints' => [
  32. 'type' => 'ReadOnlyStream',
  33. 'prefixes' => [
  34. '' => ['user://blueprints', 'system/blueprints'],
  35. ]
  36. ],
  37. 'config' => [
  38. 'type' => 'ReadOnlyStream',
  39. 'prefixes' => [
  40. '' => ['user://config', 'system/config'],
  41. ]
  42. ],
  43. 'plugins' => [
  44. 'type' => 'ReadOnlyStream',
  45. 'prefixes' => [
  46. '' => ['user://plugins'],
  47. ]
  48. ],
  49. 'plugin' => [
  50. 'type' => 'ReadOnlyStream',
  51. 'prefixes' => [
  52. '' => ['user://plugins'],
  53. ]
  54. ],
  55. 'themes' => [
  56. 'type' => 'ReadOnlyStream',
  57. 'prefixes' => [
  58. '' => ['user://themes'],
  59. ]
  60. ],
  61. 'languages' => [
  62. 'type' => 'ReadOnlyStream',
  63. 'prefixes' => [
  64. '' => ['user://languages', 'system/languages'],
  65. ]
  66. ],
  67. 'cache' => [
  68. 'type' => 'Stream',
  69. 'prefixes' => [
  70. '' => ['cache'],
  71. 'images' => ['images']
  72. ]
  73. ],
  74. 'log' => [
  75. 'type' => 'Stream',
  76. 'prefixes' => [
  77. '' => ['logs']
  78. ]
  79. ],
  80. 'backup' => [
  81. 'type' => 'Stream',
  82. 'prefixes' => [
  83. '' => ['backup']
  84. ]
  85. ]
  86. ];
  87. protected $setup = [];
  88. protected $blueprintFiles = [];
  89. protected $configFiles = [];
  90. protected $languageFiles = [];
  91. protected $checksum;
  92. protected $timestamp;
  93. protected $configLookup;
  94. protected $blueprintLookup;
  95. protected $pluginLookup;
  96. protected $languagesLookup;
  97. protected $finder;
  98. protected $environment;
  99. protected $messages = [];
  100. protected $languages;
  101. public function __construct(array $setup = array(), Grav $grav = null, $environment = null)
  102. {
  103. $this->grav = $grav ?: Grav::instance();
  104. $this->finder = new ConfigFinder;
  105. $this->environment = $environment ?: 'localhost';
  106. $this->messages[] = 'Environment Name: ' . $this->environment;
  107. // Make sure that
  108. if (!isset($setup['streams']['schemes'])) {
  109. $setup['streams']['schemes'] = [];
  110. }
  111. $setup['streams']['schemes'] += $this->streams;
  112. $setup = $this->autoDetectEnvironmentConfig($setup);
  113. $this->setup = $setup;
  114. parent::__construct($setup);
  115. $this->check();
  116. }
  117. public function key()
  118. {
  119. return $this->checksum();
  120. }
  121. public function reload()
  122. {
  123. $this->items = $this->setup;
  124. $this->check();
  125. $this->init();
  126. $this->debug();
  127. return $this;
  128. }
  129. protected function check()
  130. {
  131. $streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null;
  132. if (!is_array($streams)) {
  133. throw new \InvalidArgumentException('Configuration is missing streams.schemes!');
  134. }
  135. $diff = array_keys(array_diff_key($this->streams, $streams));
  136. if ($diff) {
  137. throw new \InvalidArgumentException(
  138. sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff))
  139. );
  140. }
  141. }
  142. public function debug()
  143. {
  144. foreach ($this->messages as $message) {
  145. $this->grav['debugger']->addMessage($message);
  146. }
  147. $this->messages = [];
  148. }
  149. public function init()
  150. {
  151. /** @var UniformResourceLocator $locator */
  152. $locator = $this->grav['locator'];
  153. $this->configLookup = $locator->findResources('config://');
  154. $this->blueprintLookup = $locator->findResources('blueprints://config');
  155. $this->pluginLookup = $locator->findResources('plugins://');
  156. $this->loadCompiledBlueprints($this->blueprintLookup, $this->pluginLookup, 'master');
  157. $this->loadCompiledConfig($this->configLookup, $this->pluginLookup, 'master');
  158. // process languages if supported
  159. if ($this->get('system.languages.translations', true)) {
  160. $this->languagesLookup = $locator->findResources('languages://');
  161. $this->loadCompiledLanguages($this->languagesLookup, $this->pluginLookup, 'master');
  162. }
  163. $this->initializeLocator($locator);
  164. }
  165. public function checksum()
  166. {
  167. if (empty($this->checksum)) {
  168. $checkBlueprints = $this->get('system.cache.check.blueprints', false);
  169. $checkLanguages = $this->get('system.cache.check.languages', false);
  170. $checkConfig = $this->get('system.cache.check.config', true);
  171. $checkSystem = $this->get('system.cache.check.system', true);
  172. if (!$checkBlueprints && !$checkLanguages && !$checkConfig && !$checkSystem) {
  173. $this->messages[] = 'Skip configuration timestamp check.';
  174. return false;
  175. }
  176. // Generate checksum according to the configuration settings.
  177. if (!$checkConfig) {
  178. // Just check changes in system.yaml files and ignore all the other files.
  179. $cc = $checkSystem ? $this->finder->locateConfigFile($this->configLookup, 'system') : [];
  180. } else {
  181. // Check changes in all configuration files.
  182. $cc = $this->finder->locateConfigFiles($this->configLookup, $this->pluginLookup);
  183. }
  184. if ($checkBlueprints) {
  185. $cb = $this->finder->locateBlueprintFiles($this->blueprintLookup, $this->pluginLookup);
  186. } else {
  187. $cb = [];
  188. }
  189. if ($checkLanguages) {
  190. $cl = $this->finder->locateLanguageFiles($this->languagesLookup, $this->pluginLookup);
  191. } else {
  192. $cl = [];
  193. }
  194. $this->checksum = md5(json_encode([$cc, $cb, $cl]));
  195. }
  196. return $this->checksum;
  197. }
  198. protected function autoDetectEnvironmentConfig($items)
  199. {
  200. $environment = $this->environment;
  201. $env_stream = 'user://'.$environment.'/config';
  202. if (file_exists(USER_DIR.$environment.'/config')) {
  203. array_unshift($items['streams']['schemes']['config']['prefixes'][''], $env_stream);
  204. }
  205. return $items;
  206. }
  207. protected function loadCompiledBlueprints($blueprints, $plugins, $filename = null)
  208. {
  209. $checksum = md5(json_encode($blueprints));
  210. $filename = $filename
  211. ? CACHE_DIR . 'compiled/blueprints/' . $filename . '-' . $this->environment . '.php'
  212. : CACHE_DIR . 'compiled/blueprints/' . $checksum . '-' . $this->environment . '.php';
  213. $file = PhpFile::instance($filename);
  214. $cache = $file->exists() ? $file->content() : null;
  215. $blueprintFiles = $this->finder->locateBlueprintFiles($blueprints, $plugins);
  216. $checksum .= ':'.md5(json_encode($blueprintFiles));
  217. $class = get_class($this);
  218. // Load real file if cache isn't up to date (or is invalid).
  219. if (
  220. !is_array($cache)
  221. || !isset($cache['checksum'])
  222. || !isset($cache['@class'])
  223. || $cache['checksum'] != $checksum
  224. || $cache['@class'] != $class
  225. ) {
  226. // Attempt to lock the file for writing.
  227. $file->lock(false);
  228. // Load blueprints.
  229. $this->blueprints = new Blueprints;
  230. foreach ($blueprintFiles as $files) {
  231. $this->loadBlueprintFiles($files);
  232. }
  233. $cache = [
  234. '@class' => $class,
  235. 'checksum' => $checksum,
  236. 'files' => $blueprintFiles,
  237. 'data' => $this->blueprints->toArray()
  238. ];
  239. // If compiled file wasn't already locked by another process, save it.
  240. if ($file->locked() !== false) {
  241. $this->messages[] = 'Saving compiled blueprints.';
  242. $file->save($cache);
  243. $file->unlock();
  244. }
  245. } else {
  246. $this->blueprints = new Blueprints($cache['data']);
  247. }
  248. }
  249. protected function loadCompiledConfig($configs, $plugins, $filename = null)
  250. {
  251. $filename = $filename
  252. ? CACHE_DIR . 'compiled/config/' . $filename . '-' . $this->environment . '.php'
  253. : CACHE_DIR . 'compiled/config/' . $checksum . '-' . $this->environment . '.php';
  254. $file = PhpFile::instance($filename);
  255. $cache = $file->exists() ? $file->content() : null;
  256. $class = get_class($this);
  257. $checksum = $this->checksum();
  258. if (
  259. !is_array($cache)
  260. || !isset($cache['checksum'])
  261. || !isset($cache['@class'])
  262. || $cache['@class'] != $class
  263. ) {
  264. $this->messages[] = 'No cached configuration, compiling new configuration..';
  265. } else if ($cache['checksum'] !== $checksum) {
  266. $this->messages[] = 'Configuration checksum mismatch, reloading configuration..';
  267. } else {
  268. $this->messages[] = 'Configuration checksum matches, using cached version.';
  269. $this->items = $cache['data'];
  270. return;
  271. }
  272. $configFiles = $this->finder->locateConfigFiles($configs, $plugins);
  273. // Attempt to lock the file for writing.
  274. $file->lock(false);
  275. // Load configuration.
  276. foreach ($configFiles as $files) {
  277. $this->loadConfigFiles($files);
  278. }
  279. $cache = [
  280. '@class' => $class,
  281. 'timestamp' => time(),
  282. 'checksum' => $checksum,
  283. 'data' => $this->toArray()
  284. ];
  285. // If compiled file wasn't already locked by another process, save it.
  286. if ($file->locked() !== false) {
  287. $this->messages[] = 'Saving compiled configuration.';
  288. $file->save($cache);
  289. $file->unlock();
  290. }
  291. $this->items = $cache['data'];
  292. }
  293. /**
  294. * @param $languages
  295. * @param $plugins
  296. * @param null $filename
  297. */
  298. protected function loadCompiledLanguages($languages, $plugins, $filename = null)
  299. {
  300. $checksum = md5(json_encode($languages));
  301. $filename = $filename
  302. ? CACHE_DIR . 'compiled/languages/' . $filename . '-' . $this->environment . '.php'
  303. : CACHE_DIR . 'compiled/languages/' . $checksum . '-' . $this->environment . '.php';
  304. $file = PhpFile::instance($filename);
  305. $cache = $file->exists() ? $file->content() : null;
  306. $languageFiles = $this->finder->locateLanguageFiles($languages, $plugins);
  307. $checksum .= ':' . md5(json_encode($languageFiles));
  308. $class = get_class($this);
  309. // Load real file if cache isn't up to date (or is invalid).
  310. if (
  311. !is_array($cache)
  312. || !isset($cache['checksum'])
  313. || !isset($cache['@class'])
  314. || $cache['checksum'] != $checksum
  315. || $cache['@class'] != $class
  316. ) {
  317. // Attempt to lock the file for writing.
  318. $file->lock(false);
  319. // Load languages.
  320. $this->languages = new Languages;
  321. $pluginPaths = str_ireplace(GRAV_ROOT . '/', '', array_reverse($plugins));
  322. foreach ($pluginPaths as $path) {
  323. if (isset($languageFiles[$path])) {
  324. foreach ((array) $languageFiles[$path] as $plugin => $item) {
  325. $lang_file = CompiledYamlFile::instance($item['file']);
  326. $content = $lang_file->content();
  327. $this->languages->mergeRecursive($content);
  328. }
  329. unset($languageFiles[$path]);
  330. }
  331. }
  332. foreach ($languageFiles as $location) {
  333. foreach ($location as $lang => $item) {
  334. $lang_file = CompiledYamlFile::instance($item['file']);
  335. $content = $lang_file->content();
  336. $this->languages->join($lang, $content, '/');
  337. }
  338. }
  339. $cache = [
  340. '@class' => $class,
  341. 'checksum' => $checksum,
  342. 'files' => $languageFiles,
  343. 'data' => $this->languages->toArray()
  344. ];
  345. // If compiled file wasn't already locked by another process, save it.
  346. if ($file->locked() !== false) {
  347. $this->messages[] = 'Saving compiled languages.';
  348. $file->save($cache);
  349. $file->unlock();
  350. }
  351. } else {
  352. $this->languages = new Languages($cache['data']);
  353. }
  354. }
  355. /**
  356. * Load blueprints.
  357. *
  358. * @param array $files
  359. */
  360. public function loadBlueprintFiles(array $files)
  361. {
  362. foreach ($files as $name => $item) {
  363. $file = CompiledYamlFile::instance($item['file']);
  364. $this->blueprints->embed($name, $file->content(), '/');
  365. }
  366. }
  367. /**
  368. * Load configuration.
  369. *
  370. * @param array $files
  371. */
  372. public function loadConfigFiles(array $files)
  373. {
  374. foreach ($files as $name => $item) {
  375. $file = CompiledYamlFile::instance($item['file']);
  376. $this->join($name, $file->content(), '/');
  377. }
  378. }
  379. /**
  380. * Initialize resource locator by using the configuration.
  381. *
  382. * @param UniformResourceLocator $locator
  383. */
  384. public function initializeLocator(UniformResourceLocator $locator)
  385. {
  386. $locator->reset();
  387. $schemes = (array) $this->get('streams.schemes', []);
  388. foreach ($schemes as $scheme => $config) {
  389. if (isset($config['paths'])) {
  390. $locator->addPath($scheme, '', $config['paths']);
  391. }
  392. if (isset($config['prefixes'])) {
  393. foreach ($config['prefixes'] as $prefix => $paths) {
  394. $locator->addPath($scheme, $prefix, $paths);
  395. }
  396. }
  397. }
  398. }
  399. /**
  400. * Get available streams and their types from the configuration.
  401. *
  402. * @return array
  403. */
  404. public function getStreams()
  405. {
  406. $schemes = [];
  407. foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
  408. $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
  409. if ($type[0] != '\\') {
  410. $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
  411. }
  412. $schemes[$scheme] = $type;
  413. }
  414. return $schemes;
  415. }
  416. public function getLanguages()
  417. {
  418. return $this->languages;
  419. }
  420. }