Completed
Push — master ( 91f8d9...64265e )
by Mohamed
09:45 queued 06:57
created

Install::getFilesystem()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6
Metric Value
dl 0
loc 8
ccs 0
cts 7
cp 0
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
crap 6
1
<?php
2
3
/*
4
 * This file is part of the Tinyissue package.
5
 *
6
 * (c) Mohamed Alsharaf <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Tinyissue\Console\Commands;
13
14
use Illuminate\Console\Command;
15
use Illuminate\Support\Facades\Artisan;
16
use League\Flysystem\Adapter\Local as Adapter;
17
use League\Flysystem\Filesystem;
18
use Tinyissue\Model;
19
20
/**
21
 * Install is console command to install the Tiny Issue application.
22
 *
23
 * @author Mohamed Alsharaf <[email protected]>
24
 */
25
class Install extends Command
26
{
27
    const COLOR_GOOD  = 'green';
28
    const COLOR_BAD   = 'red';
29
    const COLOR_INFO  = 'blue';
30
    const EMPTY_VALUE = 'empty value';
31
32
    /**
33
     * The console command name.
34
     *
35
     * @var string
36
     */
37
    protected $name = 'tinyissue:install';
38
39
    /**
40
     * The console command description.
41
     *
42
     * @var string
43
     */
44
    protected $description = 'Install Tinyissue.';
45
46
    /**
47
     * Required PHP modules.
48
     *
49
     * @var array
50
     */
51
    protected $modules = [
52
        'pdo'      => 0,
53
        'mcrypt'   => 0,
54
        'openssl'  => 0,
55
        'curl'     => 0,
56
        'json'     => 0,
57
        'mbstring' => 0,
58
        'xml'      => 0,
59
    ];
60
61
    /**
62
     * Minimum PHP version.
63
     *
64
     * @var string
65
     */
66
    protected $phpVersion = '5.5.0';
67
68
    /**
69
     * Supported drivers.
70
     *
71
     * @var array
72
     */
73
    protected $dbDrivers = [
74
        'pdo_sqlite' => 0,
75
        'pdo_mysql'  => 0,
76
        'pdo_pgsql'  => 0,
77
        'pdo_sqlsrv' => 0,
78
    ];
79
80
    /**
81
     * Current user entered data & default values.
82
     *
83
     * @var array
84
     */
85
    protected $data = [
86
        'key'            => '',
87
        'timezone'       => 'Pacific/Auckland',
88
        'dbHost'         => 'localhost',
89
        'dbName'         => 'tinyissue',
90
        'dbUser'         => 'root',
91
        'dbPass'         => self::EMPTY_VALUE,
92
        'dbDriver'       => 'mysql',
93
        'dbPrefix'       => '',
94
        'sysEmail'       => '',
95
        'sysName'        => '',
96
        'adminEmail'     => '',
97
        'adminFirstName' => '',
98
        'adminLastName'  => '',
99
        'adminPass'      => '',
100
    ];
101
102
    /**
103
     * The status of the environment check.
104
     *
105
     * @var bool
106
     */
107
    protected $envStatus = true;
108
109
    /**
110
     * Environment requirements for display status table.
111
     *
112
     * @var array
113
     */
114
    protected $envRequirements = [];
115
116
    /**
117
     * @var Filesystem
118
     */
119
    protected $filesystem;
120
121
    /**
122
     * Execute the console command.
123
     *
124
     * @return bool
125
     */
126
    public function fire()
127
    {
128
        if (!$this->checkEnvironment()) {
129
            return false;
130
        }
131
132
        Artisan::call('down');
133
        $this->loop('stageOne');
134
        $this->loop('stageTwo');
135
        Artisan::call('up');
136
137
        $this->line('<fg=green>Instalation complete.</fg=green>');
138
139
        return true;
140
    }
141
142
    /**
143
     * Check the current environment and display the result in table.
144
     *
145
     * @return bool
146
     */
147
    protected function checkEnvironment()
148
    {
149
        // Check PHP modules
150
        $this->checkPhpExtension($this->modules, '{module} extension', true, 'No');
151
152
        // Check db drivers
153
        $this->checkPhpExtension($this->dbDrivers, '{module} driver for pdo', false, 'Not Found');
154
155
        // Whether or not one or more valid drivers were found
156
        $validDrivers   = $this->getValidDbDrivers();
157
        $dbDriverStatus = !empty($validDrivers);
158
        if (!$dbDriverStatus) {
159
            $dbDriverTitle = 'Install one of the following pdo drivers ('
160
                . implode(', ', array_keys($this->dbDrivers)) . ')';
161
        } else {
162
            $dbDriverTitle = 'You can choose one of the following pdo drivers ('
163
                . implode(', ', $validDrivers) . ')';
164
        }
165
        $this->envRequirementsRow($dbDriverTitle, $dbDriverStatus, true, 'No Found');
166
167
        // Check PHP version
168
        $phpVersionStatus = version_compare(PHP_VERSION, $this->phpVersion, '>=');
169
        $this->envRequirementsRow('PHP version ' . $this->phpVersion . ' or above is needed.', $phpVersionStatus, true);
170
171
        // Check application upload directory
172
        $this->envRequirementsRow('Upload directory is writable.', is_writable(base_path('storage/app/uploads')));
173
174
        // Check if upload directory is accessible to the public
175
        $url = $this->isUploadDirectoryPublic();
176
        if (!empty($url)) {
177
            $this->envRequirementsRow('Upload directory maybe accessible from (' . $url . ').');
178
        }
179
180
        // Display the result table
181
        $this->table(['Requirement', 'Status'], $this->envRequirements);
182
183
        return $this->envStatus;
184
    }
185
186
    /**
187
     * Check the availability of list of php extensions.
188
     *
189
     * @param array  $modules
190
     * @param string $labelFormat
191
     * @param bool   $required
192
     * @param string $failedLabel
193
     *
194
     * @return $this
195
     */
196
    protected function checkPhpExtension(array &$modules, $labelFormat, $required = true, $failedLabel = 'No')
197
    {
198
        foreach ($modules as $module => $status) {
199
            $title            = str_replace(['{module}'], [$module], $labelFormat);
200
            $status           = extension_loaded($module);
201
            $modules[$module] = $status;
202
            $this->envRequirementsRow($title, $status, $required, $failedLabel);
203
        }
204
205
        return $this;
206
    }
207
208
    /**
209
     * Render environment requirement row.
210
     *
211
     * @param string $label
212
     * @param bool   $status
213
     * @param bool   $required
214
     * @param string $failedLabel
215
     *
216
     * @return $this
217
     */
218
    protected function envRequirementsRow($label, $status = false, $required = false, $failedLabel = 'No')
219
    {
220
        $statusColor             = $status ? static::COLOR_GOOD : ($required ? static::COLOR_BAD : static::COLOR_INFO);
221
        $statusTitle             = $status ? 'OK' : $failedLabel;
222
        $this->envRequirements[] = $this->formatTableCells([$label, $statusTitle], $statusColor);
223
        if ($required) {
224
            $this->envStatus = $status;
225
        }
226
227
        return $this;
228
    }
229
230
    /**
231
     * Format cell text color.
232
     *
233
     * @param string[] $cells
234
     * @param string   $color
235
     *
236
     * @return array
237
     */
238
    protected function formatTableCells(array $cells, $color)
239
    {
240
        return array_map(function ($cell) use ($color) {
241
            return '<fg=' . $color . '>' . $cell . '</fg=' . $color . '>';
242
        }, $cells);
243
    }
244
245
    /**
246
     * Returns list of valid db drivers.
247
     *
248
     * @return array
249
     */
250
    protected function getValidDbDrivers()
251
    {
252
        return array_keys(array_filter($this->dbDrivers, function ($item) {
253
            return $item === true;
254
        }));
255
    }
256
257
    /**
258
     * Check if upload directory is accessible to the public.
259
     *
260
     * @return string
261
     */
262
    protected function isUploadDirectoryPublic()
263
    {
264
        $pathSegments = explode('/', base_path());
265
        $count        = count($pathSegments);
266
        $indexes      = [];
267
        for ($i = 0; $i < $count; ++$i) {
268
            $indexes[] = $i;
269
            $path      = implode('/', array_except($pathSegments, $indexes));
270
            $guessUrl  = url($path . '/storage/app/uploads');
271
            $curl      = curl_init($guessUrl);
272
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
273
            curl_setopt($curl, CURLOPT_HEADER, true);
274
            curl_exec($curl);
275
            $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
276
            curl_close($curl);
277
            if ($code != '404') {
278
                return $guessUrl;
279
            }
280
        }
281
282
        return '';
283
    }
284
285
    /**
286
     * Start a stage loop.
287
     *
288
     * @param string $method The method name to execute in a loop
289
     *
290
     * @return void
291
     */
292
    protected function loop($method)
293
    {
294
        while (true) {
295
            try {
296
                $this->$method();
297
                break;
298
            } catch (\Exception $e) {
299
                $this->error($e->getMessage());
300
                $this->comment($e->getTraceAsString());
301
                $this->line('... Start again');
302
                $this->line('');
303
            }
304
        }
305
    }
306
307
    /**
308
     * Stage one:
309
     * - Collect data for the configuration file
310
     * - Create .env file
311
     * - Install the database.
312
     *
313
     * @return void
314
     *
315
     * @throws \Exception
316
     */
317
    protected function stageOne()
318
    {
319
        $this->section('Local configurations:');
320
321
        $validDbDrivers = $this->getValidDbDrivers();
322
        $this->askQuestions([
323
            'dbDriver' => ['choice', ['Select a database driver', $validDbDrivers, 0]],
324
            'dbHost'   => 'Enter the database host',
325
            'dbName'   => 'Enter the database name',
326
            'dbUser'   => 'Enter the database username',
327
            'dbPass'   => 'Enter the database password',
328
            'dbPrefix' => 'Enter the tables prefix',
329
            'sysEmail' => 'Email address used by the Tiny Issue to send emails from',
330
            'sysName'  => 'Email name used by the Tiny Issue for the email address above',
331
            'timezone' => 'The application timezone. Find your timezone from: http://php.net/manual/en/timezones.php)',
332
        ]);
333
        $this->data['key']      = md5(str_random(40));
334
        $this->data['dbDriver'] = substr($this->data['dbDriver'], 4);
335
336
        // Prefix application path for sqlite driver && database name not absolute path
337
        if ($this->data['dbDriver'] === 'sqlite'
338
            && strpos($this->data['dbName'], ':\\') === false
339
            && strpos($this->data['dbName'], '/') === false) {
340
            $this->data['dbName'] = base_path($this->data['dbName']);
341
        }
342
343
        // Create .env from .env.example and populate with user data
344
        $filesystem = $this->getFilesystem();
345
        $content    = $filesystem->read('.env.example');
346
        if (empty($content)) {
347
            throw new \Exception('Unable to read .env.example to create .env file.');
348
        }
349
350
        $dbPass = $this->getInputValue('dbPass');
351
        foreach ($this->data as $key => $value) {
352
            $value   = $key == 'dbPass' ? $dbPass : $value;
353
            $content = str_replace('{' . $key . '}', $value, $content);
354
        }
355
        if ($filesystem->has('.env')) {
356
            $filesystem->delete('.env');
357
        }
358
        $filesystem->put('.env', $content);
359
360
        // Update the current database connection
361
        $config             = \Config::get('database.connections.' . $this->data['dbDriver']);
362
        $config['driver']   = $this->data['dbDriver'];
363
        $config['host']     = $this->data['dbHost'];
364
        $config['database'] = $this->data['dbName'];
365
        $config['username'] = $this->data['dbUser'];
366
        $config['password'] = $dbPass;
367
        $config['prefix']   = $this->data['dbPrefix'];
368
        \Config::set('database.connections.' . $this->data['dbDriver'], $config);
369
        \Config::set('database.default', $this->data['dbDriver']);
370
371
        // Install the new database
372
        $this->section('Setting up the database:');
373
        Artisan::call('migrate:install');
374
        Artisan::call('migrate', ['--force' => true]);
375
        $this->info('Database created successfully.');
376
    }
377
378
    /**
379
     * Prints out a section title.
380
     *
381
     * @param string $title
382
     *
383
     * @return void
384
     */
385
    protected function section($title)
386
    {
387
        $this->line('');
388
        $this->info($title);
389
        $this->line('------------------------');
390
    }
391
392
    /**
393
     * Ask user questions.
394
     *
395
     * @param array $questions
396
     *
397
     * @return $this
398
     */
399
    protected function askQuestions(array $questions)
400
    {
401
        $labelFormat = function ($label, $value) {
402
            return sprintf('%s: (%s)', $label, $value);
403
        };
404
405
        foreach ($questions as $name => $question) {
406
            if (is_array($question)) {
407
                $question[1][0]    = $labelFormat($question[1][0], $this->data[$name]);
408
                $this->data[$name] = call_user_func_array([$this, $question[0]], $question[1]);
409
            } else {
410
                $question          = $labelFormat($question, $this->data[$name]);
411
                $this->data[$name] = $this->ask($question, $this->data[$name]);
412
            }
413
        }
414
415
        return $this;
416
    }
417
418
    /**
419
     * Returns an object for application file system.
420
     *
421
     * @return Filesystem
422
     */
423
    protected function getFilesystem()
424
    {
425
        if (null === $this->filesystem) {
426
            $this->filesystem = new Filesystem(new Adapter(base_path()));
427
        }
428
429
        return $this->filesystem;
430
    }
431
432
    /**
433
     * Stage two:
434
     * - Collect details for admin user
435
     * - Create the admin user.
436
     *
437
     * @return void
438
     */
439
    protected function stageTwo()
440
    {
441
        $this->section('Setting up the admin account:');
442
443
        $this->askQuestions([
444
            'adminEmail'     => 'Email address',
445
            'adminFirstName' => 'First Name',
446
            'adminLastName'  => 'Last Name',
447
            'adminPass'      => 'Password',
448
        ]);
449
450
        Model\User::updateOrCreate(['email' => $this->data['adminEmail']], [
0 ignored issues
show
Bug introduced by
The method updateOrCreate() does not exist on Tinyissue\Model\User. Did you maybe mean create()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
451
            'email'     => $this->data['adminEmail'],
452
            'firstname' => $this->data['adminFirstName'],
453
            'lastname'  => $this->data['adminLastName'],
454
            'password'  => \Hash::make($this->data['adminPass']),
455
            'deleted'   => Model\User::NOT_DELETED_USERS,
456
            'role_id'   => 4,
457
            'language'  => 'en',
458
        ]);
459
460
        $this->info('Admin account created successfully.');
461
    }
462
463
    /**
464
     * Returns the actual value of user input.
465
     *
466
     * @param $name
467
     *
468
     * @return string
469
     */
470
    protected function getInputValue($name)
471
    {
472
        return $this->data[$name] === self::EMPTY_VALUE ? '' : $this->data[$name];
473
    }
474
}
475