Browse Source

Merge pull request #4976 from ADmad/3.0-plugin-assets-symlink

3.0 - Add plugin shell.
José Lorenzo Rodríguez 11 years ago
parent
commit
d97e4b7ac4

+ 201 - 0
src/Shell/PluginAssetsShell.php

@@ -0,0 +1,201 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Shell;
+
+use Cake\Console\Shell;
+use Cake\Core\Plugin;
+use Cake\Filesystem\Folder;
+use Cake\Utility\Inflector;
+
+/**
+ * Shell for symlinking / copying plugin assets to app's webroot.
+ *
+ */
+class PluginAssetsShell extends Shell {
+
+/**
+ * Attempt to symlink plugin assets to app's webroot. If symlinking fails it
+ * fallback to copying the assets. For vendor namespaced plugin, parent folder
+ * for vendor name are created if required.
+ *
+ * @return void
+ */
+	public function symlink() {
+		$this->_process($this->_list());
+	}
+
+/**
+ * Get list of plugins to process. Plugins without a webroot directory are skipped.
+ *
+ * @return array
+ */
+	protected function _list() {
+		$plugins = [];
+		foreach (Plugin::loaded() as $plugin) {
+			$path = Plugin::path($plugin) . 'webroot';
+			if (!is_dir($path)) {
+				$this->out('', 1, Shell::VERBOSE);
+				$this->out(
+					sprintf('Skipping plugin %s. It does not have webroot folder.', $plugin),
+					2,
+					Shell::VERBOSE
+				);
+				continue;
+			}
+
+			$link = Inflector::underscore($plugin);
+			$dir = WWW_ROOT;
+			$namespaced = false;
+			if (strpos($link, '/') !== false) {
+				$namespaced = true;
+				$parts = explode('/', $link);
+				$link = array_pop($parts);
+				$dir = WWW_ROOT . implode(DS, $parts) . DS;
+			}
+
+			$plugins[$plugin] = [
+				'srcPath' => Plugin::path($plugin) . 'webroot',
+				'destDir' => $dir,
+				'link' => $link,
+				'namespaced' => $namespaced
+			];
+		}
+		return $plugins;
+	}
+
+/**
+ * Process plugins
+ *
+ * @param array $plugins List of plugins to process
+ * @return void
+ */
+	protected function _process($plugins) {
+		foreach ($plugins as $plugin => $config) {
+			$path = Plugin::path($plugin) . 'webroot';
+
+			$this->out();
+			$this->out('For plugin: ' . $plugin);
+			$this->hr();
+
+			if ($config['namespaced'] &&
+				!is_dir($config['destDir']) &&
+				!$this->_createDirectory($config['destDir'])
+			) {
+				continue;
+			}
+
+			if (file_exists($config['destDir'] . $config['link'])) {
+				$this->out(
+					$config['destDir'] . $config['link'] . ' already exists',
+					1,
+					Shell::VERBOSE
+				);
+				continue;
+			}
+
+			$result = $this->_createSymlink(
+				$config['srcPath'],
+				$config['destDir'] . $config['link']
+			);
+			if ($result) {
+				continue;
+			}
+
+			$this->_copyDirectory(
+				$config['srcPath'],
+				$config['destDir'] . $config['link']
+			);
+		}
+
+		$this->out();
+		$this->out('Done');
+	}
+
+/**
+ * Create directory
+ *
+ * @param string $dir Directory name
+ * @return bool
+ */
+	protected function _createDirectory($dir) {
+		$old = umask(0);
+		// @codingStandardsIgnoreStart
+		$result = @mkdir($dir, 0755, true);
+		// @codingStandardsIgnoreEnd
+		umask($old);
+
+		if ($result) {
+			$this->out('Created directory ' . $dir);
+			return true;
+		}
+
+		$this->err('Failed creating directory ' . $dir);
+		return false;
+	}
+
+/**
+ * Create symlink
+ *
+ * @param string $target Target directory
+ * @param string $link Link name
+ * @return bool
+ */
+	protected function _createSymlink($target, $link) {
+		// @codingStandardsIgnoreStart
+		$result = @symlink($target, $link);
+		// @codingStandardsIgnoreEnd
+
+		if ($result) {
+			$this->out('Created symlink ' . $link);
+			return true;
+		}
+
+		return false;
+	}
+
+/**
+ * Copy directory
+ *
+ * @param string $source Source directory
+ * @param string $destination Destination directory
+ * @return bool
+ */
+	protected function _copyDirectory($source, $destination) {
+		$folder = new Folder($source);
+		if ($folder->copy(['to' => $destination])) {
+			$this->out('Copied assets to directory ' . $destination);
+			return true;
+		}
+
+		$this->err('Error copying assets to directory ' . $destination);
+		return false;
+	}
+
+/**
+ * Gets the option parser instance and configures it.
+ *
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+	public function getOptionParser() {
+		$parser = parent::getOptionParser();
+
+		$parser->addSubcommand('symlink', [
+			'help' => 'Symlink / copy assets to app\'s webroot'
+		]);
+
+		return $parser;
+	}
+
+}

+ 1 - 1
tests/TestCase/Shell/CommandListShellTest.php

@@ -94,7 +94,7 @@ class CommandListShellTest extends TestCase {
 		$expected = "/\[.*TestPluginTwo.*\] example, welcome/";
 		$this->assertRegExp($expected, $output);
 
-		$expected = "/\[.*CORE.*\] i18n, orm_cache, server, test/";
+		$expected = "/\[.*CORE.*\] i18n, orm_cache, plugin_assets, server, test/";
 		$this->assertRegExp($expected, $output);
 
 		$expected = "/\[.*app.*\] sample/";

+ 1 - 1
tests/TestCase/Shell/CompletionShellTest.php

@@ -113,7 +113,7 @@ class CompletionShellTest extends TestCase {
 		$output = $this->out->output;
 
 		$expected = "TestPlugin.example TestPlugin.sample " .
-			"TestPluginTwo.example TestPluginTwo.welcome i18n orm_cache server test sample\n";
+			"TestPluginTwo.example TestPluginTwo.welcome i18n orm_cache plugin_assets server test sample\n";
 		$this->assertTextEquals($expected, $output);
 	}
 

+ 153 - 0
tests/TestCase/Shell/PluginAssetsShellTest.php

@@ -0,0 +1,153 @@
+<?php
+/**
+ * CakePHP :  Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Shell;
+
+use Cake\Core\App;
+use Cake\Core\Configure;
+use Cake\Core\Plugin;
+use Cake\Filesystem\Folder;
+use Cake\Shell\PluginAssetsTask;
+use Cake\TestSuite\TestCase;
+
+/**
+ * PluginAssetsShellTest class
+ *
+ */
+class PluginAssetsShellTest extends TestCase {
+
+/**
+ * setUp method
+ *
+ * @return void
+ */
+	public function setUp() {
+		parent::setUp();
+
+		$this->skipIf(
+			DIRECTORY_SEPARATOR === '\\',
+			'Skip PluginAssetsShell tests on windows to prevent side effects for UrlHelper tests on AppVeyor.'
+		);
+
+		$this->io = $this->getMock('Cake\Console\ConsoleIo', [], [], '', false);
+
+		$this->shell = $this->getMock(
+			'Cake\Shell\PluginAssetsShell',
+			array('in', 'out', 'err', '_stop'),
+			array($this->io)
+		);
+	}
+
+/**
+ * tearDown method
+ *
+ * @return void
+ */
+	public function tearDown() {
+		parent::tearDown();
+		unset($this->shell);
+		Plugin::unload();
+	}
+
+/**
+ * testSymlink method
+ *
+ * @return void
+ */
+	public function testSymlink() {
+		Plugin::load('TestPlugin');
+		Plugin::load('Company/TestPluginThree');
+
+		$this->shell->symlink();
+
+		$path = WWW_ROOT . 'test_plugin';
+		$link = new \SplFileInfo($path);
+		$this->assertTrue(file_exists($path . DS . 'root.js'));
+		if (DIRECTORY_SEPARATOR === '\\') {
+			$this->assertTrue($link->isDir());
+			$folder = new Folder($path);
+			$folder->delete();
+		} else {
+			$this->assertTrue($link->isLink());
+			unlink($path);
+		}
+
+		$path = WWW_ROOT . 'company' . DS . 'test_plugin_three';
+		$link = new \SplFileInfo($path);
+		// If "company" directory exists beforehand "test_plugin_three" would
+		// be a link. But if the directory is created by the shell itself
+		// symlinking fails and the assets folder is copied as fallback.
+		$this->assertTrue($link->isDir());
+		$this->assertTrue(file_exists($path . DS . 'css' . DS . 'company.css'));
+		$folder = new Folder(WWW_ROOT . 'company');
+		$folder->delete();
+	}
+
+/**
+ * testSymlinkWhenVendorDirectoryExits
+ *
+ * @return void
+ */
+	public function testSymlinkWhenVendorDirectoryExits() {
+		Plugin::load('Company/TestPluginThree');
+
+		mkdir(WWW_ROOT . 'company');
+
+		$this->shell->symlink();
+		$path = WWW_ROOT . 'company' . DS . 'test_plugin_three';
+		$link = new \SplFileInfo($path);
+		if (DIRECTORY_SEPARATOR === '\\') {
+			$this->assertTrue($link->isDir());
+		} else {
+			$this->assertTrue($link->isLink());
+		}
+		$this->assertTrue(file_exists($path . DS . 'css' . DS . 'company.css'));
+		$folder = new Folder(WWW_ROOT . 'company');
+		$folder->delete();
+	}
+
+/**
+ * testSymlinkWhenTargetAlreadyExits
+ *
+ * @return void
+ */
+	public function testSymlinkWhenTargetAlreadyExits() {
+		Plugin::load('TestTheme');
+
+		$shell = $this->getMock(
+			'Cake\Shell\PluginAssetsShell',
+			array('in', 'out', 'err', '_stop', '_createSymlink', '_copyDirectory'),
+			array($this->io)
+		);
+
+		$this->assertTrue(is_dir(WWW_ROOT . 'test_theme'));
+
+		$shell->expects($this->never())->method('_createSymlink');
+		$shell->expects($this->never())->method('_copyDirectory');
+		$shell->symlink();
+	}
+
+/**
+ * test that plugins without webroot are not processed
+ *
+ * @return void
+ */
+	public function testForPluginWithoutWebroot() {
+		Plugin::load('TestPluginTwo');
+
+		$this->shell->symlink();
+		$this->assertFalse(file_exists(WWW_ROOT . 'test_plugin_two'));
+	}
+
+}