diff --git a/framework/zand/composer.json b/framework/zand/composer.json new file mode 100644 index 0000000000..abea986fa2 --- /dev/null +++ b/framework/zand/composer.json @@ -0,0 +1,9 @@ +{ + "require": { + "spiral/roadrunner": "v2.0", + "nyholm/psr7": "^1.8", + "spiral/roadrunner-jobs": "^2.0", + "spiral/goridge": "^3.2", + "spiral/roadrunner-kv": "^3.0" + } +} diff --git a/framework/zand/response.class.php b/framework/zand/response.class.php new file mode 100644 index 0000000000..3ad2984229 --- /dev/null +++ b/framework/zand/response.class.php @@ -0,0 +1,289 @@ + 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', + 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', + 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', + 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required', + ); + + public function __construct() + { + $this->stream = Stream::create(''); + } + + public function getProtocolVersion(): string + { + return $this->protocol; + } + + public function withProtocolVersion($version): MessageInterface + { + if (!is_scalar($version)) { + throw new InvalidArgumentException('Protocol version must be a string'); + } + + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = (string) $version; + + return $new; + } + + public function cleanup(): void + { + $this->headers = array(); + $this->statusCode = 200; + $this->sent = false; + $this->reasonPhrase = ''; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function getHeaderLine($header): string + { + return implode(', ', $this->getHeader($header)); + } + + public function setHeader(string $name, ?string $value = null): static + { + $this->headers[$name] = [$value]; + return $this; + } + + public function addHeader(string $name, string $value): static + { + $this->headers[$name][] = $value; + return $this; + } + + public function hasHeader($header): bool + { + return isset($this->headerNames[strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); + } + + public function withHeader($header, $value): MessageInterface + { + $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader($header, $value): MessageInterface + { + if (!is_string($header) || '' === $header) { + throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + + $new = clone $this; + $new->setHeaders([$header => $value]); + + return $new; + } + + public function withoutHeader($header): MessageInterface + { + if (!is_string($header)) { + throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + + $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getHeader($header): array + { + if (!is_string($header)) { + throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + + $header = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function deleteHeader(string $name): static + { + unset($this->headers[$name]); + + return $this; + } + + public function setContentType(string $type, string $charset = null): static + { + $this->setHeader('Content-Type', $type . ($charset ? '; charset=' . $charset : '')); + return $this; + } + + public function redirect(string $url, int $code = 302): void + { + $this->setCode($code); + $this->setHeader('Location', $url); + } + + public function isSent(): bool + { + return $this->sent; + } + + public function setSent(bool $sent): static + { + $this->sent = true; + return $this; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function setStatus($code, $reasonPhrase = '') + { + $this->statusCode = $code; + + if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$this->statusCode])) { + $reasonPhrase = self::PHRASES[$this->statusCode]; + } + $this->reasonPhrase = $reasonPhrase; + } + + public function withStatus($code, $reasonPhrase = ''): ResponseInterface + { + if (!is_int($code) && !is_string($code)) { + throw new InvalidArgumentException('Status code has to be an integer'); + } + + $code = (int) $code; + if ($code < 100 || $code > 599) { + throw new InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); + } + + $new = clone $this; + $new->statusCode = $code; + if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) { + $reasonPhrase = self::PHRASES[$new->statusCode]; + } + $new->reasonPhrase = $reasonPhrase; + + return $new; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + public function getBody(): StreamInterface + { + if (null === $this->stream) { + $this->stream = Stream::create(''); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body): MessageInterface + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + + return $new; + } + + public function setBody(string $body) + { + $this->stream = Stream::create($body); + } + + public function setCookie(string $name, string $value, $expire, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httpOnly = null, ?string $sameSite = null): static + { + $headerValue = sprintf('%s=%s; path=%s; SameSite=%s', $name, urlencode($value), $path ?? ($domain ? '/' : $this->cookiePath), $sameSite ?? 'Lax'); + + if($expire) + { + $headerValue .= '; Expires='.(date('D, d M Y H:i:s T', strtotime($expire))); + } + + $cookieDomain = $domain ?? $this->cookieDomain; + if($cookieDomain && !$path) $headerValue .= '; domain='.$cookieDomain; + + if($secure ?? $this->cookieSecure) $headerValue .= '; secure'; + + if($httpOnly || $httpOnly === null) $headerValue .= '; HttpOnly'; + + $this->addHeader('Set-Cookie', $headerValue); + + return $this; + } + + public function deleteCookie(string $name, string $path = null, string $domain = null, bool $secure = null): static + { + $this->setCookie($name, '', 0, $path, $domain, $secure); + + return $this; + } +} diff --git a/framework/zand/router.class.php b/framework/zand/router.class.php new file mode 100644 index 0000000000..908dabc7c3 --- /dev/null +++ b/framework/zand/router.class.php @@ -0,0 +1,361 @@ +worker = new zandWorker(); + $this->consumer = new Consumer(); + + parent::__construct($appName, $appRoot); + } + + /** + * 开启session。 + * Start session. + * + * @access public + * @return void + */ + public function startSession() + { + $sessionName = $this->config->sessionVar; + + if(!defined('SESSION_STARTED')) + { + global $config; + + $driver = $config->db->driver; + if(!class_exists($driver)) + { + $classFile = $this->coreLibRoot . 'dao' . DS . $driver . '.class.php'; + include($classFile); + } + $dao = new $driver(); + + $ztSessionHandler = new zandSession($dao); + session_set_save_handler( + $ztSessionHandler->open(...), + $ztSessionHandler->close(...), + $ztSessionHandler->read(...), + $ztSessionHandler->write(...), + $ztSessionHandler->destroy(...), + $ztSessionHandler->gc(...) + ); + + session_name($sessionName); + session_set_cookie_params(0, $this->config->webRoot, '', $this->config->cookieSecure, true); + + define('SESSION_STARTED', true); + } + else + { + $this->sessionID = isset($_COOKIE[$sessionName]) ? $_COOKIE[$sessionName] : session_create_id(); + session_id($this->sessionID); + session_start(); + + $this->worker->response->setCookie($sessionName, $this->sessionID, 0); + } + } +} + +/** + * 消息队列的消息类型。 + * Message in queue. + * + * @package zand + */ +class zandMessage +{ + public $id; + public $type; + public $command; +} + +/** + * 消息队列。 + * Message queue. + * + * @package zand + */ +class zandQueue +{ + private $mq; + + public function __construct($queueName) + { + $jobs = new Jobs(RPC::create('tcp://127.0.0.1:6001')); + + $this->mq = $jobs->connect('crons'); + } + + public function push($message) + { + $task = $this->mq->create(zandMessage::class, $message); + $this->mq->dispatch($task); + } +} + +/** + * HTTP worker. + * + * @package zand + */ +class zandWorker +{ + /** + * RoadRunner PSR7 worker. + * + * @var object + * @access private + */ + private $psr7; + + /** + * response. + * + * @var object + * @access public + */ + public $response; + + /** + * Constructor. + * + * @access public + * @return void + */ + public function __construct() + { + $worker = Worker::create(); + $factory = new Psr17Factory(); + $this->psr7 = new PSR7Worker($worker, $factory, $factory, $factory); + $this->response = new zandResponse(); + } + + /** + * Wait request to run. + * + * @access public + * @return void + */ + public function waitRequest() + { + $request = $this->psr7->waitRequest(); + $this->initGlobal($request); + $this->response = new zandResponse(); + } + + /** + * Init global variables. + * + * @param object $request + * @access public + * @return void + */ + public function initGlobal($request) + { + $_SERVER = $request->getServerParams(); + $_SERVER['REQUEST_TIME'] = time(); + $_SERVER['REQUEST_TIME_FLOAT'] = microtime(true); + $_SERVER['SERVER_PROTOCOL'] = $request->getUri()->getScheme(); + $_SERVER['REQUEST_METHOD'] = $request->getMethod(); + $_SERVER['SERVER_NAME'] = $request->getUri()->getHost(); + $_SERVER['SERVER_PORT'] = $request->getUri()->getPort(); + $_SERVER['REQUEST_URI'] = $request->getUri()->getPath(); + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['PHP_SELF'] = 'index.php'; + $_SERVER['PATH_TRANSLATED'] = 'index.php'; + $_SERVER['HTTP_HOST'] = $_SERVER['SERVER_NAME'] . (in_array($_SERVER['SERVER_PORT'], array(80, 443)) ? '' : ':' . $_SERVER['SERVER_PORT']); + + $_GET = $request->getQueryParams(); + $_POST = $request->getParsedBody(); + $_COOKIE = $request->getCookieParams(); + $_FILE = $request->getUploadedFiles(); + } + + /** + * Send response. + * + * @param string $body + * @access public + * @return void + */ + public function respond(string $body) + { + $this->response->setBody($body); + $this->psr7->respond($this->response); + } + + /** + * Send error. + * + * @param Exception $e + * @access public + * @return void + */ + public function error(Exception $e) + { + $this->psr7->getWorker()->error((string)$e); + } +} + +/** + * MySQL实现的Session管理. + * Session handler implements by MySQL. + * + * @package zand + */ +class zandSession +{ + /** + * DAO for database. + * + * @var object + * @access private + */ + private $dao; + + /** + * Constructor. + * + * @param object $dao + * @access public + * @return void + */ + public function __construct($dao) + { + $this->dao = $dao; + } + + /** + * Open session. + * + * @param string $savePath + * @param string $sessionName + * @access public + * @return bool + */ + public function open($savePath, $sessionName) + { + $this->savePath = $savePath; + $this->sessionName = $sessionName; + return true; + } + + /** + * Close session. + * + * @access public + * @return bool + */ + public function close() + { + return true; + } + + /** + * Read session. + * + * @param string $id + * @access public + * @return string + */ + public function read($id) + { + $result = $this->dao->select('data')->from(TABLE_SESSION)->where('id')->eq($id)->fetch(); + return $result ? $result->data : ''; + } + + /** + * Write session. + * + * @param string $id + * @param string $data + * @access public + * @return bool + */ + public function write($id, $data) + { + $data = array('id' => $id, 'data' => $data, 'timestamp' => time()); + $this->dao->replace(TABLE_SESSION)->data($data)->exec(); + return true; + } + + /** + * Destroy session. + * + * @param string $id + * @access public + * @return bool + */ + public function destroy($id) + { + $this->dao->delete(TABLE_SESSION)->where('id')->eq($id)->exec(); + return true; + } + + /** + * GC for session. + * + * @param int $maxlifetime + * @access public + * @return bool + */ + public function gc($maxlifetime) + { + $this->dao->delete(TABLE_SESSION)->where('timestamp')->lt(time() - intval($maxlifetime))->exec(); + return true; + } +} diff --git a/module/api/v1/entries/zfilecontent.php b/module/api/v1/entries/zfilecontent.php index 7b91f3e6e8..4fe08db320 100644 --- a/module/api/v1/entries/zfilecontent.php +++ b/module/api/v1/entries/zfilecontent.php @@ -20,13 +20,13 @@ class zfileContentEntry extends entry */ public function get($fileID) { - ob_end_clean(); + ob_end_clean(); - header("Content-type: application/octet-stream"); - header("Content-Transfer-Encoding: binary"); - header("Accept-Ranges: bytes"); - // header("Content-Length: " . filesize($filePath)); - header("Content-Disposition: attachment; filename=\"hello.txt\""); + helper::header('Content-type', 'application/octet-stream'); + helper::header('Content-Transfer-Encoding', 'binary'); + helper::header('Accept-Ranges', 'bytes'); + // helper::header('Content-Length', filesize($filePath)); + helper::header('Content-Disposition', 'attachment; filename="hello.txt"'); echo 'hello'; } diff --git a/module/cron/control.php b/module/cron/control.php index 085bc408c6..06605713a8 100644 --- a/module/cron/control.php +++ b/module/cron/control.php @@ -131,8 +131,7 @@ class cron extends control if(empty($this->config->global->cron)) return; /* Zand queue. */ - $zand = $this->app->loadClass('zand'); - $queue = $zand->connectQueue('crons'); + $queue = new zandQueue('crons'); /* Schedule loop. */ $cronTimes = array(); diff --git a/module/execution/control.php b/module/execution/control.php index da78f169ce..61818ebed3 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -2589,7 +2589,7 @@ class execution extends control $this->loadModel('kanban'); /* Compatibility IE8. */ - if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) header("X-UA-Compatible: IE=EmulateIE7"); + if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) helper::header('X-UA-Compatible', 'IE=EmulateIE7'); $this->execution->setMenu($executionID); $execution = $this->execution->getById($executionID); @@ -2934,7 +2934,7 @@ class execution extends control public function storyKanban($executionID) { /* Compatibility IE8*/ - if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) header("X-UA-Compatible: IE=EmulateIE7"); + if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) helper::header('X-UA-Compatible', 'IE=EmulateIE7'); $this->execution->setMenu($executionID); $execution = $this->loadModel('execution')->getById($executionID); diff --git a/module/file/control.php b/module/file/control.php index fa6a06880a..17caffafc8 100755 --- a/module/file/control.php +++ b/module/file/control.php @@ -496,13 +496,13 @@ class file extends control for($i = 0; $i < $obLevel; $i++) ob_end_clean(); $mime = (isset($file->extension) and in_array($file->extension, $this->config->file->imageExtensions)) ? "image/{$file->extension}" : $this->config->file->mimes['default']; - header("Content-type: $mime"); + helper::header('Content-type', $mime); $cacheMaxAge = 10 * 365 * 24 * 3600; - header("Cache-Control: private"); - header("Pragma: cache"); - header("Expires:" . gmdate("D, d M Y H:i:s", time() + $cacheMaxAge) . " GMT"); - header("Cache-Control: max-age=$cacheMaxAge"); + helper::header('Cache-Control', 'private'); + helper::header('Pragma', 'cache'); + helper::header('Expires', gmdate('D, d M Y H:i:s', time() + $cacheMaxAge) . ' GMT'); + helper::header('Cache-Control', "max-age=$cacheMaxAge"); $handle = fopen($file->realPath, "r"); if($handle) diff --git a/module/file/model.php b/module/file/model.php index 1ac57750f9..b8fcfa96f2 100755 --- a/module/file/model.php +++ b/module/file/model.php @@ -1088,10 +1088,10 @@ class fileModel extends model $mimes = $this->config->file->mimes; $contentType = isset($mimes[$fileType]) ? $mimes[$fileType] : $mimes['default']; - header("Content-type: $contentType"); - header("Content-Disposition: attachment; filename=\"$fileName\""); - header("Pragma: no-cache"); - header("Expires: 0"); + helper::header('Content-type', $contentType); + helper::header('Content-Disposition', "attachment; filename=\"$fileName\""); + helper::header('Pragma', 'no-cache'); + helper::header('Expires', '0'); if($type == 'content') helper::end($content); if($type == 'file' and file_exists($content)) { diff --git a/module/instance/control.php b/module/instance/control.php index ace6bbf474..03d95c670b 100644 --- a/module/instance/control.php +++ b/module/instance/control.php @@ -734,7 +734,7 @@ class instance extends control { if(!$this->checkCneToken()) { - header("HTTP/1.1 401"); + helper::setStatus(401); return print(json_encode(array('code' => 401, 'message' => 'Invalid token.'))); } @@ -760,7 +760,7 @@ class instance extends control { if(!$this->checkCneToken()) { - header("HTTP/1.1 401"); + helper::setStatus(401); return print(json_encode(array('code' => 401, 'message' => 'Invalid token.'))); } @@ -796,7 +796,7 @@ class instance extends control { if(!$this->checkCneToken()) { - header("HTTP/1.1 401"); + helper::setStatus(401); return print(json_encode(array('code' => 401, 'message' => 'Invalid token.'))); } diff --git a/module/misc/control.php b/module/misc/control.php index 2681f0fbce..76288eb7ec 100644 --- a/module/misc/control.php +++ b/module/misc/control.php @@ -234,7 +234,7 @@ class misc extends control $obLevel = ob_get_level(); for($i = 0; $i < $obLevel; $i++) ob_end_clean(); - header('Content-Type: image/jpeg'); + helper::header('Content-Type', 'image/jpeg'); $captcha = $this->app->loadClass('captcha'); $this->session->set($sessionVar, $captcha->getPhrase()); $captcha->build()->output(); diff --git a/module/sso/control.php b/module/sso/control.php index 1e4a495296..a283b095fe 100644 --- a/module/sso/control.php +++ b/module/sso/control.php @@ -344,7 +344,7 @@ class sso extends control $url = "https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=%s&app_id=%s"; $url = sprintf($url, $redirectURI, $appID, $state); - header("location: $url"); + helper::header('location', $url); } /** @@ -412,7 +412,7 @@ class sso extends control $this->user->login($user); $indexUrl = $this->createLink('my', 'index'); - header("location: $indexUrl"); + helper::header('location', $indexUrl); } /** diff --git a/module/user/view/deny.html.php b/module/user/view/deny.html.php index bb7114d031..4f1e740839 100644 --- a/module/user/view/deny.html.php +++ b/module/user/view/deny.html.php @@ -41,7 +41,7 @@ include '../../common/view/header.lite.html.php'; if($denyType == 'noview') { $menuName = isset($lang->$menu->common) ? $lang->$module->common : $menu; - if(isset($lang->menu->$menu))list($menuName) = explode('|', $lang->menu->$menu); + if(isset($lang->menu->$menu)) list($menuName) = explode('|', $lang->menu->$menu); printf($lang->user->errorView, $menuName); } ?>