WebOrbiton
v2.0.0.1

Publisium

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