WebOrbiton
v1.0.0.0

StocketBase

1,102 lines · 46.5 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/includes/config.php';
  6. require_once __DIR__ . '/includes/asset.php';
  7. ​
  8. session_start();
  9. ​
  10. $envRoot = dirname(__DIR__) . '/stocketbase-env';
  11. $lockFile = $envRoot . '/install.lock';
  12. ​
  13. if (is_file($lockFile)) {
  14. http_response_code(403);
  15. echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>StocketBase</title></head><body>';
  16. echo '<p>Installation already completed. Delete install.lock manually to reinstall.</p>';
  17. echo '</body></html>';
  18. exit;
  19. }
  20. ​
  21. $errors = [];
  22. $step = isset($_POST['step']) ? (int) $_POST['step'] : (isset($_GET['step']) ? (int) $_GET['step'] : 1);
  23. $step = max(1, min(5, $step));
  24. ​
  25. function pdoConnect(string $host, string $port, string $name, string $user, string $pass): PDO
  26. {
  27. $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $name);
  28. return new PDO($dsn, $user, $pass, [
  29. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  30. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  31. ]);
  32. }
  33. ​
  34. function runSqlFile(PDO $pdo, string $path): void
  35. {
  36. $sql = (string) file_get_contents($path);
  37. $statements = array_filter(array_map('trim', explode(';', $sql)));
  38. foreach ($statements as $statement) {
  39. if ($statement === '') {
  40. continue;
  41. }
  42. $pdo->exec($statement);
  43. }
  44. }
  45. ​
  46. function installerLanguages(): array
  47. {
  48. return [
  49. ['en-US', 'English (US)', 'English (United States)'],
  50. ['en-GB', 'English (UK)', 'English (United Kingdom)'],
  51. ['pt-BR', 'Portuguese (Brazil)', 'Português (Brasil)'],
  52. ['pt-PT', 'Portuguese (Portugal)', 'Português (Portugal)'],
  53. ['zh-CN', 'Chinese (Simplified)', '中文(简体)'],
  54. ['zh-TW', 'Chinese (Traditional)', '中文(繁體)'],
  55. ['es-ES', 'Spanish (Spain)', 'Español (España)'],
  56. ['es-MX', 'Spanish (Mexico)', 'Español (México)'],
  57. ['fr-FR', 'French (France)', 'Français (France)'],
  58. ['fr-CA', 'French (Canada)', 'Français (Canada)'],
  59. ['fr-BE', 'French (Belgium)', 'Français (Belgique)'],
  60. ['fr-CH', 'French (Switzerland)', 'Français (Suisse)'],
  61. ['de-DE', 'German (Germany)', 'Deutsch (Deutschland)'],
  62. ['de-AT', 'German (Austria)', 'Deutsch (Österreich)'],
  63. ['de-CH', 'German (Switzerland)', 'Deutsch (Schweiz)'],
  64. ['nl-NL', 'Dutch (Netherlands)', 'Nederlands (Nederland)'],
  65. ['nl-BE', 'Dutch (Belgium)', 'Nederlands (België)'],
  66. ['it-IT', 'Italian (Italy)', 'Italiano (Italia)'],
  67. ['it-CH', 'Italian (Switzerland)', 'Italiano (Svizzera)'],
  68. ['ar-SA', 'Arabic (Saudi Arabia)', 'العربية (السعودية)'],
  69. ['ar-EG', 'Arabic (Egypt)', 'العربية (مصر)'],
  70. ['ar-AE', 'Arabic (UAE)', 'العربية (الإمارات)'],
  71. ['ru-RU', 'Russian (Russia)', 'Русский (Россия)'],
  72. ['sv-SE', 'Swedish (Sweden)', 'Svenska (Sverige)'],
  73. ['sv-FI', 'Swedish (Finland)', 'Svenska (Finland)'],
  74. ['en-CA', 'English (Canada)', 'English (Canada)'],
  75. ['en-AU', 'English (Australia)', 'English (Australia)'],
  76. ['en-NZ', 'English (New Zealand)', 'English (New Zealand)'],
  77. ['en-IE', 'English (Ireland)', 'English (Ireland)'],
  78. ['en-IN', 'English (India)', 'English (India)'],
  79. ['en-ZA', 'English (South Africa)', 'English (South Africa)'],
  80. ['es-AR', 'Spanish (Argentina)', 'Español (Argentina)'],
  81. ['es-CO', 'Spanish (Colombia)', 'Español (Colombia)'],
  82. ['es-CL', 'Spanish (Chile)', 'Español (Chile)'],
  83. ['es-US', 'Spanish (United States)', 'Español (Estados Unidos)'],
  84. ['zh-HK', 'Chinese (Hong Kong)', '中文(香港)'],
  85. ['zh-SG', 'Chinese (Singapore)', '中文(新加坡)'],
  86. ['aa', 'Afar', 'Afaraf'],
  87. ['ab', 'Abkhazian', 'аҧсуа бызшәа'],
  88. ['ae', 'Avestan', 'avesta'],
  89. ['af', 'Afrikaans', 'Afrikaans'],
  90. ['ak', 'Akan', 'Akan'],
  91. ['am', 'Amharic', 'አማርኛ'],
  92. ['an', 'Aragonese', 'aragonés'],
  93. ['ar', 'Arabic', 'العربية'],
  94. ['as', 'Assamese', 'অসমীয়া'],
  95. ['av', 'Avaric', 'авар мацӀ'],
  96. ['ay', 'Aymara', 'aymar aru'],
  97. ['az', 'Azerbaijani', 'azərbaycan dili'],
  98. ['ba', 'Bashkir', 'башҡорт теле'],
  99. ['be', 'Belarusian', 'беларуская мова'],
  100. ['bg', 'Bulgarian', 'български език'],
  101. ['bh', 'Bihari', 'भोजपुरी'],
  102. ['bi', 'Bislama', 'Bislama'],
  103. ['bm', 'Bambara', 'bamanankan'],
  104. ['bn', 'Bengali', 'বাংলা'],
  105. ['bo', 'Tibetan', 'བོད་ཡིག'],
  106. ['br', 'Breton', 'brezhoneg'],
  107. ['bs', 'Bosnian', 'bosanski jezik'],
  108. ['ca', 'Catalan', 'català'],
  109. ['ce', 'Chechen', 'нохчийн мотт'],
  110. ['ch', 'Chamorro', 'Chamoru'],
  111. ['co', 'Corsican', 'corsu'],
  112. ['cr', 'Cree', 'ᓀᐦᐃᔭᐍᐏᐣ'],
  113. ['cs', 'Czech', 'čeština'],
  114. ['cu', 'Church Slavic', 'ѩзыкъ словѣньскъ'],
  115. ['cv', 'Chuvash', 'чӑваш чӗлхи'],
  116. ['cy', 'Welsh', 'Cymraeg'],
  117. ['da', 'Danish', 'dansk'],
  118. ['de', 'German', 'Deutsch'],
  119. ['dv', 'Divehi', 'ދިވެހި'],
  120. ['dz', 'Dzongkha', 'རྫོང་ཁ'],
  121. ['ee', 'Ewe', 'Eʋegbe'],
  122. ['el', 'Greek', 'ελληνικά'],
  123. ['en', 'English', 'English'],
  124. ['eo', 'Esperanto', 'Esperanto'],
  125. ['es', 'Spanish', 'Español'],
  126. ['et', 'Estonian', 'eesti'],
  127. ['eu', 'Basque', 'euskara'],
  128. ['fa', 'Persian', 'فارسی'],
  129. ['ff', 'Fulah', 'Fulfulde'],
  130. ['fi', 'Finnish', 'suomi'],
  131. ['fj', 'Fijian', 'vosa Vakaviti'],
  132. ['fo', 'Faroese', 'føroyskt'],
  133. ['fr', 'French', 'Français'],
  134. ['fy', 'Western Frisian', 'Frysk'],
  135. ['ga', 'Irish', 'Gaeilge'],
  136. ['gd', 'Scottish Gaelic', 'Gàidhlig'],
  137. ['gl', 'Galician', 'galego'],
  138. ['gn', 'Guarani', "Avañe'ẽ"],
  139. ['gu', 'Gujarati', 'ગુજરાતી'],
  140. ['gv', 'Manx', 'Gaelg'],
  141. ['ha', 'Hausa', 'هَوُسَ'],
  142. ['he', 'Hebrew', 'עברית'],
  143. ['hi', 'Hindi', 'हिन्दी'],
  144. ['ho', 'Hiri Motu', 'Hiri Motu'],
  145. ['hr', 'Croatian', 'hrvatski jezik'],
  146. ['ht', 'Haitian Creole', 'Kreyòl ayisyen'],
  147. ['hu', 'Hungarian', 'magyar'],
  148. ['hy', 'Armenian', 'Հայերեն'],
  149. ['hz', 'Herero', 'Otjiherero'],
  150. ['ia', 'Interlingua', 'Interlingua'],
  151. ['id', 'Indonesian', 'Bahasa Indonesia'],
  152. ['ie', 'Interlingue', 'Interlingue'],
  153. ['ig', 'Igbo', 'Asụsụ Igbo'],
  154. ['ii', 'Sichuan Yi', 'ꆈꌠ꒿ Nuosuhxop'],
  155. ['ik', 'Inupiaq', 'Iñupiaq'],
  156. ['io', 'Ido', 'Ido'],
  157. ['is', 'Icelandic', 'Íslenska'],
  158. ['it', 'Italian', 'Italiano'],
  159. ['iu', 'Inuktitut', 'ᐃᓄᒃᑎᑐᑦ'],
  160. ['ja', 'Japanese', '日本語'],
  161. ['jv', 'Javanese', 'ꦧꦱꦗꦮ'],
  162. ['ka', 'Georgian', 'ქართული'],
  163. ['kg', 'Kongo', 'Kikongo'],
  164. ['ki', 'Kikuyu', 'Gĩkũyũ'],
  165. ['kj', 'Kwanyama', 'Kuanyama'],
  166. ['kk', 'Kazakh', 'қазақ тілі'],
  167. ['kl', 'Kalaallisut', 'kalaallisut'],
  168. ['km', 'Khmer', 'ខ្មែរ'],
  169. ['kn', 'Kannada', 'ಕನ್ನಡ'],
  170. ['ko', 'Korean', '한국어'],
  171. ['kr', 'Kanuri', 'Kanuri'],
  172. ['ks', 'Kashmiri', 'कॉशुर'],
  173. ['ku', 'Kurdish', 'Kurdî'],
  174. ['kv', 'Komi', 'коми кыв'],
  175. ['kw', 'Cornish', 'Kernewek'],
  176. ['ky', 'Kyrgyz', 'Кыргызча'],
  177. ['la', 'Latin', 'latine'],
  178. ['lb', 'Luxembourgish', 'Lëtzebuergesch'],
  179. ['lg', 'Ganda', 'Luganda'],
  180. ['li', 'Limburgish', 'Limburgs'],
  181. ['ln', 'Lingala', 'Lingála'],
  182. ['lo', 'Lao', 'ພາສາລາວ'],
  183. ['lt', 'Lithuanian', 'lietuvių kalba'],
  184. ['lu', 'Luba-Katanga', 'Kiluba'],
  185. ['lv', 'Latvian', 'latviešu valoda'],
  186. ['mg', 'Malagasy', 'fiteny malagasy'],
  187. ['mh', 'Marshallese', 'Kajin M̧ajeļ'],
  188. ['mi', 'Maori', 'te reo Māori'],
  189. ['mk', 'Macedonian', 'македонски јазик'],
  190. ['ml', 'Malayalam', 'മലയാളം'],
  191. ['mn', 'Mongolian', 'Монгол хэл'],
  192. ['mr', 'Marathi', 'मराठी'],
  193. ['ms', 'Malay', 'Bahasa Melayu'],
  194. ['mt', 'Maltese', 'Malti'],
  195. ['my', 'Burmese', 'ဗမာစာ'],
  196. ['na', 'Nauru', 'Dorerin Naoero'],
  197. ['nb', 'Norwegian Bokmål', 'Norsk Bokmål'],
  198. ['nd', 'North Ndebele', 'isiNdebele'],
  199. ['ne', 'Nepali', 'नेपाली'],
  200. ['ng', 'Ndonga', 'Owambo'],
  201. ['nl', 'Dutch', 'Nederlands'],
  202. ['nn', 'Norwegian Nynorsk', 'Norsk Nynorsk'],
  203. ['no', 'Norwegian', 'Norsk'],
  204. ['nr', 'South Ndebele', 'isiNdebele'],
  205. ['nv', 'Navajo', 'Diné bizaad'],
  206. ['ny', 'Chichewa', 'chiCheŵa'],
  207. ['oc', 'Occitan', 'occitan'],
  208. ['oj', 'Ojibwe', 'ᐊᓂᔑᓈᐯᒧᐎᓐ'],
  209. ['om', 'Oromo', 'Afaan Oromoo'],
  210. ['or', 'Odia', 'ଓଡ଼ିଆ'],
  211. ['os', 'Ossetian', 'ирон æвзаг'],
  212. ['pa', 'Punjabi', 'ਪੰਜਾਬੀ'],
  213. ['pi', 'Pali', 'पालि'],
  214. ['pl', 'Polish', 'Polski'],
  215. ['ps', 'Pashto', 'پښتو'],
  216. ['pt', 'Portuguese', 'Português'],
  217. ['qu', 'Quechua', 'Runa Simi'],
  218. ['rm', 'Romansh', 'rumantsch grischun'],
  219. ['rn', 'Kirundi', 'Ikirundi'],
  220. ['ro', 'Romanian', 'Română'],
  221. ['ru', 'Russian', 'Русский'],
  222. ['rw', 'Kinyarwanda', 'Ikinyarwanda'],
  223. ['sa', 'Sanskrit', 'संस्कृतम्'],
  224. ['sc', 'Sardinian', 'sardu'],
  225. ['sd', 'Sindhi', 'सिन्धी'],
  226. ['se', 'Northern Sami', 'Davvisámegiella'],
  227. ['sg', 'Sango', 'yângâ tî sängö'],
  228. ['si', 'Sinhala', 'සිංහල'],
  229. ['sk', 'Slovak', 'slovenčina'],
  230. ['sl', 'Slovenian', 'slovenščina'],
  231. ['sm', 'Samoan', "gagana fa'a Samoa"],
  232. ['sn', 'Shona', 'chiShona'],
  233. ['so', 'Somali', 'Soomaaliga'],
  234. ['sq', 'Albanian', 'Shqip'],
  235. ['sr', 'Serbian', 'српски језик'],
  236. ['ss', 'Swati', 'SiSwati'],
  237. ['st', 'Southern Sotho', 'Sesotho'],
  238. ['su', 'Sundanese', 'Basa Sunda'],
  239. ['sv', 'Swedish', 'Svenska'],
  240. ['sw', 'Swahili', 'Kiswahili'],
  241. ['ta', 'Tamil', 'தமிழ்'],
  242. ['te', 'Telugu', 'తెలుగు'],
  243. ['tg', 'Tajik', 'тоҷикӣ'],
  244. ['th', 'Thai', 'ไทย'],
  245. ['ti', 'Tigrinya', 'ትግርኛ'],
  246. ['tk', 'Turkmen', 'Türkmençe'],
  247. ['tl', 'Tagalog', 'Wikang Tagalog'],
  248. ['tn', 'Tswana', 'Setswana'],
  249. ['to', 'Tonga', 'faka Tonga'],
  250. ['tr', 'Turkish', 'Türkçe'],
  251. ['ts', 'Tsonga', 'Xitsonga'],
  252. ['tt', 'Tatar', 'татар теле'],
  253. ['tw', 'Twi', 'Twi'],
  254. ['ty', 'Tahitian', 'Reo Tahiti'],
  255. ['ug', 'Uyghur', 'ئۇيغۇرچە'],
  256. ['uk', 'Ukrainian', 'Українська'],
  257. ['ur', 'Urdu', 'اردو'],
  258. ['uz', 'Uzbek', "Oʻzbek"],
  259. ['ve', 'Venda', 'Tshivenḓa'],
  260. ['vi', 'Vietnamese', 'Tiếng Việt'],
  261. ['vo', 'Volapük', 'Volapük'],
  262. ['wa', 'Walloon', 'walon'],
  263. ['wo', 'Wolof', 'Wollof'],
  264. ['xh', 'Xhosa', 'isiXhosa'],
  265. ['yi', 'Yiddish', 'ייִדיש'],
  266. ['yo', 'Yoruba', 'Yorùbá'],
  267. ['za', 'Zhuang', 'Saɯ cueŋƅ'],
  268. ['zh', 'Chinese', '中文'],
  269. ['zu', 'Zulu', 'isiZulu'],
  270. ];
  271. }
  272. ​
  273. function installerLanguageAliases(): array
  274. {
  275. return [
  276. 'en' => ['english', 'eng', 'angielski'],
  277. 'en-US' => ['english us', 'american english', 'angielski amerykanski', 'us', 'usa'],
  278. 'en-GB' => ['english uk', 'british english', 'angielski brytyjski', 'gb', 'uk'],
  279. 'pl' => ['polish', 'polski', 'pol'],
  280. 'de' => ['german', 'niemiecki', 'deutsch', 'ger'],
  281. 'fr' => ['french', 'francuski', 'francais'],
  282. 'fr-FR' => ['french france', 'francuski francuski'],
  283. 'fr-CA' => ['french canada', 'canadian french', 'quebecois', 'francuski kanadyjski'],
  284. 'fr-BE' => ['french belgium', 'belgian french', 'francuski belgijski'],
  285. 'fr-CH' => ['french switzerland', 'swiss french', 'francuski szwajcarski'],
  286. 'de-DE' => ['german germany', 'niemiecki niemcy'],
  287. 'de-AT' => ['german austria', 'austrian german', 'niemiecki austriacki'],
  288. 'de-CH' => ['german switzerland', 'swiss german', 'niemiecki szwajcarski'],
  289. 'nl-NL' => ['dutch netherlands', 'holenderski holandia'],
  290. 'nl-BE' => ['dutch belgium', 'flemish', 'flamandzki', 'holenderski belgijski'],
  291. 'it-IT' => ['italian italy', 'wloski wlochy'],
  292. 'it-CH' => ['italian switzerland', 'swiss italian', 'wloski szwajcarski'],
  293. 'ar-SA' => ['arabic saudi arabia', 'arabski saudyjski'],
  294. 'ar-EG' => ['arabic egypt', 'egyptian arabic', 'arabski egipski'],
  295. 'ar-AE' => ['arabic uae', 'arabski emiraty'],
  296. 'ru-RU' => ['russian russia', 'rosyjski rosja'],
  297. 'sv-SE' => ['swedish sweden', 'szwedzki szwecja'],
  298. 'sv-FI' => ['swedish finland', 'szwedzki finlandia'],
  299. 'en-CA' => ['english canada', 'canadian english', 'angielski kanadyjski'],
  300. 'en-AU' => ['english australia', 'australian english', 'angielski australijski', 'aussie'],
  301. 'en-NZ' => ['english new zealand', 'kiwi english', 'angielski nowozelandzki'],
  302. 'en-IE' => ['english ireland', 'irish english', 'angielski irlandzki'],
  303. 'en-IN' => ['english india', 'indian english', 'angielski indyjski'],
  304. 'en-ZA' => ['english south africa', 'south african english', 'angielski poludniowoafrykanski'],
  305. 'es-AR' => ['spanish argentina', 'argentinian spanish', 'hiszpanski argentynski'],
  306. 'es-CO' => ['spanish colombia', 'colombian spanish', 'hiszpanski kolumbijski'],
  307. 'es-CL' => ['spanish chile', 'chilean spanish', 'hiszpanski chilijski'],
  308. 'es-US' => ['spanish united states', 'us spanish', 'hiszpanski amerykanski'],
  309. 'zh-HK' => ['chinese hong kong', 'cantonese hong kong', 'chinski hongkong'],
  310. 'zh-SG' => ['chinese singapore', 'chinski singapur'],
  311. 'es' => ['spanish', 'hiszpanski', 'espanol', 'castellano'],
  312. 'es-ES' => ['spanish spain', 'castellano', 'hiszpanski europejski'],
  313. 'es-MX' => ['spanish mexico', 'mexican spanish', 'hiszpanski meksykanski'],
  314. 'it' => ['italian', 'wloski', 'italiano'],
  315. 'pt' => ['portuguese', 'portugalski', 'portugues'],
  316. 'pt-BR' => ['portuguese brazil', 'brazilian portuguese', 'brazylijski', 'portugalski brazylijski'],
  317. 'pt-PT' => ['portuguese portugal', 'european portuguese', 'portugalski europejski'],
  318. 'ru' => ['russian', 'rosyjski', 'russkiy'],
  319. 'uk' => ['ukrainian', 'ukrainski'],
  320. 'zh' => ['chinese', 'chinski', 'mandarin', 'mandarynski', 'zhongwen'],
  321. 'zh-CN' => ['chinese simplified', 'simplified chinese', 'chinski uproszczony', 'mainland china', 'putonghua'],
  322. 'zh-TW' => ['chinese traditional', 'traditional chinese', 'chinski tradycyjny', 'taiwan', 'taiwanski'],
  323. 'ja' => ['japanese', 'japonski', 'nihongo'],
  324. 'ko' => ['korean', 'koreanski', 'hangugeo'],
  325. 'ar' => ['arabic', 'arabski'],
  326. 'he' => ['hebrew', 'hebrajski', 'iw'],
  327. 'nl' => ['dutch', 'holenderski', 'niderlandzki', 'flemish'],
  328. 'sv' => ['swedish', 'szwedzki', 'svenska'],
  329. 'no' => ['norwegian', 'norweski'],
  330. 'nb' => ['norwegian bokmal', 'bokmal'],
  331. 'nn' => ['nynorsk'],
  332. 'da' => ['danish', 'dunski'],
  333. 'fi' => ['finnish', 'finski', 'suomi'],
  334. 'cs' => ['czech', 'czeski', 'cestina'],
  335. 'sk' => ['slovak', 'slowacki'],
  336. 'hu' => ['hungarian', 'wegierski'],
  337. 'ro' => ['romanian', 'rumunski'],
  338. 'bg' => ['bulgarian', 'bulgarski'],
  339. 'el' => ['greek', 'grecki'],
  340. 'tr' => ['turkish', 'turecki'],
  341. 'hi' => ['hindi'],
  342. 'bn' => ['bengali', 'bengalski'],
  343. 'vi' => ['vietnamese', 'wietnamski'],
  344. 'th' => ['thai', 'tajski'],
  345. 'id' => ['indonesian', 'indonezyjski', 'bahasa'],
  346. 'ms' => ['malay', 'malajski'],
  347. 'sr' => ['serbian', 'serbski'],
  348. 'hr' => ['croatian', 'chorwacki'],
  349. 'sl' => ['slovenian', 'slowenski'],
  350. 'lt' => ['lithuanian', 'litewski'],
  351. 'lv' => ['latvian', 'lotewski'],
  352. 'et' => ['estonian', 'estonski'],
  353. 'is' => ['icelandic', 'islandzki'],
  354. 'ga' => ['irish', 'irlandzki', 'gaelic'],
  355. 'cy' => ['welsh', 'walijski'],
  356. 'ca' => ['catalan', 'katalonski'],
  357. 'eu' => ['basque', 'baskijski'],
  358. 'fa' => ['persian', 'perski', 'farsi'],
  359. 'ur' => ['urdu'],
  360. 'sw' => ['swahili'],
  361. 'af' => ['afrikaans'],
  362. 'am' => ['amharic', 'amharski'],
  363. 'az' => ['azerbaijani', 'azerski'],
  364. 'ka' => ['georgian', 'gruzinski'],
  365. 'hy' => ['armenian', 'armenski'],
  366. 'kk' => ['kazakh', 'kazachski'],
  367. 'uz' => ['uzbek', 'uzbecki'],
  368. 'mn' => ['mongolian', 'mongolski'],
  369. 'ta' => ['tamil', 'tamilski'],
  370. 'te' => ['telugu'],
  371. 'ml' => ['malayalam'],
  372. 'kn' => ['kannada'],
  373. 'gu' => ['gujarati'],
  374. 'pa' => ['punjabi'],
  375. 'mr' => ['marathi'],
  376. 'ne' => ['nepali', 'nepalski'],
  377. 'si' => ['sinhala', 'sinhalski'],
  378. 'km' => ['khmer'],
  379. 'lo' => ['lao'],
  380. 'my' => ['burmese', 'birmanski'],
  381. 'ps' => ['pashto'],
  382. 'ku' => ['kurdish', 'kurdyjski'],
  383. 'yi' => ['yiddish', 'jidysz'],
  384. 'eo' => ['esperanto'],
  385. 'la' => ['latin', 'lacina'],
  386. 'sq' => ['albanian', 'albanski'],
  387. 'mk' => ['macedonian', 'macedonski'],
  388. 'be' => ['belarusian', 'bialoruski'],
  389. 'gl' => ['galician', 'galicyjski'],
  390. 'mt' => ['maltese', 'maltanski'],
  391. 'tl' => ['tagalog', 'filipino'],
  392. 'zu' => ['zulu'],
  393. 'xh' => ['xhosa'],
  394. 'so' => ['somali', 'somalijski'],
  395. 'ha' => ['hausa'],
  396. 'yo' => ['yoruba'],
  397. 'ig' => ['igbo'],
  398. ];
  399. }
  400. ​
  401. function installerLanguageCodes(): array
  402. {
  403. return array_column(installerLanguages(), 0);
  404. }
  405. ​
  406. function installerLanguagesJson(): string
  407. {
  408. $aliases = installerLanguageAliases();
  409. $rows = [];
  410. foreach (installerLanguages() as [$code, $name, $native]) {
  411. $extra = $aliases[$code] ?? [];
  412. $rows[] = [
  413. 'code' => $code,
  414. 'name' => $name,
  415. 'native' => $native,
  416. 'aliases' => $extra,
  417. ];
  418. }
  419. return json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  420. }
  421. ​
  422. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  423. if ($step === 1) {
  424. $locale = trim((string) ($_POST['locale'] ?? 'en'));
  425. if (!in_array($locale, installerLanguageCodes(), true)) {
  426. $locale = 'en';
  427. }
  428. ​
  429. $_SESSION['install']['project_name'] = trim((string) ($_POST['project_name'] ?? 'default'));
  430. $_SESSION['install']['app_name'] = trim((string) ($_POST['app_name'] ?? 'StocketBase'));
  431. $_SESSION['install']['app_url'] = trim((string) ($_POST['app_url'] ?? ''));
  432. $_SESSION['install']['base_path'] = trim((string) ($_POST['base_path'] ?? ''));
  433. $_SESSION['install']['locale'] = $locale;
  434. header('Location: install.php?step=2');
  435. exit;
  436. }
  437. ​
  438. if ($step === 2) {
  439. $dbSite = [
  440. 'host' => trim((string) ($_POST['db_site_host'] ?? '127.0.0.1')),
  441. 'port' => trim((string) ($_POST['db_site_port'] ?? '3306')),
  442. 'name' => trim((string) ($_POST['db_site_name'] ?? '')),
  443. 'user' => trim((string) ($_POST['db_site_user'] ?? '')),
  444. 'pass' => (string) ($_POST['db_site_pass'] ?? ''),
  445. ];
  446. ​
  447. $dbUsers = [
  448. 'host' => trim((string) ($_POST['db_users_host'] ?? '127.0.0.1')),
  449. 'port' => trim((string) ($_POST['db_users_port'] ?? '3306')),
  450. 'name' => trim((string) ($_POST['db_users_name'] ?? '')),
  451. 'user' => trim((string) ($_POST['db_users_user'] ?? '')),
  452. 'pass' => (string) ($_POST['db_users_pass'] ?? ''),
  453. ];
  454. ​
  455. try {
  456. $siteConnection = pdoConnect($dbSite['host'], $dbSite['port'], $dbSite['name'], $dbSite['user'], $dbSite['pass']);
  457. $usersConnection = pdoConnect($dbUsers['host'], $dbUsers['port'], $dbUsers['name'], $dbUsers['user'], $dbUsers['pass']);
  458. runSqlFile($siteConnection, __DIR__ . '/sql/schema_site.sql');
  459. runSqlFile($usersConnection, __DIR__ . '/sql/schema_users.sql');
  460. ​
  461. $_SESSION['install']['db_site'] = $dbSite;
  462. $_SESSION['install']['db_users'] = $dbUsers;
  463. header('Location: install.php?step=3');
  464. exit;
  465. } catch (Throwable $exception) {
  466. $errors[] = 'Database error: ' . $exception->getMessage();
  467. }
  468. }
  469. ​
  470. if ($step === 3) {
  471. $username = trim((string) ($_POST['admin_username'] ?? ''));
  472. $email = trim((string) ($_POST['admin_email'] ?? ''));
  473. $displayName = trim((string) ($_POST['admin_display_name'] ?? ''));
  474. $password = (string) ($_POST['admin_password'] ?? '');
  475. $passwordConfirm = (string) ($_POST['admin_password_confirm'] ?? '');
  476. ​
  477. if ($username === '' || $email === '' || $password === '') {
  478. $errors[] = 'Username, email and password are required.';
  479. } elseif ($password !== $passwordConfirm) {
  480. $errors[] = 'Passwords do not match.';
  481. } elseif (strlen($password) < 10) {
  482. $errors[] = 'Password must be at least 10 characters long.';
  483. }
  484. ​
  485. if (empty($errors)) {
  486. try {
  487. $dbSite = $_SESSION['install']['db_site'];
  488. $siteConnection = pdoConnect($dbSite['host'], $dbSite['port'], $dbSite['name'], $dbSite['user'], $dbSite['pass']);
  489. ​
  490. $countStatement = $siteConnection->query("SELECT COUNT(*) AS total FROM team_accounts WHERE role = 'super_admin'");
  491. $existingSuperAdmins = (int) $countStatement->fetch()['total'];
  492. ​
  493. if ($existingSuperAdmins > 0) {
  494. $errors[] = 'A Super Admin account already exists.';
  495. } else {
  496. $hash = password_hash($password, PASSWORD_DEFAULT);
  497. $insert = $siteConnection->prepare(
  498. 'INSERT INTO team_accounts (username, email, password_hash, display_name, role, status) VALUES (:username, :email, :hash, :display_name, :role, :status)'
  499. );
  500. $insert->execute([
  501. 'username' => $username,
  502. 'email' => $email,
  503. 'hash' => $hash,
  504. 'display_name' => $displayName !== '' ? $displayName : $username,
  505. 'role' => 'super_admin',
  506. 'status' => 'active',
  507. ]);
  508. ​
  509. $_SESSION['install']['admin_created'] = true;
  510. header('Location: install.php?step=4');
  511. exit;
  512. }
  513. } catch (Throwable $exception) {
  514. $errors[] = 'Could not create Super Admin: ' . $exception->getMessage();
  515. }
  516. }
  517. }
  518. ​
  519. if ($step === 4) {
  520. $siteName = trim((string) ($_POST['site_name'] ?? ($_SESSION['install']['app_name'] ?? 'StocketBase')));
  521. $siteDescription = trim((string) ($_POST['site_description'] ?? ''));
  522. $storeCategory = in_array($_POST['store_category'] ?? '', ['general', 'saas'], true) ? $_POST['store_category'] : 'general';
  523. $pricingModel = in_array($_POST['pricing_model'] ?? '', ['per_product', 'subscription'], true) ? $_POST['pricing_model'] : 'per_product';
  524. ​
  525. try {
  526. $dbSite = $_SESSION['install']['db_site'];
  527. $siteConnection = pdoConnect($dbSite['host'], $dbSite['port'], $dbSite['name'], $dbSite['user'], $dbSite['pass']);
  528. ​
  529. $defaults = [
  530. 'site_name' => $siteName,
  531. 'site_description' => $siteDescription,
  532. 'store_category' => $storeCategory,
  533. 'pricing_model' => $pricingModel,
  534. 'payment_provider' => 'stripe',
  535. 'clean_product_urls' => '0',
  536. 'menu_position' => 'top',
  537. 'featured_banner_enabled' => '0',
  538. 'featured_banner_mode' => 'static',
  539. 'featured_banner_hide_duplicates' => '0',
  540. 'featured_banner_product_1' => '0',
  541. 'featured_banner_product_2' => '0',
  542. 'featured_banner_product_3' => '0',
  543. 'featured_banner_product_4' => '0',
  544. 'featured_banner_product_5' => '0',
  545. 'featured_banner_auto_window_value' => '24',
  546. 'featured_banner_auto_window_unit' => 'hours',
  547. 'featured_banner_auto_category_mode' => 'mixed',
  548. 'related_products_enabled' => '1',
  549. 'sitemap_rss_enabled' => '1',
  550. 'product_loading_mode' => 'scroll',
  551. 'reviews_enabled' => '1',
  552. ];
  553. ​
  554. $insert = $siteConnection->prepare(
  555. 'INSERT INTO site_settings (setting_key, setting_value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
  556. );
  557. foreach ($defaults as $key => $value) {
  558. $insert->execute(['key' => $key, 'value' => $value]);
  559. }
  560. ​
  561. $_SESSION['install']['site_name'] = $siteName;
  562. $_SESSION['install']['site_description'] = $siteDescription;
  563. header('Location: install.php?step=5');
  564. exit;
  565. } catch (Throwable $exception) {
  566. $errors[] = 'Could not save site settings: ' . $exception->getMessage();
  567. }
  568. }
  569. ​
  570. if ($step === 5) {
  571. $projectName = (string) ($_SESSION['install']['project_name'] ?? 'default');
  572. $dbSite = $_SESSION['install']['db_site'];
  573. $dbUsers = $_SESSION['install']['db_users'];
  574. ​
  575. $envValues = [
  576. 'APP_NAME' => (string) ($_SESSION['install']['app_name'] ?? 'StocketBase'),
  577. 'APP_URL' => (string) ($_SESSION['install']['app_url'] ?? ''),
  578. 'APP_BASE_PATH' => (string) ($_SESSION['install']['base_path'] ?? ''),
  579. 'APP_ENV' => 'production',
  580. 'APP_KEY' => bin2hex(random_bytes(32)),
  581. 'APP_LOCALE' => (string) ($_SESSION['install']['locale'] ?? 'en'),
  582. 'DB_SITE_HOST' => $dbSite['host'],
  583. 'DB_SITE_PORT' => $dbSite['port'],
  584. 'DB_SITE_NAME' => $dbSite['name'],
  585. 'DB_SITE_USER' => $dbSite['user'],
  586. 'DB_SITE_PASS' => $dbSite['pass'],
  587. 'DB_USERS_HOST' => $dbUsers['host'],
  588. 'DB_USERS_PORT' => $dbUsers['port'],
  589. 'DB_USERS_NAME' => $dbUsers['name'],
  590. 'DB_USERS_USER' => $dbUsers['user'],
  591. 'DB_USERS_PASS' => $dbUsers['pass'],
  592. 'SESSION_COOKIE_NAME' => 'stocketbase_session',
  593. 'SESSION_SECURE' => '1',
  594. 'INSTALL_LOCKED' => '1',
  595. ];
  596. ​
  597. $written = Config::write($projectName, $envValues);
  598. ​
  599. if ($written) {
  600. if (!is_dir($envRoot)) {
  601. mkdir($envRoot, 0750, true);
  602. }
  603. file_put_contents($lockFile, date('c'));
  604. chmod($lockFile, 0640);
  605. unset($_SESSION['install']);
  606. header('Location: install.php?step=6');
  607. exit;
  608. }
  609. ​
  610. $errors[] = 'Could not write environment file. Check folder permissions for ' . htmlspecialchars($envRoot);
  611. }
  612. }
  613. ​
  614. if (isset($_GET['step']) && (int) $_GET['step'] === 6) {
  615. $step = 6;
  616. }
  617. ​
  618. $selectedLocale = (string) ($_SESSION['install']['locale'] ?? 'en');
  619. $selectedLocaleLabel = 'English';
  620. foreach (installerLanguages() as [$code, $name, $native]) {
  621. if ($code === $selectedLocale) {
  622. $selectedLocaleLabel = $name . ' (' . $native . ')';
  623. break;
  624. }
  625. }
  626. ​
  627. ?>
  628. <!DOCTYPE html>
  629. <html lang="en">
  630. ​
  631. <head>
  632. <meta charset="utf-8">
  633. <meta name="viewport" content="width=device-width, initial-scale=1">
  634. <title>StocketBase Installer</title>
  635. <?= Asset::favicon() ?>
  636. <style>
  637. :root {
  638. --bg: #050506;
  639. --panel: #0c0c0f;
  640. --border: #232329;
  641. --text: #f8fafc;
  642. --muted: #8b8b97;
  643. --accent: #3862f5;
  644. }
  645. ​
  646. * {
  647. box-sizing: border-box;
  648. }
  649. ​
  650. body {
  651. margin: 0;
  652. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  653. background: var(--bg);
  654. color: var(--text);
  655. display: flex;
  656. min-height: 100vh;
  657. align-items: center;
  658. justify-content: center;
  659. padding: 24px;
  660. }
  661. ​
  662. .card {
  663. width: 100%;
  664. max-width: 560px;
  665. background: var(--panel);
  666. border: 1px solid var(--border);
  667. border-radius: 14px;
  668. padding: 32px;
  669. }
  670. ​
  671. h1 {
  672. font-size: 20px;
  673. margin: 0 0 4px;
  674. }
  675. ​
  676. p.sub {
  677. color: var(--muted);
  678. margin: 0 0 24px;
  679. font-size: 14px;
  680. }
  681. ​
  682. .steps {
  683. display: flex;
  684. gap: 6px;
  685. margin-bottom: 24px;
  686. }
  687. ​
  688. .steps span {
  689. flex: 1;
  690. height: 4px;
  691. border-radius: 2px;
  692. background: var(--border);
  693. }
  694. ​
  695. .steps span.done {
  696. background: var(--accent);
  697. }
  698. ​
  699. label {
  700. display: block;
  701. font-size: 13px;
  702. color: var(--muted);
  703. margin: 14px 0 6px;
  704. }
  705. ​
  706. input,
  707. select {
  708. width: 100%;
  709. padding: 10px 12px;
  710. border-radius: 8px;
  711. border: 1px solid var(--border);
  712. background: #08080a;
  713. color: var(--text);
  714. font-size: 14px;
  715. }
  716. ​
  717. button {
  718. margin-top: 24px;
  719. width: 100%;
  720. padding: 12px;
  721. border-radius: 8px;
  722. border: none;
  723. background: var(--accent);
  724. color: #fff;
  725. font-size: 14px;
  726. font-weight: 600;
  727. cursor: pointer;
  728. }
  729. ​
  730. .error {
  731. background: #2a1215;
  732. border: 1px solid #5c1f26;
  733. color: #ff9aa2;
  734. padding: 10px 12px;
  735. border-radius: 8px;
  736. font-size: 13px;
  737. margin-bottom: 14px;
  738. }
  739. ​
  740. .row {
  741. display: flex;
  742. gap: 12px;
  743. }
  744. ​
  745. .row>div {
  746. flex: 1;
  747. }
  748. ​
  749. .success {
  750. text-align: center;
  751. }
  752. ​
  753. .success h1 {
  754. font-size: 22px;
  755. }
  756. ​
  757. .lang-picker {
  758. position: relative;
  759. }
  760. ​
  761. .lang-picker input.lang-search {
  762. cursor: text;
  763. }
  764. ​
  765. .lang-picker .lang-panel {
  766. display: none;
  767. position: absolute;
  768. z-index: 20;
  769. top: calc(100% + 6px);
  770. left: 0;
  771. right: 0;
  772. max-height: 260px;
  773. overflow-y: auto;
  774. background: #08080a;
  775. border: 1px solid var(--border);
  776. border-radius: 8px;
  777. box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
  778. }
  779. ​
  780. .lang-picker.open .lang-panel {
  781. display: block;
  782. }
  783. ​
  784. .lang-option {
  785. padding: 9px 12px;
  786. font-size: 14px;
  787. cursor: pointer;
  788. display: flex;
  789. justify-content: space-between;
  790. gap: 10px;
  791. }
  792. ​
  793. .lang-option:hover,
  794. .lang-option.active {
  795. background: #1a1a1f;
  796. }
  797. ​
  798. .lang-option .code {
  799. color: var(--muted);
  800. font-size: 12px;
  801. text-transform: uppercase;
  802. align-self: center;
  803. }
  804. ​
  805. .lang-empty {
  806. padding: 10px 12px;
  807. font-size: 13px;
  808. color: var(--muted);
  809. }
  810. </style>
  811. </head>
  812. ​
  813. <body>
  814. <div class="card">
  815. <div class="steps">
  816. <span class="<?= $step >= 1 ? 'done' : '' ?>"></span>
  817. <span class="<?= $step >= 2 ? 'done' : '' ?>"></span>
  818. <span class="<?= $step >= 3 ? 'done' : '' ?>"></span>
  819. <span class="<?= $step >= 4 ? 'done' : '' ?>"></span>
  820. <span class="<?= $step >= 5 ? 'done' : '' ?>"></span>
  821. </div>
  822. ​
  823. <?php foreach ($errors as $error): ?>
  824. <div class="error"><?= htmlspecialchars($error) ?></div>
  825. <?php endforeach; ?>
  826. ​
  827. <?php if ($step === 1): ?>
  828. <h1>Project setup</h1>
  829. <p class="sub">Basic identity for this StocketBase instance.</p>
  830. <form method="post" autocomplete="off">
  831. <input type="hidden" name="step" value="1">
  832. <label>Project name (used for env file naming)</label>
  833. <input type="text" name="project_name" placeholder="mystore" autocomplete="off" required>
  834. <label>Store name</label>
  835. <input type="text" name="app_name" placeholder="StocketBase" autocomplete="off" required>
  836. <label>Store URL</label>
  837. <input type="text" name="app_url" placeholder="https://example.com" autocomplete="off" required>
  838. <label>Base path (leave empty if installed at domain root)</label>
  839. <input type="text" name="base_path" placeholder="/store" autocomplete="off">
  840. <label>Default language</label>
  841. <div class="lang-picker" id="lang-picker">
  842. <input type="text" class="lang-search" id="lang-search" autocomplete="off"
  843. placeholder="Type a language, native name or code (e.g. PL, Polish, Polski)"
  844. value="<?= htmlspecialchars($selectedLocaleLabel) ?>">
  845. <input type="hidden" name="locale" id="lang-code" value="<?= htmlspecialchars($selectedLocale) ?>">
  846. <div class="lang-panel" id="lang-panel"></div>
  847. </div>
  848. <button type="submit">Continue</button>
  849. </form>
  850. <script>
  851. (function() {
  852. var LANGS = <?= installerLanguagesJson() ?>;
  853. ​
  854. var picker = document.getElementById('lang-picker');
  855. var search = document.getElementById('lang-search');
  856. var hidden = document.getElementById('lang-code');
  857. var panel = document.getElementById('lang-panel');
  858. var activeIndex = -1;
  859. var currentResults = [];
  860. ​
  861. function norm(s) {
  862. return (s || '')
  863. .toString()
  864. .toLowerCase()
  865. .normalize('NFD')
  866. .replace(/[\u0300-\u036f]/g, '');
  867. }
  868. ​
  869. LANGS.forEach(function(lang) {
  870. var parts = [lang.code, lang.name, lang.native].concat(lang.aliases || []);
  871. lang._haystack = parts.map(norm);
  872. });
  873. ​
  874. function search_matches(query) {
  875. var q = norm(query);
  876. if (!q) {
  877. return LANGS.slice(0, 20);
  878. }
  879. var starts = [];
  880. var contains = [];
  881. LANGS.forEach(function(lang) {
  882. var hit = false;
  883. var startHit = false;
  884. for (var i = 0; i < lang._haystack.length; i++) {
  885. var h = lang._haystack[i];
  886. if (h === q) {
  887. startHit = true;
  888. hit = true;
  889. break;
  890. }
  891. if (h.indexOf(q) === 0) {
  892. startHit = true;
  893. hit = true;
  894. }
  895. if (h.indexOf(q) !== -1) {
  896. hit = true;
  897. }
  898. }
  899. if (startHit) {
  900. starts.push(lang);
  901. } else if (hit) {
  902. contains.push(lang);
  903. }
  904. });
  905. return starts.concat(contains).slice(0, 30);
  906. }
  907. ​
  908. function render(results) {
  909. currentResults = results;
  910. activeIndex = -1;
  911. panel.innerHTML = '';
  912. if (!results.length) {
  913. var empty = document.createElement('div');
  914. empty.className = 'lang-empty';
  915. empty.textContent = 'No language matches that search.';
  916. panel.appendChild(empty);
  917. return;
  918. }
  919. results.forEach(function(lang, idx) {
  920. var opt = document.createElement('div');
  921. opt.className = 'lang-option';
  922. opt.dataset.index = idx;
  923. ​
  924. var label = document.createElement('span');
  925. label.textContent = lang.name + (lang.native && lang.native !== lang.name ? ' — ' + lang.native : '');
  926. ​
  927. var code = document.createElement('span');
  928. code.className = 'code';
  929. code.textContent = lang.code;
  930. ​
  931. opt.appendChild(label);
  932. opt.appendChild(code);
  933. ​
  934. opt.addEventListener('mousedown', function(e) {
  935. e.preventDefault();
  936. select(lang);
  937. });
  938. ​
  939. panel.appendChild(opt);
  940. });
  941. }
  942. ​
  943. function select(lang) {
  944. hidden.value = lang.code;
  945. search.value = lang.name + ' (' + lang.native + ')';
  946. close();
  947. }
  948. ​
  949. function open() {
  950. picker.classList.add('open');
  951. render(search_matches(search.value === (hidden.value ? search.value : '') ? '' : search.value));
  952. }
  953. ​
  954. function close() {
  955. picker.classList.remove('open');
  956. }
  957. ​
  958. search.addEventListener('focus', function() {
  959. search.select();
  960. picker.classList.add('open');
  961. render(search_matches(''));
  962. });
  963. ​
  964. search.addEventListener('input', function() {
  965. picker.classList.add('open');
  966. render(search_matches(search.value));
  967. });
  968. ​
  969. search.addEventListener('keydown', function(e) {
  970. if (!picker.classList.contains('open')) {
  971. return;
  972. }
  973. var options = panel.querySelectorAll('.lang-option');
  974. if (e.key === 'ArrowDown') {
  975. e.preventDefault();
  976. activeIndex = Math.min(activeIndex + 1, options.length - 1);
  977. updateActive(options);
  978. } else if (e.key === 'ArrowUp') {
  979. e.preventDefault();
  980. activeIndex = Math.max(activeIndex - 1, 0);
  981. updateActive(options);
  982. } else if (e.key === 'Enter') {
  983. e.preventDefault();
  984. if (activeIndex >= 0 && currentResults[activeIndex]) {
  985. select(currentResults[activeIndex]);
  986. } else if (currentResults.length === 1) {
  987. select(currentResults[0]);
  988. }
  989. } else if (e.key === 'Escape') {
  990. close();
  991. }
  992. });
  993. ​
  994. function updateActive(options) {
  995. options.forEach(function(opt, idx) {
  996. opt.classList.toggle('active', idx === activeIndex);
  997. });
  998. if (options[activeIndex]) {
  999. options[activeIndex].scrollIntoView({
  1000. block: 'nearest'
  1001. });
  1002. }
  1003. }
  1004. ​
  1005. document.addEventListener('click', function(e) {
  1006. if (!picker.contains(e.target)) {
  1007. close();
  1008. }
  1009. });
  1010. })();
  1011. </script>
  1012. ​
  1013. <?php elseif ($step === 2): ?>
  1014. <h1>Database connections</h1>
  1015. <p class="sub">Two separate databases on the same MySQL server: one for your store/team data, one for customers and orders.</p>
  1016. <form method="post" autocomplete="off" id="db-form">
  1017. <input type="text" name="fake_username" autocomplete="username" style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0;" tabindex="-1" aria-hidden="true">
  1018. <input type="password" name="fake_password" autocomplete="current-password" style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0;" tabindex="-1" aria-hidden="true">
  1019. <input type="hidden" name="step" value="2">
  1020. <label>MySQL 1 — Site & Team database</label>
  1021. <div class="row">
  1022. <div><input type="text" name="db_site_host" placeholder="Host" value="127.0.0.1" autocomplete="off" required></div>
  1023. <div><input type="text" name="db_site_port" placeholder="Port" value="3306" autocomplete="off" required></div>
  1024. </div>
  1025. <input type="text" name="db_site_name" placeholder="Database name" autocomplete="off" required style="margin-top:10px;">
  1026. <input type="text" name="db_site_user" placeholder="Database user" autocomplete="off" data-lpignore="true" data-1p-ignore required style="margin-top:10px;">
  1027. <input type="password" name="db_site_pass" placeholder="Database password" autocomplete="new-password" data-lpignore="true" data-1p-ignore style="margin-top:10px;">
  1028. ​
  1029. <label>MySQL 2 — Users & Webhooks database</label>
  1030. <div class="row">
  1031. <div><input type="text" name="db_users_host" placeholder="Host" value="127.0.0.1" autocomplete="off" required></div>
  1032. <div><input type="text" name="db_users_port" placeholder="Port" value="3306" autocomplete="off" required></div>
  1033. </div>
  1034. <input type="text" name="db_users_name" placeholder="Database name" autocomplete="off" required style="margin-top:10px;">
  1035. <input type="text" name="db_users_user" placeholder="Database user" autocomplete="off" data-lpignore="true" data-1p-ignore required style="margin-top:10px;">
  1036. <input type="password" name="db_users_pass" placeholder="Database password" autocomplete="new-password" data-lpignore="true" data-1p-ignore style="margin-top:10px;">
  1037. <button type="submit">Test connection & create tables</button>
  1038. </form>
  1039. ​
  1040. <?php elseif ($step === 3): ?>
  1041. <h1>Super Admin account</h1>
  1042. <p class="sub">Exactly one Super Admin account is allowed.</p>
  1043. <form method="post" autocomplete="off">
  1044. <input type="hidden" name="step" value="3">
  1045. <label>Username</label>
  1046. <input type="text" name="admin_username" autocomplete="off" data-lpignore="true" data-1p-ignore required>
  1047. <label>Email</label>
  1048. <input type="text" name="admin_email" autocomplete="off" data-lpignore="true" data-1p-ignore required>
  1049. <label>Display name</label>
  1050. <input type="text" name="admin_display_name" autocomplete="off">
  1051. <label>Password</label>
  1052. <input type="password" name="admin_password" minlength="10" autocomplete="new-password" data-lpignore="true" data-1p-ignore required>
  1053. <label>Confirm password</label>
  1054. <input type="password" name="admin_password_confirm" minlength="10" autocomplete="new-password" data-lpignore="true" data-1p-ignore required>
  1055. <button type="submit">Create account</button>
  1056. </form>
  1057. ​
  1058. <?php elseif ($step === 4): ?>
  1059. <h1>Store details</h1>
  1060. <p class="sub">Can be changed later in Settings.</p>
  1061. <form method="post" autocomplete="off">
  1062. <input type="hidden" name="step" value="4">
  1063. <label>Store name</label>
  1064. <input type="text" name="site_name" value="<?= htmlspecialchars((string) ($_SESSION['install']['app_name'] ?? '')) ?>" autocomplete="off" required>
  1065. <label>Store description</label>
  1066. <input type="text" name="site_description" placeholder="A short tagline for your store" autocomplete="off">
  1067. ​
  1068. <label>What kind of store is this?</label>
  1069. <select name="store_category">
  1070. <option value="general">General store (physical/digital products)</option>
  1071. <option value="saas">SaaS software (subscription plans)</option>
  1072. </select>
  1073. ​
  1074. <label>How do you price your products?</label>
  1075. <select name="pricing_model">
  1076. <option value="per_product">Per-product pricing</option>
  1077. <option value="subscription">Subscription</option>
  1078. </select>
  1079. ​
  1080. <button type="submit">Continue</button>
  1081. </form>
  1082. ​
  1083. <?php elseif ($step === 5): ?>
  1084. <h1>Finish installation</h1>
  1085. <p class="sub">This writes the environment file and locks the installer.</p>
  1086. <form method="post">
  1087. <input type="hidden" name="step" value="5">
  1088. <button type="submit">Install StocketBase</button>
  1089. </form>
  1090. ​
  1091. <?php elseif ($step === 6): ?>
  1092. <div class="success">
  1093. <h1>Installation complete</h1>
  1094. <p class="sub">StocketBase is ready. Delete install.php or rely on the automatic lock file, then log in via team-login.php.</p>
  1095. <a href="team-login.php" style="color:#fff;">Go to team login</a>
  1096. </div>
  1097. <?php endif; ?>
  1098. ​
  1099. </div>
  1100. </body>
  1101. ​
  1102. </html>