View source
<?php
namespace Drupal\Composer\Plugin\VendorHardening;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Package\BasePackage;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Util\Filesystem;
class VendorHardeningPlugin implements PluginInterface, EventSubscriberInterface {
protected $composer;
protected $io;
protected $config;
protected $packagesAlreadyCleaned = [];
public function activate(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->config = new Config($this->composer
->getPackage());
}
public function deactivate(Composer $composer, IOInterface $io) {
}
public function uninstall(Composer $composer, IOInterface $io) {
}
public static function getSubscribedEvents() {
return [
ScriptEvents::POST_AUTOLOAD_DUMP => 'onPostAutoloadDump',
ScriptEvents::POST_UPDATE_CMD => 'onPostCmd',
ScriptEvents::POST_INSTALL_CMD => 'onPostCmd',
PackageEvents::PRE_PACKAGE_INSTALL => 'onPrePackageInstall',
PackageEvents::PRE_PACKAGE_UPDATE => 'onPrePackageUpdate',
PackageEvents::POST_PACKAGE_INSTALL => 'onPostPackageInstall',
PackageEvents::POST_PACKAGE_UPDATE => 'onPostPackageUpdate',
];
}
public function onPostAutoloadDump(Event $event) {
$this
->writeAccessRestrictionFiles($this->composer
->getConfig()
->get('vendor-dir'));
}
public function onPostCmd(Event $event) {
$this
->cleanAllPackages($this->composer
->getConfig()
->get('vendor-dir'));
}
public function onPrePackageInstall(PackageEvent $event) {
$package = $event
->getOperation()
->getPackage();
$this
->removeBinBeforeCleanup($package);
}
public function onPrePackageUpdate(PackageEvent $event) {
$package = $event
->getOperation()
->getTargetPackage();
$this
->removeBinBeforeCleanup($package);
}
public function onPostPackageInstall(PackageEvent $event) {
$package = $event
->getOperation()
->getPackage();
$package_name = $package
->getName();
$this
->cleanPackage($this->composer
->getConfig()
->get('vendor-dir'), $package_name);
}
public function onPostPackageUpdate(PackageEvent $event) {
$package = $event
->getOperation()
->getTargetPackage();
$package_name = $package
->getName();
$this
->cleanPackage($this->composer
->getConfig()
->get('vendor-dir'), $package_name);
}
protected function removeBinBeforeCleanup(BasePackage $package) {
if (!method_exists($package, 'setBinaries')) {
return;
}
$binaries = $package
->getBinaries();
$clean_paths = $this->config
->getPathsForPackage($package
->getName());
if (!$binaries || !$clean_paths) {
return;
}
if ($unset_these_binaries = $this
->findBinOverlap($binaries, $clean_paths)) {
$this->io
->writeError(sprintf('%sModifying bin config for <info>%s</info> which overlaps with cleanup directories.', str_repeat(' ', 4), $package
->getName()), TRUE, IOInterface::VERBOSE);
$modified_binaries = [];
foreach ($binaries as $binary) {
if (!in_array($binary, $unset_these_binaries)) {
$modified_binaries[] = $binary;
}
}
$package
->setBinaries($modified_binaries);
}
}
protected function findBinOverlap($binaries, $clean_paths) {
$filesystem = [];
foreach ($clean_paths as $clean_path) {
$clean_pieces = explode("/", $clean_path);
$current =& $filesystem;
foreach ($clean_pieces as $clean_piece) {
$current =& $current[$clean_piece];
}
$current = TRUE;
}
$unset_these_binaries = [];
foreach ($binaries as $binary) {
$binary_pieces = explode('/', $binary);
$current =& $filesystem;
foreach ($binary_pieces as $binary_piece) {
if (!isset($current[$binary_piece])) {
break;
}
else {
if ($current[$binary_piece] === TRUE) {
$unset_these_binaries[$binary] = $binary;
break;
}
}
$current =& $filesystem[$binary_piece];
}
}
return $unset_these_binaries;
}
protected function getInstalledPackages() {
return $this->composer
->getRepositoryManager()
->getLocalRepository()
->getPackages();
}
public function cleanAllPackages($vendor_dir) {
$installed_packages = [];
foreach ($this
->getInstalledPackages() as $package) {
$installed_packages[strtolower($package
->getName())] = $package;
}
$cleanup_packages = array_diff_key($this->config
->getAllCleanupPaths(), $this->packagesAlreadyCleaned);
$packages_to_be_cleaned = array_intersect_key($cleanup_packages, $installed_packages);
if (!$packages_to_be_cleaned) {
$this->io
->writeError('<info>Vendor directory already clean.</info>');
return;
}
$this->io
->writeError('<info>Cleaning vendor directory.</info>');
foreach ($packages_to_be_cleaned as $package_name => $paths_for_package) {
$this
->cleanPathsForPackage($vendor_dir, $package_name, $paths_for_package);
}
}
public function cleanPackage($vendor_dir, $package_name) {
$package_name = strtolower($package_name);
if (isset($this->packagesAlreadyCleaned[$package_name])) {
$this->io
->writeError(sprintf('%s<info>%s</info> already cleaned.', str_repeat(' ', 4), $package_name), TRUE, IOInterface::VERY_VERBOSE);
return;
}
$paths_for_package = $this->config
->getPathsForPackage($package_name);
if ($paths_for_package) {
$this->io
->writeError(sprintf('%sCleaning: <info>%s</info>', str_repeat(' ', 4), $package_name));
$this
->cleanPathsForPackage($vendor_dir, $package_name, $paths_for_package);
}
}
protected function cleanPathsForPackage($vendor_dir, $package_name, $paths_for_package) {
$this->packagesAlreadyCleaned[$package_name] = TRUE;
$package_dir = $vendor_dir . '/' . $package_name;
if (!is_dir($package_dir)) {
return;
}
$this->io
->writeError(sprintf('%sCleaning directories in <comment>%s</comment>', str_repeat(' ', 4), $package_name), TRUE, IOInterface::VERY_VERBOSE);
$fs = new Filesystem();
foreach ($paths_for_package as $cleanup_item) {
$cleanup_path = $package_dir . '/' . $cleanup_item;
if (!is_dir($cleanup_path)) {
$this->io
->writeError(sprintf("%s<comment>Directory '%s' does not exist.</comment>", str_repeat(' ', 6), $cleanup_path), TRUE, IOInterface::VERY_VERBOSE);
continue;
}
if (!$fs
->removeDirectory($cleanup_path)) {
$this->io
->writeError(sprintf("%s<error>Failure removing directory '%s'</error> in package <comment>%s</comment>.", str_repeat(' ', 6), $cleanup_item, $package_name), TRUE, IOInterface::NORMAL);
continue;
}
$this->io
->writeError(sprintf("%sRemoving directory <info>'%s'</info>", str_repeat(' ', 4), $cleanup_item), TRUE, IOInterface::VERBOSE);
}
}
public function writeAccessRestrictionFiles($vendor_dir) {
$this->io
->writeError('<info>Hardening vendor directory with .htaccess and web.config files.</info>');
FileSecurity::writeHtaccess($vendor_dir, TRUE);
FileSecurity::writeWebConfig($vendor_dir);
}
}