Installer.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Facades\Redis;
  5. use \PDO;
  6. class Installer extends Command
  7. {
  8. /**
  9. * The name and signature of the console command.
  10. *
  11. * @var string
  12. */
  13. protected $signature = 'install {--dangerously-overwrite-env : Re-run installation and overwrite current .env }';
  14. /**
  15. * The console command description.
  16. *
  17. * @var string
  18. */
  19. protected $description = 'CLI Installer';
  20. public $installType = 'Simple';
  21. /**
  22. * Create a new command instance.
  23. *
  24. * @return void
  25. */
  26. public function __construct()
  27. {
  28. parent::__construct();
  29. }
  30. /**
  31. * Execute the console command.
  32. *
  33. * @return mixed
  34. */
  35. public function handle()
  36. {
  37. $this->welcome();
  38. }
  39. protected function welcome()
  40. {
  41. $this->info(' ____ _ ______ __ ');
  42. $this->info(' / __ \(_) _____ / / __/__ ____/ / ');
  43. $this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
  44. $this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
  45. $this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
  46. $this->info(' ');
  47. $this->info(' Welcome to the Pixelfed Installer!');
  48. $this->info(' ');
  49. $this->info(' ');
  50. $this->info('Pixelfed version: ' . config('pixelfed.version'));
  51. $this->line(' ');
  52. $this->installerSteps();
  53. }
  54. protected function installerSteps()
  55. {
  56. $this->envCheck();
  57. $this->envCreate();
  58. $this->installType();
  59. if ($this->installType === 'Advanced') {
  60. $this->info('Installer: Advanced...');
  61. $this->checkPHPRequiredDependencies();
  62. $this->checkFFmpegDependencies();
  63. $this->checkOptimiseDependencies();
  64. $this->checkDiskPermissions();
  65. $this->envProd();
  66. $this->instanceDB();
  67. $this->instanceRedis();
  68. $this->instanceURL();
  69. $this->activityPubSettings();
  70. $this->laravelSettings();
  71. $this->instanceSettings();
  72. $this->mediaSettings();
  73. $this->dbMigrations();
  74. $this->validateEnv();
  75. $this->resetArtisanCache();
  76. } else {
  77. $this->info('Installer: Simple...');
  78. $this->checkDiskPermissions();
  79. $this->envProd();
  80. $this->instanceDB();
  81. $this->instanceRedis();
  82. $this->instanceURL();
  83. $this->activityPubSettings();
  84. $this->instanceSettings();
  85. $this->dbMigrations();
  86. $this->validateEnv();
  87. $this->resetArtisanCache();
  88. }
  89. }
  90. protected function envCheck()
  91. {
  92. if (file_exists(base_path('.env')) &&
  93. filesize(base_path('.env')) !== 0 &&
  94. !$this->option('dangerously-overwrite-env')
  95. ) {
  96. $this->line('');
  97. $this->error('Existing .env File Found - Installation Aborted');
  98. $this->line('Run the following command to re-run the installer: php artisan install --dangerously-overwrite-env');
  99. $this->line('');
  100. exit;
  101. }
  102. }
  103. protected function envCreate()
  104. {
  105. $this->line('');
  106. $this->info('Creating .env if required');
  107. if (!file_exists(app()->environmentFilePath())) {
  108. exec('cp .env.example .env');
  109. }
  110. }
  111. protected function installType()
  112. {
  113. $type = $this->choice('Select installation type', ['Simple', 'Advanced'], 1);
  114. $this->installType = $type;
  115. }
  116. protected function checkPHPRequiredDependencies()
  117. {
  118. $this->line(' ');
  119. $this->info('Checking for Required PHP Extensions...');
  120. $extensions = [
  121. 'bcmath',
  122. 'ctype',
  123. 'curl',
  124. 'json',
  125. 'mbstring',
  126. 'openssl',
  127. 'gd',
  128. 'intl',
  129. 'xml',
  130. 'zip',
  131. 'redis',
  132. ];
  133. foreach ($extensions as $ext) {
  134. if (extension_loaded($ext) == false) {
  135. $this->error("- \"{$ext}\" not found");
  136. } else {
  137. $this->info("- \"{$ext}\" found");
  138. }
  139. }
  140. $continue = $this->choice('Do you wish to continue?', ['yes', 'no'], 0);
  141. $this->continue = $continue;
  142. if ($this->continue === 'no') {
  143. $this->info('Exiting Installer.');
  144. exit;
  145. }
  146. }
  147. protected function checkFFmpegDependencies()
  148. {
  149. $this->line(' ');
  150. $this->info('Checking for Required FFmpeg dependencies...');
  151. $ffmpeg = exec('which ffmpeg');
  152. if (empty($ffmpeg)) {
  153. $this->error("- \"{$ext}\" FFmpeg not found, aborting installation");
  154. exit;
  155. } else {
  156. $this->info('- Found FFmpeg!');
  157. }
  158. }
  159. protected function checkOptimiseDependencies()
  160. {
  161. $this->line(' ');
  162. $this->info('Checking for Optional Media Optimisation dependencies...');
  163. $dependencies = [
  164. 'jpegoptim',
  165. 'optipng',
  166. 'pngquant',
  167. 'gifsicle',
  168. ];
  169. foreach ($dependencies as $dep) {
  170. $which = exec("which $dep");
  171. if (empty($which)) {
  172. $this->error("- \"{$dep}\" not found");
  173. } else {
  174. $this->info("- \"{$dep}\" found");
  175. }
  176. }
  177. }
  178. protected function checkDiskPermissions()
  179. {
  180. $this->line('');
  181. $this->info('Checking for proper filesystem permissions...');
  182. $this->callSilently('storage:link');
  183. $paths = [
  184. base_path('bootstrap'),
  185. base_path('storage'),
  186. ];
  187. foreach ($paths as $path) {
  188. if (is_writeable($path) == false) {
  189. $this->error("- Invalid permission found! Aborting installation.");
  190. $this->error(" Please make the following path writeable by the web server:");
  191. $this->error(" $path");
  192. exit;
  193. } else {
  194. $this->info("- Found valid permissions for {$path}");
  195. }
  196. }
  197. }
  198. protected function envProd()
  199. {
  200. $this->line('');
  201. $this->info('Enabling production');
  202. $this->updateEnvFile('APP_ENV', 'production');
  203. $this->updateEnvFile('APP_DEBUG', 'false');
  204. $this->call('key:generate', ['--force' => true]);
  205. }
  206. protected function instanceDB()
  207. {
  208. $this->line('');
  209. $this->info('Database Settings:');
  210. $database = $this->choice('Select database driver', ['mysql', 'pgsql'], 0);
  211. $database_host = $this->ask('Select database host', '127.0.0.1');
  212. $database_port_default = $database === 'mysql' ? 3306 : 5432;
  213. $database_port = $this->ask('Select database port', $database_port_default);
  214. $database_db = $this->ask('Select database', 'pixelfed');
  215. $database_username = $this->ask('Select database username', 'pixelfed');
  216. $database_password = $this->secret('Select database password');
  217. $this->updateEnvFile('DB_CONNECTION', $database);
  218. $this->updateEnvFile('DB_HOST', $database_host);
  219. $this->updateEnvFile('DB_PORT', $database_port);
  220. $this->updateEnvFile('DB_DATABASE', $database_db);
  221. $this->updateEnvFile('DB_USERNAME', $database_username);
  222. $this->updateEnvFile('DB_PASSWORD', $database_password);
  223. $this->info('Testing Database...');
  224. $dsn = "{$database}:dbname={$database_db};host={$database_host};port={$database_port};";
  225. try {
  226. $dbh = new PDO($dsn, $database_username, $database_password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
  227. } catch (\PDOException $e) {
  228. $this->error('Cannot connect to database, check your details and try again');
  229. exit;
  230. }
  231. $this->info('- Connected to DB Successfully');
  232. }
  233. protected function instanceRedis()
  234. {
  235. $this->line('');
  236. $this->info('Redis Settings:');
  237. $redis_client = $this->choice('Set redis client (PHP extension)', ['phpredis', 'predis'], 0);
  238. $redis_host = $this->ask('Set redis host', 'localhost');
  239. $redis_password = $this->ask('Set redis password', 'null');
  240. $redis_port = $this->ask('Set redis port', 6379);
  241. $this->updateEnvFile('REDIS_CLIENT', $redis_client);
  242. $this->updateEnvFile('REDIS_SCHEME', 'tcp');
  243. $this->updateEnvFile('REDIS_HOST', $redis_host);
  244. $this->updateEnvFile('REDIS_PASSWORD', $redis_password);
  245. $this->updateEnvFile('REDIS_PORT', $redis_port);
  246. $this->info('Testing Redis...');
  247. $redis = Redis::connection();
  248. if ($redis->ping()) {
  249. $this->info('- Connected to Redis Successfully!');
  250. } else {
  251. $this->error('Cannot connect to Redis, check your details and try again');
  252. exit;
  253. }
  254. }
  255. protected function instanceURL()
  256. {
  257. $this->line('');
  258. $this->info('Instance URL Settings:');
  259. $name = $this->ask('Site name [ex: Pixelfed]', 'Pixelfed');
  260. $domain = $this->ask('Site Domain [ex: pixelfed.com]');
  261. $domain = strtolower($domain);
  262. if (empty($domain)) {
  263. $this->error('You must set the site domain');
  264. exit;
  265. }
  266. if (starts_with($domain, 'http')) {
  267. $this->error('The site domain cannot start with https://, you must use the FQDN (eg: example.org)');
  268. exit;
  269. }
  270. if (strpos($domain, '.') == false) {
  271. $this->error('You must enter a valid site domain');
  272. exit;
  273. }
  274. $this->updateEnvFile('APP_NAME', $name);
  275. $this->updateEnvFile('APP_URL', 'https://' . $domain);
  276. $this->updateEnvFile('APP_DOMAIN', $domain);
  277. $this->updateEnvFile('ADMIN_DOMAIN', $domain);
  278. $this->updateEnvFile('SESSION_DOMAIN', $domain);
  279. }
  280. protected function laravelSettings()
  281. {
  282. $this->line('');
  283. $this->info('Laravel Settings (Defaults are recommended):');
  284. $session = $this->choice('Select session driver', ["database", "file", "cookie", "redis", "memcached", "array"], 0);
  285. $cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
  286. $queue = $this->choice('Select queue driver', ["redis", "database", "sync", "beanstalkd", "sqs", "null"], 0);
  287. $broadcast = $this->choice('Select broadcast driver', ["log", "redis", "pusher", "null"], 0);
  288. $log = $this->choice('Select Log Channel', ["stack", "single", "daily", "stderr", "syslog", "null"], 0);
  289. $horizon = $this->ask('Set Horizon Prefix [ex: horizon-]', 'horizon-');
  290. $this->updateEnvFile('SESSION_DRIVER', $session);
  291. $this->updateEnvFile('CACHE_DRIVER', $cache);
  292. $this->updateEnvFile('QUEUE_DRIVER', $queue);
  293. $this->updateEnvFile('BROADCAST_DRIVER', $broadcast);
  294. $this->updateEnvFile('LOG_CHANNEL', $log);
  295. $this->updateEnvFile('HORIZON_PREFIX', $horizon);
  296. }
  297. protected function instanceSettings()
  298. {
  299. $this->line('');
  300. $this->info('Instance Settings:');
  301. $max_registration = $this->ask('Set Maximum users on this instance.', '1000');
  302. $open_registration = $this->choice('Allow new registrations?', ['false', 'true'], 0);
  303. $enforce_email_verification = $this->choice('Enforce email verification?', ['false', 'true'], 0);
  304. $enable_mobile_apis = $this->choice('Enable mobile app/apis support?', ['false', 'true'], 1);
  305. $this->updateEnvFile('PF_MAX_USERS', $max_registration);
  306. $this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
  307. $this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
  308. $this->updateEnvFile('OAUTH_ENABLED', $enable_mobile_apis);
  309. $this->updateEnvFile('EXP_EMC', $enable_mobile_apis);
  310. }
  311. protected function activityPubSettings()
  312. {
  313. $this->line('');
  314. $this->info('Federation Settings:');
  315. $activitypub_federation = $this->choice('Enable ActivityPub federation?', ['false', 'true'], 1);
  316. $this->updateEnvFile('ACTIVITY_PUB', $activitypub_federation);
  317. $this->updateEnvFile('AP_REMOTE_FOLLOW', $activitypub_federation);
  318. $this->updateEnvFile('AP_INBOX', $activitypub_federation);
  319. $this->updateEnvFile('AP_OUTBOX', $activitypub_federation);
  320. $this->updateEnvFile('AP_SHAREDINBOX', $activitypub_federation);
  321. }
  322. protected function mediaSettings()
  323. {
  324. $this->line('');
  325. $this->info('Media Settings:');
  326. $optimize_media = $this->choice('Optimize media uploads? Requires jpegoptim and other dependencies!', ['false', 'true'], 1);
  327. $image_quality = $this->ask('Set image optimization quality between 1-100. Default is 80%, lower values use less disk space at the expense of image quality.', '80');
  328. if ($image_quality < 1) {
  329. $this->error('Min image quality is 1. You should avoid such a low value, 60 at minimum is recommended.');
  330. exit;
  331. }
  332. if ($image_quality > 100) {
  333. $this->error('Max image quality is 100');
  334. exit;
  335. }
  336. $this->info('Note: Max photo size cannot exceed `post_max_size` in php.ini.');
  337. $max_photo_size = $this->ask('Max photo upload size in kilobytes. Default 15000 which is equal to 15MB', '15000');
  338. $max_caption_length = $this->ask('Max caption limit. Default to 500, max 5000.', '500');
  339. if ($max_caption_length > 5000) {
  340. $this->error('Max caption length is 5000 characters.');
  341. exit;
  342. }
  343. $max_album_length = $this->ask('Max photos allowed per album. Choose a value between 1 and 10.', '4');
  344. if ($max_album_length < 1) {
  345. $this->error('Min album length is 1 photos per album.');
  346. exit;
  347. }
  348. if ($max_album_length > 10) {
  349. $this->error('Max album length is 10 photos per album.');
  350. exit;
  351. }
  352. $this->updateEnvFile('PF_OPTIMIZE_IMAGES', $optimize_media);
  353. $this->updateEnvFile('IMAGE_QUALITY', $image_quality);
  354. $this->updateEnvFile('MAX_PHOTO_SIZE', $max_photo_size);
  355. $this->updateEnvFile('MAX_CAPTION_LENGTH', $max_caption_length);
  356. $this->updateEnvFile('MAX_ALBUM_LENGTH', $max_album_length);
  357. }
  358. protected function dbMigrations()
  359. {
  360. $this->line('');
  361. $this->info('Note: We recommend running database migrations now!');
  362. $confirm = $this->choice('Do you want to run the database migrations?', ['Yes', 'No'], 0);
  363. if ($confirm === 'Yes') {
  364. sleep(3);
  365. $this->call('config:cache');
  366. $this->line('');
  367. $this->info('Migrating DB:');
  368. $this->call('migrate', ['--force' => true]);
  369. $this->line('');
  370. $this->info('Importing Cities:');
  371. $this->call('import:cities');
  372. $this->line('');
  373. $this->info('Creating Federation Instance Actor:');
  374. $this->call('instance:actor');
  375. $this->line('');
  376. $this->info('Creating Password Keys for API:');
  377. $this->call('passport:keys', ['--force' => true]);
  378. $confirm = $this->choice('Do you want to create an admin account?', ['Yes', 'No'], 0);
  379. if ($confirm === 'Yes') {
  380. $this->call('user:create');
  381. }
  382. }
  383. }
  384. protected function resetArtisanCache()
  385. {
  386. $this->call('config:cache');
  387. $this->call('route:cache');
  388. $this->call('view:cache');
  389. }
  390. protected function validateEnv()
  391. {
  392. $this->checkEnvKeys('APP_KEY', "key:generate failed?");
  393. $this->checkEnvKeys('APP_ENV', "APP_ENV value should be production");
  394. $this->checkEnvKeys('APP_DEBUG', "APP_DEBUG value should be false");
  395. }
  396. #####
  397. # Installer Functions
  398. #####
  399. protected function checkEnvKeys($key, $error)
  400. {
  401. $envPath = app()->environmentFilePath();
  402. $payload = file_get_contents($envPath);
  403. if ($existing = $this->existingEnv($key, $payload)) {
  404. } else {
  405. $this->error("$key empty - $error");
  406. }
  407. }
  408. protected function updateEnvFile($key, $value)
  409. {
  410. $envPath = app()->environmentFilePath();
  411. $payload = file_get_contents($envPath);
  412. if ($existing = $this->existingEnv($key, $payload)) {
  413. $payload = str_replace("{$key}={$existing}", "{$key}=\"{$value}\"", $payload);
  414. $this->storeEnv($payload);
  415. } else {
  416. $payload = $payload . "\n{$key}=\"{$value}\"\n";
  417. $this->storeEnv($payload);
  418. }
  419. }
  420. protected function existingEnv($needle, $haystack)
  421. {
  422. preg_match("/^{$needle}=[^\r\n]*/m", $haystack, $matches);
  423. if ($matches && count($matches)) {
  424. return substr($matches[0], strlen($needle) + 1);
  425. }
  426. return false;
  427. }
  428. protected function storeEnv($payload)
  429. {
  430. $file = fopen(app()->environmentFilePath(), 'w');
  431. fwrite($file, $payload);
  432. fclose($file);
  433. }
  434. protected function parseSize($size)
  435. {
  436. $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
  437. $size = preg_replace('/[^0-9\.]/', '', $size);
  438. if ($unit) {
  439. return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
  440. } else {
  441. return round($size);
  442. }
  443. }
  444. }