modlexicon.class.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <?php
  2. /*
  3. * This file is part of MODX Revolution.
  4. *
  5. * Copyright (c) MODX, LLC. All Rights Reserved.
  6. *
  7. * For complete copyright and license information, see the COPYRIGHT and LICENSE
  8. * files found in the top-level directory of this distribution.
  9. */
  10. /**
  11. * The lexicon handling class. Handles all lexicon topics by loading and storing their entries into a cached array.
  12. * Also considers database-based overrides for specific lexicon entries that preserve the originals and allow reversion.
  13. *
  14. * @package modx
  15. */
  16. class modLexicon {
  17. /**
  18. * Reference to the MODX instance.
  19. *
  20. * @var modX $modx
  21. * @access protected
  22. */
  23. public $modx = null;
  24. /**
  25. * The actual language array.
  26. *
  27. * @todo Separate into separate arrays for each namespace (and maybe topic)
  28. * so that no namespacing in lexicon entries is needed. Maybe keep a master
  29. * array of entries, but then have subarrays for topic-specific referencing.
  30. *
  31. * @var array $_lexicon
  32. * @access protected
  33. */
  34. protected $_lexicon = array();
  35. /**
  36. * Directories to search for language strings in.
  37. *
  38. * @deprecated
  39. * @var array $_paths
  40. * @access protected
  41. */
  42. protected $_paths = array();
  43. /**
  44. * An array of loaded topic strings
  45. *
  46. * @var array $_loadedTopics
  47. */
  48. protected $_loadedTopics = array();
  49. /**
  50. * Creates the modLexicon instance.
  51. *
  52. * @constructor
  53. * @param xPDO $modx A reference to the modX instance.
  54. * @param array $config An array of configuration properties
  55. */
  56. function __construct(xPDO &$modx,array $config = array()) {
  57. $this->modx =& $modx;
  58. $this->_paths = array(
  59. 'core' => $this->modx->getOption('core_path') . 'cache/lexicon/',
  60. );
  61. $this->_lexicon = array($this->modx->getOption('cultureKey',null,'en') => array());
  62. $this->config = array_merge($config,array());
  63. }
  64. /**
  65. * Clears the lexicon cache for the specified path.
  66. *
  67. * @access public
  68. * @param string $path The path to clear.
  69. * @return string The results of the cache clearing.
  70. */
  71. public function clearCache($path = '') {
  72. $path = 'lexicon/'.$path;
  73. return $this->modx->cacheManager->refresh(array(
  74. 'lexicon_topics' => array($path),
  75. ));
  76. }
  77. /**
  78. * Returns if the key exists in the lexicon.
  79. *
  80. * @access public
  81. * @param string $index
  82. * @return boolean True if exists.
  83. */
  84. public function exists($index,$language = '') {
  85. $language = !empty($language) ? $language : $this->modx->getOption('cultureKey',null,'en');
  86. return (is_string($index) && isset($this->_lexicon[$language][$index]));
  87. }
  88. /**
  89. * Accessor method for the lexicon array.
  90. *
  91. * @access public
  92. * @param string $prefix If set, will only return the lexicon entries with this prefix.
  93. * @param boolean $removePrefix If true, will strip the prefix from the returned indexes
  94. * @param string $language
  95. * @return array The internal lexicon.
  96. */
  97. public function fetch($prefix = '',$removePrefix = false,$language = '') {
  98. $language = !empty($language) ? $language : $this->modx->getOption('cultureKey',null,'en');
  99. if (!empty($prefix)) {
  100. $lex = array();
  101. $lang = $this->_lexicon[$language];
  102. if (is_array($lang)) {
  103. foreach ($lang as $k => $v) {
  104. if (strpos($k,$prefix) !== false) {
  105. $key = $removePrefix ? str_replace($prefix,'',$k) : $k;
  106. $lex[$key] = $v;
  107. }
  108. }
  109. }
  110. return $lex;
  111. }
  112. return $this->_lexicon[$language];
  113. }
  114. /**
  115. * Return the cache key representing the specified lexicon topic.
  116. *
  117. * @access public
  118. * @param string $namespace The namespace for the topic
  119. * @param string $topic The topic to grab
  120. * @param string $language The language for the topic
  121. * @return string The cache key for the specified topic
  122. */
  123. public function getCacheKey($namespace = 'core',$topic = 'default',$language = '') {
  124. if (empty($namespace)) $namespace = 'core';
  125. if (empty($topic)) $topic = 'default';
  126. if (empty($language)) $language = $this->modx->getOption('cultureKey',null,'en');
  127. return 'lexicon/'.$language.'/'.$namespace.'/'.$topic;
  128. }
  129. /**
  130. * Loads a variable number of topic areas. They must reside as topicname.
  131. * inc.php files in their proper culture directory. Can load an infinite
  132. * number of topic areas via a dynamic number of arguments.
  133. *
  134. * They are loaded by language:namespace:topic, namespace:topic, or just
  135. * topic. Examples: $modx->lexicon->load('en:core:snippet'); $modx->lexicon-
  136. * >load ('demo:test'); $modx->lexicon->load('chunk');
  137. *
  138. * @access public
  139. */
  140. public function load() {
  141. $topics = func_get_args(); /* allow for dynamic number of lexicons to load */
  142. if ($this->modx->context && $this->modx->context->get('key') == 'mgr') {
  143. $defaultLanguage = $this->modx->getOption('manager_language',null,$this->modx->getOption('cultureKey',null,'en'));
  144. } else {
  145. $defaultLanguage = $this->modx->getOption('cultureKey',null,'en');
  146. }
  147. foreach ($topics as $topicStr) {
  148. if (!is_string($topicStr) || $topicStr == '') continue;
  149. if (in_array($topicStr,$this->_loadedTopics)) continue;
  150. $nspos = strpos($topicStr,':');
  151. $topic = str_replace('.','/',$topicStr); /** @deprecated 2.0.0 Allow for lexicon subdirs */
  152. /* if no namespace, search all lexicons */
  153. if ($nspos === false) {
  154. foreach ($this->_paths as $namespace => $path) {
  155. $entries = $this->loadCache($namespace,$topic);
  156. if (is_array($entries)) {
  157. if (!array_key_exists($defaultLanguage,$this->_lexicon)) $this->_lexicon[$defaultLanguage] = array();
  158. $this->_lexicon[$defaultLanguage] = is_array($this->_lexicon[$defaultLanguage]) ? array_merge($this->_lexicon[$defaultLanguage],$entries) : $entries;
  159. }
  160. }
  161. } else { /* if namespace, search specified lexicon */
  162. $params = explode(':',$topic);
  163. if (count($params) <= 2) {
  164. $language = $defaultLanguage;
  165. $namespace = $params[0];
  166. $topic_parsed = $params[1];
  167. } else {
  168. $language = $params[0];
  169. $namespace = $params[1];
  170. $topic_parsed = $params[2];
  171. }
  172. $englishEntries = $language != 'en' ? $this->loadCache($namespace,$topic_parsed,'en') : false;
  173. $entries = $this->loadCache($namespace,$topic_parsed,$language);
  174. if (!is_array($entries)) {
  175. if (is_string($entries) && !empty($entries)) $entries = $this->modx->fromJSON($entries);
  176. if (empty($entries)) $entries = array();
  177. }
  178. if (is_array($englishEntries) && !empty($englishEntries)) {
  179. $entries = array_merge($englishEntries,$entries);
  180. }
  181. if (is_array($entries)) {
  182. $this->_loadedTopics[] = $topicStr;
  183. if (!array_key_exists($language,$this->_lexicon)) $this->_lexicon[$language] = array();
  184. $this->_lexicon[$language] = is_array($this->_lexicon[$language]) ? array_merge($this->_lexicon[$language], $entries) : $entries;
  185. }
  186. }
  187. }
  188. }
  189. /**
  190. * Loads a lexicon topic from the cache. If not found, tries to generate a
  191. * cache file from the database.
  192. *
  193. * @access public
  194. * @param string $namespace The namespace to load from. Defaults to 'core'.
  195. * @param string $topic The topic to load. Defaults to 'default'.
  196. * @param string $language The language to load. Defaults to 'en'.
  197. * @return array The loaded lexicon array.
  198. */
  199. public function loadCache($namespace = 'core', $topic = 'default', $language = '') {
  200. if (empty($language)) $language = $this->modx->getOption('cultureKey',null,'en');
  201. $key = $this->getCacheKey($namespace, $topic, $language);
  202. $enableCache = ($namespace != 'core' && !$this->modx->getOption('cache_noncore_lexicon_topics',null,true)) ? false : true;
  203. if (!$this->modx->cacheManager) {
  204. $this->modx->getCacheManager();
  205. }
  206. $cached = $this->modx->cacheManager->get($key, array(
  207. xPDO::OPT_CACHE_KEY => $this->modx->getOption('cache_lexicon_topics_key', null, 'lexicon_topics'),
  208. xPDO::OPT_CACHE_HANDLER => $this->modx->getOption('cache_lexicon_topics_handler', null, $this->modx->getOption(xPDO::OPT_CACHE_HANDLER)),
  209. xPDO::OPT_CACHE_FORMAT => (integer) $this->modx->getOption('cache_lexicon_topics_format', null, $this->modx->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)),
  210. ));
  211. if (!$enableCache || $cached == null) {
  212. $results= false;
  213. /* load file-based lexicon */
  214. $results = $this->getFileTopic($language,$namespace,$topic);
  215. if ($results === false) { /* default back to en */
  216. $results = $this->getFileTopic('en',$namespace,$topic);
  217. if ($results === false) {
  218. $results = array();
  219. }
  220. }
  221. /* get DB overrides */
  222. $c= $this->modx->newQuery('modLexiconEntry');
  223. $c->innerJoin('modNamespace','Namespace');
  224. $c->where(array(
  225. 'modLexiconEntry.topic' => $topic,
  226. 'modLexiconEntry.language' => $language,
  227. 'Namespace.name' => $namespace,
  228. ));
  229. $c->sortby($this->modx->getSelectColumns('modLexiconEntry','modLexiconEntry','',array('name')),'ASC');
  230. $entries= $this->modx->getCollection('modLexiconEntry',$c);
  231. if (!empty($entries)) {
  232. /** @var modLexiconEntry $entry */
  233. foreach ($entries as $entry) {
  234. $results[$entry->get('name')]= $entry->get('value');
  235. }
  236. }
  237. if ($enableCache) {
  238. $cached = $this->modx->cacheManager->generateLexiconTopic($key,$results);
  239. } else {
  240. $cached = $results;
  241. }
  242. }
  243. if (empty($cached)) {
  244. $this->modx->log(xPDO::LOG_LEVEL_DEBUG, "An error occurred while trying to cache {$key} (lexicon/language/namespace/topic)");
  245. }
  246. return $cached;
  247. }
  248. /**
  249. * Get entries from file-based lexicon topic
  250. *
  251. * @param string $language The language to filter by.
  252. * @param string $namespace The namespace to filter by.
  253. * @param string $topic The topic to filter by.
  254. * @return array An array of lexicon entries in key - value pairs for the specified filter.
  255. */
  256. public function getFileTopic($language = 'en',$namespace = 'core',$topic = 'default') {
  257. $corePath = $this->getNamespacePath($namespace);
  258. $corePath = str_replace(array(
  259. '{base_path}',
  260. '{core_path}',
  261. '{assets_path}',
  262. ),array(
  263. $this->modx->getOption('base_path'),
  264. $this->modx->getOption('core_path'),
  265. $this->modx->getOption('assets_path'),
  266. ),$corePath);
  267. $topicPath = str_replace('//','/',$corePath.'/lexicon/'.$language.'/'.$topic.'.inc.php');
  268. $results = array();
  269. $_lang = array();
  270. if (file_exists($topicPath)) {
  271. include $topicPath;
  272. $results = $_lang;
  273. } else {
  274. return false;
  275. }
  276. return $results;
  277. }
  278. /**
  279. * Get the path of the specified Namespace
  280. *
  281. * @param string $namespace The key of the Namespace
  282. * @return string The path for the Namespace
  283. */
  284. public function getNamespacePath($namespace = 'core') {
  285. $corePath = $this->modx->getOption('core_path',null,MODX_CORE_PATH);
  286. if ($namespace != 'core') {
  287. /** @var modNamespace $namespaceObj */
  288. $namespaceObj = $this->modx->getObject('modNamespace',$namespace);
  289. if ($namespaceObj) {
  290. $corePath = $namespaceObj->getCorePath();
  291. }
  292. }
  293. return $corePath;
  294. }
  295. /**
  296. * Get a list of available Topics when given a Language and Namespace.
  297. *
  298. * @param string $language The language to filter by.
  299. * @param string $namespace The language to filter by.
  300. * @return array An array of Topic names.
  301. */
  302. public function getTopicList($language = 'en',$namespace = 'core') {
  303. $corePath = $this->getNamespacePath($namespace);
  304. $lexPath = str_replace('//','/',$corePath.'/lexicon/'.$language.'/');
  305. $topics = array();
  306. if (!is_dir($lexPath)) return $topics;
  307. /** @var DirectoryIterator $topic */
  308. foreach (new DirectoryIterator($lexPath) as $topic) {
  309. if (in_array($topic,array('.','..','.svn','.git','_notes'))) continue;
  310. if (!$topic->isReadable()) continue;
  311. if ($topic->isFile()) {
  312. $fileName = $topic->getFilename();
  313. if (strpos($fileName,'.inc.php')) {
  314. $topics[] = str_replace('.inc.php','',$fileName);
  315. }
  316. }
  317. }
  318. $c = $this->modx->newQuery('modLexiconEntry');
  319. $c->where(array(
  320. 'namespace' => $namespace,
  321. 'topic:NOT IN' => $topics,
  322. ));
  323. $c->select(array('topic'));
  324. $c->query['distinct'] = 'DISTINCT';
  325. if ($c->prepare() && $c->stmt->execute()) {
  326. $entries = $c->stmt->fetchAll(\PDO::FETCH_ASSOC);
  327. if (is_array($entries) and count($entries) > 0) {
  328. foreach ($entries as $v) {
  329. $topics[] = $v['topic'];
  330. }
  331. }
  332. }
  333. sort($topics);
  334. return $topics;
  335. }
  336. /**
  337. * Get a list of available languages for a Namespace.
  338. *
  339. * @param string $namespace The Namespace to filter by.
  340. * @return array An array of available languages
  341. */
  342. public function getLanguageList($namespace = 'core') {
  343. $corePath = $this->getNamespacePath($namespace);
  344. $lexPath = str_replace('//','/',$corePath.'/lexicon/');
  345. if (!is_dir($lexPath)) {
  346. return array();
  347. }
  348. $languages = array();
  349. /** @var DirectoryIterator $language */
  350. foreach (new DirectoryIterator($lexPath) as $language) {
  351. if (in_array($language,array('.','..','.svn','.git','_notes','country'))) continue;
  352. if (!$language->isReadable()) continue;
  353. if ($language->isDir()) {
  354. $languages[] = $language->getFilename();
  355. }
  356. }
  357. $c = $this->modx->newQuery('modLexiconEntry');
  358. $c->where(array(
  359. 'namespace' => $namespace,
  360. 'language:NOT IN' => $languages,
  361. ));
  362. $c->select(array('language'));
  363. $c->query['distinct'] = 'DISTINCT';
  364. if ($c->prepare() && $c->stmt->execute()) {
  365. $entries = $c->stmt->fetchAll(\PDO::FETCH_ASSOC);
  366. if (is_array($entries) and count($entries) > 0) {
  367. foreach ($entries as $v) {
  368. $languages[] = $v['language'];
  369. }
  370. }
  371. }
  372. sort($languages);
  373. return $languages;
  374. }
  375. /**
  376. * Get a lexicon string by its index.
  377. *
  378. * @access public
  379. * @param string $key The key of the lexicon string.
  380. * @param array $params An assocative array of placeholder
  381. * keys and values to parse
  382. * @param string $language
  383. * @return string The text of the lexicon key, blank if not found.
  384. */
  385. public function process($key,array $params = array(),$language = '') {
  386. $language = !empty($language) ? $language : $this->modx->getOption('cultureKey',null,'en');
  387. /* make sure key exists */
  388. if (!is_string($key) || !isset($this->_lexicon[$language][$key])) {
  389. $this->modx->log(xPDO::LOG_LEVEL_DEBUG,'Language string not found: "'.$key.'"');
  390. return $key;
  391. }
  392. /* if params are passed, allow for parsing of [[+key]] values to strings */
  393. return empty($params)
  394. ? $this->_lexicon[$language][$key]
  395. : $this->_parse($this->_lexicon[$language][$key],$params);
  396. }
  397. /**
  398. * Sets a lexicon key to a value. Not recommended, since doesn't query the
  399. * database.
  400. *
  401. * @access public
  402. * @param string|array $keys Either an array of array pairs of key/values or
  403. * a key string.
  404. * @param string $text The text to set, if the first parameter is a string.
  405. * @param string $language The language to set the key in. Defaults to current.
  406. */
  407. public function set($keys, $text = '', $language = '') {
  408. $language = !empty($language) ? $language : $this->modx->getOption('cultureKey',null,$language);
  409. if (is_array($keys)) {
  410. foreach ($keys as $key => $str) {
  411. if ($key == '') continue;
  412. $this->_lexicon[$language][$key] = $str;
  413. }
  414. } else if (is_string($keys) && $keys != '') {
  415. $this->_lexicon[$language][$keys] = $text;
  416. }
  417. }
  418. /**
  419. * Parses a lexicon string, replacing placeholders with
  420. * specified strings.
  421. *
  422. * @access private
  423. * @param string $str The string to parse
  424. * @param array $params An associative array of keys to replace
  425. * @return string The processed string
  426. */
  427. private function _parse($str,$params) {
  428. if (!$str) return '';
  429. if (empty($params)) return $str;
  430. foreach ($params as $k => $v) {
  431. $str = str_replace('[[+'.$k.']]',$v,$str);
  432. }
  433. return $str;
  434. }
  435. /**
  436. * Returns the total # of entries in the active lexicon
  437. * @param string $language
  438. * @return int
  439. */
  440. public function total($language = '') {
  441. $language = !empty($language) ? $language : $this->modx->getOption('cultureKey',null,'en');
  442. return count($this->_lexicon[$language]);
  443. }
  444. /**
  445. * Completely clears the lexicon
  446. * @param string $language
  447. * @return void
  448. */
  449. public function clear($language = '') {
  450. if (!empty($language)) {
  451. $this->_lexicon[$language] = array();
  452. } else {
  453. $this->_lexicon = array($this->modx->getOption('cultureKey',null,'en') => array());
  454. }
  455. }
  456. }