Completed
Push — master ( 875b1a...deeeda )
by Christian
03:55
created

SelfUpdateCommand::isOutdatedVersion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 2
1
<?php
2
3
namespace N98\Magento\Command;
4
5
use Composer\Downloader\FilesystemException;
6
use Composer\IO\ConsoleIO;
7
use Composer\Util\RemoteFilesystem;
8
use N98\Util\Markdown\VersionFilePrinter;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Output\OutputInterface;
12
13
/**
14
 * @codeCoverageIgnore
15
 * @author Igor Wiedler <[email protected]>
16
 * @author Christian Münch <[email protected]>
17
 */
18
class SelfUpdateCommand extends AbstractMagentoCommand
19
{
20
    const VERSION_TXT_URL_UNSTABLE = 'https://raw.githubusercontent.com/netz98/n98-magerun2/develop/version.txt';
21
    const MAGERUN_DOWNLOAD_URL_UNSTABLE = 'https://files.magerun.net/n98-magerun2-dev.phar';
22
    const VERSION_TXT_URL_STABLE = 'https://raw.githubusercontent.com/netz98/n98-magerun2/master/version.txt';
23
    const MAGERUN_DOWNLOAD_URL_STABLE = 'https://files.magerun.net/n98-magerun2.phar';
24
    const CHANGELOG_DOWNLOAD_URL_UNSTABLE = 'https://raw.github.com/netz98/n98-magerun2/develop/CHANGELOG.md';
25
    const CHANGELOG_DOWNLOAD_URL_STABLE = 'https://raw.github.com/netz98/n98-magerun2/master/CHANGELOG.md';
26
27
    protected function configure()
28
    {
29
        $this
30
            ->setName('self-update')
31
            ->setAliases(array('selfupdate'))
32
            ->addOption('unstable', null, InputOption::VALUE_NONE, 'Load unstable version from develop branch')
33
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Tests if there is a new version without any update.')
34
            ->setDescription('Updates n98-magerun2.phar to the latest version.')
35
            ->setHelp(
36
<<<EOT
37
The <info>self-update</info> command checks github for newer
38
versions of n98-magerun2 and if found, installs the latest.
39
40
<info>php n98-magerun2.phar self-update</info>
41
42
EOT
43
            )
44
        ;
45
    }
46
47
    /**
48
     * @return bool
49
     */
50
    public function isEnabled()
51
    {
52
        return $this->getApplication()->isPharMode();
53
    }
54
55
    /**
56
     * @param \Symfony\Component\Console\Input\InputInterface $input
57
     * @param \Symfony\Component\Console\Output\OutputInterface $output
58
     * @return int|null|void
59
     * @throws \Composer\Downloader\FilesystemException
60
     * @throws \Exception
61
     */
62
    protected function execute(InputInterface $input, OutputInterface $output)
0 ignored issues
show
Coding Style introduced by
execute uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
63
    {
64
        $isDryRun = $input->getOption('dry-run');
65
        $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0];
66
        $tempFilename = dirname($localFilename) . '/' . basename($localFilename, '.phar') . '-temp.phar';
67
68
        // check for permissions in local filesystem before start connection process
69
        if (!is_writable($tempDirectory = dirname($tempFilename))) {
70
            throw new FilesystemException(
71
                'n98-magerun2 update failed: the "' . $tempDirectory .
72
                '" directory used to download the temp file could not be written'
73
            );
74
        }
75
76
        if (!is_writable($localFilename)) {
77
            throw new FilesystemException(
78
                'n98-magerun2 update failed: the "' . $localFilename . '" file could not be written'
79
            );
80
        }
81
82
        $io = new ConsoleIO($input, $output, $this->getHelperSet());
0 ignored issues
show
Bug introduced by
It seems like $this->getHelperSet() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
83
        $rfs = new RemoteFilesystem($io);
84
85
        $loadUnstable = $input->getOption('unstable');
86
        if ($loadUnstable) {
87
            $versionTxtUrl = self::VERSION_TXT_URL_UNSTABLE;
88
            $remoteFilename = self::MAGERUN_DOWNLOAD_URL_UNSTABLE;
89
        } else {
90
            $versionTxtUrl = self::VERSION_TXT_URL_STABLE;
91
            $remoteFilename = self::MAGERUN_DOWNLOAD_URL_STABLE;
92
        }
93
94
        $latest = trim($rfs->getContents('raw.githubusercontent.com', $versionTxtUrl, false));
95
96
        if ($this->isOutdatedVersion($latest, $loadUnstable)) {
97
            $output->writeln(sprintf("Updating to version <info>%s</info>.", $latest));
98
99
            try {
100
                $this->downloadNewVersion($output, $rfs, $remoteFilename, $tempFilename);
101
                $this->checkNewPharFile($tempFilename, $localFilename);
102
103
                if (!$isDryRun) {
104
                    $this->replaceExistingPharFile($tempFilename, $localFilename);
105
                }
106
107
                $output->writeln('<info>Successfully updated n98-magerun2</info>');
108
                $this->showChangelog($output, $loadUnstable, $rfs);
109
110
                $this->_exit(0);
111
            } catch (\Exception $e) {
112
                @unlink($tempFilename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
113
                if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) {
114
                    throw $e;
115
                }
116
                $output->writeln('<error>The download is corrupted (' . $e->getMessage() . ').</error>');
117
                $output->writeln('<error>Please re-run the self-update command to try again.</error>');
118
            }
119
        } else {
120
            $output->writeln("<info>You are using the latest n98-magerun2 version.</info>");
121
        }
122
    }
123
124
    /**
125
     * Stop execution
126
     *
127
     * This is a workaround to prevent warning of dispatcher after replacing
128
     * the phar file.
129
     *
130
     * @param int $statusCode
131
     * @return void
132
     */
133
    protected function _exit($statusCode = 0)
134
    {
135
        exit($statusCode);
0 ignored issues
show
Coding Style Compatibility introduced by
The method _exit() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
136
    }
137
138
    /**
139
     * @param \Symfony\Component\Console\Output\OutputInterface $output
140
     * @param $rfs
141
     * @param $remoteFilename
142
     * @param $tempFilename
143
     */
144
    private function downloadNewVersion(OutputInterface $output, $rfs, $remoteFilename, $tempFilename)
145
    {
146
        $rfs->copy('raw.github.com', $remoteFilename, $tempFilename);
147
148
        if (!file_exists($tempFilename)) {
149
            $output->writeln('<error>The download of the new n98-magerun2 version failed for an unexpected reason');
150
        }
151
    }
152
153
    /**
154
     * @param string $tempFilename
155
     * @param string $localFilename
156
     */
157
    private function checkNewPharFile($tempFilename, $localFilename)
0 ignored issues
show
Unused Code introduced by
The parameter $localFilename is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
158
    {
159
        \error_reporting(E_ALL); // supress notices
160
161
        @chmod($tempFilename, 0777 & ~umask());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
162
        // test the phar validity
163
        $phar = new \Phar($tempFilename);
164
        // free the variable to unlock the file
165
        unset($phar);
166
    }
167
168
    /**
169
     * @param string $tempFilename
170
     * @param string $localFilename
171
     */
172
    private function replaceExistingPharFile($tempFilename, $localFilename)
173
    {
174
        if (!@rename($tempFilename, $localFilename)) {
175
            throw new \RuntimeException(
176
                sprintf('Cannot replace existing phar file "%s". Please check permissions.', $localFilename)
177
            );
178
        }
179
    }
180
181
    /**
182
     * @param \Symfony\Component\Console\Output\OutputInterface $output
183
     * @param bool $loadUnstable
184
     * @param RemoteFilesystem $rfs
185
     */
186
    private function showChangelog(OutputInterface $output, $loadUnstable, $rfs)
187
    {
188
        if ($loadUnstable) {
189
            $changeLogContent = $rfs->getContents(
190
                'raw.github.com',
191
                self::CHANGELOG_DOWNLOAD_URL_UNSTABLE,
192
                false
193
            );
194
        } else {
195
            $changeLogContent = $rfs->getContents(
196
                'raw.github.com',
197
                self::CHANGELOG_DOWNLOAD_URL_STABLE,
198
                false
199
            );
200
        }
201
202
        if ($changeLogContent) {
203
            $versionFilePrinter = new VersionFilePrinter($changeLogContent);
0 ignored issues
show
Bug introduced by
It seems like $changeLogContent can also be of type boolean; however, N98\Util\Markdown\Versio...ePrinter::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
204
            $previousVersion = $this->getApplication()->getVersion();
205
            $output->writeln(
206
                $versionFilePrinter->printFromVersion($previousVersion)
207
            );
208
        }
209
210
        if ($loadUnstable) {
211
            $unstableFooterMessage = <<<UNSTABLE_FOOTER
212
<comment>
213
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
214
!! DEVELOPMENT VERSION. DO NOT USE IN PRODUCTION !!
215
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
216
</comment>
217
UNSTABLE_FOOTER;
218
            $output->writeln($unstableFooterMessage);
219
        }
220
    }
221
222
    /**
223
     * @param $latest
224
     * @param $loadUnstable
225
     * @return bool
226
     */
227
    private function isOutdatedVersion($latest, $loadUnstable)
228
    {
229
        return $this->getApplication()->getVersion() !== $latest || $loadUnstable;
230
    }
231
}
232