WebOrbiton
v3.0.0.2

FlatlyPage

612 lines · 19.0 KB
  1. <?php
  2. ​
  3. const FLATLY_LANG_CODE_PATTERN = '/^[a-z]{2,3}(?:-[a-z0-9]{2,8})?$/';
  4. const FLATLY_LANG_HTACCESS_MARKER = '# FlatlyPage languages';
  5. ​
  6. function flatly_lang_code_valid(string $code): bool
  7. {
  8. return preg_match(FLATLY_LANG_CODE_PATTERN, $code) === 1;
  9. }
  10. ​
  11. function flatly_limit_text(string $text, int $length): string
  12. {
  13. return preg_match('/^.{0,' . $length . '}/su', $text, $m) === 1 ? $m[0] : substr($text, 0, $length);
  14. }
  15. ​
  16. function flatly_main_settings(): array
  17. {
  18. static $settings = null;
  19. if ($settings === null) {
  20. $loaded = load_page('settings');
  21. $settings = is_array($loaded) ? $loaded : [];
  22. }
  23. return $settings;
  24. }
  25. ​
  26. function flatly_default_lang(): string
  27. {
  28. $code = strtolower(trim((string) (flatly_main_settings()['site_language'] ?? 'en')));
  29. return flatly_lang_code_valid($code) ? $code : 'en';
  30. }
  31. ​
  32. function flatly_default_language_name(): string
  33. {
  34. $custom = flatly_limit_text(trim((string) (flatly_main_settings()['site_language_name'] ?? '')), 50);
  35. if ($custom !== '') {
  36. return $custom;
  37. }
  38. $default = flatly_default_lang();
  39. return flatly_configured_languages()[$default] ?? strtoupper($default);
  40. }
  41. ​
  42. function flatly_configured_languages(): array
  43. {
  44. $list = flatly_main_settings()['languages'] ?? [];
  45. $languages = [];
  46. if (!is_array($list)) {
  47. return $languages;
  48. }
  49. foreach ($list as $row) {
  50. if (!is_array($row)) {
  51. continue;
  52. }
  53. $code = strtolower(trim((string) ($row['code'] ?? '')));
  54. $name = trim((string) ($row['name'] ?? ''));
  55. if (!flatly_lang_code_valid($code) || $name === '' || isset($languages[$code])) {
  56. continue;
  57. }
  58. $languages[$code] = flatly_limit_text($name, 50);
  59. }
  60. return $languages;
  61. }
  62. ​
  63. function flatly_extra_languages(): array
  64. {
  65. $languages = flatly_configured_languages();
  66. unset($languages[flatly_default_lang()]);
  67. return $languages;
  68. }
  69. ​
  70. function flatly_is_extra_lang(?string $code): bool
  71. {
  72. return $code !== null && $code !== '' && isset(flatly_extra_languages()[$code]);
  73. }
  74. ​
  75. function flatly_sanitize_languages(mixed $raw): array
  76. {
  77. if (is_string($raw)) {
  78. $raw = json_decode($raw, true);
  79. }
  80. $clean = [];
  81. $seen = [];
  82. if (!is_array($raw)) {
  83. return $clean;
  84. }
  85. foreach ($raw as $row) {
  86. if (!is_array($row)) {
  87. continue;
  88. }
  89. $code = strtolower(trim((string) ($row['code'] ?? '')));
  90. $name = flatly_limit_text(trim((string) ($row['name'] ?? '')), 50);
  91. if ($name === '' || !flatly_lang_code_valid($code) || isset($seen[$code])) {
  92. continue;
  93. }
  94. $seen[$code] = true;
  95. $clean[] = ['name' => $name, 'code' => $code];
  96. if (count($clean) >= 30) {
  97. break;
  98. }
  99. }
  100. return $clean;
  101. }
  102. ​
  103. function flatly_lang_file_id(string $lang, string $id): string
  104. {
  105. return $lang . '-lang-' . $id;
  106. }
  107. ​
  108. function flatly_split_lang_file_id(string $file_id): array
  109. {
  110. if (preg_match('/^([a-z]{2,3}(?:-[a-z0-9]{2,8})?)-lang-(.+)$/', $file_id, $m)) {
  111. return [$m[1], $m[2]];
  112. }
  113. return [null, $file_id];
  114. }
  115. ​
  116. function load_page_lang(string $id, ?string $lang): ?array
  117. {
  118. if ($lang === null || $lang === '') {
  119. return load_page($id);
  120. }
  121. if (!flatly_lang_code_valid($lang)) {
  122. return null;
  123. }
  124. $path = DATA_DIR . flatly_lang_file_id($lang, $id) . '.php';
  125. if (!is_file($path)) {
  126. return null;
  127. }
  128. $data = include $path;
  129. return is_array($data) ? $data : null;
  130. }
  131. ​
  132. function save_page_lang(string $id, string $lang, array $data): bool
  133. {
  134. if (!flatly_lang_code_valid($lang)) {
  135. return false;
  136. }
  137. return save_page(flatly_lang_file_id($lang, $id), $data);
  138. }
  139. ​
  140. function flatly_root_relative_urls(string $html): string
  141. {
  142. $fix = static function (string $value): string {
  143. if ($value === '' || preg_match('#^(?:[a-z][a-z0-9+.\-]*:|//|/|\#|\?)#i', $value)) {
  144. return $value;
  145. }
  146. return '/' . preg_replace('#^(?:\./)+#', '', $value);
  147. };
  148. ​
  149. $html = preg_replace_callback(
  150. '/(\s(?:href|src|action|poster|formaction|data-src)\s*=\s*)(["\'])([^"\']*)\2/i',
  151. static fn(array $m): string => $m[1] . $m[2] . $fix($m[3]) . $m[2],
  152. $html
  153. ) ?? $html;
  154. ​
  155. $html = preg_replace_callback(
  156. '/(\ssrcset\s*=\s*)(["\'])([^"\']*)\2/i',
  157. static function (array $m) use ($fix): string {
  158. $parts = array_map(static function (string $candidate) use ($fix): string {
  159. $pieces = preg_split('/\s+/', trim($candidate), 2);
  160. $pieces[0] = $fix($pieces[0]);
  161. return implode(' ', $pieces);
  162. }, explode(',', $m[3]));
  163. return $m[1] . $m[2] . implode(', ', $parts) . $m[2];
  164. },
  165. $html
  166. ) ?? $html;
  167. ​
  168. return preg_replace_callback(
  169. '/url\(\s*(["\']?)([^)"\']*)\1\s*\)/i',
  170. static fn(array $m): string => 'url(' . $m[1] . $fix($m[2]) . $m[1] . ')',
  171. $html
  172. ) ?? $html;
  173. }
  174. ​
  175. function flatly_start_root_relative_buffer(): void
  176. {
  177. static $started = false;
  178. if ($started) {
  179. return;
  180. }
  181. $started = true;
  182. ob_start(static function (string $buffer): string {
  183. foreach (headers_list() as $header) {
  184. if (stripos($header, 'content-type:') === 0 && stripos($header, 'html') === false) {
  185. return $buffer;
  186. }
  187. }
  188. return flatly_root_relative_urls($buffer);
  189. });
  190. }
  191. ​
  192. function flatly_set_current_lang(?string $code): void
  193. {
  194. $GLOBALS['flatly_lang'] = $code;
  195. unset($GLOBALS['_products_cache']);
  196. ​
  197. if ($code !== null) {
  198. flatly_start_root_relative_buffer();
  199. }
  200. }
  201. ​
  202. function flatly_current_lang(): ?string
  203. {
  204. $code = $GLOBALS['flatly_lang'] ?? null;
  205. return is_string($code) && $code !== '' ? $code : null;
  206. }
  207. ​
  208. function flatly_init_front_lang(): bool
  209. {
  210. if (!isset($_GET['lang']) || !is_string($_GET['lang']) || $_GET['lang'] === '') {
  211. flatly_set_current_lang(null);
  212. return true;
  213. }
  214. $code = strtolower(trim($_GET['lang']));
  215. if ($code === flatly_default_lang() && isset(flatly_configured_languages()[$code])) {
  216. flatly_set_current_lang(null);
  217. flatly_start_root_relative_buffer();
  218. return true;
  219. }
  220. if (flatly_is_extra_lang($code)) {
  221. flatly_set_current_lang($code);
  222. return true;
  223. }
  224. flatly_set_current_lang(null);
  225. return false;
  226. }
  227. ​
  228. function flatly_lang_prefix_supported(): bool
  229. {
  230. static $supported = null;
  231. if ($supported === null) {
  232. $file = BASE_DIR . '/.htaccess';
  233. $supported = is_file($file) && strpos((string) @file_get_contents($file), FLATLY_LANG_HTACCESS_MARKER) !== false;
  234. }
  235. return $supported;
  236. }
  237. ​
  238. function flatly_lang_url(?string $lang, string $path = '/'): string
  239. {
  240. $path = '/' . ltrim($path, '/');
  241. if ($lang === null || $lang === '' || $lang === flatly_default_lang()) {
  242. return $path;
  243. }
  244. if (flatly_lang_prefix_supported()) {
  245. return '/' . $lang . ($path === '/' ? '/' : $path);
  246. }
  247. return $path . '?lang=' . rawurlencode($lang);
  248. }
  249. ​
  250. function flatly_set_page_ref(string $id): void
  251. {
  252. $GLOBALS['flatly_page_ref'] = $id;
  253. }
  254. ​
  255. function flatly_page_ref(): ?string
  256. {
  257. $ref = $GLOBALS['flatly_page_ref'] ?? null;
  258. return is_string($ref) && $ref !== '' ? $ref : null;
  259. }
  260. ​
  261. function flatly_lang_page_path(string $lang_code, string $ref): ?string
  262. {
  263. if ($ref === 'index') {
  264. return '/';
  265. }
  266. $default = flatly_default_lang();
  267. $data = $lang_code === $default ? load_page($ref) : load_page_lang($ref, $lang_code);
  268. if (!is_array($data) || empty($data['slug'])) {
  269. return null;
  270. }
  271. if (function_exists('is_page_published') && !is_page_published($data)) {
  272. return null;
  273. }
  274. return '/' . $data['slug'];
  275. }
  276. ​
  277. function flatly_language_alternates(): array
  278. {
  279. $ref = flatly_page_ref();
  280. $languages = flatly_configured_languages();
  281. $default = flatly_default_lang();
  282. if ($ref === null || !flatly_extra_languages()) {
  283. return [];
  284. }
  285. if (!isset($languages[$default])) {
  286. $languages = [$default => ''] + $languages;
  287. }
  288. $languages[$default] = flatly_default_language_name();
  289. $alternates = [];
  290. foreach ($languages as $code => $name) {
  291. $path = flatly_lang_page_path($code, $ref);
  292. $alternates[$code] = [
  293. 'name' => $name,
  294. 'path' => $path,
  295. 'url' => flatly_lang_url($code, $path ?? '/'),
  296. 'exact' => $path !== null,
  297. ];
  298. }
  299. return $alternates;
  300. }
  301. ​
  302. function flatly_hreflang_links(): string
  303. {
  304. $html = '';
  305. foreach (flatly_language_alternates() as $code => $alt) {
  306. if (!$alt['exact']) {
  307. continue;
  308. }
  309. $html .= '<link rel="alternate" hreflang="' . htmlspecialchars($code, ENT_QUOTES, 'UTF-8') . '" href="' . htmlspecialchars(SITE_URL . $alt['url'], ENT_QUOTES, 'UTF-8') . '">' . "\n ";
  310. if ($code === flatly_default_lang()) {
  311. $html .= '<link rel="alternate" hreflang="x-default" href="' . htmlspecialchars(SITE_URL . $alt['url'], ENT_QUOTES, 'UTF-8') . '">' . "\n ";
  312. }
  313. }
  314. return $html;
  315. }
  316. ​
  317. function flatly_canonical_link(): string
  318. {
  319. $uri = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'));
  320. $path = (string) ($uri['path'] ?? '/');
  321. if (BASE_URL !== '' && str_starts_with($path, BASE_URL)) {
  322. $path = substr($path, strlen(BASE_URL));
  323. }
  324. $path = '/' . ltrim($path, '/');
  325. ​
  326. $query = '';
  327. parse_str((string) ($uri['query'] ?? ''), $params);
  328. if (isset($params['lang']) && is_string($params['lang']) && preg_match(FLATLY_LANG_CODE_PATTERN, $params['lang']) === 1) {
  329. $query = '?lang=' . rawurlencode($params['lang']);
  330. }
  331. ​
  332. return '<link rel="canonical" href="' . htmlspecialchars(SITE_URL . $path . $query, ENT_QUOTES, 'UTF-8') . '">' . "\n ";
  333. }
  334. ​
  335. function flatly_language_autodetect_enabled(): bool
  336. {
  337. return !empty(flatly_main_settings()['language_autodetect']);
  338. }
  339. ​
  340. function flatly_language_detect_script(): string
  341. {
  342. if (!flatly_language_autodetect_enabled() || flatly_current_lang() !== null || isset($_GET['lang'])) {
  343. return '';
  344. }
  345. ​
  346. $urls = [];
  347. foreach (flatly_language_alternates() as $code => $alt) {
  348. if ($alt['exact']) {
  349. $urls[$code] = $alt['url'];
  350. }
  351. }
  352. if (count($urls) < 2) {
  353. return '';
  354. }
  355. ​
  356. $config = json_encode(
  357. ['primary' => flatly_default_lang(), 'urls' => $urls],
  358. JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES
  359. );
  360. ​
  361. $script = <<<'JS'
  362. (function () {
  363. var cfg = __CONFIG__;
  364. try {
  365. if (window.self !== window.top) return;
  366. var stored = null;
  367. try { stored = localStorage.getItem('flatly-lang'); } catch (e) {}
  368. var target = null;
  369. ​
  370. if (stored) {
  371. target = cfg.urls[stored] ? stored : null;
  372. } else {
  373. if (document.referrer) {
  374. try { if (new URL(document.referrer).origin === location.origin) return; } catch (e) {}
  375. }
  376. var prefs = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language || ''];
  377. for (var i = 0; i < prefs.length && !target; i++) {
  378. var tag = String(prefs[i]).toLowerCase();
  379. if (cfg.urls[tag]) target = tag;
  380. else if (cfg.urls[tag.split('-')[0]]) target = tag.split('-')[0];
  381. }
  382. }
  383. ​
  384. if (!target || target === cfg.primary) return;
  385. location.replace((window.FLATLY_BASE || '') + cfg.urls[target]);
  386. } catch (e) {}
  387. })();
  388. JS;
  389. ​
  390. return '<script>' . str_replace('__CONFIG__', (string) $config, $script) . '</script>' . "\n ";
  391. }
  392. ​
  393. function flatly_home_url(): string
  394. {
  395. return flatly_lang_url(flatly_current_lang(), '/');
  396. }
  397. ​
  398. function render_language_switcher(): string
  399. {
  400. $alternates = flatly_language_alternates();
  401. if (count($alternates) < 2) {
  402. return '';
  403. }
  404. $current = flatly_current_lang() ?? flatly_default_lang();
  405. ​
  406. ob_start(); ?>
  407. <div class="footer-language" style="margin-left: 10px;">
  408. <select id="language-select" aria-label="Select language">
  409. <?php foreach ($alternates as $code => $alt): ?>
  410. <option value="<?= htmlspecialchars($alt['url'], ENT_QUOTES, 'UTF-8') ?>" lang="<?= htmlspecialchars($code, ENT_QUOTES, 'UTF-8') ?>" data-code="<?= htmlspecialchars($code, ENT_QUOTES, 'UTF-8') ?>" <?= $current === $code ? 'selected' : '' ?>>
  411. <?= htmlspecialchars($alt['name'], ENT_QUOTES, 'UTF-8') ?>
  412. </option>
  413. <?php endforeach; ?>
  414. </select>
  415. </div>
  416. <script>
  417. (function() {
  418. 'use strict';
  419. const languageSelect = document.getElementById('language-select');
  420. if (languageSelect) {
  421. languageSelect.addEventListener('change', function() {
  422. const selectedUrl = this.value;
  423. <?php if (flatly_language_autodetect_enabled()): ?>
  424. try { localStorage.setItem('flatly-lang', this.options[this.selectedIndex].getAttribute('data-code')); } catch (e) {}
  425. <?php endif; ?>
  426. if (selectedUrl && selectedUrl.startsWith('/')) {
  427. window.location.href = (window.FLATLY_BASE || '') + selectedUrl;
  428. }
  429. });
  430. }
  431. })();
  432. </script>
  433. <?php return ob_get_clean();
  434. }
  435. ​
  436. function flatly_translatable_setting_keys(): array
  437. {
  438. return ['site_name', 'site_description', 'logo_text', 'nav_links', 'nav_buttons', 'footer'];
  439. }
  440. ​
  441. function flatly_is_blank(mixed $value): bool
  442. {
  443. return $value === null || $value === '' || $value === [];
  444. }
  445. ​
  446. function flatly_merge_translation(array $base, array $translation): array
  447. {
  448. foreach (flatly_translatable_setting_keys() as $key) {
  449. if (!array_key_exists($key, $translation) || flatly_is_blank($translation[$key])) {
  450. continue;
  451. }
  452. if ($key === 'footer' && is_array($translation[$key]) && is_array($base[$key] ?? null)) {
  453. foreach ($translation[$key] as $sub => $value) {
  454. if (!flatly_is_blank($value)) {
  455. $base[$key][$sub] = $value;
  456. }
  457. }
  458. continue;
  459. }
  460. $base[$key] = $translation[$key];
  461. }
  462. return $base;
  463. }
  464. ​
  465. function flatly_block_keys(array $blocks): array
  466. {
  467. $keys = [];
  468. $counters = [];
  469. foreach ($blocks as $block) {
  470. $type = is_array($block) ? (string) ($block['type'] ?? '') : '';
  471. if (is_array($block) && !empty($block['id'])) {
  472. $keys[] = 'id:' . $block['id'];
  473. continue;
  474. }
  475. $counters[$type] = ($counters[$type] ?? 0) + 1;
  476. $keys[] = 'type:' . $type . ':' . $counters[$type];
  477. }
  478. return $keys;
  479. }
  480. ​
  481. function flatly_merge_blocks(array $main, array $translated): array
  482. {
  483. $result = array_values($translated);
  484. $result_keys = flatly_block_keys($result);
  485. $main = array_values($main);
  486. $main_keys = flatly_block_keys($main);
  487. $added = 0;
  488. $previous = null;
  489. ​
  490. foreach ($main as $i => $block) {
  491. $key = $main_keys[$i];
  492. if (in_array($key, $result_keys, true)) {
  493. $previous = $key;
  494. continue;
  495. }
  496. $position = 0;
  497. if ($previous !== null) {
  498. $found = array_search($previous, $result_keys, true);
  499. $position = $found === false ? count($result) : $found + 1;
  500. }
  501. array_splice($result, $position, 0, [$block]);
  502. array_splice($result_keys, $position, 0, [$key]);
  503. $previous = $key;
  504. $added++;
  505. }
  506. ​
  507. return [$result, $added];
  508. }
  509. ​
  510. function flatly_link_key(array $item, int $index, string $field = 'url'): string
  511. {
  512. $value = trim((string) ($item[$field] ?? ''));
  513. return ($value === '' || $value === '#') ? 'pos:' . $index : $field . ':' . $value;
  514. }
  515. ​
  516. function flatly_merge_link_list(array $main, array $translated, string $field = 'url'): array
  517. {
  518. $result = array_values($translated);
  519. $existing = [];
  520. foreach ($result as $i => $item) {
  521. $existing[flatly_link_key(is_array($item) ? $item : [], $i, $field)] = true;
  522. }
  523. $added = 0;
  524. foreach (array_values($main) as $i => $item) {
  525. if (!is_array($item)) {
  526. continue;
  527. }
  528. $key = flatly_link_key($item, $i, $field);
  529. if (isset($existing[$key])) {
  530. continue;
  531. }
  532. $result[] = $item;
  533. $existing[$key] = true;
  534. $added++;
  535. }
  536. return [$result, $added];
  537. }
  538. ​
  539. function flatly_sync_settings(array $main, array $translated): array
  540. {
  541. $added = 0;
  542. ​
  543. foreach (['site_name', 'site_description', 'logo_text'] as $key) {
  544. if (flatly_is_blank($translated[$key] ?? null) && !flatly_is_blank($main[$key] ?? null)) {
  545. $translated[$key] = $main[$key];
  546. $added++;
  547. }
  548. }
  549. ​
  550. foreach (['nav_links', 'nav_buttons'] as $key) {
  551. [$translated[$key], $count] = flatly_merge_link_list(
  552. is_array($main[$key] ?? null) ? $main[$key] : [],
  553. is_array($translated[$key] ?? null) ? $translated[$key] : []
  554. );
  555. $added += $count;
  556. }
  557. ​
  558. $main_footer = is_array($main['footer'] ?? null) ? $main['footer'] : [];
  559. $footer = is_array($translated['footer'] ?? null) ? $translated['footer'] : [];
  560. ​
  561. foreach (['brand_description', 'copyright'] as $key) {
  562. if (flatly_is_blank($footer[$key] ?? null) && !flatly_is_blank($main_footer[$key] ?? null)) {
  563. $footer[$key] = $main_footer[$key];
  564. $added++;
  565. }
  566. }
  567. ​
  568. $columns = is_array($footer['columns'] ?? null) ? array_values($footer['columns']) : [];
  569. $main_columns = is_array($main_footer['columns'] ?? null) ? array_values($main_footer['columns']) : [];
  570. for ($i = count($columns); $i < count($main_columns); $i++) {
  571. $columns[] = $main_columns[$i];
  572. $added++;
  573. }
  574. $footer['columns'] = $columns;
  575. ​
  576. [$footer['bottom_links'], $count] = flatly_merge_link_list(
  577. is_array($main_footer['bottom_links'] ?? null) ? $main_footer['bottom_links'] : [],
  578. is_array($footer['bottom_links'] ?? null) ? $footer['bottom_links'] : []
  579. );
  580. $added += $count;
  581. ​
  582. [$footer['social_links'], $count] = flatly_merge_link_list(
  583. is_array($main_footer['social_links'] ?? null) ? $main_footer['social_links'] : [],
  584. is_array($footer['social_links'] ?? null) ? $footer['social_links'] : [],
  585. 'platform'
  586. );
  587. $added += $count;
  588. ​
  589. $translated['footer'] = $footer;
  590. return [$translated, $added];
  591. }
  592. ​
  593. function flatly_sync_page_blocks(array $main_page, ?array $translated_page, string $file_id): array
  594. {
  595. if ($translated_page === null) {
  596. $page = $main_page;
  597. $page['id'] = $file_id;
  598. $page['updated_at'] = date('Y-m-d H:i:s');
  599. $count = count($main_page['blocks'] ?? []);
  600. return [$page, $count];
  601. }
  602. [$blocks, $added] = flatly_merge_blocks(
  603. is_array($main_page['blocks'] ?? null) ? $main_page['blocks'] : [],
  604. is_array($translated_page['blocks'] ?? null) ? $translated_page['blocks'] : []
  605. );
  606. if ($added > 0) {
  607. $translated_page['blocks'] = $blocks;
  608. $translated_page['updated_at'] = date('Y-m-d H:i:s');
  609. }
  610. return [$translated_page, $added];
  611. }
  612. ​