* @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use Closure; use FastRoute\Dispatcher; use Monolog\Logger; use Psr\Container\ContainerInterface; use Throwable; use Webman\Exception\ExceptionHandler; use Webman\Exception\ExceptionHandlerInterface; use Webman\Http\Request; use Webman\Http\Response; use Webman\Route\Route as RouteObject; use Workerman\Connection\TcpConnection; use Workerman\Protocols\Http; use Workerman\Worker; /** * Class App * @package Webman */ class App { /** * @var array */ protected static $_callbacks = []; /** * @var Worker */ protected static $_worker = null; /** * @var ContainerInterface */ protected static $_container = null; /** * @var Logger */ protected static $_logger = null; /** * @var string */ protected static $_appPath = ''; /** * @var string */ protected static $_publicPath = ''; /** * @var string */ protected static $_configPath = ''; /** * @var TcpConnection */ protected static $_connection = null; /** * @var Request */ protected static $_request = null; /** * @var string */ protected static $_requestClass = ''; /** * App constructor. * * @param string $request_class * @param Logger $logger * @param string $app_path * @param string $public_path */ public function __construct(string $request_class, Logger $logger, string $app_path, string $public_path) { static::$_requestClass = $request_class; static::$_logger = $logger; static::$_publicPath = $public_path; static::$_appPath = $app_path; } /** * @param TcpConnection $connection * @param Request $request * @return null */ public function onMessage($connection, $request) { try { static::$_request = $request; static::$_connection = $connection; $path = $request->path(); $key = $request->method() . $path; if (isset(static::$_callbacks[$key])) { [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$_callbacks[$key]; return static::send($connection, $callback($request), $request); } if (static::unsafeUri($connection, $path, $request) || static::findFile($connection, $path, $key, $request) || static::findRoute($connection, $path, $key, $request) ) { return null; } $controller_and_action = static::parseControllerAction($path); if (!$controller_and_action || Route::hasDisableDefaultRoute()) { $callback = static::getFallback(); $request->app = $request->controller = $request->action = ''; static::send($connection, $callback($request), $request); return null; } $plugin = $controller_and_action['plugin']; $app = $controller_and_action['app']; $controller = $controller_and_action['controller']; $action = $controller_and_action['action']; $callback = static::getCallback($plugin, $app, [$controller, $action]); static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]); [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$_callbacks[$key]; static::send($connection, $callback($request), $request); } catch (Throwable $e) { static::send($connection, static::exceptionResponse($e, $request), $request); } return null; } /** * @param $worker * @return void */ public function onWorkerStart($worker) { static::$_worker = $worker; Http::requestClass(static::$_requestClass); } /** * @param string $key * @param array $data * @return void */ protected static function collectCallbacks(string $key, array $data) { static::$_callbacks[$key] = $data; if (\count(static::$_callbacks) >= 1024) { unset(static::$_callbacks[\key(static::$_callbacks)]); } } /** * @param TcpConnection $connection * @param string $path * @param $request * @return bool */ protected static function unsafeUri(TcpConnection $connection, string $path, $request) { if (\strpos($path, '..') !== false || \strpos($path, "\\") !== false || \strpos($path, "\0") !== false || \strpos($path, '//') !== false || !$path) { $callback = static::getFallback(); $request->app = $request->controller = $request->action = ''; static::send($connection, $callback($request), $request); return true; } return false; } /** * @return Closure */ protected static function getFallback() { // when route, controller and action not found, try to use Route::fallback return Route::getFallback() ?: function () { return new Response(404, [], \file_get_contents(static::$_publicPath . '/404.html')); }; } /** * @param Throwable $e * @param $request * @return string|Response */ protected static function exceptionResponse(Throwable $e, $request) { try { $app = $request->app ?: ''; $plugin = $request->plugin ?: ''; $exception_config = static::config($plugin, 'exception'); $default_exception = $exception_config[''] ?? ExceptionHandler::class; $exception_handler_class = $exception_config[$app] ?? $default_exception; /** @var ExceptionHandlerInterface $exception_handler */ $exception_handler = static::container($plugin)->make($exception_handler_class, [ 'logger' => static::$_logger, 'debug' => static::config($plugin, 'app.debug') ]); $exception_handler->report($e); $response = $exception_handler->render($request, $e); $response->exception($e); return $response; } catch (Throwable $e) { $response = new Response(500, [], static::config($plugin, 'app.debug') ? (string)$e : $e->getMessage()); $response->exception($e); return $response; } } /** * @param $app * @param $call * @param null $args * @param bool $with_global_middleware * @param RouteObject $route * @return callable */ protected static function getCallback(string $plugin, string $app, $call, array $args = null, bool $with_global_middleware = true, RouteObject $route = null) { $args = $args === null ? null : \array_values($args); $middlewares = []; if ($route) { $route_middlewares = \array_reverse($route->getMiddleware()); foreach ($route_middlewares as $class_name) { $middlewares[] = [$class_name, 'process']; } } $middlewares = \array_merge($middlewares, Middleware::getMiddleware($plugin, $app, $with_global_middleware)); foreach ($middlewares as $key => $item) { $middlewares[$key][0] = static::container($plugin)->get($item[0]); } $controller_reuse = static::config($plugin, 'app.controller_reuse', true); if (\is_array($call) && \is_string($call[0])) { if (!$controller_reuse) { $call = function ($request, ...$args) use ($call, $plugin) { $call[0] = static::container($plugin)->make($call[0]); return $call($request, ...$args); }; } else { $call[0] = static::container($plugin)->get($call[0]); } } if ($middlewares) { $callback = \array_reduce($middlewares, function ($carry, $pipe) { return function ($request) use ($carry, $pipe) { return $pipe($request, $carry); }; }, function ($request) use ($call, $args) { try { if ($args === null) { $response = $call($request); } else { $response = $call($request, ...$args); } } catch (Throwable $e) { return static::exceptionResponse($e, $request); } if (!$response instanceof Response) { if (\is_array($response)) { $response = 'Array'; } $response = new Response(200, [], $response); } return $response; }); } else { if ($args === null) { $callback = $call; } else { $callback = function ($request) use ($call, $args) { return $call($request, ...$args); }; } } return $callback; } /** * @param string $plugin * @return ContainerInterface */ public static function container(string $plugin = '') { return static::config($plugin, 'container'); } /** * @return Request */ public static function request() { return static::$_request; } /** * @return TcpConnection */ public static function connection() { return static::$_connection; } /** * @return Worker */ public static function worker() { return static::$_worker; } /** * @param TcpConnection $connection * @param string $path * @param string $key * @param Request $request * @return bool */ protected static function findRoute(TcpConnection $connection, string $path, string $key, $request) { $ret = Route::dispatch($request->method(), $path); if ($ret[0] === Dispatcher::FOUND) { $ret[0] = 'route'; $callback = $ret[1]['callback']; $route = clone $ret[1]['route']; $plugin = $app = $controller = $action = ''; $args = !empty($ret[2]) ? $ret[2] : null; if ($args) { $route->setParams($args); } if (\is_array($callback)) { $controller = $callback[0]; $plugin = static::getPluginByClass($controller); $app = static::getAppByController($controller); $action = static::getRealMethod($controller, $callback[1]) ?? ''; } $callback = static::getCallback($plugin, $app, $callback, $args, true, $route); static::collectCallbacks($key, [$callback, $plugin, $app, $controller ?: '', $action, $route]); [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$_callbacks[$key]; static::send($connection, $callback($request), $request); return true; } return false; } /** * @param TcpConnection $connection * @param string $path * @param string $key * @param Request $request * @return bool */ protected static function findFile(TcpConnection $connection, string $path, string $key, $request) { if (preg_match('/%[0-9a-f]{2}/i', $path)) { $path = urldecode($path); if (static::unsafeUri($connection, $path, $request)) { return true; } } $path_explodes = \explode('/', trim($path, '/')); $plugin = ''; if (isset($path_explodes[1]) && $path_explodes[0] === 'app') { $public_dir = BASE_PATH . "/plugin/{$path_explodes[1]}/public"; $plugin = $path_explodes[1]; $path = \substr($path, strlen("/app/{$path_explodes[1]}/")); } else { $public_dir = static::$_publicPath; } $file = "$public_dir/$path"; if (!\is_file($file)) { return false; } if (\pathinfo($file, PATHINFO_EXTENSION) === 'php') { if (!static::config($plugin, 'app.support_php_files', false)) { return false; } static::collectCallbacks($key, [function () use ($file) { return static::execPhpFile($file); }, '', '', '', '', null]); [, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$_callbacks[$key]; static::send($connection, static::execPhpFile($file), $request); return true; } if (!static::config($plugin, 'static.enable', false)) { return false; } static::collectCallbacks($key, [static::getCallback($plugin, '__static__', function ($request) use ($file) { \clearstatcache(true, $file); if (!\is_file($file)) { $callback = static::getFallback(); return $callback($request); } return (new Response())->file($file); }, null, false), '', '', '', '', null]); [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$_callbacks[$key]; static::send($connection, $callback($request), $request); return true; } /** * @param TcpConnection $connection * @param Response $response * @param Request $request * @return void */ protected static function send(TcpConnection $connection, $response, $request) { $keep_alive = $request->header('connection'); static::$_request = static::$_connection = null; if (($keep_alive === null && $request->protocolVersion() === '1.1') || $keep_alive === 'keep-alive' || $keep_alive === 'Keep-Alive' ) { $connection->send($response); return; } $connection->close($response); } /** * @param string $path * @return array|false * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface * @throws \ReflectionException */ protected static function parseControllerAction(string $path) { $path_explode = \explode('/', trim($path, '/')); $is_plugin = isset($path_explode[1]) && $path_explode[0] === 'app'; $config_prefix = $is_plugin ? "plugin.{$path_explode[1]}." : ''; $path_prefix = $is_plugin ? "/app/{$path_explode[1]}" : ''; $class_prefix = $is_plugin ? "plugin\\{$path_explode[1]}" : ''; $suffix = Config::get("{$config_prefix}app.controller_suffix", ''); $relative_path = \trim(substr($path, strlen($path_prefix)), '/'); $path_explode = $relative_path ? \explode('/', $relative_path) : []; $action = 'index'; if ($controller_action = static::guessControllerAction($path_explode, $action, $suffix, $class_prefix)) { return $controller_action; } $action = \end($path_explode); unset($path_explode[count($path_explode) - 1]); return static::guessControllerAction($path_explode, $action, $suffix, $class_prefix); } /** * @param $path_explode * @param $action * @param $suffix * @return array|false * @throws \ReflectionException */ protected static function guessControllerAction($path_explode, $action, $suffix, $class_prefix) { $map[] = "$class_prefix\\app\\controller\\" . \implode('\\', $path_explode); foreach ($path_explode as $index => $section) { $tmp = $path_explode; \array_splice($tmp, $index, 1, [$section, 'controller']); $map[] = "$class_prefix\\" . \implode('\\', \array_merge(['app'], $tmp)); } $last_index = \count($map) - 1; $map[$last_index] = \trim($map[$last_index], '\\') . '\\index'; foreach ($map as $controller_class) { $controller_class .= $suffix; if ($controller_action = static::getControllerAction($controller_class, $action)) { return $controller_action; } } return false; } /** * @param string $controller_class * @param string $action * @return array|false * @throws \ReflectionException */ protected static function getControllerAction(string $controller_class, string $action) { if (static::loadController($controller_class) && ($controller_class = (new \ReflectionClass($controller_class))->name) && \method_exists($controller_class, $action)) { return [ 'plugin' => static::getPluginByClass($controller_class), 'app' => static::getAppByController($controller_class), 'controller' => $controller_class, 'action' => static::getRealMethod($controller_class, $action) ]; } return false; } /** * @param string $controller_class * @return bool */ protected static function loadController(string $controller_class) { if (\class_exists($controller_class)) { return true; } $explodes = \explode('\\', strtolower(ltrim($controller_class, '\\'))); $base_path = $explodes[0] === 'plugin' ? BASE_PATH . '/plugin' : static::$_appPath; unset($explodes[0]); $file_name = \array_pop($explodes) . '.php'; $finded = true; foreach ($explodes as $path_section) { if (!$finded) { break; } $dirs = Util::scanDir($base_path, false); $finded = false; foreach ($dirs as $name) { if (\strtolower($name) === $path_section) { $base_path = "$base_path/$name"; $finded = true; break; } } } if (!$finded) { return false; } foreach (\scandir($base_path) ?: [] as $name) { if (\strtolower($name) === $file_name) { require_once "$base_path/$name"; if (\class_exists($controller_class, false)) { return true; } } } return false; } /** * @param string $controller_class * @return mixed|string */ public static function getPluginByClass(string $controller_class) { $controller_class = \trim($controller_class, '\\'); $tmp = \explode('\\', $controller_class, 3); if ($tmp[0] !== 'plugin') { return ''; } return $tmp[1] ?? ''; } /** * @param string $controller_class * @return mixed|string */ protected static function getAppByController(string $controller_class) { $controller_class = \trim($controller_class, '\\'); $tmp = \explode('\\', $controller_class, 5); $pos = $tmp[0] === 'plugin' ? 3 : 1; if (!isset($tmp[$pos])) { return ''; } return \strtolower($tmp[$pos]) === 'controller' ? '' : $tmp[$pos]; } /** * @param string $file * @return false|string */ public static function execPhpFile(string $file) { \ob_start(); // Try to include php file. try { include $file; } catch (\Exception $e) { echo $e; } return \ob_get_clean(); } /** * @param string $class * @param string $method * @return string */ protected static function getRealMethod(string $class, string $method) { $method = \strtolower($method); $methods = \get_class_methods($class); foreach ($methods as $candidate) { if (\strtolower($candidate) === $method) { return $candidate; } } return $method; } /** * @param string $plugin * @param string $key * @param $default * @return array|mixed|null */ protected static function config(string $plugin, string $key, $default = null) { return Config::get($plugin ? "plugin.$plugin.$key" : $key, $default); } }