Skip to content

Commit

Permalink
patch using shell scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
adragus-inviqa committed May 12, 2015
1 parent ecf6617 commit 43757e0
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 17 deletions.
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ The patching is idempotent as much as the `patch` tool is, meaning patches will
a) Patches need to be declared in the `extra` config area of Composer (root package only):
```json
"extra": {
"magento-root-dir": "public",
"patches": {
"patch-group-1": {
"patch-name-1": {
"type": "patch",
"title": "Allow composer autoloader to be applied to Mage.php",
"url": "https://url/to/file1.patch"
}
Expand All @@ -19,10 +21,26 @@ a) Patches need to be declared in the `extra` config area of Composer (root pack
"title": "Fixes Windows 8.1",
"url": "https://url/to/file2.patch"
}
}
},
"shell-patch-group-1": {
"magento-shell-patch-name-1": {
"type": "shell",
"title": "Magento security fix",
"url": "https://url/to/magento/shell/patch.sh"
}
}
}
}
```

There are two types of patches:
- type **"patch"** - generic patch/diff files, applied using the patch tool;
- type **"shell"** - official Magento shell patches, which are able to apply and/or revert themselves and are self-contained.
If no type is declared, **"patch"** is assumed. If you have such a patch type declared, you **must** set the **"magento-root-dir"**
extra config, pointing to the Mage root folder.

"Shell" patches will be copied in the Mage root (set by the **"magento-root-dir"** extra config), triggered, then removed.

A patch's _group_ and _name_ will create its ID, used internally (i.e. `patch-group-1/patch-name-1`), so make sure you follow these 2 rules:
- `patch-group-1` MUST be unique in the `patches` object literal
- `patch-name-1` MUST be unique in its patch _group_
Expand All @@ -33,12 +51,8 @@ Examples of patch names: "CVS-1", "composer-autoloader".
b) Additional scripts callbacks need to be added for automatic patching on `install` or `update` (root package only):
```json
"scripts": {
"post-install-cmd": [
"Inviqa\\Command::patch"
],
"post-update-cmd": [
"Inviqa\\Command::patch"
]
"post-install-cmd": "Inviqa\\Command::patch",
"post-update-cmd": "Inviqa\\Command::patch"
}
```
You can use whatever [Composer *Command* event](https://getcomposer.org/doc/articles/scripts.md#event-names) you want,
Expand Down
57 changes: 57 additions & 0 deletions src/Inviqa/EnvChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Inviqa;
use Inviqa\Patch\Factory;
use Inviqa\Patch\Shell;

/**
* Checks the validity of the environment before applying the patches.
*/
class EnvChecker
{
private $event;

public function __construct(\Composer\Script\Event $event)
{
$this->event = $event;
}

public function check()
{
$this->clientDeclaredMageRootOnShellPatches();
}

private function clientDeclaredMageRootOnShellPatches()
{
$extra = $this->event->getComposer()->getPackage()->getExtra();

if (empty($extra['patches'])) {
return;
}

$containsShellPatches = false;

foreach ($extra['patches'] as $patchGroupName => $patchGroup) {
foreach ($patchGroup as $patchName => $patchDetails) {
$patch = Factory::create(
$patchName,
$patchGroupName,
$patchDetails,
array()
);

if ($patch instanceof Shell) {
$containsShellPatches = true;
break 2;
}
}
}

if ($containsShellPatches && empty($extra[Patcher::EXTRA_KEY_MAGE_ROOT_DIR])) {
throw new \Exception(
'When using shell patches, you must declare the Mage root using the extra key: ' .
Patcher::EXTRA_KEY_MAGE_ROOT_DIR
);
}
}
}
5 changes: 3 additions & 2 deletions src/Inviqa/Patch/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ class Factory
* @param string $patchName
* @param string $patchGroup
* @param array $patchDetails
* @param array $composerExtra
* @return Patch
*/
public static function create($patchName, $patchGroup, array $patchDetails)
public static function create($patchName, $patchGroup, array $patchDetails, array $composerExtra)
{
if (empty($patchDetails['type'])) {
$patchDetails['type'] = DotPatch::TYPE;
Expand All @@ -19,7 +20,7 @@ public static function create($patchName, $patchGroup, array $patchDetails)
$type = $patchDetails['type'] === DotPatch::TYPE ? 'DotPatch' : 'Shell';
$patchClass = '\\Inviqa\\Patch\\' . $type;

$patch = new $patchClass($patchName, $patchGroup, $patchDetails);
$patch = new $patchClass($patchName, $patchGroup, $patchDetails, $composerExtra);

return $patch;
}
Expand Down
48 changes: 45 additions & 3 deletions src/Inviqa/Patch/Patch.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ abstract class Patch

private $log;

/**
* @var array
*/
private $composerExtra = array();

/**
* @return boolean
*/
Expand All @@ -36,10 +41,11 @@ abstract protected function doApply();
*/
abstract protected function canApply();

public function __construct($name, $group, array $details)
public function __construct($name, $group, array $details, array $composerExtra)
{
$this->setName($name);
$this->setGroup($group);
$this->setComposerExtra($composerExtra);

if (!empty($details['url'])) {
$this->setUrl($details['url']);
Expand All @@ -54,6 +60,7 @@ public final function apply()
{
$namespace = $this->getNamespace();
if ($this->canApply()) {
$this->beforeApply();
$res = (bool) $this->doApply();

if ($res) {
Expand All @@ -62,12 +69,20 @@ public final function apply()
$this->getOutput()->writeln("<comment>Patch $namespace was not applied.</comment>");
}

$this->afterApply($res);

return $res;
}
$this->getOutput()->writeln("<comment>Patch $namespace skipped. Patch was already applied?</comment>");
return null;
}

protected function beforeApply()
{}

protected function afterApply($patchingWasSuccessful)
{}

/**
* @return string
*/
Expand Down Expand Up @@ -173,15 +188,26 @@ protected function getPatchTemporaryPath()
throw new \Exception("Could not get contents from {$this->getUrl()}");
}

$patchFilePath = sprintf("%s/%s_%s.patch", sys_get_temp_dir(), $this->getGroup(), $this->getName());
file_put_contents($patchFilePath, $patch);
$patchFilePath = $this->getPatchTempAbsolutePath();
if (!file_put_contents($patchFilePath, $patch)) {
throw new \Exception("Could not save patch content to $patchFilePath");
}

$this->tempPatchFilePath = $patchFilePath;
}

return $this->tempPatchFilePath;
}

/**
* @return string
*/
private function getPatchTempAbsolutePath()
{
// digest unsafe characters
return sys_get_temp_dir() . '/mage_patch_' . md5($this->getGroup() . $this->getName()) . '.tmp';
}

/**
* @return Output
*/
Expand All @@ -200,4 +226,20 @@ public function setOutput(Output $output)
{
$this->output = $output;
}

/**
* @return Output
*/
public function getComposerExtra()
{
return $this->composerExtra;
}

/**
* @param array $composerExtra
*/
private function setComposerExtra(array $composerExtra)
{
$this->composerExtra = $composerExtra;
}
}
59 changes: 58 additions & 1 deletion src/Inviqa/Patch/Shell.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,74 @@

namespace Inviqa\Patch;

use Inviqa\Patcher;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\ProcessUtils;
use Symfony\Component\Process\Exception\ProcessFailedException;

class Shell extends Patch
{
const TYPE = 'shell';

const SHELL_SCRIPT_TMP_NAME = 'mage_shell_patch.sh';

private $shellScriptTmpPath;

/**
* @throws ProcessFailedException
* @return boolean
*/
protected function doApply()
{
return true;
$patchPath = ProcessUtils::escapeArgument($this->shellScriptTmpPath);
$process = new Process("sh $patchPath");
$process->mustRun();
return $process->getExitCode() === 0;
}

/**
* Official Magento patches check if they can be applied beforehand,
* so no need to check it ourselves.
*
* @return bool
*/
protected function canApply()
{
return true;
}

/**
* Magento "sh" patch-scripts need to be in the Mage root when applying.
*
* @throws \Exception
*/
protected function beforeApply()
{
$tempPath = $this->getPatchTemporaryPath();
$extra = $this->getComposerExtra();

$mageDir = $extra[Patcher::EXTRA_KEY_MAGE_ROOT_DIR];

$destinationFilePath = realpath("./$mageDir") . '/' . self::SHELL_SCRIPT_TMP_NAME;

if (!@rename($tempPath, $destinationFilePath)) {
throw new \Exception("Could not move form $tempPath to $destinationFilePath");
}

if ($this->getOutput()->isDebug()) {
$this->getOutput()->writeln("Shell script moved from $tempPath to $destinationFilePath");
}

$this->shellScriptTmpPath = $destinationFilePath;
}

protected function afterApply($patchWasOk)
{
if (file_exists($this->shellScriptTmpPath)) {
if ($this->getOutput()->isDebug()) {
$this->getOutput()->writeln("Deleting {$this->shellScriptTmpPath}");
}
@unlink($this->shellScriptTmpPath);
}
}
}
40 changes: 36 additions & 4 deletions src/Inviqa/Patcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
use Inviqa\Patch\Factory;
use Inviqa\Patch\Patch;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;

class Patcher
{
const EXTRA_KEY_MAGE_ROOT_DIR = 'magento-root-dir';

/** @var ConsoleOutput */
private $output;

Expand All @@ -16,19 +19,48 @@ class Patcher

public function patch(\Composer\Script\Event $event)
{
$this->output = new ConsoleOutput();
$this->event = $event;
$this->init($event);

$extraTmp = $extra = $this->event->getComposer()->getPackage()->getExtra();

if (empty($extra['patches'])) {
$this->output->writeln('<info>No Magento patches were found</info>');
}

// don't pass the patch information
unset($extraTmp['patches']);

$extra = $this->event->getComposer()->getPackage()->getExtra();
foreach ($extra['patches'] as $patchGroupName => $patchGroup) {
foreach ($patchGroup as $patchName => $patchDetails) {
$patch = Factory::create($patchName, $patchGroupName, $patchDetails);
$patch = Factory::create(
$patchName,
$patchGroupName,
$patchDetails,
$extraTmp
);
$patch->setOutput($this->output);
$this->applyPatch($patch);
}
}
}

/**
* @param \Composer\Script\Event $event
*/
private function init(\Composer\Script\Event $event)
{
$this->output = new ConsoleOutput();

if ($event->getIo()->isDebug()) {
$this->output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
}

$this->event = $event;

$checker = new EnvChecker($event);
$checker->check();
}

private function applyPatch(Patch $patch)
{
try {
Expand Down

0 comments on commit 43757e0

Please sign in to comment.