v1.0.0.0
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/includes/config.php';
- require_once __DIR__ . '/includes/asset.php';
-
- session_start();
-
- $envRoot = dirname(__DIR__) . '/stocketbase-env';
- $lockFile = $envRoot . '/install.lock';
-
- if (is_file($lockFile)) {
- http_response_code(403);
- echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>StocketBase</title></head><body>';
- echo '<p>Installation already completed. Delete install.lock manually to reinstall.</p>';
- echo '</body></html>';
- exit;
- }
-
- $errors = [];
- $step = isset($_POST['step']) ? (int) $_POST['step'] : (isset($_GET['step']) ? (int) $_GET['step'] : 1);
- $step = max(1, min(5, $step));
-
- function pdoConnect(string $host, string $port, string $name, string $user, string $pass): PDO
- {
- $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $name);
- return new PDO($dsn, $user, $pass, [
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
- ]);
- }
-
- function runSqlFile(PDO $pdo, string $path): void
- {
- $sql = (string) file_get_contents($path);
- $statements = array_filter(array_map('trim', explode(';', $sql)));
- foreach ($statements as $statement) {
- if ($statement === '') {
- continue;
- }
- $pdo->exec($statement);
- }
- }
-
- function installerLanguages(): array
- {
- return [
- ['en-US', 'English (US)', 'English (United States)'],
- ['en-GB', 'English (UK)', 'English (United Kingdom)'],
- ['pt-BR', 'Portuguese (Brazil)', 'Português (Brasil)'],
- ['pt-PT', 'Portuguese (Portugal)', 'Português (Portugal)'],
- ['zh-CN', 'Chinese (Simplified)', '中文(简体)'],
- ['zh-TW', 'Chinese (Traditional)', '中文(繁體)'],
- ['es-ES', 'Spanish (Spain)', 'Español (España)'],
- ['es-MX', 'Spanish (Mexico)', 'Español (México)'],
- ['fr-FR', 'French (France)', 'Français (France)'],
- ['fr-CA', 'French (Canada)', 'Français (Canada)'],
- ['fr-BE', 'French (Belgium)', 'Français (Belgique)'],
- ['fr-CH', 'French (Switzerland)', 'Français (Suisse)'],
- ['de-DE', 'German (Germany)', 'Deutsch (Deutschland)'],
- ['de-AT', 'German (Austria)', 'Deutsch (Österreich)'],
- ['de-CH', 'German (Switzerland)', 'Deutsch (Schweiz)'],
- ['nl-NL', 'Dutch (Netherlands)', 'Nederlands (Nederland)'],
- ['nl-BE', 'Dutch (Belgium)', 'Nederlands (België)'],
- ['it-IT', 'Italian (Italy)', 'Italiano (Italia)'],
- ['it-CH', 'Italian (Switzerland)', 'Italiano (Svizzera)'],
- ['ar-SA', 'Arabic (Saudi Arabia)', 'العربية (السعودية)'],
- ['ar-EG', 'Arabic (Egypt)', 'العربية (مصر)'],
- ['ar-AE', 'Arabic (UAE)', 'العربية (الإمارات)'],
- ['ru-RU', 'Russian (Russia)', 'Русский (Россия)'],
- ['sv-SE', 'Swedish (Sweden)', 'Svenska (Sverige)'],
- ['sv-FI', 'Swedish (Finland)', 'Svenska (Finland)'],
- ['en-CA', 'English (Canada)', 'English (Canada)'],
- ['en-AU', 'English (Australia)', 'English (Australia)'],
- ['en-NZ', 'English (New Zealand)', 'English (New Zealand)'],
- ['en-IE', 'English (Ireland)', 'English (Ireland)'],
- ['en-IN', 'English (India)', 'English (India)'],
- ['en-ZA', 'English (South Africa)', 'English (South Africa)'],
- ['es-AR', 'Spanish (Argentina)', 'Español (Argentina)'],
- ['es-CO', 'Spanish (Colombia)', 'Español (Colombia)'],
- ['es-CL', 'Spanish (Chile)', 'Español (Chile)'],
- ['es-US', 'Spanish (United States)', 'Español (Estados Unidos)'],
- ['zh-HK', 'Chinese (Hong Kong)', '中文(香港)'],
- ['zh-SG', 'Chinese (Singapore)', '中文(新加坡)'],
- ['aa', 'Afar', 'Afaraf'],
- ['ab', 'Abkhazian', 'аҧсуа бызшәа'],
- ['ae', 'Avestan', 'avesta'],
- ['af', 'Afrikaans', 'Afrikaans'],
- ['ak', 'Akan', 'Akan'],
- ['am', 'Amharic', 'አማርኛ'],
- ['an', 'Aragonese', 'aragonés'],
- ['ar', 'Arabic', 'العربية'],
- ['as', 'Assamese', 'অসমীয়া'],
- ['av', 'Avaric', 'авар мацӀ'],
- ['ay', 'Aymara', 'aymar aru'],
- ['az', 'Azerbaijani', 'azərbaycan dili'],
- ['ba', 'Bashkir', 'башҡорт теле'],
- ['be', 'Belarusian', 'беларуская мова'],
- ['bg', 'Bulgarian', 'български език'],
- ['bh', 'Bihari', 'भोजपुरी'],
- ['bi', 'Bislama', 'Bislama'],
- ['bm', 'Bambara', 'bamanankan'],
- ['bn', 'Bengali', 'বাংলা'],
- ['bo', 'Tibetan', 'བོད་ཡིག'],
- ['br', 'Breton', 'brezhoneg'],
- ['bs', 'Bosnian', 'bosanski jezik'],
- ['ca', 'Catalan', 'català'],
- ['ce', 'Chechen', 'нохчийн мотт'],
- ['ch', 'Chamorro', 'Chamoru'],
- ['co', 'Corsican', 'corsu'],
- ['cr', 'Cree', 'ᓀᐦᐃᔭᐍᐏᐣ'],
- ['cs', 'Czech', 'čeština'],
- ['cu', 'Church Slavic', 'ѩзыкъ словѣньскъ'],
- ['cv', 'Chuvash', 'чӑваш чӗлхи'],
- ['cy', 'Welsh', 'Cymraeg'],
- ['da', 'Danish', 'dansk'],
- ['de', 'German', 'Deutsch'],
- ['dv', 'Divehi', 'ދިވެހި'],
- ['dz', 'Dzongkha', 'རྫོང་ཁ'],
- ['ee', 'Ewe', 'Eʋegbe'],
- ['el', 'Greek', 'ελληνικά'],
- ['en', 'English', 'English'],
- ['eo', 'Esperanto', 'Esperanto'],
- ['es', 'Spanish', 'Español'],
- ['et', 'Estonian', 'eesti'],
- ['eu', 'Basque', 'euskara'],
- ['fa', 'Persian', 'فارسی'],
- ['ff', 'Fulah', 'Fulfulde'],
- ['fi', 'Finnish', 'suomi'],
- ['fj', 'Fijian', 'vosa Vakaviti'],
- ['fo', 'Faroese', 'føroyskt'],
- ['fr', 'French', 'Français'],
- ['fy', 'Western Frisian', 'Frysk'],
- ['ga', 'Irish', 'Gaeilge'],
- ['gd', 'Scottish Gaelic', 'Gàidhlig'],
- ['gl', 'Galician', 'galego'],
- ['gn', 'Guarani', "Avañe'ẽ"],
- ['gu', 'Gujarati', 'ગુજરાતી'],
- ['gv', 'Manx', 'Gaelg'],
- ['ha', 'Hausa', 'هَوُسَ'],
- ['he', 'Hebrew', 'עברית'],
- ['hi', 'Hindi', 'हिन्दी'],
- ['ho', 'Hiri Motu', 'Hiri Motu'],
- ['hr', 'Croatian', 'hrvatski jezik'],
- ['ht', 'Haitian Creole', 'Kreyòl ayisyen'],
- ['hu', 'Hungarian', 'magyar'],
- ['hy', 'Armenian', 'Հայերեն'],
- ['hz', 'Herero', 'Otjiherero'],
- ['ia', 'Interlingua', 'Interlingua'],
- ['id', 'Indonesian', 'Bahasa Indonesia'],
- ['ie', 'Interlingue', 'Interlingue'],
- ['ig', 'Igbo', 'Asụsụ Igbo'],
- ['ii', 'Sichuan Yi', 'ꆈꌠ꒿ Nuosuhxop'],
- ['ik', 'Inupiaq', 'Iñupiaq'],
- ['io', 'Ido', 'Ido'],
- ['is', 'Icelandic', 'Íslenska'],
- ['it', 'Italian', 'Italiano'],
- ['iu', 'Inuktitut', 'ᐃᓄᒃᑎᑐᑦ'],
- ['ja', 'Japanese', '日本語'],
- ['jv', 'Javanese', 'ꦧꦱꦗꦮ'],
- ['ka', 'Georgian', 'ქართული'],
- ['kg', 'Kongo', 'Kikongo'],
- ['ki', 'Kikuyu', 'Gĩkũyũ'],
- ['kj', 'Kwanyama', 'Kuanyama'],
- ['kk', 'Kazakh', 'қазақ тілі'],
- ['kl', 'Kalaallisut', 'kalaallisut'],
- ['km', 'Khmer', 'ខ្មែរ'],
- ['kn', 'Kannada', 'ಕನ್ನಡ'],
- ['ko', 'Korean', '한국어'],
- ['kr', 'Kanuri', 'Kanuri'],
- ['ks', 'Kashmiri', 'कॉशुर'],
- ['ku', 'Kurdish', 'Kurdî'],
- ['kv', 'Komi', 'коми кыв'],
- ['kw', 'Cornish', 'Kernewek'],
- ['ky', 'Kyrgyz', 'Кыргызча'],
- ['la', 'Latin', 'latine'],
- ['lb', 'Luxembourgish', 'Lëtzebuergesch'],
- ['lg', 'Ganda', 'Luganda'],
- ['li', 'Limburgish', 'Limburgs'],
- ['ln', 'Lingala', 'Lingála'],
- ['lo', 'Lao', 'ພາສາລາວ'],
- ['lt', 'Lithuanian', 'lietuvių kalba'],
- ['lu', 'Luba-Katanga', 'Kiluba'],
- ['lv', 'Latvian', 'latviešu valoda'],
- ['mg', 'Malagasy', 'fiteny malagasy'],
- ['mh', 'Marshallese', 'Kajin M̧ajeļ'],
- ['mi', 'Maori', 'te reo Māori'],
- ['mk', 'Macedonian', 'македонски јазик'],
- ['ml', 'Malayalam', 'മലയാളം'],
- ['mn', 'Mongolian', 'Монгол хэл'],
- ['mr', 'Marathi', 'मराठी'],
- ['ms', 'Malay', 'Bahasa Melayu'],
- ['mt', 'Maltese', 'Malti'],
- ['my', 'Burmese', 'ဗမာစာ'],
- ['na', 'Nauru', 'Dorerin Naoero'],
- ['nb', 'Norwegian Bokmål', 'Norsk Bokmål'],
- ['nd', 'North Ndebele', 'isiNdebele'],
- ['ne', 'Nepali', 'नेपाली'],
- ['ng', 'Ndonga', 'Owambo'],
- ['nl', 'Dutch', 'Nederlands'],
- ['nn', 'Norwegian Nynorsk', 'Norsk Nynorsk'],
- ['no', 'Norwegian', 'Norsk'],
- ['nr', 'South Ndebele', 'isiNdebele'],
- ['nv', 'Navajo', 'Diné bizaad'],
- ['ny', 'Chichewa', 'chiCheŵa'],
- ['oc', 'Occitan', 'occitan'],
- ['oj', 'Ojibwe', 'ᐊᓂᔑᓈᐯᒧᐎᓐ'],
- ['om', 'Oromo', 'Afaan Oromoo'],
- ['or', 'Odia', 'ଓଡ଼ିଆ'],
- ['os', 'Ossetian', 'ирон æвзаг'],
- ['pa', 'Punjabi', 'ਪੰਜਾਬੀ'],
- ['pi', 'Pali', 'पालि'],
- ['pl', 'Polish', 'Polski'],
- ['ps', 'Pashto', 'پښتو'],
- ['pt', 'Portuguese', 'Português'],
- ['qu', 'Quechua', 'Runa Simi'],
- ['rm', 'Romansh', 'rumantsch grischun'],
- ['rn', 'Kirundi', 'Ikirundi'],
- ['ro', 'Romanian', 'Română'],
- ['ru', 'Russian', 'Русский'],
- ['rw', 'Kinyarwanda', 'Ikinyarwanda'],
- ['sa', 'Sanskrit', 'संस्कृतम्'],
- ['sc', 'Sardinian', 'sardu'],
- ['sd', 'Sindhi', 'सिन्धी'],
- ['se', 'Northern Sami', 'Davvisámegiella'],
- ['sg', 'Sango', 'yângâ tî sängö'],
- ['si', 'Sinhala', 'සිංහල'],
- ['sk', 'Slovak', 'slovenčina'],
- ['sl', 'Slovenian', 'slovenščina'],
- ['sm', 'Samoan', "gagana fa'a Samoa"],
- ['sn', 'Shona', 'chiShona'],
- ['so', 'Somali', 'Soomaaliga'],
- ['sq', 'Albanian', 'Shqip'],
- ['sr', 'Serbian', 'српски језик'],
- ['ss', 'Swati', 'SiSwati'],
- ['st', 'Southern Sotho', 'Sesotho'],
- ['su', 'Sundanese', 'Basa Sunda'],
- ['sv', 'Swedish', 'Svenska'],
- ['sw', 'Swahili', 'Kiswahili'],
- ['ta', 'Tamil', 'தமிழ்'],
- ['te', 'Telugu', 'తెలుగు'],
- ['tg', 'Tajik', 'тоҷикӣ'],
- ['th', 'Thai', 'ไทย'],
- ['ti', 'Tigrinya', 'ትግርኛ'],
- ['tk', 'Turkmen', 'Türkmençe'],
- ['tl', 'Tagalog', 'Wikang Tagalog'],
- ['tn', 'Tswana', 'Setswana'],
- ['to', 'Tonga', 'faka Tonga'],
- ['tr', 'Turkish', 'Türkçe'],
- ['ts', 'Tsonga', 'Xitsonga'],
- ['tt', 'Tatar', 'татар теле'],
- ['tw', 'Twi', 'Twi'],
- ['ty', 'Tahitian', 'Reo Tahiti'],
- ['ug', 'Uyghur', 'ئۇيغۇرچە'],
- ['uk', 'Ukrainian', 'Українська'],
- ['ur', 'Urdu', 'اردو'],
- ['uz', 'Uzbek', "Oʻzbek"],
- ['ve', 'Venda', 'Tshivenḓa'],
- ['vi', 'Vietnamese', 'Tiếng Việt'],
- ['vo', 'Volapük', 'Volapük'],
- ['wa', 'Walloon', 'walon'],
- ['wo', 'Wolof', 'Wollof'],
- ['xh', 'Xhosa', 'isiXhosa'],
- ['yi', 'Yiddish', 'ייִדיש'],
- ['yo', 'Yoruba', 'Yorùbá'],
- ['za', 'Zhuang', 'Saɯ cueŋƅ'],
- ['zh', 'Chinese', '中文'],
- ['zu', 'Zulu', 'isiZulu'],
- ];
- }
-
- function installerLanguageAliases(): array
- {
- return [
- 'en' => ['english', 'eng', 'angielski'],
- 'en-US' => ['english us', 'american english', 'angielski amerykanski', 'us', 'usa'],
- 'en-GB' => ['english uk', 'british english', 'angielski brytyjski', 'gb', 'uk'],
- 'pl' => ['polish', 'polski', 'pol'],
- 'de' => ['german', 'niemiecki', 'deutsch', 'ger'],
- 'fr' => ['french', 'francuski', 'francais'],
- 'fr-FR' => ['french france', 'francuski francuski'],
- 'fr-CA' => ['french canada', 'canadian french', 'quebecois', 'francuski kanadyjski'],
- 'fr-BE' => ['french belgium', 'belgian french', 'francuski belgijski'],
- 'fr-CH' => ['french switzerland', 'swiss french', 'francuski szwajcarski'],
- 'de-DE' => ['german germany', 'niemiecki niemcy'],
- 'de-AT' => ['german austria', 'austrian german', 'niemiecki austriacki'],
- 'de-CH' => ['german switzerland', 'swiss german', 'niemiecki szwajcarski'],
- 'nl-NL' => ['dutch netherlands', 'holenderski holandia'],
- 'nl-BE' => ['dutch belgium', 'flemish', 'flamandzki', 'holenderski belgijski'],
- 'it-IT' => ['italian italy', 'wloski wlochy'],
- 'it-CH' => ['italian switzerland', 'swiss italian', 'wloski szwajcarski'],
- 'ar-SA' => ['arabic saudi arabia', 'arabski saudyjski'],
- 'ar-EG' => ['arabic egypt', 'egyptian arabic', 'arabski egipski'],
- 'ar-AE' => ['arabic uae', 'arabski emiraty'],
- 'ru-RU' => ['russian russia', 'rosyjski rosja'],
- 'sv-SE' => ['swedish sweden', 'szwedzki szwecja'],
- 'sv-FI' => ['swedish finland', 'szwedzki finlandia'],
- 'en-CA' => ['english canada', 'canadian english', 'angielski kanadyjski'],
- 'en-AU' => ['english australia', 'australian english', 'angielski australijski', 'aussie'],
- 'en-NZ' => ['english new zealand', 'kiwi english', 'angielski nowozelandzki'],
- 'en-IE' => ['english ireland', 'irish english', 'angielski irlandzki'],
- 'en-IN' => ['english india', 'indian english', 'angielski indyjski'],
- 'en-ZA' => ['english south africa', 'south african english', 'angielski poludniowoafrykanski'],
- 'es-AR' => ['spanish argentina', 'argentinian spanish', 'hiszpanski argentynski'],
- 'es-CO' => ['spanish colombia', 'colombian spanish', 'hiszpanski kolumbijski'],
- 'es-CL' => ['spanish chile', 'chilean spanish', 'hiszpanski chilijski'],
- 'es-US' => ['spanish united states', 'us spanish', 'hiszpanski amerykanski'],
- 'zh-HK' => ['chinese hong kong', 'cantonese hong kong', 'chinski hongkong'],
- 'zh-SG' => ['chinese singapore', 'chinski singapur'],
- 'es' => ['spanish', 'hiszpanski', 'espanol', 'castellano'],
- 'es-ES' => ['spanish spain', 'castellano', 'hiszpanski europejski'],
- 'es-MX' => ['spanish mexico', 'mexican spanish', 'hiszpanski meksykanski'],
- 'it' => ['italian', 'wloski', 'italiano'],
- 'pt' => ['portuguese', 'portugalski', 'portugues'],
- 'pt-BR' => ['portuguese brazil', 'brazilian portuguese', 'brazylijski', 'portugalski brazylijski'],
- 'pt-PT' => ['portuguese portugal', 'european portuguese', 'portugalski europejski'],
- 'ru' => ['russian', 'rosyjski', 'russkiy'],
- 'uk' => ['ukrainian', 'ukrainski'],
- 'zh' => ['chinese', 'chinski', 'mandarin', 'mandarynski', 'zhongwen'],
- 'zh-CN' => ['chinese simplified', 'simplified chinese', 'chinski uproszczony', 'mainland china', 'putonghua'],
- 'zh-TW' => ['chinese traditional', 'traditional chinese', 'chinski tradycyjny', 'taiwan', 'taiwanski'],
- 'ja' => ['japanese', 'japonski', 'nihongo'],
- 'ko' => ['korean', 'koreanski', 'hangugeo'],
- 'ar' => ['arabic', 'arabski'],
- 'he' => ['hebrew', 'hebrajski', 'iw'],
- 'nl' => ['dutch', 'holenderski', 'niderlandzki', 'flemish'],
- 'sv' => ['swedish', 'szwedzki', 'svenska'],
- 'no' => ['norwegian', 'norweski'],
- 'nb' => ['norwegian bokmal', 'bokmal'],
- 'nn' => ['nynorsk'],
- 'da' => ['danish', 'dunski'],
- 'fi' => ['finnish', 'finski', 'suomi'],
- 'cs' => ['czech', 'czeski', 'cestina'],
- 'sk' => ['slovak', 'slowacki'],
- 'hu' => ['hungarian', 'wegierski'],
- 'ro' => ['romanian', 'rumunski'],
- 'bg' => ['bulgarian', 'bulgarski'],
- 'el' => ['greek', 'grecki'],
- 'tr' => ['turkish', 'turecki'],
- 'hi' => ['hindi'],
- 'bn' => ['bengali', 'bengalski'],
- 'vi' => ['vietnamese', 'wietnamski'],
- 'th' => ['thai', 'tajski'],
- 'id' => ['indonesian', 'indonezyjski', 'bahasa'],
- 'ms' => ['malay', 'malajski'],
- 'sr' => ['serbian', 'serbski'],
- 'hr' => ['croatian', 'chorwacki'],
- 'sl' => ['slovenian', 'slowenski'],
- 'lt' => ['lithuanian', 'litewski'],
- 'lv' => ['latvian', 'lotewski'],
- 'et' => ['estonian', 'estonski'],
- 'is' => ['icelandic', 'islandzki'],
- 'ga' => ['irish', 'irlandzki', 'gaelic'],
- 'cy' => ['welsh', 'walijski'],
- 'ca' => ['catalan', 'katalonski'],
- 'eu' => ['basque', 'baskijski'],
- 'fa' => ['persian', 'perski', 'farsi'],
- 'ur' => ['urdu'],
- 'sw' => ['swahili'],
- 'af' => ['afrikaans'],
- 'am' => ['amharic', 'amharski'],
- 'az' => ['azerbaijani', 'azerski'],
- 'ka' => ['georgian', 'gruzinski'],
- 'hy' => ['armenian', 'armenski'],
- 'kk' => ['kazakh', 'kazachski'],
- 'uz' => ['uzbek', 'uzbecki'],
- 'mn' => ['mongolian', 'mongolski'],
- 'ta' => ['tamil', 'tamilski'],
- 'te' => ['telugu'],
- 'ml' => ['malayalam'],
- 'kn' => ['kannada'],
- 'gu' => ['gujarati'],
- 'pa' => ['punjabi'],
- 'mr' => ['marathi'],
- 'ne' => ['nepali', 'nepalski'],
- 'si' => ['sinhala', 'sinhalski'],
- 'km' => ['khmer'],
- 'lo' => ['lao'],
- 'my' => ['burmese', 'birmanski'],
- 'ps' => ['pashto'],
- 'ku' => ['kurdish', 'kurdyjski'],
- 'yi' => ['yiddish', 'jidysz'],
- 'eo' => ['esperanto'],
- 'la' => ['latin', 'lacina'],
- 'sq' => ['albanian', 'albanski'],
- 'mk' => ['macedonian', 'macedonski'],
- 'be' => ['belarusian', 'bialoruski'],
- 'gl' => ['galician', 'galicyjski'],
- 'mt' => ['maltese', 'maltanski'],
- 'tl' => ['tagalog', 'filipino'],
- 'zu' => ['zulu'],
- 'xh' => ['xhosa'],
- 'so' => ['somali', 'somalijski'],
- 'ha' => ['hausa'],
- 'yo' => ['yoruba'],
- 'ig' => ['igbo'],
- ];
- }
-
- function installerLanguageCodes(): array
- {
- return array_column(installerLanguages(), 0);
- }
-
- function installerLanguagesJson(): string
- {
- $aliases = installerLanguageAliases();
- $rows = [];
- foreach (installerLanguages() as [$code, $name, $native]) {
- $extra = $aliases[$code] ?? [];
- $rows[] = [
- 'code' => $code,
- 'name' => $name,
- 'native' => $native,
- 'aliases' => $extra,
- ];
- }
- return json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
- }
-
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if ($step === 1) {
- $locale = trim((string) ($_POST['locale'] ?? 'en'));
- if (!in_array($locale, installerLanguageCodes(), true)) {
- $locale = 'en';
- }
-
- $_SESSION['install']['project_name'] = trim((string) ($_POST['project_name'] ?? 'default'));
- $_SESSION['install']['app_name'] = trim((string) ($_POST['app_name'] ?? 'StocketBase'));
- $_SESSION['install']['app_url'] = trim((string) ($_POST['app_url'] ?? ''));
- $_SESSION['install']['base_path'] = trim((string) ($_POST['base_path'] ?? ''));
- $_SESSION['install']['locale'] = $locale;
- header('Location: install.php?step=2');
- exit;
- }
-
- if ($step === 2) {
- $dbSite = [
- 'host' => trim((string) ($_POST['db_site_host'] ?? '127.0.0.1')),
- 'port' => trim((string) ($_POST['db_site_port'] ?? '3306')),
- 'name' => trim((string) ($_POST['db_site_name'] ?? '')),
- 'user' => trim((string) ($_POST['db_site_user'] ?? '')),
- 'pass' => (string) ($_POST['db_site_pass'] ?? ''),
- ];
-
- $dbUsers = [
- 'host' => trim((string) ($_POST['db_users_host'] ?? '127.0.0.1')),
- 'port' => trim((string) ($_POST['db_users_port'] ?? '3306')),
- 'name' => trim((string) ($_POST['db_users_name'] ?? '')),
- 'user' => trim((string) ($_POST['db_users_user'] ?? '')),
- 'pass' => (string) ($_POST['db_users_pass'] ?? ''),
- ];
-
- try {
- $siteConnection = pdoConnect($dbSite['host'], $dbSite['port'], $dbSite['name'], $dbSite['user'], $dbSite['pass']);
- $usersConnection = pdoConnect($dbUsers['host'], $dbUsers['port'], $dbUsers['name'], $dbUsers['user'], $dbUsers['pass']);
- runSqlFile($siteConnection, __DIR__ . '/sql/schema_site.sql');
- runSqlFile($usersConnection, __DIR__ . '/sql/schema_users.sql');
-
- $_SESSION['install']['db_site'] = $dbSite;
- $_SESSION['install']['db_users'] = $dbUsers;
- header('Location: install.php?step=3');
- exit;
- } catch (Throwable $exception) {
- $errors[] = 'Database error: ' . $exception->getMessage();
- }
- }
-
- if ($step === 3) {
- $username = trim((string) ($_POST['admin_username'] ?? ''));
- $email = trim((string) ($_POST['admin_email'] ?? ''));
- $displayName = trim((string) ($_POST['admin_display_name'] ?? ''));
- $password = (string) ($_POST['admin_password'] ?? '');
- $passwordConfirm = (string) ($_POST['admin_password_confirm'] ?? '');
-
- if ($username === '' || $email === '' || $password === '') {
- $errors[] = 'Username, email and password are required.';
- } elseif ($password !== $passwordConfirm) {
- $errors[] = 'Passwords do not match.';
- } elseif (strlen($password) < 10) {
- $errors[] = 'Password must be at least 10 characters long.';
- }
-
- if (empty($errors)) {
- try {
- $dbSite = $_SESSION['install']['db_site'];
- $siteConnection = pdoConnect($dbSite['host'], $dbSite['port'], $dbSite['name'], $dbSite['user'], $dbSite['pass']);
-
- $countStatement = $siteConnection->query("SELECT COUNT(*) AS total FROM team_accounts WHERE role = 'super_admin'");
- $existingSuperAdmins = (int) $countStatement->fetch()['total'];
-
- if ($existingSuperAdmins > 0) {
- $errors[] = 'A Super Admin account already exists.';
- } else {
- $hash = password_hash($password, PASSWORD_DEFAULT);
- $insert = $siteConnection->prepare(
- 'INSERT INTO team_accounts (username, email, password_hash, display_name, role, status) VALUES (:username, :email, :hash, :display_name, :role, :status)'
- );
- $insert->execute([
- 'username' => $username,
- 'email' => $email,
- 'hash' => $hash,
- 'display_name' => $displayName !== '' ? $displayName : $username,
- 'role' => 'super_admin',
- 'status' => 'active',
- ]);
-
- $_SESSION['install']['admin_created'] = true;
- header('Location: install.php?step=4');
- exit;
- }
- } catch (Throwable $exception) {
- $errors[] = 'Could not create Super Admin: ' . $exception->getMessage();
- }
- }
- }
-
- if ($step === 4) {
- $siteName = trim((string) ($_POST['site_name'] ?? ($_SESSION['install']['app_name'] ?? 'StocketBase')));
- $siteDescription = trim((string) ($_POST['site_description'] ?? ''));
- $storeCategory = in_array($_POST['store_category'] ?? '', ['general', 'saas'], true) ? $_POST['store_category'] : 'general';
- $pricingModel = in_array($_POST['pricing_model'] ?? '', ['per_product', 'subscription'], true) ? $_POST['pricing_model'] : 'per_product';
-
- try {
- $dbSite = $_SESSION['install']['db_site'];
- $siteConnection = pdoConnect($dbSite['host'], $dbSite['port'], $dbSite['name'], $dbSite['user'], $dbSite['pass']);
-
- $defaults = [
- 'site_name' => $siteName,
- 'site_description' => $siteDescription,
- 'store_category' => $storeCategory,
- 'pricing_model' => $pricingModel,
- 'payment_provider' => 'stripe',
- 'clean_product_urls' => '0',
- 'menu_position' => 'top',
- 'featured_banner_enabled' => '0',
- 'featured_banner_mode' => 'static',
- 'featured_banner_hide_duplicates' => '0',
- 'featured_banner_product_1' => '0',
- 'featured_banner_product_2' => '0',
- 'featured_banner_product_3' => '0',
- 'featured_banner_product_4' => '0',
- 'featured_banner_product_5' => '0',
- 'featured_banner_auto_window_value' => '24',
- 'featured_banner_auto_window_unit' => 'hours',
- 'featured_banner_auto_category_mode' => 'mixed',
- 'related_products_enabled' => '1',
- 'sitemap_rss_enabled' => '1',
- 'product_loading_mode' => 'scroll',
- 'reviews_enabled' => '1',
- ];
-
- $insert = $siteConnection->prepare(
- 'INSERT INTO site_settings (setting_key, setting_value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
- );
- foreach ($defaults as $key => $value) {
- $insert->execute(['key' => $key, 'value' => $value]);
- }
-
- $_SESSION['install']['site_name'] = $siteName;
- $_SESSION['install']['site_description'] = $siteDescription;
- header('Location: install.php?step=5');
- exit;
- } catch (Throwable $exception) {
- $errors[] = 'Could not save site settings: ' . $exception->getMessage();
- }
- }
-
- if ($step === 5) {
- $projectName = (string) ($_SESSION['install']['project_name'] ?? 'default');
- $dbSite = $_SESSION['install']['db_site'];
- $dbUsers = $_SESSION['install']['db_users'];
-
- $envValues = [
- 'APP_NAME' => (string) ($_SESSION['install']['app_name'] ?? 'StocketBase'),
- 'APP_URL' => (string) ($_SESSION['install']['app_url'] ?? ''),
- 'APP_BASE_PATH' => (string) ($_SESSION['install']['base_path'] ?? ''),
- 'APP_ENV' => 'production',
- 'APP_KEY' => bin2hex(random_bytes(32)),
- 'APP_LOCALE' => (string) ($_SESSION['install']['locale'] ?? 'en'),
- 'DB_SITE_HOST' => $dbSite['host'],
- 'DB_SITE_PORT' => $dbSite['port'],
- 'DB_SITE_NAME' => $dbSite['name'],
- 'DB_SITE_USER' => $dbSite['user'],
- 'DB_SITE_PASS' => $dbSite['pass'],
- 'DB_USERS_HOST' => $dbUsers['host'],
- 'DB_USERS_PORT' => $dbUsers['port'],
- 'DB_USERS_NAME' => $dbUsers['name'],
- 'DB_USERS_USER' => $dbUsers['user'],
- 'DB_USERS_PASS' => $dbUsers['pass'],
- 'SESSION_COOKIE_NAME' => 'stocketbase_session',
- 'SESSION_SECURE' => '1',
- 'INSTALL_LOCKED' => '1',
- ];
-
- $written = Config::write($projectName, $envValues);
-
- if ($written) {
- if (!is_dir($envRoot)) {
- mkdir($envRoot, 0750, true);
- }
- file_put_contents($lockFile, date('c'));
- chmod($lockFile, 0640);
- unset($_SESSION['install']);
- header('Location: install.php?step=6');
- exit;
- }
-
- $errors[] = 'Could not write environment file. Check folder permissions for ' . htmlspecialchars($envRoot);
- }
- }
-
- if (isset($_GET['step']) && (int) $_GET['step'] === 6) {
- $step = 6;
- }
-
- $selectedLocale = (string) ($_SESSION['install']['locale'] ?? 'en');
- $selectedLocaleLabel = 'English';
- foreach (installerLanguages() as [$code, $name, $native]) {
- if ($code === $selectedLocale) {
- $selectedLocaleLabel = $name . ' (' . $native . ')';
- break;
- }
- }
-
- ?>
- <!DOCTYPE html>
- <html lang="en">
-
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>StocketBase Installer</title>
- <?= Asset::favicon() ?>
- <style>
- :root {
- --bg: #050506;
- --panel: #0c0c0f;
- --border: #232329;
- --text: #f8fafc;
- --muted: #8b8b97;
- --accent: #3862f5;
- }
-
- * {
- box-sizing: border-box;
- }
-
- body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: var(--bg);
- color: var(--text);
- display: flex;
- min-height: 100vh;
- align-items: center;
- justify-content: center;
- padding: 24px;
- }
-
- .card {
- width: 100%;
- max-width: 560px;
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: 14px;
- padding: 32px;
- }
-
- h1 {
- font-size: 20px;
- margin: 0 0 4px;
- }
-
- p.sub {
- color: var(--muted);
- margin: 0 0 24px;
- font-size: 14px;
- }
-
- .steps {
- display: flex;
- gap: 6px;
- margin-bottom: 24px;
- }
-
- .steps span {
- flex: 1;
- height: 4px;
- border-radius: 2px;
- background: var(--border);
- }
-
- .steps span.done {
- background: var(--accent);
- }
-
- label {
- display: block;
- font-size: 13px;
- color: var(--muted);
- margin: 14px 0 6px;
- }
-
- input,
- select {
- width: 100%;
- padding: 10px 12px;
- border-radius: 8px;
- border: 1px solid var(--border);
- background: #08080a;
- color: var(--text);
- font-size: 14px;
- }
-
- button {
- margin-top: 24px;
- width: 100%;
- padding: 12px;
- border-radius: 8px;
- border: none;
- background: var(--accent);
- color: #fff;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- }
-
- .error {
- background: #2a1215;
- border: 1px solid #5c1f26;
- color: #ff9aa2;
- padding: 10px 12px;
- border-radius: 8px;
- font-size: 13px;
- margin-bottom: 14px;
- }
-
- .row {
- display: flex;
- gap: 12px;
- }
-
- .row>div {
- flex: 1;
- }
-
- .success {
- text-align: center;
- }
-
- .success h1 {
- font-size: 22px;
- }
-
- .lang-picker {
- position: relative;
- }
-
- .lang-picker input.lang-search {
- cursor: text;
- }
-
- .lang-picker .lang-panel {
- display: none;
- position: absolute;
- z-index: 20;
- top: calc(100% + 6px);
- left: 0;
- right: 0;
- max-height: 260px;
- overflow-y: auto;
- background: #08080a;
- border: 1px solid var(--border);
- border-radius: 8px;
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
- }
-
- .lang-picker.open .lang-panel {
- display: block;
- }
-
- .lang-option {
- padding: 9px 12px;
- font-size: 14px;
- cursor: pointer;
- display: flex;
- justify-content: space-between;
- gap: 10px;
- }
-
- .lang-option:hover,
- .lang-option.active {
- background: #1a1a1f;
- }
-
- .lang-option .code {
- color: var(--muted);
- font-size: 12px;
- text-transform: uppercase;
- align-self: center;
- }
-
- .lang-empty {
- padding: 10px 12px;
- font-size: 13px;
- color: var(--muted);
- }
- </style>
- </head>
-
- <body>
- <div class="card">
- <div class="steps">
- <span class="<?= $step >= 1 ? 'done' : '' ?>"></span>
- <span class="<?= $step >= 2 ? 'done' : '' ?>"></span>
- <span class="<?= $step >= 3 ? 'done' : '' ?>"></span>
- <span class="<?= $step >= 4 ? 'done' : '' ?>"></span>
- <span class="<?= $step >= 5 ? 'done' : '' ?>"></span>
- </div>
-
- <?php foreach ($errors as $error): ?>
- <div class="error"><?= htmlspecialchars($error) ?></div>
- <?php endforeach; ?>
-
- <?php if ($step === 1): ?>
- <h1>Project setup</h1>
- <p class="sub">Basic identity for this StocketBase instance.</p>
- <form method="post" autocomplete="off">
- <input type="hidden" name="step" value="1">
- <label>Project name (used for env file naming)</label>
- <input type="text" name="project_name" placeholder="mystore" autocomplete="off" required>
- <label>Store name</label>
- <input type="text" name="app_name" placeholder="StocketBase" autocomplete="off" required>
- <label>Store URL</label>
- <input type="text" name="app_url" placeholder="https://example.com" autocomplete="off" required>
- <label>Base path (leave empty if installed at domain root)</label>
- <input type="text" name="base_path" placeholder="/store" autocomplete="off">
- <label>Default language</label>
- <div class="lang-picker" id="lang-picker">
- <input type="text" class="lang-search" id="lang-search" autocomplete="off"
- placeholder="Type a language, native name or code (e.g. PL, Polish, Polski)"
- value="<?= htmlspecialchars($selectedLocaleLabel) ?>">
- <input type="hidden" name="locale" id="lang-code" value="<?= htmlspecialchars($selectedLocale) ?>">
- <div class="lang-panel" id="lang-panel"></div>
- </div>
- <button type="submit">Continue</button>
- </form>
- <script>
- (function() {
- var LANGS = <?= installerLanguagesJson() ?>;
-
- var picker = document.getElementById('lang-picker');
- var search = document.getElementById('lang-search');
- var hidden = document.getElementById('lang-code');
- var panel = document.getElementById('lang-panel');
- var activeIndex = -1;
- var currentResults = [];
-
- function norm(s) {
- return (s || '')
- .toString()
- .toLowerCase()
- .normalize('NFD')
- .replace(/[\u0300-\u036f]/g, '');
- }
-
- LANGS.forEach(function(lang) {
- var parts = [lang.code, lang.name, lang.native].concat(lang.aliases || []);
- lang._haystack = parts.map(norm);
- });
-
- function search_matches(query) {
- var q = norm(query);
- if (!q) {
- return LANGS.slice(0, 20);
- }
- var starts = [];
- var contains = [];
- LANGS.forEach(function(lang) {
- var hit = false;
- var startHit = false;
- for (var i = 0; i < lang._haystack.length; i++) {
- var h = lang._haystack[i];
- if (h === q) {
- startHit = true;
- hit = true;
- break;
- }
- if (h.indexOf(q) === 0) {
- startHit = true;
- hit = true;
- }
- if (h.indexOf(q) !== -1) {
- hit = true;
- }
- }
- if (startHit) {
- starts.push(lang);
- } else if (hit) {
- contains.push(lang);
- }
- });
- return starts.concat(contains).slice(0, 30);
- }
-
- function render(results) {
- currentResults = results;
- activeIndex = -1;
- panel.innerHTML = '';
- if (!results.length) {
- var empty = document.createElement('div');
- empty.className = 'lang-empty';
- empty.textContent = 'No language matches that search.';
- panel.appendChild(empty);
- return;
- }
- results.forEach(function(lang, idx) {
- var opt = document.createElement('div');
- opt.className = 'lang-option';
- opt.dataset.index = idx;
-
- var label = document.createElement('span');
- label.textContent = lang.name + (lang.native && lang.native !== lang.name ? ' — ' + lang.native : '');
-
- var code = document.createElement('span');
- code.className = 'code';
- code.textContent = lang.code;
-
- opt.appendChild(label);
- opt.appendChild(code);
-
- opt.addEventListener('mousedown', function(e) {
- e.preventDefault();
- select(lang);
- });
-
- panel.appendChild(opt);
- });
- }
-
- function select(lang) {
- hidden.value = lang.code;
- search.value = lang.name + ' (' + lang.native + ')';
- close();
- }
-
- function open() {
- picker.classList.add('open');
- render(search_matches(search.value === (hidden.value ? search.value : '') ? '' : search.value));
- }
-
- function close() {
- picker.classList.remove('open');
- }
-
- search.addEventListener('focus', function() {
- search.select();
- picker.classList.add('open');
- render(search_matches(''));
- });
-
- search.addEventListener('input', function() {
- picker.classList.add('open');
- render(search_matches(search.value));
- });
-
- search.addEventListener('keydown', function(e) {
- if (!picker.classList.contains('open')) {
- return;
- }
- var options = panel.querySelectorAll('.lang-option');
- if (e.key === 'ArrowDown') {
- e.preventDefault();
- activeIndex = Math.min(activeIndex + 1, options.length - 1);
- updateActive(options);
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- activeIndex = Math.max(activeIndex - 1, 0);
- updateActive(options);
- } else if (e.key === 'Enter') {
- e.preventDefault();
- if (activeIndex >= 0 && currentResults[activeIndex]) {
- select(currentResults[activeIndex]);
- } else if (currentResults.length === 1) {
- select(currentResults[0]);
- }
- } else if (e.key === 'Escape') {
- close();
- }
- });
-
- function updateActive(options) {
- options.forEach(function(opt, idx) {
- opt.classList.toggle('active', idx === activeIndex);
- });
- if (options[activeIndex]) {
- options[activeIndex].scrollIntoView({
- block: 'nearest'
- });
- }
- }
-
- document.addEventListener('click', function(e) {
- if (!picker.contains(e.target)) {
- close();
- }
- });
- })();
- </script>
-
- <?php elseif ($step === 2): ?>
- <h1>Database connections</h1>
- <p class="sub">Two separate databases on the same MySQL server: one for your store/team data, one for customers and orders.</p>
- <form method="post" autocomplete="off" id="db-form">
- <input type="text" name="fake_username" autocomplete="username" style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0;" tabindex="-1" aria-hidden="true">
- <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">
- <input type="hidden" name="step" value="2">
- <label>MySQL 1 — Site & Team database</label>
- <div class="row">
- <div><input type="text" name="db_site_host" placeholder="Host" value="127.0.0.1" autocomplete="off" required></div>
- <div><input type="text" name="db_site_port" placeholder="Port" value="3306" autocomplete="off" required></div>
- </div>
- <input type="text" name="db_site_name" placeholder="Database name" autocomplete="off" required style="margin-top:10px;">
- <input type="text" name="db_site_user" placeholder="Database user" autocomplete="off" data-lpignore="true" data-1p-ignore required style="margin-top:10px;">
- <input type="password" name="db_site_pass" placeholder="Database password" autocomplete="new-password" data-lpignore="true" data-1p-ignore style="margin-top:10px;">
-
- <label>MySQL 2 — Users & Webhooks database</label>
- <div class="row">
- <div><input type="text" name="db_users_host" placeholder="Host" value="127.0.0.1" autocomplete="off" required></div>
- <div><input type="text" name="db_users_port" placeholder="Port" value="3306" autocomplete="off" required></div>
- </div>
- <input type="text" name="db_users_name" placeholder="Database name" autocomplete="off" required style="margin-top:10px;">
- <input type="text" name="db_users_user" placeholder="Database user" autocomplete="off" data-lpignore="true" data-1p-ignore required style="margin-top:10px;">
- <input type="password" name="db_users_pass" placeholder="Database password" autocomplete="new-password" data-lpignore="true" data-1p-ignore style="margin-top:10px;">
- <button type="submit">Test connection & create tables</button>
- </form>
-
- <?php elseif ($step === 3): ?>
- <h1>Super Admin account</h1>
- <p class="sub">Exactly one Super Admin account is allowed.</p>
- <form method="post" autocomplete="off">
- <input type="hidden" name="step" value="3">
- <label>Username</label>
- <input type="text" name="admin_username" autocomplete="off" data-lpignore="true" data-1p-ignore required>
- <label>Email</label>
- <input type="text" name="admin_email" autocomplete="off" data-lpignore="true" data-1p-ignore required>
- <label>Display name</label>
- <input type="text" name="admin_display_name" autocomplete="off">
- <label>Password</label>
- <input type="password" name="admin_password" minlength="10" autocomplete="new-password" data-lpignore="true" data-1p-ignore required>
- <label>Confirm password</label>
- <input type="password" name="admin_password_confirm" minlength="10" autocomplete="new-password" data-lpignore="true" data-1p-ignore required>
- <button type="submit">Create account</button>
- </form>
-
- <?php elseif ($step === 4): ?>
- <h1>Store details</h1>
- <p class="sub">Can be changed later in Settings.</p>
- <form method="post" autocomplete="off">
- <input type="hidden" name="step" value="4">
- <label>Store name</label>
- <input type="text" name="site_name" value="<?= htmlspecialchars((string) ($_SESSION['install']['app_name'] ?? '')) ?>" autocomplete="off" required>
- <label>Store description</label>
- <input type="text" name="site_description" placeholder="A short tagline for your store" autocomplete="off">
-
- <label>What kind of store is this?</label>
- <select name="store_category">
- <option value="general">General store (physical/digital products)</option>
- <option value="saas">SaaS software (subscription plans)</option>
- </select>
-
- <label>How do you price your products?</label>
- <select name="pricing_model">
- <option value="per_product">Per-product pricing</option>
- <option value="subscription">Subscription</option>
- </select>
-
- <button type="submit">Continue</button>
- </form>
-
- <?php elseif ($step === 5): ?>
- <h1>Finish installation</h1>
- <p class="sub">This writes the environment file and locks the installer.</p>
- <form method="post">
- <input type="hidden" name="step" value="5">
- <button type="submit">Install StocketBase</button>
- </form>
-
- <?php elseif ($step === 6): ?>
- <div class="success">
- <h1>Installation complete</h1>
- <p class="sub">StocketBase is ready. Delete install.php or rely on the automatic lock file, then log in via team-login.php.</p>
- <a href="team-login.php" style="color:#fff;">Go to team login</a>
- </div>
- <?php endif; ?>
-
- </div>
- </body>
-
- </html>