Installer.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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->checkPHPDependencies();
  62. $this->checkFFmpegDependencies();
  63. $this->checkDiskPermissions();
  64. $this->envProd();
  65. $this->instanceDB();
  66. $this->instanceRedis();
  67. $this->instanceURL();
  68. $this->activityPubSettings();
  69. $this->laravelSettings();
  70. $this->instanceSettings();
  71. $this->mediaSettings();
  72. $this->dbMigrations();
  73. $this->resetArtisanCache();
  74. } else {
  75. $this->info('Installer: Simple...');
  76. $this->checkDiskPermissions();
  77. $this->envProd();
  78. $this->instanceDB();
  79. $this->instanceRedis();
  80. $this->instanceURL();
  81. $this->activityPubSettings();
  82. $this->instanceSettings();
  83. $this->dbMigrations();
  84. $this->resetArtisanCache();
  85. }
  86. }
  87. protected function envCheck()
  88. {
  89. if( file_exists(base_path('.env')) &&
  90. filesize(base_path('.env')) !== 0 &&
  91. !$this->option('dangerously-overwrite-env')
  92. ) {
  93. $this->line('');
  94. $this->error('Existing .env File Found - Installation Aborted');
  95. $this->line('Run the following command to re-run the installer: php artisan install --dangerously-overwrite-env');
  96. $this->line('');
  97. exit;
  98. }
  99. }
  100. protected function envCreate()
  101. {
  102. $this->line('');
  103. if(!file_exists(app()->environmentFilePath())) {
  104. exec('cp .env.example .env');
  105. }
  106. }
  107. protected function installType()
  108. {
  109. $type = $this->choice('Select installation type', ['Simple', 'Advanced'], 1);
  110. $this->installType = $type;
  111. }
  112. protected function checkPHPDependencies()
  113. {
  114. $this->line(' ');
  115. $this->info('Checking for required php extensions...');
  116. $extensions = [
  117. 'bcmath',
  118. 'ctype',
  119. 'curl',
  120. 'json',
  121. 'mbstring',
  122. 'openssl',
  123. ];
  124. foreach($extensions as $ext) {
  125. if(extension_loaded($ext) == false) {
  126. $this->error("\"{$ext}\" PHP extension not found, aborting installation");
  127. exit;
  128. }
  129. }
  130. $this->info("- Required PHP extensions found!");
  131. }
  132. protected function checkFFmpegDependencies()
  133. {
  134. $this->line(' ');
  135. $this->info('Checking for required FFmpeg dependencies...');
  136. $ffmpeg = exec('which ffmpeg');
  137. if(empty($ffmpeg)) {
  138. $this->error("\"{$ext}\" FFmpeg not found, aborting installation");
  139. exit;
  140. } else {
  141. $this->info('- Found FFmpeg!');
  142. }
  143. }
  144. protected function checkDiskPermissions()
  145. {
  146. $this->line('');
  147. $this->info('Checking for proper filesystem permissions...');
  148. $this->callSilently('storage:link');
  149. $paths = [
  150. base_path('bootstrap'),
  151. base_path('storage')
  152. ];
  153. foreach($paths as $path) {
  154. if(is_writeable($path) == false) {
  155. $this->error("- Invalid permission found! Aborting installation.");
  156. $this->error(" Please make the following path writeable by the web server:");
  157. $this->error(" $path");
  158. exit;
  159. } else {
  160. $this->info("- Found valid permissions for {$path}");
  161. }
  162. }
  163. }
  164. protected function envProd()
  165. {
  166. $this->line('');
  167. $this->info('Enabling production');
  168. $this->updateEnvFile('APP_ENV', 'production');
  169. $this->updateEnvFile('APP_DEBUG', 'false');
  170. $this->call('key:generate', ['--force' => true]);
  171. }
  172. protected function instanceDB()
  173. {
  174. $this->line('');
  175. $this->info('Database Settings:');
  176. $database = $this->choice('Select database driver', ['mysql', 'pgsql'], 0);
  177. $database_host = $this->ask('Select database host', '127.0.0.1');
  178. $database_port_default = $database === 'mysql' ? 3306 : 5432;
  179. $database_port = $this->ask('Select database port', $database_port_default);
  180. $database_db = $this->ask('Select database', 'pixelfed');
  181. $database_username = $this->ask('Select database username', 'pixelfed');
  182. $database_password = $this->secret('Select database password');
  183. $this->updateEnvFile('DB_CONNECTION', $database);
  184. $this->updateEnvFile('DB_HOST', $database_host);
  185. $this->updateEnvFile('DB_PORT', $database_port);
  186. $this->updateEnvFile('DB_DATABASE', $database_db);
  187. $this->updateEnvFile('DB_USERNAME', $database_username);
  188. $this->updateEnvFile('DB_PASSWORD', $database_password);
  189. $this->info('Testing Database...');
  190. $dsn = "{$database}:dbname={$database_db};host={$database_host};port={$database_port};";
  191. try {
  192. $dbh = new PDO($dsn, $database_username, $database_password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
  193. } catch (\PDOException $e) {
  194. $this->error('Cannot connect to database, check your details and try again');
  195. exit;
  196. }
  197. $this->info('- Connected to DB Successfully');
  198. }
  199. protected function instanceRedis()
  200. {
  201. $this->line('');
  202. $this->info('Redis Settings:');
  203. $redis_client = $this->choice('Set redis client (PHP extension)', ['phpredis', 'predis'], 0);
  204. $redis_host = $this->ask('Set redis host', 'localhost');
  205. $redis_password = $this->ask('Set redis password', 'null');
  206. $redis_port = $this->ask('Set redis port', 6379);
  207. $this->updateEnvFile('REDIS_SCHEME', 'tcp');
  208. $this->updateEnvFile('REDIS_HOST', $redis_host);
  209. $this->updateEnvFile('REDIS_PASSWORD', $redis_password);
  210. $this->updateEnvFile('REDIS_PORT', $redis_port);
  211. $this->info('Testing Redis...');
  212. $redis = Redis::connection();
  213. if($redis->ping()) {
  214. $this->info('- Connected to Redis Successfully!');
  215. } else {
  216. $this->error('Cannot connect to Redis, check your details and try again');
  217. exit;
  218. }
  219. }
  220. protected function instanceURL()
  221. {
  222. $this->line('');
  223. $this->info('Instance URL Settings:');
  224. $name = $this->ask('Site name [ex: Pixelfed]', 'Pixelfed');
  225. $domain = $this->ask('Site Domain [ex: pixelfed.com]');
  226. $domain = strtolower($domain);
  227. if(empty($domain)) {
  228. $this->error('You must set the site domain');
  229. exit;
  230. }
  231. if(starts_with($domain, 'http')) {
  232. $this->error('The site domain cannot start with https://, you must use the FQDN (eg: example.org)');
  233. exit;
  234. }
  235. if(strpos($domain, '.') == false) {
  236. $this->error('You must enter a valid site domain');
  237. exit;
  238. }
  239. $this->updateEnvFile('APP_NAME', $name);
  240. $this->updateEnvFile('APP_URL', 'https://' . $domain);
  241. $this->updateEnvFile('APP_DOMAIN', $domain);
  242. $this->updateEnvFile('ADMIN_DOMAIN', $domain);
  243. $this->updateEnvFile('SESSION_DOMAIN', $domain);
  244. }
  245. protected function laravelSettings()
  246. {
  247. $this->line('');
  248. $this->info('Laravel Settings (Defaults are recommended):');
  249. $session = $this->choice('Select session driver', ["database", "file", "cookie", "redis", "memcached", "array"], 0);
  250. $cache = $this->choice('Select cache driver', ["redis", "apc", "array", "database", "file", "memcached"], 0);
  251. $queue = $this->choice('Select queue driver', ["redis", "database", "sync", "beanstalkd", "sqs", "null"], 0);
  252. $broadcast = $this->choice('Select broadcast driver', ["log", "redis", "pusher", "null"], 0);
  253. $log = $this->choice('Select Log Channel', ["stack", "single", "daily", "stderr", "syslog", "null"], 0);
  254. $horizon = $this->ask('Set Horizon Prefix [ex: horizon-]', 'horizon-');
  255. $this->updateEnvFile('SESSION_DRIVER', $session);
  256. $this->updateEnvFile('CACHE_DRIVER', $cache);
  257. $this->updateEnvFile('QUEUE_DRIVER', $cache);
  258. $this->updateEnvFile('BROADCAST_DRIVER', $cache);
  259. $this->updateEnvFile('LOG_CHANNEL', $log);
  260. $this->updateEnvFile('HORIZON_PREFIX', $horizon);
  261. }
  262. protected function instanceSettings()
  263. {
  264. $this->line('');
  265. $this->info('Instance Settings:');
  266. $max_registration = $this->ask('Set Maximum users on this instance.', '1000');
  267. $open_registration = $this->choice('Allow new registrations?', ['false', 'true'], 0);
  268. $enforce_email_verification = $this->choice('Enforce email verification?', ['false', 'true'], 0);
  269. $enable_mobile_apis = $this->choice('Enable mobile app/apis support?', ['false', 'true'], 1);
  270. $this->updateEnvFile('PF_MAX_USERS', $max_registration);
  271. $this->updateEnvFile('OPEN_REGISTRATION', $open_registration);
  272. $this->updateEnvFile('ENFORCE_EMAIL_VERIFICATION', $enforce_email_verification);
  273. $this->updateEnvFile('OAUTH_ENABLED', $enable_mobile_apis);
  274. $this->updateEnvFile('EXP_EMC', $enable_mobile_apis);
  275. }
  276. protected function activityPubSettings()
  277. {
  278. $this->line('');
  279. $this->info('Federation Settings:');
  280. $activitypub_federation = $this->choice('Enable ActivityPub federation?', ['false', 'true'], 1);
  281. $this->updateEnvFile('ACTIVITY_PUB', $activitypub_federation);
  282. $this->updateEnvFile('AP_REMOTE_FOLLOW', $activitypub_federation);
  283. $this->updateEnvFile('AP_INBOX', $activitypub_federation);
  284. $this->updateEnvFile('AP_OUTBOX', $activitypub_federation);
  285. $this->updateEnvFile('AP_SHAREDINBOX', $activitypub_federation);
  286. }
  287. protected function mediaSettings()
  288. {
  289. $this->line('');
  290. $this->info('Media Settings:');
  291. $optimize_media = $this->choice('Optimize media uploads? Requires jpegoptim and other dependencies!', ['false', 'true'], 1);
  292. $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');
  293. if($image_quality < 1) {
  294. $this->error('Min image quality is 1. You should avoid such a low value, 60 at minimum is recommended.');
  295. exit;
  296. }
  297. if($image_quality > 100) {
  298. $this->error('Max image quality is 100');
  299. exit;
  300. }
  301. $this->info('Note: Max photo size cannot exceed `post_max_size` in php.ini.');
  302. $max_photo_size = $this->ask('Max photo upload size in kilobytes. Default 15000 which is equal to 15MB', '15000');
  303. $max_caption_length = $this->ask('Max caption limit. Default to 500, max 5000.', '500');
  304. if($max_caption_length > 5000) {
  305. $this->error('Max caption length is 5000 characters.');
  306. exit;
  307. }
  308. $max_album_length = $this->ask('Max photos allowed per album. Choose a value between 1 and 10.', '4');
  309. if($max_album_length < 1) {
  310. $this->error('Min album length is 1 photos per album.');
  311. exit;
  312. }
  313. if($max_album_length > 10) {
  314. $this->error('Max album length is 10 photos per album.');
  315. exit;
  316. }
  317. $this->updateEnvFile('PF_OPTIMIZE_IMAGES', $optimize_media);
  318. $this->updateEnvFile('IMAGE_QUALITY', $image_quality);
  319. $this->updateEnvFile('MAX_PHOTO_SIZE', $max_photo_size);
  320. $this->updateEnvFile('MAX_CAPTION_LENGTH', $max_caption_length);
  321. $this->updateEnvFile('MAX_ALBUM_LENGTH', $max_album_length);
  322. }
  323. protected function dbMigrations()
  324. {
  325. $this->line('');
  326. $this->info('Note: We recommend running database migrations now!');
  327. $confirm = $this->choice('Do you want to run the database migrations?', ['Yes', 'No'], 0);
  328. if($confirm === 'Yes') {
  329. sleep(3);
  330. $this->call('migrate', ['--force' => true]);
  331. $this->callSilently('import:cities');
  332. $this->callSilently('instance:actor');
  333. $this->callSilently('passport:keys, ['--force' => true]);
  334. $confirm = $this->choice('Do you want to create an admin account?', ['Yes', 'No'], 0);
  335. if($confirm === 'Yes') {
  336. $this->call('user:create');
  337. }
  338. }
  339. }
  340. protected function resetArtisanCache()
  341. {
  342. $this->call('config:cache');
  343. $this->call('route:cache');
  344. $this->call('view:cache');
  345. }
  346. #####
  347. # Installer Functions
  348. #####
  349. protected function updateEnvFile($key, $value)
  350. {
  351. $envPath = app()->environmentFilePath();
  352. $payload = file_get_contents($envPath);
  353. if ($existing = $this->existingEnv($key, $payload)) {
  354. $payload = str_replace("{$key}={$existing}", "{$key}=\"{$value}\"", $payload);
  355. $this->storeEnv($payload);
  356. } else {
  357. $payload = $payload . "\n{$key}=\"{$value}\"\n";
  358. $this->storeEnv($payload);
  359. }
  360. }
  361. protected function existingEnv($needle, $haystack)
  362. {
  363. preg_match("/^{$needle}=[^\r\n]*/m", $haystack, $matches);
  364. if ($matches && count($matches)) {
  365. return substr($matches[0], strlen($needle) + 1);
  366. }
  367. return false;
  368. }
  369. protected function storeEnv($payload)
  370. {
  371. $file = fopen(app()->environmentFilePath(), 'w');
  372. fwrite($file, $payload);
  373. fclose($file);
  374. }
  375. protected function parseSize($size) {
  376. $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
  377. $size = preg_replace('/[^0-9\.]/', '', $size);
  378. if ($unit) {
  379. return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
  380. }
  381. else {
  382. return round($size);
  383. }
  384. }
  385. }