PageRenderTime 54ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Backend/Modules/Extensions/Engine/Model.php

http://github.com/forkcms/forkcms
PHP | 1088 lines | 856 code | 97 blank | 135 comment | 60 complexity | 85be257ec7b386c6683720d424c785c8 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, MIT, AGPL-3.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. namespace Backend\Modules\Extensions\Engine;
  3. use Backend\Modules\Locale\Engine\Model as BackendLocaleModel;
  4. use Common\ModulesSettings;
  5. use Symfony\Component\Filesystem\Filesystem;
  6. use Symfony\Component\Finder\Finder;
  7. use Backend\Core\Engine\Authentication as BackendAuthentication;
  8. use Backend\Core\Engine\DataGridFunctions as BackendDataGridFunctions;
  9. use Backend\Core\Engine\Exception;
  10. use Backend\Core\Language\Language as BL;
  11. use Backend\Core\Engine\Model as BackendModel;
  12. /**
  13. * In this file we store all generic functions that we will be using in the extensions module.
  14. */
  15. class Model
  16. {
  17. /**
  18. * Overview of templates.
  19. *
  20. * @var string
  21. */
  22. const QUERY_BROWSE_TEMPLATES = 'SELECT i.id, i.label AS title
  23. FROM themes_templates AS i
  24. WHERE i.theme = ?
  25. ORDER BY i.label ASC';
  26. /**
  27. * Modules which are part of the core and can not be managed.
  28. *
  29. * @var array
  30. */
  31. private static $ignoredModules = [
  32. 'Authentication',
  33. 'Dashboard',
  34. 'Error',
  35. 'Extensions',
  36. 'Settings',
  37. ];
  38. /**
  39. * Build HTML for a template (visual representation)
  40. *
  41. * @param string $format The template format.
  42. * @param bool $large Will the HTML be used in a large version?
  43. *
  44. * @return string
  45. */
  46. public static function buildTemplateHTML(string $format, bool $large = false): string
  47. {
  48. // cleanup
  49. $table = self::templateSyntaxToArray($format);
  50. // init var
  51. $rows = count($table);
  52. if ($rows === 0) {
  53. throw new Exception('Invalid template-format.');
  54. }
  55. $cells = count($table[0]);
  56. $htmlContent = [];
  57. // loop rows
  58. for ($y = 0; $y < $rows; ++$y) {
  59. $htmlContent[$y] = [];
  60. // loop cells
  61. for ($x = 0; $x < $cells; ++$x) {
  62. // skip if needed
  63. if (!isset($table[$y][$x])) {
  64. continue;
  65. }
  66. // get value
  67. $value = $table[$y][$x];
  68. // init var
  69. $colspan = 1;
  70. // reset items in the same column
  71. while ($x + $colspan < $cells && $table[$y][$x + $colspan] === $value) {
  72. $table[$y][$x + $colspan++] = null;
  73. }
  74. // init var
  75. $rowspan = 1;
  76. $rowMatches = true;
  77. // loop while the rows match
  78. while ($rowMatches && $y + $rowspan < $rows) {
  79. // loop columns inside spanned columns
  80. for ($i = 0; $i < $colspan; ++$i) {
  81. // check value
  82. if ($table[$y + $rowspan][$x + $i] !== $value) {
  83. // no match, so stop
  84. $rowMatches = false;
  85. break;
  86. }
  87. }
  88. // any rowmatches?
  89. if ($rowMatches) {
  90. // loop columns and reset value
  91. for ($i = 0; $i < $colspan; ++$i) {
  92. $table[$y + $rowspan][$x + $i] = null;
  93. }
  94. // increment
  95. ++$rowspan;
  96. }
  97. }
  98. $htmlContent[$y][$x] = [
  99. 'title' => \SpoonFilter::ucfirst($value),
  100. 'value' => $value,
  101. 'exists' => $value != '/',
  102. 'rowspan' => $rowspan,
  103. 'colspan' => $colspan,
  104. 'large' => $large,
  105. ];
  106. }
  107. }
  108. $templating = BackendModel::get('template');
  109. $templating->assign('table', $htmlContent);
  110. $html = $templating->getContent('Extensions/Layout/Templates/Templates.html.twig');
  111. return $html;
  112. }
  113. /**
  114. * Checks the settings and optionally returns an array with warnings
  115. *
  116. * @return array
  117. */
  118. public static function checkSettings(): array
  119. {
  120. $warnings = [];
  121. $akismetModules = self::getModulesThatRequireAkismet();
  122. $googleMapsModules = self::getModulesThatRequireGoogleMaps();
  123. // check if this action is allowed
  124. if (!BackendAuthentication::isAllowedAction('Index', 'Settings')) {
  125. return [];
  126. }
  127. // check if the akismet key is available if there are modules that require it
  128. if (!empty($akismetModules) && BackendModel::get('fork.settings')->get('Core', 'akismet_key', null) == '') {
  129. // add warning
  130. $warnings[] = [
  131. 'message' => sprintf(
  132. BL::err('AkismetKey'),
  133. BackendModel::createUrlForAction('Index', 'Settings')
  134. ),
  135. ];
  136. }
  137. // check if the google maps key is available if there are modules that require it
  138. if (!empty($googleMapsModules)
  139. && BackendModel::get('fork.settings')->get('Core', 'google_maps_key', null) == '') {
  140. // add warning
  141. $warnings[] = [
  142. 'message' => sprintf(
  143. BL::err('GoogleMapsKey'),
  144. BackendModel::createUrlForAction('Index', 'Settings')
  145. ),
  146. ];
  147. }
  148. return $warnings;
  149. }
  150. /**
  151. * Clear all applications cache.
  152. *
  153. * Note: we do not need to rebuild anything, the core will do this when noticing the cache files are missing.
  154. */
  155. public static function clearCache(): void
  156. {
  157. $finder = new Finder();
  158. $filesystem = new Filesystem();
  159. $files = $finder->files()
  160. ->name('*.php')
  161. ->name('*.js')
  162. ->in(BACKEND_CACHE_PATH . '/Locale')
  163. ->in(FRONTEND_CACHE_PATH . '/Navigation')
  164. ->in(FRONTEND_CACHE_PATH . '/Locale');
  165. foreach ($files as $file) {
  166. $filesystem->remove($file->getRealPath());
  167. }
  168. BackendModel::getContainer()->get('cache.backend_navigation')->delete();
  169. }
  170. /**
  171. * Delete a template.
  172. *
  173. * @param int $id The id of the template to delete.
  174. *
  175. * @return bool
  176. */
  177. public static function deleteTemplate(int $id): bool
  178. {
  179. $templates = self::getTemplates();
  180. // we can't delete a template that doesn't exist
  181. if (!isset($templates[$id])) {
  182. return false;
  183. }
  184. // we can't delete the last template
  185. if (count($templates) === 1) {
  186. return false;
  187. }
  188. // we can't delete the default template
  189. if ($id == BackendModel::get('fork.settings')->get('Pages', 'default_template')) {
  190. return false;
  191. }
  192. if (self::isTemplateInUse($id)) {
  193. return false;
  194. }
  195. $database = BackendModel::getContainer()->get('database');
  196. $database->delete('themes_templates', 'id = ?', $id);
  197. $ids = (array) $database->getColumn(
  198. 'SELECT i.revision_id
  199. FROM pages AS i
  200. WHERE i.template_id = ? AND i.status != ?',
  201. [$id, 'active']
  202. );
  203. if (!empty($ids)) {
  204. // delete those pages and the linked blocks
  205. $database->delete('pages', 'revision_id IN(' . implode(',', $ids) . ')');
  206. $database->delete('pages_blocks', 'revision_id IN(' . implode(',', $ids) . ')');
  207. }
  208. return true;
  209. }
  210. /**
  211. * Does this module exist.
  212. * This does not check for existence in the database but on the filesystem.
  213. *
  214. * @param string $module Module to check for existence.
  215. *
  216. * @return bool
  217. */
  218. public static function existsModule(string $module): bool
  219. {
  220. return is_dir(BACKEND_MODULES_PATH . '/' . $module);
  221. }
  222. /**
  223. * Check if a template exists
  224. *
  225. * @param int $id The Id of the template to check for existence.
  226. *
  227. * @return bool
  228. */
  229. public static function existsTemplate(int $id): bool
  230. {
  231. return (bool) BackendModel::getContainer()->get('database')->getVar(
  232. 'SELECT i.id FROM themes_templates AS i WHERE i.id = ?',
  233. [$id]
  234. );
  235. }
  236. /**
  237. * Does this template exist.
  238. * This does not check for existence in the database but on the filesystem.
  239. *
  240. * @param string $theme Theme to check for existence.
  241. *
  242. * @return bool
  243. */
  244. public static function existsTheme(string $theme): bool
  245. {
  246. return is_dir(FRONTEND_PATH . '/Themes/' . (string) $theme) || $theme === 'Core';
  247. }
  248. public static function getExtras(): array
  249. {
  250. $extras = (array) BackendModel::getContainer()->get('database')->getRecords(
  251. 'SELECT i.id, i.module, i.type, i.label, i.data
  252. FROM modules_extras AS i
  253. INNER JOIN modules AS m ON i.module = m.name
  254. WHERE i.hidden = ?
  255. ORDER BY i.module, i.sequence',
  256. [false],
  257. 'id'
  258. );
  259. $itemsToRemove = [];
  260. foreach ($extras as $id => &$row) {
  261. $row['data'] = $row['data'] === null ? [] : @unserialize($row['data']);
  262. if (isset($row['data']['language']) && $row['data']['language'] != BL::getWorkingLanguage()) {
  263. $itemsToRemove[] = $id;
  264. }
  265. // set URL if needed, we use '' instead of null, because otherwise the module of the current action (modules) is used.
  266. if (!isset($row['data']['url'])) {
  267. $row['data']['url'] = BackendModel::createUrlForAction('', $row['module']);
  268. }
  269. $name = \SpoonFilter::ucfirst(BL::lbl($row['label']));
  270. if (isset($row['data']['extra_label'])) {
  271. $name = $row['data']['extra_label'];
  272. }
  273. if (isset($row['data']['label_variables'])) {
  274. $name = vsprintf($name, $row['data']['label_variables']);
  275. }
  276. // add human readable name
  277. $module = \SpoonFilter::ucfirst(BL::lbl(\SpoonFilter::toCamelCase($row['module'])));
  278. $extraTypeLabel = \SpoonFilter::ucfirst(BL::lbl(\SpoonFilter::toCamelCase('ExtraType_' . $row['type'])));
  279. $row['human_name'] = $extraTypeLabel . ': ' . $name;
  280. $row['path'] = $extraTypeLabel . ' › ' . $module . ($module !== $name ? ' › ' . $name : '');
  281. }
  282. // any items to remove?
  283. if (!empty($itemsToRemove)) {
  284. foreach ($itemsToRemove as $id) {
  285. unset($extras[$id]);
  286. }
  287. }
  288. return $extras;
  289. }
  290. public static function getExtrasData(): array
  291. {
  292. $extras = (array) BackendModel::getContainer()->get('database')->getRecords(
  293. 'SELECT i.id, i.module, i.type, i.label, i.data
  294. FROM modules_extras AS i
  295. INNER JOIN modules AS m ON i.module = m.name
  296. WHERE i.hidden = ?
  297. ORDER BY i.module, i.sequence',
  298. [false]
  299. );
  300. $values = [];
  301. foreach ($extras as $row) {
  302. $row['data'] = @unserialize($row['data']);
  303. // remove items that are not for the current language
  304. if (isset($row['data']['language']) && $row['data']['language'] != BL::getWorkingLanguage()) {
  305. continue;
  306. }
  307. // set URL if needed
  308. if (!isset($row['data']['url'])) {
  309. $row['data']['url'] = BackendModel::createUrlForAction(
  310. 'Index',
  311. $row['module']
  312. );
  313. }
  314. $name = \SpoonFilter::ucfirst(BL::lbl($row['label']));
  315. if (isset($row['data']['extra_label'])) {
  316. $name = $row['data']['extra_label'];
  317. }
  318. if (isset($row['data']['label_variables'])) {
  319. $name = vsprintf($name, $row['data']['label_variables']);
  320. }
  321. $moduleName = \SpoonFilter::ucfirst(BL::lbl(\SpoonFilter::toCamelCase($row['module'])));
  322. if (!isset($values[$row['module']])) {
  323. $values[$row['module']] = [
  324. 'value' => $row['module'],
  325. 'name' => $moduleName,
  326. 'items' => [],
  327. ];
  328. }
  329. $values[$row['module']]['items'][$row['type']][$name] = ['id' => $row['id'], 'label' => $name];
  330. }
  331. return $values;
  332. }
  333. /**
  334. * Fetch the module information from the info.xml file.
  335. *
  336. * @param string $module
  337. *
  338. * @return array
  339. */
  340. public static function getModuleInformation(string $module): array
  341. {
  342. $pathInfoXml = BACKEND_MODULES_PATH . '/' . $module . '/info.xml';
  343. $information = ['data' => [], 'warnings' => []];
  344. if (is_file($pathInfoXml)) {
  345. try {
  346. $infoXml = @new \SimpleXMLElement($pathInfoXml, LIBXML_NOCDATA, true);
  347. $information['data'] = self::processModuleXml($infoXml);
  348. if (empty($information['data'])) {
  349. $information['warnings'][] = [
  350. 'message' => BL::getMessage('InformationFileIsEmpty'),
  351. ];
  352. }
  353. } catch (Exception $e) {
  354. $information['warnings'][] = [
  355. 'message' => BL::getMessage('InformationFileCouldNotBeLoaded'),
  356. ];
  357. }
  358. } else {
  359. $information['warnings'][] = [
  360. 'message' => BL::getMessage('InformationFileIsMissing'),
  361. ];
  362. }
  363. return $information;
  364. }
  365. /**
  366. * Get modules based on the directory listing in the backend application.
  367. *
  368. * If a module contains a info.xml it will be parsed.
  369. *
  370. * @return array
  371. */
  372. public static function getModules(): array
  373. {
  374. $installedModules = (array) BackendModel::getContainer()
  375. ->getParameter('installed_modules');
  376. $modules = BackendModel::getModulesOnFilesystem(false);
  377. $manageableModules = [];
  378. // get more information for each module
  379. foreach ($modules as $moduleName) {
  380. if (in_array($moduleName, self::$ignoredModules)) {
  381. continue;
  382. }
  383. $module = [];
  384. $module['id'] = 'module_' . $moduleName;
  385. $module['raw_name'] = $moduleName;
  386. $module['name'] = \SpoonFilter::ucfirst(BL::getLabel(\SpoonFilter::toCamelCase($moduleName)));
  387. $module['description'] = '';
  388. $module['version'] = '';
  389. $module['installed'] = false;
  390. if (in_array($moduleName, $installedModules)) {
  391. $module['installed'] = true;
  392. }
  393. try {
  394. $infoXml = @new \SimpleXMLElement(
  395. BACKEND_MODULES_PATH . '/' . $module['raw_name'] . '/info.xml',
  396. LIBXML_NOCDATA,
  397. true
  398. );
  399. $info = self::processModuleXml($infoXml);
  400. // set fields if they were found in the XML
  401. if (isset($info['description'])) {
  402. $module['description'] = BackendDataGridFunctions::truncate($info['description'], 80);
  403. }
  404. if (isset($info['version'])) {
  405. $module['version'] = $info['version'];
  406. }
  407. } catch (\Exception $e) {
  408. // don't act upon error, we simply won't possess some info
  409. }
  410. $manageableModules[] = $module;
  411. }
  412. return $manageableModules;
  413. }
  414. /**
  415. * Fetch the list of modules that require Akismet API key
  416. *
  417. * @return array
  418. */
  419. public static function getModulesThatRequireAkismet(): array
  420. {
  421. return self::getModulesThatRequireSetting('akismet');
  422. }
  423. /**
  424. * Fetch the list of modules that require Google Maps API key
  425. *
  426. * @return array
  427. */
  428. public static function getModulesThatRequireGoogleMaps(): array
  429. {
  430. return self::getModulesThatRequireSetting('google_maps');
  431. }
  432. /**
  433. * Fetch the list of modules that require Google Recaptcha API key
  434. *
  435. * @return array
  436. */
  437. public static function getModulesThatRequireGoogleRecaptcha(): array
  438. {
  439. return self::getModulesThatRequireSetting('google_recaptcha');
  440. }
  441. /**
  442. * Fetch the list of modules that require a certain setting. The setting is affixed by 'requires_'
  443. *
  444. * @param string $setting
  445. *
  446. * @return array
  447. */
  448. private static function getModulesThatRequireSetting(string $setting): array
  449. {
  450. if ($setting === '') {
  451. return [];
  452. }
  453. /** @var ModulesSettings $moduleSettings */
  454. $moduleSettings = BackendModel::get('fork.settings');
  455. return array_filter(
  456. BackendModel::getModules(),
  457. function (string $module) use ($moduleSettings, $setting): bool {
  458. return $moduleSettings->get($module, 'requires_' . $setting, false);
  459. }
  460. );
  461. }
  462. public static function getTemplate(int $id): array
  463. {
  464. return (array) BackendModel::getContainer()->get('database')->getRecord(
  465. 'SELECT i.* FROM themes_templates AS i WHERE i.id = ?',
  466. [$id]
  467. );
  468. }
  469. public static function getTemplates(string $theme = null): array
  470. {
  471. $database = BackendModel::getContainer()->get('database');
  472. $theme = \SpoonFilter::getValue(
  473. (string) $theme,
  474. null,
  475. BackendModel::get('fork.settings')->get('Core', 'theme', 'Fork')
  476. );
  477. $templates = (array) $database->getRecords(
  478. 'SELECT i.id, i.label, i.path, i.data
  479. FROM themes_templates AS i
  480. WHERE i.theme = ? AND i.active = ?
  481. ORDER BY i.label ASC',
  482. [$theme, true],
  483. 'id'
  484. );
  485. $extras = (array) self::getExtras();
  486. $half = (int) ceil(count($templates) / 2);
  487. $i = 0;
  488. foreach ($templates as &$row) {
  489. $row['data'] = unserialize($row['data']);
  490. $row['has_block'] = false;
  491. // reset
  492. if (isset($row['data']['default_extras_' . BL::getWorkingLanguage()])) {
  493. $row['data']['default_extras'] = $row['data']['default_extras_' . BL::getWorkingLanguage()];
  494. }
  495. // any extras?
  496. if (isset($row['data']['default_extras'])) {
  497. foreach ($row['data']['default_extras'] as $value) {
  498. if (\SpoonFilter::isInteger($value)
  499. && isset($extras[$value]) && $extras[$value]['type'] == 'block'
  500. ) {
  501. $row['has_block'] = true;
  502. }
  503. }
  504. }
  505. // validate
  506. if (!isset($row['data']['format'])) {
  507. throw new Exception('Invalid template-format.');
  508. }
  509. $row['html'] = self::buildTemplateHTML($row['data']['format']);
  510. $row['htmlLarge'] = self::buildTemplateHTML($row['data']['format'], true);
  511. $row['json'] = json_encode($row);
  512. if ($i == $half) {
  513. $row['break'] = true;
  514. }
  515. ++$i;
  516. }
  517. return (array) $templates;
  518. }
  519. public static function getThemes(): array
  520. {
  521. $records = [];
  522. $finder = new Finder();
  523. foreach ($finder->directories()->in(FRONTEND_PATH . '/Themes')->depth(0) as $directory) {
  524. $pathInfoXml = BackendModel::getContainer()->getParameter('site.path_www') . '/src/Frontend/Themes/'
  525. . $directory->getBasename() . '/info.xml';
  526. if (!is_file($pathInfoXml)) {
  527. throw new Exception('info.xml is missing for the theme ' . $directory->getBasename());
  528. }
  529. try {
  530. $infoXml = @new \SimpleXMLElement($pathInfoXml, LIBXML_NOCDATA, true);
  531. $information = self::processThemeXml($infoXml);
  532. if (empty($information)) {
  533. throw new Exception('Invalid info.xml');
  534. }
  535. } catch (Exception $e) {
  536. $information['thumbnail'] = 'thumbnail.png';
  537. }
  538. $item = [];
  539. $item['value'] = $directory->getBasename();
  540. $item['label'] = $directory->getBasename();
  541. $item['thumbnail'] = '/src/Frontend/Themes/' . $item['value'] . '/' . $information['thumbnail'];
  542. $item['installed'] = self::isThemeInstalled($item['value']);
  543. $item['installable'] = isset($information['templates']);
  544. $records[$item['value']] = $item;
  545. }
  546. return (array) $records;
  547. }
  548. public static function createTemplateXmlForExport(string $theme): string
  549. {
  550. $charset = BackendModel::getContainer()->getParameter('kernel.charset');
  551. // build xml
  552. $xml = new \DOMDocument('1.0', $charset);
  553. $xml->preserveWhiteSpace = false;
  554. $xml->formatOutput = true;
  555. $root = $xml->createElement('templates');
  556. $xml->appendChild($root);
  557. $database = BackendModel::getContainer()->get('database');
  558. $records = $database->getRecords(self::QUERY_BROWSE_TEMPLATES, [$theme]);
  559. foreach ($records as $row) {
  560. $template = self::getTemplate($row['id']);
  561. $data = unserialize($template['data']);
  562. $templateElement = $xml->createElement('template');
  563. $templateElement->setAttribute('label', $template['label']);
  564. $templateElement->setAttribute('path', $template['path']);
  565. $root->appendChild($templateElement);
  566. $positionsElement = $xml->createElement('positions');
  567. $templateElement->appendChild($positionsElement);
  568. foreach ($data['names'] as $name) {
  569. $positionElement = $xml->createElement('position');
  570. $positionElement->setAttribute('name', $name);
  571. $positionsElement->appendChild($positionElement);
  572. }
  573. $formatElement = $xml->createElement('format');
  574. $templateElement->appendChild($formatElement);
  575. $formatElement->nodeValue = $data['format'];
  576. }
  577. return $xml->saveXML();
  578. }
  579. public static function hasModuleWarnings(string $module): string
  580. {
  581. $moduleInformation = self::getModuleInformation($module);
  582. return !empty($moduleInformation['warnings']);
  583. }
  584. public static function insertTemplate(array $template): int
  585. {
  586. return (int) BackendModel::getContainer()->get('database')->insert('themes_templates', $template);
  587. }
  588. public static function installModule(string $module): void
  589. {
  590. $class = 'Backend\\Modules\\' . $module . '\\Installer\\Installer';
  591. $variables = [];
  592. // run installer
  593. $installer = new $class(
  594. BackendModel::getContainer()->get('database'),
  595. BL::getActiveLanguages(),
  596. array_keys(BL::getInterfaceLanguages()),
  597. false,
  598. $variables
  599. );
  600. $installer->install();
  601. // clear the cache so locale (and so much more) gets rebuilt
  602. self::clearCache();
  603. }
  604. public static function installTheme(string $theme): void
  605. {
  606. $basePath = FRONTEND_PATH . '/Themes/' . $theme;
  607. $pathInfoXml = $basePath . '/info.xml';
  608. $pathTranslations = $basePath . '/locale.xml';
  609. $infoXml = @new \SimpleXMLElement($pathInfoXml, LIBXML_NOCDATA, true);
  610. $information = self::processThemeXml($infoXml);
  611. if (empty($information)) {
  612. throw new Exception('Invalid info.xml');
  613. }
  614. if (is_file($pathTranslations)) {
  615. $translations = @simplexml_load_file($pathTranslations);
  616. if ($translations !== false) {
  617. BackendLocaleModel::importXML($translations);
  618. }
  619. }
  620. foreach ($information['templates'] as $template) {
  621. $item = [];
  622. $item['theme'] = $information['name'];
  623. $item['label'] = $template['label'];
  624. $item['path'] = $template['path'];
  625. $item['active'] = true;
  626. $item['data']['format'] = $template['format'];
  627. $item['data']['image'] = $template['image'];
  628. // build positions
  629. $item['data']['names'] = [];
  630. $item['data']['default_extras'] = [];
  631. foreach ($template['positions'] as $position) {
  632. $item['data']['names'][] = $position['name'];
  633. $item['data']['default_extras'][$position['name']] = [];
  634. // add default widgets
  635. foreach ($position['widgets'] as $widget) {
  636. // fetch extra_id for this extra
  637. $extraId = (int) BackendModel::getContainer()->get('database')->getVar(
  638. 'SELECT i.id
  639. FROM modules_extras AS i
  640. WHERE type = ? AND module = ? AND action = ? AND data IS NULL AND hidden = ?',
  641. ['widget', $widget['module'], $widget['action'], false]
  642. );
  643. // add extra to defaults
  644. if ($extraId) {
  645. $item['data']['default_extras'][$position['name']][] = $extraId;
  646. }
  647. }
  648. // add default editors
  649. foreach ($position['editors'] as $editor) {
  650. $item['data']['default_extras'][$position['name']][] = 0;
  651. }
  652. }
  653. $item['data'] = serialize($item['data']);
  654. $item['id'] = self::insertTemplate($item);
  655. }
  656. }
  657. public static function isModuleInstalled(string $module): bool
  658. {
  659. return (bool) BackendModel::getContainer()->get('database')->getVar(
  660. 'SELECT 1
  661. FROM modules
  662. WHERE name = ?
  663. LIMIT 1',
  664. $module
  665. );
  666. }
  667. /**
  668. * Is the provided template id in use by active versions of pages?
  669. *
  670. * @param int $templateId The id of the template to check.
  671. *
  672. * @return bool
  673. */
  674. public static function isTemplateInUse(int $templateId): bool
  675. {
  676. return (bool) BackendModel::getContainer()->get('database')->getVar(
  677. 'SELECT 1
  678. FROM pages AS i
  679. WHERE i.template_id = ? AND i.status = ?
  680. LIMIT 1',
  681. [$templateId, 'active']
  682. );
  683. }
  684. public static function isThemeInstalled(string $theme): bool
  685. {
  686. return (bool) BackendModeL::getContainer()->get('database')->getVar(
  687. 'SELECT 1
  688. FROM themes_templates
  689. WHERE theme = ?
  690. LIMIT 1',
  691. [$theme]
  692. );
  693. }
  694. /**
  695. * Check if a directory is writable.
  696. * The default is_writable function has problems due to Windows ACLs "bug"
  697. *
  698. * @param string $path The path to check.
  699. *
  700. * @return bool
  701. */
  702. public static function isWritable(string $path): bool
  703. {
  704. $path = rtrim((string) $path, '/');
  705. $file = uniqid('', true) . '.tmp';
  706. $return = @file_put_contents($path . '/' . $file, 'temporary file', FILE_APPEND);
  707. if ($return === false) {
  708. return false;
  709. }
  710. unlink($path . '/' . $file);
  711. return true;
  712. }
  713. public static function processModuleXml(\SimpleXMLElement $xml): array
  714. {
  715. $information = [];
  716. // fetch theme node
  717. $module = $xml->xpath('/module');
  718. if (isset($module[0])) {
  719. $module = $module[0];
  720. }
  721. // fetch general module info
  722. $information['name'] = (string) $module->name;
  723. $information['version'] = (string) $module->version;
  724. $information['requirements'] = (array) $module->requirements;
  725. $information['description'] = (string) $module->description;
  726. $information['cronjobs'] = [];
  727. // authors
  728. foreach ($xml->xpath('/module/authors/author') as $author) {
  729. $information['authors'][] = (array) $author;
  730. }
  731. // cronjobs
  732. foreach ($xml->xpath('/module/cronjobs/cronjob') as $cronjob) {
  733. $attributes = $cronjob->attributes();
  734. if (!isset($attributes['action'])) {
  735. continue;
  736. }
  737. // build cronjob information
  738. $item = [];
  739. $item['minute'] = (isset($attributes['minute'])) ? $attributes['minute'] : '*';
  740. $item['hour'] = (isset($attributes['hour'])) ? $attributes['hour'] : '*';
  741. $item['day-of-month'] = (isset($attributes['day-of-month'])) ? $attributes['day-of-month'] : '*';
  742. $item['month'] = (isset($attributes['month'])) ? $attributes['month'] : '*';
  743. $item['day-of-week'] = (isset($attributes['day-of-week'])) ? $attributes['day-of-week'] : '*';
  744. $item['action'] = $attributes['action'];
  745. $item['description'] = $cronjob[0];
  746. // check if cronjob has already been run
  747. $cronjobs = (array) BackendModel::get('fork.settings')->get('Core', 'cronjobs');
  748. $item['active'] = in_array($information['name'] . '.' . $attributes['action'], $cronjobs);
  749. $information['cronjobs'][] = $item;
  750. }
  751. // events
  752. foreach ($xml->xpath('/module/events/event') as $event) {
  753. $attributes = $event->attributes();
  754. // build event information and add it to the list
  755. $information['events'][] = [
  756. 'application' => (isset($attributes['application'])) ? $attributes['application'] : '',
  757. 'name' => (isset($attributes['name'])) ? $attributes['name'] : '',
  758. 'description' => $event[0],
  759. ];
  760. }
  761. return $information;
  762. }
  763. public static function processThemeXml(\SimpleXMLElement $xml): array
  764. {
  765. $information = [];
  766. $theme = $xml->xpath('/theme');
  767. if (isset($theme[0])) {
  768. $theme = $theme[0];
  769. }
  770. // fetch general theme info
  771. $information['name'] = (string) $theme->name;
  772. $information['version'] = (string) $theme->version;
  773. $information['requirements'] = (array) $theme->requirements;
  774. $information['thumbnail'] = (string) $theme->thumbnail;
  775. $information['description'] = (string) $theme->description;
  776. // authors
  777. foreach ($xml->xpath('/theme/authors/author') as $author) {
  778. $information['authors'][] = (array) $author;
  779. }
  780. // meta navigation
  781. $meta = $theme->metanavigation->attributes();
  782. if (isset($meta['supported'])) {
  783. $information['meta'] = (string) $meta['supported'] && (string) $meta['supported'] !== 'false';
  784. }
  785. // templates
  786. $information['templates'] = [];
  787. foreach ($xml->xpath('/theme/templates/template') as $templateXML) {
  788. $template = [];
  789. // template data
  790. $template['label'] = (string) $templateXML['label'];
  791. $template['path'] = (string) $templateXML['path'];
  792. $template['image'] = isset($templateXML['image'])
  793. ? (string) $templateXML['image'] && (string) $templateXML['image'] !== 'false' : false;
  794. $template['format'] = trim(str_replace(["\n", "\r", ' '], '', (string) $templateXML->format));
  795. // loop positions
  796. foreach ($templateXML->positions->position as $positionXML) {
  797. $position = [];
  798. $position['name'] = (string) $positionXML['name'];
  799. // widgets
  800. $position['widgets'] = [];
  801. if ($positionXML->defaults->widget) {
  802. foreach ($positionXML->defaults->widget as $widget) {
  803. $position['widgets'][] = [
  804. 'module' => (string) $widget['module'],
  805. 'action' => (string) $widget['action'],
  806. ];
  807. }
  808. }
  809. // editor
  810. $position['editors'] = [];
  811. if ($positionXML->defaults->editor) {
  812. foreach ($positionXML->defaults->editor as $editor) {
  813. $position['editors'][] = (string) trim($editor);
  814. }
  815. }
  816. $template['positions'][] = $position;
  817. }
  818. $information['templates'][] = $template;
  819. }
  820. return self::validateThemeInformation($information);
  821. }
  822. public static function templateSyntaxToArray(string $syntax): array
  823. {
  824. $syntax = (string) $syntax;
  825. $syntax = trim(str_replace(["\n", "\r", ' '], '', $syntax));
  826. $table = [];
  827. // check template settings format
  828. if (!static::isValidTemplateSyntaxFormat($syntax)) {
  829. return $table;
  830. }
  831. // split into rows
  832. $rows = explode('],[', $syntax);
  833. foreach ($rows as $i => $row) {
  834. $row = trim(str_replace(['[', ']'], '', $row));
  835. $table[$i] = (array) explode(',', $row);
  836. }
  837. if (!isset($table[0])) {
  838. return [];
  839. }
  840. $columns = count($table[0]);
  841. foreach ($table as $row) {
  842. if (count($row) !== $columns) {
  843. return [];
  844. }
  845. }
  846. return $table;
  847. }
  848. /**
  849. * Validate template syntax format
  850. *
  851. * @param string $syntax
  852. * @return bool
  853. */
  854. public static function isValidTemplateSyntaxFormat(string $syntax): bool
  855. {
  856. return \SpoonFilter::isValidAgainstRegexp(
  857. '/^\[(\/|[a-z0-9])+(,(\/|[a-z0-9]+))*\](,\[(\/|[a-z0-9])+(,(\/|[a-z0-9]+))*\])*$/i',
  858. $syntax
  859. );
  860. }
  861. public static function updateTemplate(array $templateData): void
  862. {
  863. BackendModel::getContainer()->get('database')->update(
  864. 'themes_templates',
  865. $templateData,
  866. 'id = ?',
  867. [(int) $templateData['id']]
  868. );
  869. }
  870. /**
  871. * Make sure that we have an entirely valid theme information array
  872. *
  873. * @param array $information Contains the parsed theme info.xml data.
  874. *
  875. * @return array
  876. */
  877. public static function validateThemeInformation(array $information): array
  878. {
  879. // set default thumbnail if not sets
  880. if (!$information['thumbnail']) {
  881. $information['thumbnail'] = 'thumbnail.png';
  882. }
  883. // check if there are templates
  884. if (isset($information['templates']) && $information['templates']) {
  885. foreach ($information['templates'] as $i => $template) {
  886. if (!isset($template['label']) || !$template['label'] || !isset($template['path']) || !$template['path'] || !isset($template['format']) || !$template['format']) {
  887. unset($information['templates'][$i]);
  888. continue;
  889. }
  890. // if there are no positions we should continue with the next item
  891. if (!isset($template['positions']) && $template['positions']) {
  892. continue;
  893. }
  894. // loop positions
  895. foreach ($template['positions'] as $j => $position) {
  896. if (!isset($position['name']) || !$position['name']) {
  897. unset($information['templates'][$i]['positions'][$j]);
  898. continue;
  899. }
  900. // ensure widgets are well-formed
  901. if (!isset($position['widgets']) || !$position['widgets']) {
  902. $information['templates'][$i]['positions'][$j]['widgets'] = [];
  903. }
  904. // ensure editors are well-formed
  905. if (!isset($position['editors']) || !$position['editors']) {
  906. $information['templates'][$i]['positions'][$j]['editors'] = [];
  907. }
  908. // loop widgets
  909. foreach ($position['widgets'] as $k => $widget) {
  910. // check if widget is valid
  911. if (!isset($widget['module']) || !$widget['module'] || !isset($widget['action']) || !$widget['action']) {
  912. unset($information['templates'][$i]['positions'][$j]['widgets'][$k]);
  913. continue;
  914. }
  915. }
  916. }
  917. // check if there still are valid positions
  918. if (!isset($information['templates'][$i]['positions']) || !$information['templates'][$i]['positions']) {
  919. return [];
  920. }
  921. }
  922. // check if there still are valid templates
  923. if (!isset($information['templates']) || !$information['templates']) {
  924. return [];
  925. }
  926. }
  927. return $information;
  928. }
  929. }