From 2c41fd351e2c151b2429f027a40eba53236c42ba Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Tue, 4 Feb 2020 20:50:56 +0000 Subject: [PATCH 1/2] Support whitelisting vendor classes --- composer.json | 10 ++- config/tinker.php | 15 ++++ src/ClassAliasAutoloader.php | 67 +++++++++++++--- tests/ClassAliasAutoloaderTest.php | 79 +++++++++++++++++++ tests/fixtures/app/Baz/Qux.php | 8 ++ tests/fixtures/app/Foo/Bar.php | 8 ++ .../vendor/composer/autoload_classmap.php | 9 +++ tests/fixtures/vendor/one/two/Three.php | 8 ++ tests/helpers.php | 13 +++ 9 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 tests/ClassAliasAutoloaderTest.php create mode 100644 tests/fixtures/app/Baz/Qux.php create mode 100644 tests/fixtures/app/Foo/Bar.php create mode 100644 tests/fixtures/vendor/composer/autoload_classmap.php create mode 100644 tests/fixtures/vendor/one/two/Three.php create mode 100644 tests/helpers.php diff --git a/composer.json b/composer.json index 28a6491..b29abfb 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "symfony/var-dumper": "^4.0|^5.0" }, "require-dev": { + "mockery/mockery": "^1.3", "phpunit/phpunit": "^8.0" }, "suggest": { @@ -30,8 +31,13 @@ }, "autoload-dev": { "psr-4": { - "Laravel\\Tinker\\Tests\\": "tests/" - } + "Laravel\\Tinker\\Tests\\": "tests/", + "App\\": "tests/fixtures/app", + "One\\Two\\": "tests/fixtures/vendor/one/two" + }, + "files": [ + "tests/helpers.php" + ] }, "extra": { "branch-alias": { diff --git a/config/tinker.php b/config/tinker.php index 0d2bf00..22359c8 100644 --- a/config/tinker.php +++ b/config/tinker.php @@ -32,4 +32,19 @@ 'App\Nova', ], + /* + |-------------------------------------------------------------------------- + | Alias Whitelist + |-------------------------------------------------------------------------- + | + | Tinker will not automatically alias classes in your vendor namespaces. + | However, you may wish to allow this for certain classes, which you + | may accomplish by listing the classes in the following array. + | + */ + + 'alias' => [ + // + ], + ]; diff --git a/src/ClassAliasAutoloader.php b/src/ClassAliasAutoloader.php index 7035e2c..2bb42d7 100644 --- a/src/ClassAliasAutoloader.php +++ b/src/ClassAliasAutoloader.php @@ -21,6 +21,27 @@ class ClassAliasAutoloader */ protected $classes = []; + /** + * Path to the vendor directory. + * + * @var string + */ + protected $vendorPath; + + /** + * Explicitly included namespaces/classes. + * + * @var \Illuminate\Support\Collection + */ + protected $includedAliases; + + /** + * Excluded namespaces/classes. + * + * @var \Illuminate\Support\Collection + */ + protected $excludedAliases; + /** * Register a new alias loader instance. * @@ -45,21 +66,14 @@ public static function register(Shell $shell, $classMapPath) public function __construct(Shell $shell, $classMapPath) { $this->shell = $shell; - - $vendorPath = dirname(dirname($classMapPath)); + $this->vendorPath = dirname(dirname($classMapPath)); + $this->includedAliases = collect(config('tinker.alias', [])); + $this->excludedAliases = collect(config('tinker.dont_alias', [])); $classes = require $classMapPath; - $excludedAliases = collect(config('tinker.dont_alias', [])); - foreach ($classes as $class => $path) { - if (! Str::contains($class, '\\') || Str::startsWith($path, $vendorPath)) { - continue; - } - - if (! $excludedAliases->filter(function ($alias) use ($class) { - return Str::startsWith($class, $alias); - })->isEmpty()) { + if (! $this->isAliasable($class, $path)) { continue; } @@ -113,4 +127,35 @@ public function __destruct() { $this->unregister(); } + + /** + * Whether a class may be aliased. + * + * @param string $class + * @param string $path + */ + public function isAliasable($class, $path) + { + if (! Str::contains($class, '\\')) { + return false; + } + + if (! $this->includedAliases->filter(function ($alias) use ($class) { + return Str::startsWith($class, $alias); + })->isEmpty()) { + return true; + } + + if (Str::startsWith($path, $this->vendorPath)) { + return false; + } + + if (! $this->excludedAliases->filter(function ($alias) use ($class) { + return Str::startsWith($class, $alias); + })->isEmpty()) { + return false; + } + + return true; + } } diff --git a/tests/ClassAliasAutoloaderTest.php b/tests/ClassAliasAutoloaderTest.php new file mode 100644 index 0000000..c238534 --- /dev/null +++ b/tests/ClassAliasAutoloaderTest.php @@ -0,0 +1,79 @@ +classmapPath = __DIR__.'/fixtures/vendor/composer/autoload_classmap.php'; + } + + public function tearDown(): void + { + $this->loader->unregister(); + } + + public function testCanAliasClasses() + { + $this->loader = ClassAliasAutoloader::register( + $shell = Mockery::mock(Shell::class), + $this->classmapPath + ); + + $shell->shouldReceive('writeStdout') + ->with("[!] Aliasing 'Bar' to 'App\Foo\Bar' for this Tinker session.\n") + ->once(); + + $this->assertTrue(class_exists('Bar')); + $this->assertInstanceOf(\App\Foo\Bar::class, new \Bar); + } + + public function testCanExcludeNamespacesFromAliasing() + { + set_config('tinker.dont_alias', ['App\Baz']); + + $this->loader = ClassAliasAutoloader::register( + $shell = Mockery::mock(Shell::class), + $this->classmapPath + ); + + $shell->shouldNotReceive('writeStdout'); + + $this->assertFalse(class_exists('Qux')); + } + + public function testVendorClassesAreExcluded() + { + $this->loader = ClassAliasAutoloader::register( + $shell = Mockery::mock(Shell::class), + $this->classmapPath + ); + + $shell->shouldNotReceive('writeStdout'); + + $this->assertFalse(class_exists('Three')); + } + + public function testVendorClassesCanBeWhitelisted() + { + set_config('tinker.alias', ['One\Two']); + + $this->loader = ClassAliasAutoloader::register( + $shell = Mockery::mock(Shell::class), + $this->classmapPath + ); + + $shell->shouldReceive('writeStdout') + ->with("[!] Aliasing 'Three' to 'One\Two\Three' for this Tinker session.\n") + ->once(); + + $this->assertTrue(class_exists('Three')); + $this->assertInstanceOf(\One\Two\Three::class, new \Three); + } +} diff --git a/tests/fixtures/app/Baz/Qux.php b/tests/fixtures/app/Baz/Qux.php new file mode 100644 index 0000000..e85d763 --- /dev/null +++ b/tests/fixtures/app/Baz/Qux.php @@ -0,0 +1,8 @@ + $baseDir.'/app/Foo/Bar.php', + 'App\\Baz\\Qux' => $baseDir.'/app/Baz/Qux.php', + 'One\\Two\\Three' => $vendorDir.'/one/two/src/Three.php', + 'Four\\Five\\Six' => $vendorDir.'/four/five/src/Six.php', +); diff --git a/tests/fixtures/vendor/one/two/Three.php b/tests/fixtures/vendor/one/two/Three.php new file mode 100644 index 0000000..5edfe66 --- /dev/null +++ b/tests/fixtures/vendor/one/two/Three.php @@ -0,0 +1,8 @@ + Date: Tue, 4 Feb 2020 20:51:59 +0000 Subject: [PATCH 2/2] Fixed implementation --- composer.json | 9 +++------ src/ClassAliasAutoloader.php | 18 ++++++++++-------- src/Console/TinkerCommand.php | 10 ++++++++-- tests/ClassAliasAutoloaderTest.php | 11 +++++------ tests/helpers.php | 13 ------------- 5 files changed, 26 insertions(+), 35 deletions(-) delete mode 100644 tests/helpers.php diff --git a/composer.json b/composer.json index b29abfb..f5c1c5b 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,8 @@ "symfony/var-dumper": "^4.0|^5.0" }, "require-dev": { - "mockery/mockery": "^1.3", - "phpunit/phpunit": "^8.0" + "mockery/mockery": "^1.3.1", + "phpunit/phpunit": "^8.0|^9.0" }, "suggest": { "illuminate/database": "The Illuminate Database package (^6.0|^7.0)." @@ -34,10 +34,7 @@ "Laravel\\Tinker\\Tests\\": "tests/", "App\\": "tests/fixtures/app", "One\\Two\\": "tests/fixtures/vendor/one/two" - }, - "files": [ - "tests/helpers.php" - ] + } }, "extra": { "branch-alias": { diff --git a/src/ClassAliasAutoloader.php b/src/ClassAliasAutoloader.php index 2bb42d7..4d18344 100644 --- a/src/ClassAliasAutoloader.php +++ b/src/ClassAliasAutoloader.php @@ -47,11 +47,13 @@ class ClassAliasAutoloader * * @param \Psy\Shell $shell * @param string $classMapPath + * @param array $includedAliases + * @param array $excludedAliases * @return static */ - public static function register(Shell $shell, $classMapPath) + public static function register(Shell $shell, $classMapPath, array $includedAliases = [], array $excludedAliases = []) { - return tap(new static($shell, $classMapPath), function ($loader) { + return tap(new static($shell, $classMapPath, $includedAliases, $excludedAliases), function ($loader) { spl_autoload_register([$loader, 'aliasClass']); }); } @@ -61,14 +63,16 @@ public static function register(Shell $shell, $classMapPath) * * @param \Psy\Shell $shell * @param string $classMapPath + * @param array $includedAliases + * @param array $excludedAliases * @return void */ - public function __construct(Shell $shell, $classMapPath) + public function __construct(Shell $shell, $classMapPath, array $includedAliases = [], array $excludedAliases = []) { $this->shell = $shell; $this->vendorPath = dirname(dirname($classMapPath)); - $this->includedAliases = collect(config('tinker.alias', [])); - $this->excludedAliases = collect(config('tinker.dont_alias', [])); + $this->includedAliases = collect($includedAliases); + $this->excludedAliases = collect($excludedAliases); $classes = require $classMapPath; @@ -97,9 +101,7 @@ public function aliasClass($class) return; } - $fullName = isset($this->classes[$class]) - ? $this->classes[$class] - : false; + $fullName = $this->classes[$class] ?? false; if ($fullName) { $this->shell->writeStdout("[!] Aliasing '{$class}' to '{$fullName}' for this Tinker session.\n"); diff --git a/src/Console/TinkerCommand.php b/src/Console/TinkerCommand.php index 9853285..9ec6569 100644 --- a/src/Console/TinkerCommand.php +++ b/src/Console/TinkerCommand.php @@ -62,7 +62,11 @@ public function handle() $path .= '/composer/autoload_classmap.php'; - $loader = ClassAliasAutoloader::register($shell, $path); + $config = $this->getLaravel()->make('config'); + + $loader = ClassAliasAutoloader::register( + $shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', []) + ); try { $shell->run(); @@ -88,7 +92,9 @@ protected function getCommands() } } - foreach (config('tinker.commands', []) as $command) { + $config = $this->getLaravel()->make('config'); + + foreach ($config->get('tinker.commands', []) as $command) { $commands[] = $this->getApplication()->resolve($command); } diff --git a/tests/ClassAliasAutoloaderTest.php b/tests/ClassAliasAutoloaderTest.php index c238534..e83a6e6 100644 --- a/tests/ClassAliasAutoloaderTest.php +++ b/tests/ClassAliasAutoloaderTest.php @@ -36,11 +36,11 @@ public function testCanAliasClasses() public function testCanExcludeNamespacesFromAliasing() { - set_config('tinker.dont_alias', ['App\Baz']); - $this->loader = ClassAliasAutoloader::register( $shell = Mockery::mock(Shell::class), - $this->classmapPath + $this->classmapPath, + [], + ['App\Baz'] ); $shell->shouldNotReceive('writeStdout'); @@ -62,11 +62,10 @@ public function testVendorClassesAreExcluded() public function testVendorClassesCanBeWhitelisted() { - set_config('tinker.alias', ['One\Two']); - $this->loader = ClassAliasAutoloader::register( $shell = Mockery::mock(Shell::class), - $this->classmapPath + $this->classmapPath, + ['One\Two'] ); $shell->shouldReceive('writeStdout') diff --git a/tests/helpers.php b/tests/helpers.php deleted file mode 100644 index cb1bcbc..0000000 --- a/tests/helpers.php +++ /dev/null @@ -1,13 +0,0 @@ -