WebOrbiton
v1.0.0.1

StocketBase

282 lines · 16.5 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. if (count(get_included_files()) === 1) {
  6. http_response_code(403);
  7. exit;
  8. }
  9. ​
  10. require_once __DIR__ . '/../includes/font-library.php';
  11. require_once __DIR__ . '/../includes/activity-log.php';
  12. require_once __DIR__ . '/../includes/currency.php';
  13. require_once __DIR__ . '/../includes/contact-form.php';
  14. ​
  15. $db = Database::site();
  16. ​
  17. function saveSettingsFields(PDO $db, array $fields): void
  18. {
  19. $db->exec(
  20. 'CREATE TABLE IF NOT EXISTS site_settings (
  21. setting_key VARCHAR(120) NOT NULL PRIMARY KEY,
  22. setting_value LONGTEXT NULL,
  23. updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  24. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
  25. );
  26. ​
  27. $statement = $db->prepare(
  28. 'INSERT INTO site_settings (setting_key, setting_value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
  29. );
  30. ​
  31. foreach ($fields as $key => $value) {
  32. $statement->execute(['key' => $key, 'value' => $value]);
  33. }
  34. }
  35. ​
  36. function sanitizeLogoUrl(string $url): string
  37. {
  38. $url = trim($url);
  39. ​
  40. if ($url === '') {
  41. return '';
  42. }
  43. ​
  44. if (str_starts_with($url, 'https://') || str_starts_with($url, 'http://')) {
  45. return (string) (filter_var($url, FILTER_SANITIZE_URL) ?: '');
  46. }
  47. ​
  48. if (preg_match('#^[a-zA-Z][a-zA-Z0-9+.-]*:#', $url) === 1) {
  49. return '';
  50. }
  51. ​
  52. if (str_starts_with($url, '//')) {
  53. return '';
  54. }
  55. ​
  56. $url = ltrim($url, '/');
  57. ​
  58. return (string) (filter_var($url, FILTER_SANITIZE_URL) ?: '');
  59. }
  60. ​
  61. $settingsTab = (string) ($_POST['settings_tab'] ?? 'general');
  62. ​
  63. if ($settingsTab === 'general') {
  64. $contactTopicNames = is_array($_POST['contact_subject_name'] ?? null) ? array_values($_POST['contact_subject_name']) : [];
  65. $contactTopicEmails = is_array($_POST['contact_subject_email'] ?? null) ? array_values($_POST['contact_subject_email']) : [];
  66. $contactTopicRows = [];
  67. foreach ($contactTopicNames as $topicIndex => $topicName) {
  68. $contactTopicRows[] = ['name' => (string) $topicName, 'email' => (string) ($contactTopicEmails[$topicIndex] ?? '')];
  69. }
  70. $contactSubjectsJson = (string) json_encode(ContactForm::normalizeSubjectEntries($contactTopicRows), JSON_UNESCAPED_UNICODE);
  71. ​
  72. saveSettingsFields($db, [
  73. 'site_name' => trim((string) ($_POST['site_name'] ?? '')),
  74. 'site_description' => trim((string) ($_POST['site_description'] ?? '')),
  75. 'site_logo_url' => sanitizeLogoUrl((string) ($_POST['site_logo_url'] ?? '')),
  76. 'site_show_name_with_logo' => isset($_POST['site_show_name_with_logo']) ? '1' : '0',
  77. 'site_short_name' => trim((string) ($_POST['site_short_name'] ?? '')),
  78. 'site_short_name_enabled' => isset($_POST['site_short_name_enabled']) ? '1' : '0',
  79. 'registration_enabled' => isset($_POST['registration_enabled']) ? '1' : '0',
  80. 'login_enabled' => isset($_POST['login_enabled']) ? '1' : '0',
  81. 'require_account_to_purchase' => isset($_POST['require_account_to_purchase']) ? '1' : '0',
  82. 'download_link_expiry_days' => (string) max(0, min(3650, (int) ($_POST['download_link_expiry_days'] ?? 30))),
  83. 'download_link_max_downloads' => (string) max(0, min(1000, (int) ($_POST['download_link_max_downloads'] ?? 10))),
  84. 'contact_enabled' => isset($_POST['contact_enabled']) ? '1' : '0',
  85. 'contact_recipient_email' => filter_var(trim((string) ($_POST['contact_recipient_email'] ?? '')), FILTER_VALIDATE_EMAIL) ?: '',
  86. 'contact_provider' => in_array($_POST['contact_provider'] ?? 'php_mail', ContactForm::PROVIDERS, true) ? $_POST['contact_provider'] : 'php_mail',
  87. 'contact_subject_mode' => in_array($_POST['contact_subject_mode'] ?? 'free', ['free', 'list_other', 'list_only'], true) ? $_POST['contact_subject_mode'] : 'free',
  88. 'contact_subjects' => $contactSubjectsJson,
  89. 'contact_from_email' => filter_var(trim((string) ($_POST['contact_from_email'] ?? '')), FILTER_VALIDATE_EMAIL) ?: '',
  90. 'contact_resend_api_key' => trim((string) ($_POST['contact_resend_api_key'] ?? '')),
  91. 'contact_postmark_token' => trim((string) ($_POST['contact_postmark_token'] ?? '')),
  92. 'activity_log_enabled' => isset($_POST['activity_log_enabled']) ? '1' : '0',
  93. 'store_category' => in_array($_POST['store_category'] ?? 'general', ['general', 'saas'], true) ? $_POST['store_category'] : 'general',
  94. 'pricing_model' => in_array($_POST['pricing_model'] ?? 'per_product', ['per_product', 'subscription'], true) ? $_POST['pricing_model'] : 'per_product',
  95. 'subscription_interval' => in_array($_POST['subscription_interval'] ?? 'month', ['month', 'year', 'lifetime'], true) ? $_POST['subscription_interval'] : 'month',
  96. 'store_currency' => Currency::normalize((string) ($_POST['store_currency'] ?? '')),
  97. 'payment_provider' => in_array($_POST['payment_provider'] ?? 'stripe', ['stripe', 'polar'], true) ? $_POST['payment_provider'] : 'stripe',
  98. 'payment_webhook_secret' => trim((string) ($_POST['payment_webhook_secret'] ?? '')),
  99. 'payment_polar_webhook_secret' => trim((string) ($_POST['payment_polar_webhook_secret'] ?? '')),
  100. 'payment_stripe_secret_key' => trim((string) ($_POST['payment_stripe_secret_key'] ?? '')),
  101. 'payment_stripe_publishable_key' => trim((string) ($_POST['payment_stripe_publishable_key'] ?? '')),
  102. 'payment_polar_access_token' => trim((string) ($_POST['payment_polar_access_token'] ?? '')),
  103. 'payment_polar_organization_id' => trim((string) ($_POST['payment_polar_organization_id'] ?? '')),
  104. ]);
  105. ​
  106. if (($_POST['store_category'] ?? 'general') !== 'saas' && ($_POST['payment_provider'] ?? 'stripe') === 'polar') {
  107. saveSettingsFields($db, ['payment_provider' => 'stripe']);
  108. }
  109. }
  110. ​
  111. if ($settingsTab === 'shop_icon') {
  112. $shopIconChoiceInput = (string) ($_POST['shop_icon_choice'] ?? 'cart');
  113. $shopIconPresetKeys = ['cart', 'cart-basket2', 'cart-trolley', 'cart-bag', 'cart-tote', 'cart-box', 'cart-wallet', 'cart-tag', 'cart-storefront', 'cart-percent'];
  114. $shopIconChoiceValid = in_array($shopIconChoiceInput, $shopIconPresetKeys, true) || $shopIconChoiceInput === 'custom' ? $shopIconChoiceInput : 'cart';
  115. $shopIconCustomSvg = $shopIconChoiceValid === 'custom'
  116. ? SocialIcons::sanitizeCustomSvg(mb_substr((string) ($_POST['shop_icon_custom_svg'] ?? ''), 0, 20000))
  117. : '';
  118. if ($shopIconChoiceValid === 'custom' && $shopIconCustomSvg === '') {
  119. $shopIconChoiceValid = 'cart';
  120. }
  121. ​
  122. saveSettingsFields($db, [
  123. 'shop_icon_choice' => $shopIconChoiceValid,
  124. 'shop_icon_custom_svg' => $shopIconCustomSvg,
  125. ]);
  126. }
  127. ​
  128. if ($settingsTab === 'layout') {
  129. $headingFontInput = trim((string) ($_POST['heading_font'] ?? 'Plus Jakarta Sans'));
  130. $bodyFontInput = trim((string) ($_POST['body_font'] ?? 'PT Serif'));
  131. $interfaceFontInput = trim((string) ($_POST['interface_font'] ?? 'Plus Jakarta Sans'));
  132. $validHeadingFont = FontLibrary::findByDisplayName($headingFontInput) !== null ? $headingFontInput : 'Plus Jakarta Sans';
  133. $validBodyFont = FontLibrary::findByDisplayName($bodyFontInput) !== null ? $bodyFontInput : 'PT Serif';
  134. $validInterfaceFont = FontLibrary::findByDisplayName($interfaceFontInput) !== null ? $interfaceFontInput : 'Plus Jakarta Sans';
  135. ​
  136. $productsPerPageHome = max(1, min(100, (int) ($_POST['products_per_page_home'] ?? 12)));
  137. $productsPerPageCategory = max(1, min(100, (int) ($_POST['products_per_page_category'] ?? 12)));
  138. ​
  139. saveSettingsFields($db, [
  140. 'menu_position' => in_array($_POST['menu_position'] ?? 'top', ['top', 'bottom', 'side'], true) ? $_POST['menu_position'] : 'top',
  141. 'featured_banner_enabled' => isset($_POST['featured_banner_enabled']) ? '1' : '0',
  142. 'featured_banner_mode' => in_array($_POST['featured_banner_mode'] ?? 'static', ['static', 'automatic'], true) ? $_POST['featured_banner_mode'] : 'static',
  143. 'featured_banner_hide_duplicates' => isset($_POST['featured_banner_hide_duplicates']) ? '1' : '0',
  144. 'featured_banner_product_1' => max(0, (int) ($_POST['featured_banner_product_1'] ?? 0)),
  145. 'featured_banner_product_2' => max(0, (int) ($_POST['featured_banner_product_2'] ?? 0)),
  146. 'featured_banner_product_3' => max(0, (int) ($_POST['featured_banner_product_3'] ?? 0)),
  147. 'featured_banner_product_4' => max(0, (int) ($_POST['featured_banner_product_4'] ?? 0)),
  148. 'featured_banner_product_5' => max(0, (int) ($_POST['featured_banner_product_5'] ?? 0)),
  149. 'featured_banner_auto_window_value' => max(1, min(720, (int) ($_POST['featured_banner_auto_window_value'] ?? 24))),
  150. 'featured_banner_auto_window_unit' => in_array($_POST['featured_banner_auto_window_unit'] ?? 'hours', ['minutes', 'hours'], true) ? $_POST['featured_banner_auto_window_unit'] : 'hours',
  151. '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',
  152. 'back_to_top_enabled' => isset($_POST['back_to_top_enabled']) ? '1' : '0',
  153. 'back_to_top_progress_enabled' => isset($_POST['back_to_top_progress_enabled']) ? '1' : '0',
  154. 'breadcrumbs_enabled' => isset($_POST['breadcrumbs_enabled']) ? '1' : '0',
  155. 'tags_enabled' => isset($_POST['tags_enabled']) ? '1' : '0',
  156. 'related_products_enabled' => isset($_POST['related_products_enabled']) ? '1' : '0',
  157. 'product_show_buyers' => isset($_POST['product_show_buyers']) ? '1' : '0',
  158. 'product_show_downloads' => isset($_POST['product_show_downloads']) ? '1' : '0',
  159. 'sitemap_rss_enabled' => isset($_POST['sitemap_rss_enabled']) ? '1' : '0',
  160. 'product_loading_mode' => in_array($_POST['product_loading_mode'] ?? 'scroll', ['scroll', 'static'], true) ? $_POST['product_loading_mode'] : 'scroll',
  161. 'pagination_style' => in_array($_POST['pagination_style'] ?? 'numbered', ['numbered', 'load_more', 'infinite_scroll'], true) ? $_POST['pagination_style'] : 'numbered',
  162. 'products_per_page_home' => (string) $productsPerPageHome,
  163. 'products_per_page_category' => (string) $productsPerPageCategory,
  164. 'heading_font' => $validHeadingFont,
  165. 'body_font' => $validBodyFont,
  166. 'interface_font' => $validInterfaceFont,
  167. 'share_enabled' => isset($_POST['share_enabled']) ? '1' : '0',
  168. 'share_facebook' => isset($_POST['share_facebook']) ? '1' : '0',
  169. 'share_twitter' => isset($_POST['share_twitter']) ? '1' : '0',
  170. 'share_linkedin' => isset($_POST['share_linkedin']) ? '1' : '0',
  171. 'share_whatsapp' => isset($_POST['share_whatsapp']) ? '1' : '0',
  172. 'share_telegram' => isset($_POST['share_telegram']) ? '1' : '0',
  173. 'share_email' => isset($_POST['share_email']) ? '1' : '0',
  174. 'share_copy_link' => isset($_POST['share_copy_link']) ? '1' : '0',
  175. ]);
  176. }
  177. ​
  178. if ($settingsTab === 'apps') {
  179. function sanitizeHexColor(string $color, string $fallback): string
  180. {
  181. $color = trim($color);
  182. return preg_match('/^#[0-9a-fA-F]{6}$/', $color) === 1 ? $color : $fallback;
  183. }
  184. ​
  185. saveSettingsFields($db, [
  186. 'pwa_enabled' => isset($_POST['pwa_enabled']) ? '1' : '0',
  187. 'pwa_name' => trim((string) ($_POST['pwa_name'] ?? '')),
  188. 'pwa_short_name' => trim((string) ($_POST['pwa_short_name'] ?? '')),
  189. 'pwa_description' => trim((string) ($_POST['pwa_description'] ?? '')),
  190. 'pwa_icon_url' => sanitizeLogoUrl((string) ($_POST['pwa_icon_url'] ?? '')),
  191. 'pwa_theme_color' => sanitizeHexColor((string) ($_POST['pwa_theme_color'] ?? '#ffffff'), '#ffffff'),
  192. 'pwa_background_color' => sanitizeHexColor((string) ($_POST['pwa_background_color'] ?? '#ffffff'), '#ffffff'),
  193. 'app_banner_enabled' => isset($_POST['app_banner_enabled']) ? '1' : '0',
  194. 'app_android_url' => trim((string) ($_POST['app_android_url'] ?? '')),
  195. 'app_ios_url' => trim((string) ($_POST['app_ios_url'] ?? '')),
  196. ]);
  197. }
  198. ​
  199. if ($settingsTab === 'search') {
  200. saveSettingsFields($db, [
  201. 'search_enabled' => isset($_POST['search_enabled']) ? '1' : '0',
  202. 'search_match_title' => isset($_POST['search_match_title']) ? '1' : '0',
  203. 'search_match_excerpt' => isset($_POST['search_match_excerpt']) ? '1' : '0',
  204. 'search_match_content' => isset($_POST['search_match_content']) ? '1' : '0',
  205. 'search_style' => ($_POST['search_style'] ?? 'icon') === 'field' ? 'field' : 'icon',
  206. 'search_show_in_nav' => isset($_POST['search_show_in_nav']) ? '1' : '0',
  207. 'search_show_in_footer' => isset($_POST['search_show_in_footer']) ? '1' : '0',
  208. ]);
  209. }
  210. ​
  211. if ($settingsTab === 'reviews') {
  212. saveSettingsFields($db, [
  213. 'reviews_enabled' => isset($_POST['reviews_enabled']) ? '1' : '0',
  214. ]);
  215. }
  216. ​
  217. if ($settingsTab === 'custom-code') {
  218. saveSettingsFields($db, [
  219. 'custom_css' => (string) ($_POST['custom_css'] ?? ''),
  220. 'custom_js' => (string) ($_POST['custom_js'] ?? ''),
  221. 'custom_head_html' => (string) ($_POST['custom_head_html'] ?? ''),
  222. 'custom_head_html_index' => isset($_POST['custom_head_html_index']) ? '1' : '0',
  223. 'custom_head_html_product' => isset($_POST['custom_head_html_product']) ? '1' : '0',
  224. 'custom_head_html_category' => isset($_POST['custom_head_html_category']) ? '1' : '0',
  225. 'custom_head_html_pages' => isset($_POST['custom_head_html_pages']) ? '1' : '0',
  226. 'custom_body_html' => (string) ($_POST['custom_body_html'] ?? ''),
  227. 'custom_body_html_index' => isset($_POST['custom_body_html_index']) ? '1' : '0',
  228. 'custom_body_html_product' => isset($_POST['custom_body_html_product']) ? '1' : '0',
  229. 'custom_body_html_category' => isset($_POST['custom_body_html_category']) ? '1' : '0',
  230. 'custom_body_html_pages' => isset($_POST['custom_body_html_pages']) ? '1' : '0',
  231. ]);
  232. }
  233. ​
  234. if ($settingsTab === 'email') {
  235. saveSettingsFields($db, [
  236. 'mail_transport' => in_array($_POST['mail_transport'] ?? 'smtp', ['smtp', 'resend', 'postmark'], true) ? $_POST['mail_transport'] : 'smtp',
  237. 'mail_smtp_host' => trim((string) ($_POST['mail_smtp_host'] ?? '')),
  238. 'mail_smtp_port' => (string) max(1, min(65535, (int) ($_POST['mail_smtp_port'] ?? 587))),
  239. 'mail_smtp_username' => trim((string) ($_POST['mail_smtp_username'] ?? '')),
  240. 'mail_smtp_password' => (string) ($_POST['mail_smtp_password'] ?? ''),
  241. 'mail_smtp_encryption' => in_array($_POST['mail_smtp_encryption'] ?? 'tls', ['tls', 'ssl', 'none'], true) ? $_POST['mail_smtp_encryption'] : 'tls',
  242. 'mail_resend_api_key' => trim((string) ($_POST['mail_resend_api_key'] ?? '')),
  243. 'mail_postmark_server_token' => trim((string) ($_POST['mail_postmark_server_token'] ?? '')),
  244. 'mail_from_email' => trim((string) ($_POST['mail_from_email'] ?? '')),
  245. 'mail_from_name' => trim((string) ($_POST['mail_from_name'] ?? '')),
  246. 'mail_reply_to' => trim((string) ($_POST['mail_reply_to'] ?? '')),
  247. ]);
  248. }
  249. ​
  250. if ($settingsTab === 'ai') {
  251. require_once __DIR__ . '/../includes/ai.php';
  252. $aiSaveError = Ai::saveFromRequest($db, $_POST);
  253. }
  254. ​
  255. if ($settingsTab === 'seo') {
  256. $indexNowKeyStatement = $db->prepare('SELECT setting_value FROM site_settings WHERE setting_key = :key LIMIT 1');
  257. $indexNowKeyStatement->execute(['key' => 'indexnow_key']);
  258. $indexNowKey = (string) ($indexNowKeyStatement->fetchColumn() ?: '');
  259. if (isset($_POST['indexnow_enabled']) && $indexNowKey === '') {
  260. $indexNowKey = bin2hex(random_bytes(16));
  261. }
  262. ​
  263. saveSettingsFields($db, [
  264. 'indexnow_enabled' => isset($_POST['indexnow_enabled']) ? '1' : '0',
  265. 'indexnow_key' => $indexNowKey,
  266. 'seo_title' => trim((string) ($_POST['seo_title'] ?? '')),
  267. 'seo_description' => trim((string) ($_POST['seo_description'] ?? '')),
  268. 'seo_keywords' => trim((string) ($_POST['seo_keywords'] ?? '')),
  269. 'seo_author' => trim((string) ($_POST['seo_author'] ?? '')),
  270. 'seo_reply_to' => trim((string) ($_POST['seo_reply_to'] ?? '')),
  271. 'seo_application_name' => trim((string) ($_POST['seo_application_name'] ?? '')),
  272. 'seo_robots_index' => isset($_POST['seo_robots_index']) ? '1' : '0',
  273. 'seo_robots_follow' => isset($_POST['seo_robots_follow']) ? '1' : '0',
  274. 'seo_og_product_image_enabled' => isset($_POST['seo_og_product_image_enabled']) ? '1' : '0',
  275. 'clean_product_urls' => isset($_POST['clean_product_urls']) ? '1' : '0',
  276. 'llms_txt_enabled' => isset($_POST['llms_txt_enabled']) ? '1' : '0',
  277. 'seo_preview_enabled' => isset($_POST['seo_preview_enabled']) ? '1' : '0',
  278. ]);
  279. }
  280. ​
  281. ActivityLog::record('settings.save', 'settings', null, $settingsTab);
  282. ​