WebOrbiton
v1.0.0.9

Publisium

241 lines · 14.0 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/../includes/font-library.php';
  6. require_once __DIR__ . '/../includes/activity-log.php';
  7. require_once __DIR__ . '/../includes/currency.php';
  8. ​
  9. $db = Database::site();
  10. ​
  11. function saveSettingsFields(PDO $db, array $fields): void
  12. {
  13. $db->exec(
  14. 'CREATE TABLE IF NOT EXISTS site_settings (
  15. setting_key VARCHAR(120) NOT NULL PRIMARY KEY,
  16. setting_value LONGTEXT NULL,
  17. updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  18. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
  19. );
  20. ​
  21. $statement = $db->prepare(
  22. 'INSERT INTO site_settings (setting_key, setting_value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
  23. );
  24. ​
  25. foreach ($fields as $key => $value) {
  26. $statement->execute(['key' => $key, 'value' => $value]);
  27. }
  28. }
  29. ​
  30. function sanitizeLogoUrl(string $url): string
  31. {
  32. $url = trim($url);
  33. ​
  34. if ($url === '') {
  35. return '';
  36. }
  37. ​
  38. if (str_starts_with($url, 'https://') || str_starts_with($url, 'http://')) {
  39. return (string) (filter_var($url, FILTER_SANITIZE_URL) ?: '');
  40. }
  41. ​
  42. if (preg_match('#^[a-zA-Z][a-zA-Z0-9+.-]*:#', $url) === 1) {
  43. return '';
  44. }
  45. ​
  46. if (str_starts_with($url, '//')) {
  47. return '';
  48. }
  49. ​
  50. $url = ltrim($url, '/');
  51. ​
  52. return (string) (filter_var($url, FILTER_SANITIZE_URL) ?: '');
  53. }
  54. ​
  55. $settingsTab = (string) ($_POST['settings_tab'] ?? 'general');
  56. ​
  57. if ($settingsTab === 'general') {
  58. saveSettingsFields($db, [
  59. 'site_name' => trim((string) ($_POST['site_name'] ?? '')),
  60. 'site_description' => trim((string) ($_POST['site_description'] ?? '')),
  61. 'site_logo_url' => sanitizeLogoUrl((string) ($_POST['site_logo_url'] ?? '')),
  62. 'site_show_name_with_logo' => isset($_POST['site_show_name_with_logo']) ? '1' : '0',
  63. 'site_short_name' => trim((string) ($_POST['site_short_name'] ?? '')),
  64. 'site_short_name_enabled' => isset($_POST['site_short_name_enabled']) ? '1' : '0',
  65. 'registration_enabled' => isset($_POST['registration_enabled']) ? '1' : '0',
  66. 'login_enabled' => isset($_POST['login_enabled']) ? '1' : '0',
  67. 'activity_log_enabled' => isset($_POST['activity_log_enabled']) ? '1' : '0',
  68. 'subscription_name' => trim((string) ($_POST['subscription_name'] ?? 'Full access')),
  69. 'subscription_price_cents' => Currency::toMinor((int) ($_POST['subscription_price_major'] ?? 0), (int) ($_POST['subscription_price_minor'] ?? 0), Currency::normalize((string) ($_POST['subscription_currency'] ?? ''))),
  70. 'subscription_interval' => in_array($_POST['subscription_interval'] ?? 'month', ['month', 'year', 'lifetime'], true) ? $_POST['subscription_interval'] : 'month',
  71. 'subscription_currency' => Currency::normalize((string) ($_POST['subscription_currency'] ?? '')),
  72. 'subscription_description' => trim((string) ($_POST['subscription_description'] ?? '')),
  73. 'subscription_checkout_url' => trim((string) ($_POST['subscription_checkout_url'] ?? '')),
  74. 'payment_provider' => in_array($_POST['payment_provider'] ?? 'stripe', ['stripe', 'polar'], true) ? $_POST['payment_provider'] : 'stripe',
  75. 'payment_webhook_secret' => trim((string) ($_POST['payment_webhook_secret'] ?? '')),
  76. ]);
  77. ​
  78. require_once __DIR__ . '/../includes/contact.php';
  79. $aiSaveError = ContactForm::saveFromRequest($db, $_POST);
  80. }
  81. ​
  82. if ($settingsTab === 'layout') {
  83. $headingFontInput = trim((string) ($_POST['heading_font'] ?? 'Plus Jakarta Sans'));
  84. $bodyFontInput = trim((string) ($_POST['body_font'] ?? 'PT Serif'));
  85. $interfaceFontInput = trim((string) ($_POST['interface_font'] ?? 'Plus Jakarta Sans'));
  86. $validHeadingFont = FontLibrary::findByDisplayName($headingFontInput) !== null ? $headingFontInput : 'Plus Jakarta Sans';
  87. $validBodyFont = FontLibrary::findByDisplayName($bodyFontInput) !== null ? $bodyFontInput : 'PT Serif';
  88. $validInterfaceFont = FontLibrary::findByDisplayName($interfaceFontInput) !== null ? $interfaceFontInput : 'Plus Jakarta Sans';
  89. ​
  90. $articlesPerPageHome = max(1, min(100, (int) ($_POST['articles_per_page_home'] ?? 12)));
  91. $articlesPerPageCategory = max(1, min(100, (int) ($_POST['articles_per_page_category'] ?? 12)));
  92. ​
  93. saveSettingsFields($db, [
  94. 'menu_position' => in_array($_POST['menu_position'] ?? 'top', ['top', 'bottom', 'side'], true) ? $_POST['menu_position'] : 'top',
  95. 'banner_enabled' => isset($_POST['banner_enabled']) ? '1' : '0',
  96. 'featured_banner_enabled' => isset($_POST['featured_banner_enabled']) ? '1' : '0',
  97. 'featured_banner_mode' => in_array($_POST['featured_banner_mode'] ?? 'static', ['static', 'automatic'], true) ? $_POST['featured_banner_mode'] : 'static',
  98. 'featured_banner_hide_duplicates' => isset($_POST['featured_banner_hide_duplicates']) ? '1' : '0',
  99. 'featured_banner_article_1' => max(0, (int) ($_POST['featured_banner_article_1'] ?? 0)),
  100. 'featured_banner_article_2' => max(0, (int) ($_POST['featured_banner_article_2'] ?? 0)),
  101. 'featured_banner_article_3' => max(0, (int) ($_POST['featured_banner_article_3'] ?? 0)),
  102. 'featured_banner_article_4' => max(0, (int) ($_POST['featured_banner_article_4'] ?? 0)),
  103. 'featured_banner_article_5' => max(0, (int) ($_POST['featured_banner_article_5'] ?? 0)),
  104. 'featured_banner_auto_window_value' => max(1, min(720, (int) ($_POST['featured_banner_auto_window_value'] ?? 24))),
  105. 'featured_banner_auto_window_unit' => in_array($_POST['featured_banner_auto_window_unit'] ?? 'hours', ['minutes', 'hours'], true) ? $_POST['featured_banner_auto_window_unit'] : 'hours',
  106. 'featured_banner_auto_category_mode' => in_array($_POST['featured_banner_auto_category_mode'] ?? 'mixed', ['mixed', 'one_per_category', 'no_limit'], true) ? $_POST['featured_banner_auto_category_mode'] : 'mixed',
  107. 'article_reading_time_enabled' => isset($_POST['article_reading_time_enabled']) ? '1' : '0',
  108. 'article_time_enabled' => isset($_POST['article_time_enabled']) ? '1' : '0',
  109. 'article_time_format' => ($_POST['article_time_format'] ?? '24') === '12' ? '12' : '24',
  110. 'back_to_top_enabled' => isset($_POST['back_to_top_enabled']) ? '1' : '0',
  111. 'back_to_top_progress_enabled' => isset($_POST['back_to_top_progress_enabled']) ? '1' : '0',
  112. 'toc_enabled' => isset($_POST['toc_enabled']) ? '1' : '0',
  113. 'breadcrumbs_enabled' => isset($_POST['breadcrumbs_enabled']) ? '1' : '0',
  114. 'author_box_enabled' => isset($_POST['author_box_enabled']) ? '1' : '0',
  115. 'account_consents_enabled' => isset($_POST['account_consents_enabled']) ? '1' : '0',
  116. 'author_pages_enabled' => isset($_POST['author_pages_enabled']) ? '1' : '0',
  117. 'tags_enabled' => isset($_POST['tags_enabled']) ? '1' : '0',
  118. 'related_articles_enabled' => isset($_POST['related_articles_enabled']) ? '1' : '0',
  119. 'sitemap_rss_enabled' => isset($_POST['sitemap_rss_enabled']) ? '1' : '0',
  120. 'rss_category_feeds_enabled' => isset($_POST['rss_category_feeds_enabled']) ? '1' : '0',
  121. 'article_loading_mode' => in_array($_POST['article_loading_mode'] ?? 'scroll', ['scroll', 'static'], true) ? $_POST['article_loading_mode'] : 'scroll',
  122. 'pagination_style' => in_array($_POST['pagination_style'] ?? 'numbered', ['numbered', 'load_more', 'infinite_scroll'], true) ? $_POST['pagination_style'] : 'numbered',
  123. 'articles_per_page_home' => (string) $articlesPerPageHome,
  124. 'articles_per_page_category' => (string) $articlesPerPageCategory,
  125. 'heading_font' => $validHeadingFont,
  126. 'body_font' => $validBodyFont,
  127. 'interface_font' => $validInterfaceFont,
  128. 'share_enabled' => isset($_POST['share_enabled']) ? '1' : '0',
  129. 'share_facebook' => isset($_POST['share_facebook']) ? '1' : '0',
  130. 'share_twitter' => isset($_POST['share_twitter']) ? '1' : '0',
  131. 'share_linkedin' => isset($_POST['share_linkedin']) ? '1' : '0',
  132. 'share_whatsapp' => isset($_POST['share_whatsapp']) ? '1' : '0',
  133. 'share_telegram' => isset($_POST['share_telegram']) ? '1' : '0',
  134. 'share_email' => isset($_POST['share_email']) ? '1' : '0',
  135. 'share_copy_link' => isset($_POST['share_copy_link']) ? '1' : '0',
  136. 'adblock_notice_enabled' => isset($_POST['adblock_notice_enabled']) ? '1' : '0',
  137. 'adblock_notice_mode' => array_key_exists((string) ($_POST['adblock_notice_mode'] ?? ''), SiteFront::ADBLOCK_MODES) ? (string) $_POST['adblock_notice_mode'] : 'session',
  138. ]);
  139. ​
  140. require_once __DIR__ . '/../includes/read-aloud.php';
  141. ReadAloud::saveGlobalFields($db, $_POST);
  142. }
  143. ​
  144. if ($settingsTab === 'apps') {
  145. function sanitizeHexColor(string $color, string $fallback): string
  146. {
  147. $color = trim($color);
  148. return preg_match('/^#[0-9a-fA-F]{6}$/', $color) === 1 ? $color : $fallback;
  149. }
  150. ​
  151. saveSettingsFields($db, [
  152. 'pwa_enabled' => isset($_POST['pwa_enabled']) ? '1' : '0',
  153. 'pwa_name' => trim((string) ($_POST['pwa_name'] ?? '')),
  154. 'pwa_short_name' => trim((string) ($_POST['pwa_short_name'] ?? '')),
  155. 'pwa_description' => trim((string) ($_POST['pwa_description'] ?? '')),
  156. 'pwa_icon_url' => sanitizeLogoUrl((string) ($_POST['pwa_icon_url'] ?? '')),
  157. 'pwa_theme_color' => sanitizeHexColor((string) ($_POST['pwa_theme_color'] ?? '#ffffff'), '#ffffff'),
  158. 'pwa_background_color' => sanitizeHexColor((string) ($_POST['pwa_background_color'] ?? '#ffffff'), '#ffffff'),
  159. 'app_banner_enabled' => isset($_POST['app_banner_enabled']) ? '1' : '0',
  160. 'app_android_url' => trim((string) ($_POST['app_android_url'] ?? '')),
  161. 'app_ios_url' => trim((string) ($_POST['app_ios_url'] ?? '')),
  162. ]);
  163. }
  164. ​
  165. if ($settingsTab === 'search') {
  166. saveSettingsFields($db, [
  167. 'search_enabled' => isset($_POST['search_enabled']) ? '1' : '0',
  168. 'search_match_title' => isset($_POST['search_match_title']) ? '1' : '0',
  169. 'search_match_excerpt' => isset($_POST['search_match_excerpt']) ? '1' : '0',
  170. 'search_match_content' => isset($_POST['search_match_content']) ? '1' : '0',
  171. 'search_style' => ($_POST['search_style'] ?? 'icon') === 'field' ? 'field' : 'icon',
  172. 'search_show_in_nav' => isset($_POST['search_show_in_nav']) ? '1' : '0',
  173. 'search_show_in_footer' => isset($_POST['search_show_in_footer']) ? '1' : '0',
  174. ]);
  175. }
  176. ​
  177. if ($settingsTab === 'comments') {
  178. require_once __DIR__ . '/../includes/comments.php';
  179. saveSettingsFields($db, Comments::sanitizeSettings($_POST));
  180. }
  181. ​
  182. if ($settingsTab === 'custom-code') {
  183. saveSettingsFields($db, [
  184. 'custom_css' => (string) ($_POST['custom_css'] ?? ''),
  185. 'custom_js' => (string) ($_POST['custom_js'] ?? ''),
  186. 'custom_head_html' => (string) ($_POST['custom_head_html'] ?? ''),
  187. 'custom_head_html_index' => isset($_POST['custom_head_html_index']) ? '1' : '0',
  188. 'custom_head_html_article' => isset($_POST['custom_head_html_article']) ? '1' : '0',
  189. 'custom_head_html_category' => isset($_POST['custom_head_html_category']) ? '1' : '0',
  190. 'custom_head_html_pages' => isset($_POST['custom_head_html_pages']) ? '1' : '0',
  191. 'custom_body_html' => (string) ($_POST['custom_body_html'] ?? ''),
  192. 'custom_body_html_index' => isset($_POST['custom_body_html_index']) ? '1' : '0',
  193. 'custom_body_html_article' => isset($_POST['custom_body_html_article']) ? '1' : '0',
  194. 'custom_body_html_category' => isset($_POST['custom_body_html_category']) ? '1' : '0',
  195. 'custom_body_html_pages' => isset($_POST['custom_body_html_pages']) ? '1' : '0',
  196. ]);
  197. }
  198. ​
  199. if ($settingsTab === 'ai') {
  200. require_once __DIR__ . '/../includes/ai.php';
  201. $aiSaveError = Ai::saveFromRequest($db, $_POST);
  202. require_once __DIR__ . '/../includes/read-aloud.php';
  203. $readAloudSaveError = ReadAloud::saveProviderSettings($db, $_POST);
  204. $aiSaveError = $aiSaveError ?? $readAloudSaveError;
  205. }
  206. ​
  207. if ($settingsTab === 'seo') {
  208. $indexNowKeyStatement = $db->prepare('SELECT setting_value FROM site_settings WHERE setting_key = :key LIMIT 1');
  209. $indexNowKeyStatement->execute(['key' => 'indexnow_key']);
  210. $indexNowKey = (string) ($indexNowKeyStatement->fetchColumn() ?: '');
  211. if (isset($_POST['indexnow_enabled']) && $indexNowKey === '') {
  212. $indexNowKey = bin2hex(random_bytes(16));
  213. }
  214. ​
  215. saveSettingsFields($db, [
  216. 'indexnow_enabled' => isset($_POST['indexnow_enabled']) ? '1' : '0',
  217. 'indexnow_key' => $indexNowKey,
  218. 'seo_title' => trim((string) ($_POST['seo_title'] ?? '')),
  219. 'seo_description' => trim((string) ($_POST['seo_description'] ?? '')),
  220. 'seo_keywords' => trim((string) ($_POST['seo_keywords'] ?? '')),
  221. 'seo_author' => trim((string) ($_POST['seo_author'] ?? '')),
  222. 'seo_reply_to' => trim((string) ($_POST['seo_reply_to'] ?? '')),
  223. 'seo_application_name' => trim((string) ($_POST['seo_application_name'] ?? '')),
  224. 'seo_robots_index' => isset($_POST['seo_robots_index']) ? '1' : '0',
  225. 'seo_robots_follow' => isset($_POST['seo_robots_follow']) ? '1' : '0',
  226. 'seo_og_article_image_enabled' => isset($_POST['seo_og_article_image_enabled']) ? '1' : '0',
  227. 'seo_social_tags_enabled' => isset($_POST['seo_social_tags_enabled']) ? '1' : '0',
  228. 'seo_twitter_handle' => preg_match('/^@?([A-Za-z0-9_]{1,15})$/', trim((string) ($_POST['seo_twitter_handle'] ?? '')), $twitterHandle) === 1 ? '@' . $twitterHandle[1] : '',
  229. 'seo_article_schema_enabled' => isset($_POST['seo_article_schema_enabled']) ? '1' : '0',
  230. 'clean_article_urls' => isset($_POST['clean_article_urls']) ? '1' : '0',
  231. 'clean_page_urls' => isset($_POST['clean_page_urls']) ? '1' : '0',
  232. 'clean_author_urls' => isset($_POST['clean_author_urls']) ? '1' : '0',
  233. 'clean_home_url' => isset($_POST['clean_home_url']) ? '1' : '0',
  234. 'llms_txt_enabled' => isset($_POST['llms_txt_enabled']) ? '1' : '0',
  235. 'seo_preview_enabled' => isset($_POST['seo_preview_enabled']) ? '1' : '0',
  236. ]);
  237. }
  238. ​
  239. ActivityLog::record('settings.save', 'settings', null, $settingsTab);
  240. CleanUrls::sync();
  241. ​