PageRenderTime 58ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/backend/modules/locale/engine/model.php

https://github.com/tommyvdv/forkcms
PHP | 1311 lines | 698 code | 210 blank | 403 comment | 149 complexity | 074f7d0cb534fc9bb1a1511541f266cd MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Fork CMS.
  4. *
  5. * For the full copyright and license information, please view the license
  6. * file that was distributed with this source code.
  7. */
  8. /**
  9. * In this file we store all generic functions that we will be using in the locale module
  10. *
  11. * @author Davy Hellemans <davy.hellemans@netlash.com>
  12. * @author Tijs Verkoyen <tijs@sumocoders.be>
  13. * @author Dieter Vanden Eynde <dieter@dieterve.be>
  14. * @author Lowie Benoot <lowie.benoot@netlash.com>
  15. * @author Matthias Mullie <forkcms@mullie.eu>
  16. */
  17. class BackendLocaleModel
  18. {
  19. /**
  20. * Build the language files
  21. *
  22. * @param string $language The language to build the locale-file for.
  23. * @param string $application The application to build the locale-file for.
  24. */
  25. public static function buildCache($language, $application)
  26. {
  27. // get db
  28. $db = BackendModel::getContainer()->get('database');
  29. // get types
  30. $types = $db->getEnumValues('locale', 'type');
  31. // get locale for backend
  32. $locale = (array) $db->getRecords(
  33. 'SELECT type, module, name, value
  34. FROM locale
  35. WHERE language = ? AND application = ?
  36. ORDER BY type ASC, name ASC, module ASC',
  37. array((string) $language, (string) $application)
  38. );
  39. // init var
  40. $json = array();
  41. // start generating PHP
  42. $value = '<?php' . "\n\n";
  43. $value .= '/**' . "\n";
  44. $value .= ' *' . "\n";
  45. $value .= ' * This file is generated by Fork CMS, it contains' . "\n";
  46. $value .= ' * more information about the locale. Do NOT edit.' . "\n";
  47. $value .= ' * ' . "\n";
  48. $value .= ' * @author Fork CMS' . "\n";
  49. $value .= ' * @generated ' . date('Y-m-d H:i:s') . "\n";
  50. $value .= ' */' . "\n";
  51. $value .= "\n";
  52. foreach($types as $type)
  53. {
  54. // default module
  55. $modules = array('core');
  56. // continue output
  57. $value .= "\n";
  58. $value .= '// init var' . "\n";
  59. $value .= '$' . $type . ' = array();' . "\n";
  60. $value .= '$' . $type . '[\'core\'] = array();' . "\n";
  61. // loop locale
  62. foreach($locale as $i => $item)
  63. {
  64. // types match
  65. if($item['type'] == $type)
  66. {
  67. // new module
  68. if(!in_array($item['module'], $modules))
  69. {
  70. $value .= '$' . $type . '[\'' . $item['module'] . '\'] = array();' . "\n";
  71. $modules[] = $item['module'];
  72. }
  73. // parse
  74. if($application == 'backend')
  75. {
  76. $value .= '$' . $type . '[\'' . $item['module'] . '\'][\'' . $item['name'] . '\'] = \'' . str_replace('\"', '"', addslashes($item['value'])) . '\';' . "\n";
  77. $json[$type][$item['module']][$item['name']] = $item['value'];
  78. }
  79. else
  80. {
  81. $value .= '$' . $type . '[\'' . $item['name'] . '\'] = \'' . str_replace('\"', '"', addslashes($item['value'])) . '\';' . "\n";
  82. $json[$type][$item['name']] = $item['value'];
  83. }
  84. // unset
  85. unset($locale[$i]);
  86. }
  87. }
  88. }
  89. $value .= "\n";
  90. $value .= '?>';
  91. // store
  92. SpoonFile::setContent(constant(mb_strtoupper($application) . '_CACHE_PATH') . '/locale/' . $language . '.php', $value);
  93. // get months
  94. $monthsLong = SpoonLocale::getMonths($language, false);
  95. $monthsShort = SpoonLocale::getMonths($language, true);
  96. // get days
  97. $daysLong = SpoonLocale::getWeekDays($language, false, 'sunday');
  98. $daysShort = SpoonLocale::getWeekDays($language, true, 'sunday');
  99. // build labels
  100. foreach($monthsLong as $key => $value) $json['loc']['MonthLong' . SpoonFilter::ucfirst($key)] = $value;
  101. foreach($monthsShort as $key => $value) $json['loc']['MonthShort' . SpoonFilter::ucfirst($key)] = $value;
  102. foreach($daysLong as $key => $value) $json['loc']['DayLong' . SpoonFilter::ucfirst($key)] = $value;
  103. foreach($daysShort as $key => $value) $json['loc']['DayShort' . SpoonFilter::ucfirst($key)] = $value;
  104. // store
  105. SpoonFile::setContent(constant(mb_strtoupper($application) . '_CACHE_PATH') . '/locale/' . $language . '.json', json_encode($json));
  106. }
  107. /**
  108. * Build a query for the URL based on the filter
  109. *
  110. * @param array $filter The filter.
  111. * @return array
  112. */
  113. public static function buildURLQueryByFilter($filter)
  114. {
  115. $query = http_build_query($filter);
  116. if($query != '') $query = '&' . $query;
  117. return $query;
  118. }
  119. /**
  120. * Create an XML-string that can be used for export.
  121. *
  122. * @param array $items The items.
  123. * @return string
  124. */
  125. public static function createXMLForExport(array $items)
  126. {
  127. $xml = new DOMDocument('1.0', SPOON_CHARSET);
  128. // set some properties
  129. $xml->preserveWhiteSpace = false;
  130. $xml->formatOutput = true;
  131. // locale root element
  132. $root = $xml->createElement('locale');
  133. $xml->appendChild($root);
  134. // loop applications
  135. foreach($items as $application => $modules)
  136. {
  137. // create application element
  138. $applicationElement = $xml->createElement($application);
  139. $root->appendChild($applicationElement);
  140. // loop modules
  141. foreach($modules as $module => $types)
  142. {
  143. // create application element
  144. $moduleElement = $xml->createElement($module);
  145. $applicationElement->appendChild($moduleElement);
  146. // loop types
  147. foreach($types as $type => $items)
  148. {
  149. // loop items
  150. foreach($items as $name => $translations)
  151. {
  152. // create application element
  153. $itemElement = $xml->createElement('item');
  154. $moduleElement->appendChild($itemElement);
  155. // attributes
  156. $itemElement->setAttribute('type', BackendLocaleModel::getTypeName($type));
  157. $itemElement->setAttribute('name', $name);
  158. // loop translations
  159. foreach($translations as $translation)
  160. {
  161. // create translation
  162. $translationElement = $xml->createElement('translation');
  163. $itemElement->appendChild($translationElement);
  164. // attributes
  165. $translationElement->setAttribute('language', $translation['language']);
  166. // set content
  167. $translationElement->appendChild(new DOMCdataSection($translation['value']));
  168. }
  169. }
  170. }
  171. }
  172. }
  173. return $xml->saveXML();
  174. }
  175. /**
  176. * Delete (multiple) items from locale
  177. *
  178. * @param array $ids The id(s) to delete.
  179. */
  180. public static function delete(array $ids)
  181. {
  182. // loop and cast to integers
  183. foreach($ids as &$id) $id = (int) $id;
  184. // create an array with an equal amount of questionmarks as ids provided
  185. $idPlaceHolders = array_fill(0, count($ids), '?');
  186. // delete records
  187. BackendModel::getContainer()->get('database')->delete('locale', 'id IN (' . implode(', ', $idPlaceHolders) . ')', $ids);
  188. // rebuild cache
  189. self::buildCache(BL::getWorkingLanguage(), 'backend');
  190. self::buildCache(BL::getWorkingLanguage(), 'frontend');
  191. }
  192. /**
  193. * Does an id exist.
  194. *
  195. * @param int $id The id to check for existence.
  196. * @return bool
  197. */
  198. public static function exists($id)
  199. {
  200. return (bool) BackendModel::getContainer()->get('database')->getVar(
  201. 'SELECT 1
  202. FROM locale
  203. WHERE id = ?
  204. LIMIT 1',
  205. array((int) $id)
  206. );
  207. }
  208. /**
  209. * Does a locale exists by its name.
  210. *
  211. * @param string $name The name of the locale.
  212. * @param string $type The type of the locale.
  213. * @param string $module The module wherein will be searched.
  214. * @param string $language The language to use.
  215. * @param string $application The application wherein will be searched.
  216. * @param int[optional] $id The id to exclude in the check.
  217. * @return bool
  218. */
  219. public static function existsByName($name, $type, $module, $language, $application, $id = null)
  220. {
  221. $name = (string) $name;
  222. $type = (string) $type;
  223. $module = (string) $module;
  224. $language = (string) $language;
  225. $application = (string) $application;
  226. $id = ($id !== null) ? (int) $id : null;
  227. // get db
  228. $db = BackendModel::getContainer()->get('database');
  229. // return
  230. if($id !== null) return (bool) $db->getVar(
  231. 'SELECT 1
  232. FROM locale
  233. WHERE name = ? AND type = ? AND module = ? AND language = ? AND application = ? AND id != ?
  234. LIMIT 1',
  235. array($name, $type, $module, $language, $application, $id)
  236. );
  237. return (bool) BackendModel::getContainer()->get('database')->getVar(
  238. 'SELECT 1
  239. FROM locale
  240. WHERE name = ? AND type = ? AND module = ? AND language = ? AND application = ?
  241. LIMIT 1',
  242. array($name, $type, $module, $language, $application)
  243. );
  244. }
  245. /**
  246. * Get a single item from locale.
  247. *
  248. * @param int $id The id of the item to get.
  249. * @return array
  250. */
  251. public static function get($id)
  252. {
  253. // fetch record from db
  254. $record = (array) BackendModel::getContainer()->get('database')->getRecord('SELECT * FROM locale WHERE id = ?', array((int) $id));
  255. // actions are urlencoded
  256. if($record['type'] == 'act') $record['value'] = urldecode($record['value']);
  257. return $record;
  258. }
  259. /**
  260. * Get a locale by its name
  261. *
  262. * @param string $name The name of the locale.
  263. * @param string $type The type of the locale.
  264. * @param string $module The module wherein will be searched.
  265. * @param string $language The language to use.
  266. * @param string $application The application wherein will be searched.
  267. * @return bool
  268. */
  269. public static function getByName($name, $type, $module, $language, $application)
  270. {
  271. $name = (string) $name;
  272. $type = (string) $type;
  273. $module = (string) $module;
  274. $language = (string) $language;
  275. $application = (string) $application;
  276. return BackendModel::getContainer()->get('database')->getVar(
  277. 'SELECT l.id
  278. FROM locale AS l
  279. WHERE name = ? AND type = ? AND module = ? AND language = ? AND application = ?',
  280. array($name, $type, $module, $language, $application)
  281. );
  282. }
  283. /**
  284. * Grab labels found in the backend navigation.
  285. *
  286. * @return array
  287. */
  288. private static function getLabelsFromBackendNavigation()
  289. {
  290. return (array) BackendModel::getContainer()->get('database')->getColumn('SELECT label FROM backend_navigation');
  291. }
  292. /**
  293. * Get the languages for a multicheckbox.
  294. *
  295. * @param bool[optional] $includeInterfaceLanguages Should we also get the interfacelanguages?
  296. * @return array
  297. */
  298. public static function getLanguagesForMultiCheckbox($includeInterfaceLanguages = false)
  299. {
  300. // get working languages
  301. $aLanguages = BL::getWorkingLanguages();
  302. // add the interface languages if needed
  303. if($includeInterfaceLanguages) $aLanguages = array_merge($aLanguages, BL::getInterfaceLanguages());
  304. // create a new array to redefine the languages for the multicheckbox
  305. $languages = array();
  306. // loop the languages
  307. foreach($aLanguages as $key => $lang)
  308. {
  309. // add to array
  310. $languages[$key]['value'] = $key;
  311. $languages[$key]['label'] = $lang;
  312. }
  313. return $languages;
  314. }
  315. /**
  316. * Get the locale that is used in the backend but doesn't exists.
  317. *
  318. * @param string $language The language to check.
  319. * @return array
  320. */
  321. public static function getNonExistingBackendLocale($language)
  322. {
  323. $tree = self::getTree(BACKEND_PATH);
  324. $modules = BackendModel::getModules();
  325. // search fo the error module
  326. $key = array_search('error', $modules);
  327. // remove error module
  328. if($key !== false) unset($modules[$key]);
  329. $used = array();
  330. // get labels from navigation
  331. $lbl = self::getLabelsFromBackendNavigation();
  332. foreach((array) $lbl as $label) $used['lbl'][$label] = array('files' => array('<small>used in navigation</small>'), 'module_specific' => array());
  333. // get labels from table
  334. $lbl = (array) BackendModel::getContainer()->get('database')->getColumn('SELECT label FROM modules_extras');
  335. foreach((array) $lbl as $label) $used['lbl'][$label] = array('files' => array('<small>used in database</small>'), 'module_specific' => array());
  336. // loop files
  337. foreach($tree as $file)
  338. {
  339. // grab content
  340. $content = SpoonFile::getContent($file);
  341. // process based on extension
  342. switch(SpoonFile::getExtension($file))
  343. {
  344. // javascript file
  345. case 'js':
  346. $matches = array();
  347. // get matches
  348. preg_match_all('/\{\$(act|err|lbl|msg)(.*)(\|.*)?\}/iU', $content, $matches);
  349. // any matches?
  350. if(isset($matches[2]))
  351. {
  352. // loop matches
  353. foreach($matches[2] as $key => $match)
  354. {
  355. // set type
  356. $type = $matches[1][$key];
  357. // loop modules
  358. foreach($modules as $module)
  359. {
  360. // determine if this is a module specific locale
  361. if(substr($match, 0, mb_strlen($module)) == SpoonFilter::toCamelCase($module) && mb_strlen($match) > mb_strlen($module))
  362. {
  363. // cleanup
  364. $match = str_replace(SpoonFilter::toCamelCase($module), '', $match);
  365. // init if needed
  366. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array(), 'module_specific' => array());
  367. // add module
  368. $used[$type][$match]['module_specific'][] = $module;
  369. }
  370. }
  371. // init if needed
  372. if(!isset($used[$match])) $used[$type][$match] = array('files' => array(), 'module_specific' => array());
  373. // add file
  374. if(!in_array($file, $used[$type][$match]['files'])) $used[$type][$match]['files'][] = $file;
  375. }
  376. }
  377. break;
  378. // PHP file
  379. case 'php':
  380. $matches = array();
  381. $matchesURL = array();
  382. // get matches
  383. preg_match_all('/(BackendLanguage|BL)::(get(Label|Error|Message)|act|err|lbl|msg)\(\'(.*)\'(.*)?\)/iU', $content, $matches);
  384. // match errors
  385. preg_match_all('/&(amp;)?(error|report)=([A-Z0-9-_]+)/i', $content, $matchesURL);
  386. // any errormessages
  387. if(!empty($matchesURL[0]))
  388. {
  389. // loop matches
  390. foreach($matchesURL[3] as $key => $match)
  391. {
  392. $type = 'lbl';
  393. if($matchesURL[2][$key] == 'error') $type = 'Error';
  394. if($matchesURL[2][$key] == 'report') $type = 'Message';
  395. $matches[0][] = '';
  396. $matches[1][] = 'BL';
  397. $matches[2][] = '';
  398. $matches[3][] = $type;
  399. $matches[4][] = SpoonFilter::toCamelCase(SpoonFilter::toCamelCase($match, '-'), '_');
  400. $matches[5][] = '';
  401. }
  402. }
  403. // any matches?
  404. if(!empty($matches[4]))
  405. {
  406. // loop matches
  407. foreach($matches[4] as $key => $match)
  408. {
  409. // set type
  410. $type = 'lbl';
  411. if($matches[3][$key] == 'Error' || $matches[2][$key] == 'err') $type = 'err';
  412. if($matches[3][$key] == 'Message' || $matches[2][$key] == 'msg') $type = 'msg';
  413. // specific module?
  414. if(isset($matches[5][$key]) && $matches[5][$key] != '')
  415. {
  416. // try to grab the module
  417. $specificModule = $matches[5][$key];
  418. $specificModule = trim(str_replace(array(',', '\''), '', $specificModule));
  419. // not core?
  420. if($specificModule != 'core')
  421. {
  422. // dynamic module
  423. if($specificModule == '$this->URL->getModule(' || $specificModule == '$this->getModule(')
  424. {
  425. // init var
  426. $count = 0;
  427. // replace
  428. $modulePath = str_replace(BACKEND_MODULES_PATH, '', $file, $count);
  429. // validate
  430. if($count == 1)
  431. {
  432. // split into chunks
  433. $chunks = (array) explode('/', trim($modulePath, '/'));
  434. // set specific module
  435. if(isset($chunks[0])) $specificModule = $chunks[0];
  436. // skip
  437. else continue;
  438. }
  439. }
  440. // init if needed
  441. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array(), 'module_specific' => array());
  442. // add module
  443. $used[$type][$match]['module_specific'][] = $specificModule;
  444. }
  445. }
  446. else
  447. {
  448. // loop modules
  449. foreach($modules as $module)
  450. {
  451. // determine if this is a module specific locale
  452. if(substr($match, 0, mb_strlen($module)) == SpoonFilter::toCamelCase($module) && mb_strlen($match) > mb_strlen($module) && ctype_upper(substr($match, mb_strlen($module) + 1, 1)))
  453. {
  454. // cleanup
  455. $match = str_replace(SpoonFilter::toCamelCase($module), '', $match);
  456. // init if needed
  457. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array(), 'module_specific' => array());
  458. // add module
  459. $used[$type][$match]['module_specific'][] = $module;
  460. }
  461. }
  462. }
  463. // init if needed
  464. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array(), 'module_specific' => array());
  465. // add file
  466. if(!in_array($file, $used[$type][$match]['files'])) $used[$type][$match]['files'][] = $file;
  467. }
  468. }
  469. break;
  470. // template file
  471. case 'tpl':
  472. $matches = array();
  473. // get matches
  474. preg_match_all('/\{\$(act|err|lbl|msg)([A-Z][a-zA-Z_]*)(\|.*)?\}/U', $content, $matches);
  475. // any matches?
  476. if(isset($matches[2]))
  477. {
  478. // loop matches
  479. foreach($matches[2] as $key => $match)
  480. {
  481. // set type
  482. $type = $matches[1][$key];
  483. // loop modules
  484. foreach($modules as $module)
  485. {
  486. // determine if this is a module specific locale
  487. if(substr($match, 0, mb_strlen($module)) == SpoonFilter::toCamelCase($module) && mb_strlen($match) > mb_strlen($module))
  488. {
  489. // cleanup
  490. $match = str_replace(SpoonFilter::toCamelCase($module), '', $match);
  491. // init if needed
  492. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array(), 'module_specific' => array());
  493. // add module
  494. $used[$type][$match]['module_specific'][] = $module;
  495. }
  496. }
  497. // init if needed
  498. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array(), 'module_specific' => array());
  499. // add file
  500. if(!in_array($file, $used[$type][$match]['files'])) $used[$type][$match]['files'][] = $file;
  501. }
  502. }
  503. break;
  504. }
  505. }
  506. // init var
  507. $nonExisting = array();
  508. // check if the locale is present in the current language
  509. foreach($used as $type => $items)
  510. {
  511. // loop items
  512. foreach($items as $key => $data)
  513. {
  514. // process based on type
  515. switch($type)
  516. {
  517. // error
  518. case 'err':
  519. // module specific?
  520. if(!empty($data['module_specific']))
  521. {
  522. // loop modules
  523. foreach($data['module_specific'] as $module)
  524. {
  525. // if the error isn't found add it to the list
  526. if(substr_count(BL::err($key, $module), '{$' . $type) > 0) $nonExisting['backend' . $key . $type . $module] = array('language' => $language, 'application' => 'backend', 'module' => $module, 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  527. }
  528. }
  529. // not specific
  530. else
  531. {
  532. // if the error isn't found add it to the list
  533. if(substr_count(BL::err($key), '{$' . $type) > 0)
  534. {
  535. // init var
  536. $exists = false;
  537. // loop files
  538. foreach($data['files'] as $file)
  539. {
  540. // init var
  541. $count = 0;
  542. // replace
  543. $modulePath = str_replace(BACKEND_MODULES_PATH, '', $file, $count);
  544. // validate
  545. if($count == 1)
  546. {
  547. // split into chunks
  548. $chunks = (array) explode('/', trim($modulePath, '/'));
  549. // first part is the module
  550. if(isset($chunks[0]) && BL::err($key, $chunks[0]) != '{$' . $type . SpoonFilter::toCamelCase($chunks[0]) . $key . '}') $exists = true;
  551. }
  552. }
  553. // doesn't exists
  554. if(!$exists) $nonExisting['backend' . $key . $type . 'core'] = array('language' => $language, 'application' => 'backend', 'module' => 'core', 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  555. }
  556. }
  557. break;
  558. // label
  559. case 'lbl':
  560. // module specific?
  561. if(!empty($data['module_specific']))
  562. {
  563. // loop modules
  564. foreach($data['module_specific'] as $module)
  565. {
  566. // if the label isn't found add it to the list
  567. if(substr_count(BL::lbl($key, $module), '{$' . $type) > 0) $nonExisting['backend' . $key . $type . $module] = array('language' => $language, 'application' => 'backend', 'module' => $module, 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  568. }
  569. }
  570. // not specific
  571. else
  572. {
  573. // if the label isn't found, check in the specific module
  574. if(substr_count(BL::lbl($key), '{$' . $type) > 0)
  575. {
  576. // init var
  577. $exists = false;
  578. // loop files
  579. foreach($data['files'] as $file)
  580. {
  581. // init var
  582. $count = 0;
  583. // replace
  584. $modulePath = str_replace(BACKEND_MODULES_PATH, '', $file, $count);
  585. // validate
  586. if($count == 1)
  587. {
  588. // split into chunks
  589. $chunks = (array) explode('/', trim($modulePath, '/'));
  590. // first part is the module
  591. if(isset($chunks[0]) && BL::lbl($key, $chunks[0]) != '{$' . $type . SpoonFilter::toCamelCase($chunks[0]) . $key . '}') $exists = true;
  592. }
  593. }
  594. // doesn't exists
  595. if(!$exists) $nonExisting['backend' . $key . $type . 'core'] = array('language' => $language, 'application' => 'backend', 'module' => 'core', 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  596. }
  597. }
  598. break;
  599. // message
  600. case 'msg':
  601. // module specific?
  602. if(!empty($data['module_specific']))
  603. {
  604. // loop modules
  605. foreach($data['module_specific'] as $module)
  606. {
  607. // if the message isn't found add it to the list
  608. if(substr_count(BL::msg($key, $module), '{$' . $type) > 0) $nonExisting['backend' . $key . $type . $module] = array('language' => $language, 'application' => 'backend', 'module' => $module, 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  609. }
  610. }
  611. // not specific
  612. else
  613. {
  614. // if the message isn't found add it to the list
  615. if(substr_count(BL::msg($key), '{$' . $type) > 0)
  616. {
  617. // init var
  618. $exists = false;
  619. // loop files
  620. foreach($data['files'] as $file)
  621. {
  622. // init var
  623. $count = 0;
  624. // replace
  625. $modulePath = str_replace(BACKEND_MODULES_PATH, '', $file, $count);
  626. // validate
  627. if($count == 1)
  628. {
  629. // split into chunks
  630. $chunks = (array) explode('/', trim($modulePath, '/'));
  631. // first part is the module
  632. if(isset($chunks[0]) && BL::msg($key, $chunks[0]) != '{$' . $type . SpoonFilter::toCamelCase($chunks[0]) . $key . '}') $exists = true;
  633. }
  634. }
  635. // doesn't exists
  636. if(!$exists) $nonExisting['backend' . $key . $type . 'core'] = array('language' => $language, 'application' => 'backend', 'module' => 'core', 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  637. }
  638. }
  639. break;
  640. }
  641. }
  642. }
  643. ksort($nonExisting);
  644. // return
  645. return $nonExisting;
  646. }
  647. /**
  648. * Get the locale that is used in the frontend but doesn't exists.
  649. *
  650. * @param string $language The language to check.
  651. * @return array
  652. */
  653. public static function getNonExistingFrontendLocale($language)
  654. {
  655. // get files to process
  656. $tree = self::getTree(FRONTEND_PATH);
  657. $used = array();
  658. // loop files
  659. foreach($tree as $file)
  660. {
  661. // grab content
  662. $content = SpoonFile::getContent($file);
  663. // process the file based on extension
  664. switch(SpoonFile::getExtension($file))
  665. {
  666. // javascript file
  667. case 'js':
  668. $matches = array();
  669. // get matches
  670. preg_match_all('/\{\$(act|err|lbl|msg)(.*)(\|.*)?\}/iU', $content, $matches);
  671. // any matches?
  672. if(isset($matches[2]))
  673. {
  674. // loop matches
  675. foreach($matches[2] as $key => $match)
  676. {
  677. // set type
  678. $type = $matches[1][$key];
  679. // init if needed
  680. if(!isset($used[$match])) $used[$type][$match] = array('files' => array());
  681. // add file
  682. if(!in_array($file, $used[$type][$match]['files'])) $used[$type][$match]['files'][] = $file;
  683. }
  684. }
  685. break;
  686. // PHP file
  687. case 'php':
  688. $matches = array();
  689. // get matches
  690. preg_match_all('/(FrontendLanguage|FL)::(get(Action|Label|Error|Message)|act|lbl|err|msg)\(\'(.*)\'\)/iU', $content, $matches);
  691. // any matches?
  692. if(!empty($matches[4]))
  693. {
  694. // loop matches
  695. foreach($matches[4] as $key => $match)
  696. {
  697. $type = 'lbl';
  698. if($matches[3][$key] == 'Action') $type = 'act';
  699. if($matches[2][$key] == 'act') $type = 'act';
  700. if($matches[3][$key] == 'Error') $type = 'err';
  701. if($matches[2][$key] == 'err') $type = 'err';
  702. if($matches[3][$key] == 'Message') $type = 'msg';
  703. if($matches[2][$key] == 'msg') $type = 'msg';
  704. // init if needed
  705. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array());
  706. // add file
  707. if(!in_array($file, $used[$type][$match]['files'])) $used[$type][$match]['files'][] = $file;
  708. }
  709. }
  710. break;
  711. // template file
  712. case 'tpl':
  713. $matches = array();
  714. // get matches
  715. preg_match_all('/\{\$(act|err|lbl|msg)([a-z-_]*)(\|.*)?\}/iU', $content, $matches);
  716. // any matches?
  717. if(isset($matches[2]))
  718. {
  719. // loop matches
  720. foreach($matches[2] as $key => $match)
  721. {
  722. // set type
  723. $type = $matches[1][$key];
  724. // init if needed
  725. if(!isset($used[$type][$match])) $used[$type][$match] = array('files' => array());
  726. // add file
  727. if(!in_array($file, $used[$type][$match]['files'])) $used[$type][$match]['files'][] = $file;
  728. }
  729. }
  730. break;
  731. }
  732. }
  733. // init var
  734. $nonExisting = array();
  735. // set language
  736. FrontendLanguage::setLocale($language);
  737. // check if the locale is present in the current language
  738. foreach($used as $type => $items)
  739. {
  740. // loop items
  741. foreach($items as $key => $data)
  742. {
  743. // process based on type
  744. switch($type)
  745. {
  746. case 'act':
  747. // if the action isn't available add it to the list
  748. if(FL::act($key) == '{$' . $type . $key . '}') $nonExisting['frontend' . $key . $type] = array('language' => $language, 'application' => 'frontend', 'module' => 'core', 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  749. break;
  750. case 'err':
  751. // if the error isn't available add it to the list
  752. if(FL::err($key) == '{$' . $type . $key . '}') $nonExisting['frontend' . $key . $type] = array('language' => $language, 'application' => 'frontend', 'module' => 'core', 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  753. break;
  754. case 'lbl':
  755. // if the label isn't available add it to the list
  756. if(FL::lbl($key) == '{$' . $type . $key . '}') $nonExisting['frontend' . $key . $type] = array('language' => $language, 'application' => 'frontend', 'module' => 'core', 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  757. break;
  758. case 'msg':
  759. // if the message isn't available add it to the list
  760. if(FL::msg($key) == '{$' . $type . $key . '}') $nonExisting['frontend' . $key . $type] = array('language' => $language, 'application' => 'frontend', 'module' => 'core', 'type' => $type, 'name' => $key, 'used_in' => serialize($data['files']));
  761. break;
  762. }
  763. }
  764. }
  765. ksort($nonExisting);
  766. return $nonExisting;
  767. }
  768. /**
  769. * Get the translations
  770. *
  771. * @param string $application The application.
  772. * @param string $module The module.
  773. * @param array $types The types of the translations to get.
  774. * @param array $languages The languages of the translations to get.
  775. * @param string $name The name.
  776. * @param string $value The value.
  777. * @return array
  778. */
  779. public static function getTranslations($application, $module, $types, $languages, $name, $value)
  780. {
  781. $languages = (array) $languages;
  782. // create an array for the languages, surrounded by quotes (example: 'en')
  783. $aLanguages = array();
  784. foreach($languages as $key => $val) $aLanguages[$key] = '\'' . $val . '\'';
  785. // surround the types with quotes
  786. foreach($types as $key => $val) $types[$key] = '\'' . $val . '\'';
  787. // get db
  788. $db = BackendModel::getContainer()->get('database');
  789. // build the query
  790. $query =
  791. 'SELECT l.id, l.module, l.type, l.name, l.value, l.language
  792. FROM locale AS l
  793. WHERE
  794. l.language IN (' . implode(',', $aLanguages) . ') AND
  795. l.application = ? AND
  796. l.name LIKE ? AND
  797. l.value LIKE ? AND
  798. l.type IN (' . implode(',', $types) . ')';
  799. // add the parameters
  800. $parameters = array($application, '%' . $name . '%', '%' . $value . '%');
  801. // add module to the query if needed
  802. if($module)
  803. {
  804. $query .= ' AND l.module = ?';
  805. $parameters[] = $module;
  806. }
  807. // get the translations
  808. $translations = (array) $db->getRecords($query, $parameters);
  809. // create an array for the sorted translations
  810. $sortedTranslations = array();
  811. // loop translations
  812. foreach($translations as $translation)
  813. {
  814. // add to the sorted array
  815. $sortedTranslations[$translation['type']][$translation['name']][$translation['module']][$translation['language']] = array('id' => $translation['id'], 'value' => $translation['value']);
  816. }
  817. // create an array to use in the datagrid
  818. $dataGridTranslations = array();
  819. // an id that is used for in the datagrid, this is not the id of the translation!
  820. $id = 0;
  821. // loop the sorted translations
  822. foreach($sortedTranslations as $type => $references)
  823. {
  824. // create array for each type
  825. $dataGridTranslations[$type] = array();
  826. foreach($references as $reference => $translation)
  827. {
  828. // loop modules
  829. foreach($translation as $module => $t)
  830. {
  831. // create translation (and increase id)
  832. $trans = array('module' => $module, 'name' => $reference, 'id' => $id++);
  833. // is there a translation? else empty string
  834. foreach($languages as $lang)
  835. {
  836. if(count($languages) == 1) $trans['translation_id'] = isset($t[$lang]) ? $t[$lang]['id'] : '';
  837. $trans[$lang] = isset($t[$lang]) ? $t[$lang]['value'] : '';
  838. }
  839. // add the translation to the array
  840. $dataGridTranslations[$type][] = $trans;
  841. }
  842. }
  843. }
  844. return $dataGridTranslations;
  845. }
  846. /**
  847. * Get the filetree
  848. *
  849. * @param string $path The path to get the filetree for.
  850. * @param array[optional] $tree An array to hold the results.
  851. * @return array
  852. */
  853. private static function getTree($path, array $tree = array())
  854. {
  855. // paths that should be ignored
  856. $ignore = array(
  857. BACKEND_CACHE_PATH, BACKEND_CORE_PATH . '/js/ckeditor',
  858. BACKEND_CACHE_PATH, BACKEND_CORE_PATH . '/js/ckfinder',
  859. FRONTEND_CACHE_PATH
  860. );
  861. // get modules
  862. $modules = BackendModel::getModules();
  863. // get the folder listing
  864. $items = SpoonDirectory::getList($path, true, array('.svn', '.git'));
  865. // already in the modules?
  866. if(substr_count($path, '/modules/') > 0)
  867. {
  868. // get last chunk
  869. $start = strpos($path, '/modules') + 9;
  870. $end = strpos($path, '/', $start + 1);
  871. if($end === false) $moduleName = substr($path, $start);
  872. else $moduleName = substr($path, $start, ($end - $start));
  873. // don't go any deeper
  874. if(!in_array($moduleName, $modules)) return $tree;
  875. }
  876. foreach($items as $item)
  877. {
  878. // if the path should be ignored, skip it
  879. if(in_array($path . '/' . $item, $ignore)) continue;
  880. // if the item is a directory we should index it also (recursive)
  881. if(is_dir($path . '/' . $item)) $tree = self::getTree($path . '/' . $item, $tree);
  882. else
  883. {
  884. // if the file has an extension that has to be processed add it into the tree
  885. if(in_array(SpoonFile::getExtension($item), array('js', 'php', 'tpl'))) $tree[] = $path . '/' . $item;
  886. }
  887. }
  888. return $tree;
  889. }
  890. /**
  891. * Get full type name.
  892. *
  893. * @param string $type The type of the locale.
  894. * @return string
  895. */
  896. public static function getTypeName($type)
  897. {
  898. // get full type name
  899. switch($type)
  900. {
  901. case 'act':
  902. $type = 'action';
  903. break;
  904. case 'err':
  905. $type = 'error';
  906. break;
  907. case 'lbl':
  908. $type = 'label';
  909. break;
  910. case 'msg':
  911. $type = 'message';
  912. break;
  913. }
  914. return $type;
  915. }
  916. /**
  917. * Get all locale types.
  918. *
  919. * @return array
  920. */
  921. public static function getTypesForDropDown()
  922. {
  923. // fetch types
  924. $types = BackendModel::getContainer()->get('database')->getEnumValues('locale', 'type');
  925. // init
  926. $labels = $types;
  927. // loop and build labels
  928. foreach($labels as &$row) $row = SpoonFilter::ucfirst(BL::msg(mb_strtoupper($row), 'core'));
  929. // build array
  930. return array_combine($types, $labels);
  931. }
  932. /**
  933. * Get all locale types for a multicheckbox.
  934. *
  935. * @return array
  936. */
  937. public static function getTypesForMultiCheckbox()
  938. {
  939. // fetch types
  940. $aTypes = BackendModel::getContainer()->get('database')->getEnumValues('locale', 'type');
  941. // init
  942. $labels = $aTypes;
  943. // loop and build labels
  944. foreach($labels as &$row) $row = SpoonFilter::ucfirst(BL::msg(mb_strtoupper($row), 'core'));
  945. // build array
  946. $aTypes = array_combine($aTypes, $labels);
  947. // create a new array to redefine the types for the multicheckbox
  948. $types = array();
  949. // loop the languages
  950. foreach($aTypes as $key => $type)
  951. {
  952. // add to array
  953. $types[$key]['value'] = $key;
  954. $types[$key]['label'] = $type;
  955. }
  956. // return the redefined array
  957. return $types;
  958. }
  959. /**
  960. * Import a locale XML file.
  961. *
  962. * @param SimpleXMLElement $xml The locale XML.
  963. * @param bool[optional] $overwriteConflicts Should we overwrite when there is a conflict?
  964. * @param array[optional] $frontendLanguages The frontend languages to install locale for.
  965. * @param array[optional] $backendLanguages The backend languages to install locale for.
  966. * @param int[optional] $userId Id of the user these translations should be inserted for.
  967. * @param int[optional] $date The date the translation has been inserted.
  968. * @return array The import statistics
  969. */
  970. public static function importXML(SimpleXMLElement $xml, $overwriteConflicts = false, $frontendLanguages = null, $backendLanguages = null, $userId = null, $date = null)
  971. {
  972. $overwriteConflicts = (bool) $overwriteConflicts;
  973. $statistics = array(
  974. 'total' => 0,
  975. 'imported' => 0
  976. );
  977. // set defaults if necessary
  978. // we can't simply use these right away, because this function is also calls by the installer, which does not have Backend-functions
  979. if($frontendLanguages === null) $frontendLanguages = array_keys(BL::getWorkingLanguages());
  980. if($backendLanguages === null) $backendLanguages = array_keys(BL::getInterfaceLanguages());
  981. if($userId === null) $userId = BackendAuthentication::getUser()->getUserId();
  982. if($date === null) $date = BackendModel::getUTCDate();
  983. // get database instance
  984. $db = BackendModel::getContainer()->get('database');
  985. // possible values
  986. $possibleApplications = array('frontend', 'backend');
  987. $possibleModules = (array) $db->getColumn('SELECT m.name FROM modules AS m');
  988. // types
  989. $typesShort = (array) $db->getEnumValues('locale', 'type');
  990. foreach($typesShort as $type) $possibleTypes[$type] = self::getTypeName($type);
  991. // install English translations anyhow, they're fallback
  992. $possibleLanguages = array(
  993. 'frontend' => array_unique(array_merge(array('en'), $frontendLanguages)),
  994. 'backend' => array_unique(array_merge(array('en'), $backendLanguages))
  995. );
  996. // current locale items (used to check for conflicts)
  997. $currentLocale = (array) $db->getColumn(
  998. 'SELECT CONCAT(application, module, type, language, name)
  999. FROM locale'
  1000. );
  1001. // applications
  1002. foreach($xml as $application => $modules)
  1003. {
  1004. // application does not exist
  1005. if(!in_array($application, $possibleApplications)) continue;
  1006. // modules
  1007. foreach($modules as $module => $items)
  1008. {
  1009. // module does not exist
  1010. if(!in_array($module, $possibleModules)) continue;
  1011. // items
  1012. foreach($items as $item)
  1013. {
  1014. // attributes
  1015. $attributes = $item->attributes();
  1016. $type = SpoonFilter::getValue($attributes['type'], $possibleTypes, '');
  1017. $name = SpoonFilter::getValue($attributes['name'], null, '');
  1018. // missing attributes
  1019. if($type == '' || $name == '') continue;
  1020. // real type (shortened)
  1021. $type = array_search($type, $possibleTypes);
  1022. // translations
  1023. foreach($item->translation as $translation)
  1024. {
  1025. // statistics
  1026. $statistics['total']++;
  1027. // attributes
  1028. $attributes = $translation->attributes();
  1029. $language = SpoonFilter::getValue($attributes['language'], $possibleLanguages[$application], '');
  1030. // language does not exist
  1031. if($language == '') continue;
  1032. // the actual translation
  1033. $translation = (string) $translation;
  1034. // locale item
  1035. $locale['user_id'] = $userId;
  1036. $locale['language'] = $language;
  1037. $locale['application'] = $application;
  1038. $locale['module'] = $module;
  1039. $locale['type'] = $type;
  1040. $locale['name'] = $name;
  1041. $locale['value'] = $translation;
  1042. $locale['edited_on'] = $date;
  1043. // check if translation does not yet exist, or if the translation can be overridden
  1044. if(!in_array($application . $module . $type . $language . $name, $currentLocale) || $overwriteConflicts)
  1045. {
  1046. $db->execute(
  1047. 'INSERT INTO locale (user_id, language, application, module, type, name, value, edited_on)
  1048. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  1049. ON DUPLICATE KEY UPDATE user_id = ?, value = ?, edited_on = ?',
  1050. array(
  1051. $locale['user_id'], $locale['language'], $locale['application'], $locale['module'],
  1052. $locale['type'], $locale['name'], $locale['value'], $locale['edited_on'],
  1053. $locale['user_id'], $locale['value'], $locale['edited_on'])
  1054. );
  1055. // statistics
  1056. $statistics['imported']++;
  1057. }
  1058. }
  1059. }
  1060. }
  1061. }
  1062. // rebuild cache
  1063. foreach($possibleApplications as $application)
  1064. {
  1065. foreach($possibleLanguages[$application] as $language) self::buildCache($language, $application);
  1066. }
  1067. return $statistics;
  1068. }
  1069. /**
  1070. * Insert a new locale item.
  1071. *
  1072. * @param array $item The data to insert.
  1073. * @return int
  1074. */
  1075. public static function insert(array $item)
  1076. {
  1077. // actions should be urlized
  1078. if($item['type'] == 'act' && urldecode($item['value']) != $item['value']) $item['value'] = SpoonFilter::urlise($item['value']);
  1079. // insert item
  1080. $item['id'] = (int) BackendModel::getContainer()->get('database')->insert('locale', $item);
  1081. // rebuild the cache
  1082. self::buildCache($item['language'], $item['application']);
  1083. // return the new id
  1084. return $item['id'];
  1085. }
  1086. /**
  1087. * Update a locale item.
  1088. *
  1089. * @param array $item The new data.
  1090. */
  1091. public static function update(array $item)
  1092. {
  1093. // actions should be urlized
  1094. if($item['type'] == 'act' && urldecode($item['value']) != $item['value']) $item['value'] = SpoonFilter::urlise($item['value']);
  1095. // update category
  1096. $updated = BackendModel::getContainer()->get('database')->update('locale', $item, 'id = ?', array($item['id']));
  1097. // rebuild the cache
  1098. self::buildCache($item['language'], $item['application']);
  1099. return $updated;
  1100. }
  1101. }