From 89cf5b29ae0e1aebcb72851ebb5dc4d655bdc55f Mon Sep 17 00:00:00 2001 From: liufee Date: Wed, 23 Aug 2017 14:11:41 +0800 Subject: [PATCH] add swoole session handle --- src/console/SwooleController.php | 18 +- src/swoole/SwooleServer.php | 27 +- src/web/Request.php | 230 +++++++++++-- src/web/Response.php | 573 +++++++++++++++++++++++++++++-- src/web/Session.php | 344 +++++++++++++++++++ 5 files changed, 1122 insertions(+), 70 deletions(-) create mode 100644 src/web/Session.php diff --git a/src/console/SwooleController.php b/src/console/SwooleController.php index ac3abb6..d16fbd9 100644 --- a/src/console/SwooleController.php +++ b/src/console/SwooleController.php @@ -178,21 +178,27 @@ public function actionStart() ]; $config['aliases'] = isset($config['aliases']) ? array_merge($aliases, $config['aliases']) : $aliases; - $config['components']['request'] = [ + $requestComponent = [ 'class' => \feehi\web\Request::className(), 'swooleRequest' => $request, - 'cookieValidationKey' => 'KaNMPF6oZegCr0bhED4JHYnhOse7UhrS', - 'enableCsrfValidation' => true, ]; - $config['components']['response'] = [ + $config['components']['request'] = isset($config['components']['request']) ? array_merge($config['components']['request'], $requestComponent) : $requestComponent; + + $responseComponent = [ 'class' => \feehi\web\Response::className(), 'swooleResponse' => $response, ]; + $config['components']['response'] = isset($config['components']['response']) ? array_merge($config['components']['response'], $responseComponent) : $responseComponent; - $config['components']['assetManager'] = [ + $authManagerComponent = [ 'class' => yii\web\AssetManager::className(), 'baseUrl' => '/assets' ]; + $config['components']['assetManager'] = isset( $config['components']['assetManager'] ) ? array_merge($authManagerComponent, $config['components']['assetManager']) : $authManagerComponent; + + $config['components']['session'] = [ + "class" => \feehi\web\Session::className() + ]; try { $application = new \yii\web\Application($config); @@ -204,8 +210,8 @@ public function actionStart() } }; - $server->run(); $this->stdout("server is running, listening {$this->host}:{$this->port}" . PHP_EOL); + $server->run(); } public function actionStop() diff --git a/src/swoole/SwooleServer.php b/src/swoole/SwooleServer.php index e3f0766..b54782b 100644 --- a/src/swoole/SwooleServer.php +++ b/src/swoole/SwooleServer.php @@ -8,6 +8,7 @@ namespace feehi\swoole; +use feehi\web\Session; class SwooleServer extends \yii\base\Object { @@ -23,6 +24,7 @@ public function __construct($host, $port, $swooleConfig=[]) self::$swooleConfig = $swooleConfig; $this->swoole->set($swooleConfig); $this->swoole->on('request', [$this, 'onRequest']); + $this->swoole->on('WorkerStart', [$this, 'onWorkerStart']); parent::__construct(); } @@ -44,12 +46,19 @@ public function onRequest($request, $response) //$this->staticRequest($request, $response); //转换$_FILE超全局变量 - $this->mountGlobalFilesVar($request, $response); - + $this->mountGlobalFilesVar($request); call_user_func_array($this->runApp, [$request, $response]); } + public function onWorkerStart( $serv , $worker_id) { + if( $worker_id == 0 ) { + \swoole_timer_tick(60000, function(){//一分钟清理一次session + (new Session())->gcSession(); + }); + } + } + /** * @param \swoole_http_request $request * @param \swoole_http_response $response @@ -116,6 +125,20 @@ private function mountGlobalFilesVar($request) } } } + $_GET = isset($request->get) ? $request->get : []; + $_POST = isset($request->post) ? $request->post : []; + $_COOKIE = isset($request->cookie) ? $request->cookie : []; + + $server = isset($request->server) ? $request->server : []; + $header = isset($request->header) ? $request->header : []; + foreach ($server as $key => $value) { + $_SERVER[strtoupper($key)] = $value; + unset($server[$key]); + } + foreach ($header as $key => $value) { + $_SERVER['HTTP_'.strtoupper($key)] = $value; + } + $_SERVER['SERVER_SOFTWARE'] = "swoole/" . SWOOLE_VERSION; } } \ No newline at end of file diff --git a/src/web/Request.php b/src/web/Request.php index 27095c2..e0e524e 100644 --- a/src/web/Request.php +++ b/src/web/Request.php @@ -22,23 +22,32 @@ class Request extends \yii\web\Request /* @var $swooleRequest \swoole_http_request */ public $swooleRequest; + const CSRF_HEADER = 'X-CSRF-Token'; + + const CSRF_MASK_LENGTH = 8; + public $enableCsrfValidation = true; - /** - * @var CookieCollection Collection of request cookies. - */ + public $csrfParam = '_csrf'; + + public $csrfCookie = ['httpOnly' => true]; + + public $enableCsrfCookie = true; + + public $enableCookieValidation = true; + + public $cookieValidationKey; + + public $methodParam = '_method'; + + public $parsers = []; + + private $_cookies; - /** - * @var HeaderCollection Collection of request headers. - */ + private $_headers; - /** - * Resolves the current request into a route and the associated parameters. - * @return array the first element is the route, and the second is the associated parameters. - * @throws NotFoundHttpException if the request cannot be resolved. - */ public function resolve() { $result = Yii::$app->getUrlManager()->parseRequest($this); @@ -72,15 +81,49 @@ public function getMethod() return $this->swooleRequest->server["request_method"]; } + public function getIsGet() + { + return $this->getMethod() === 'GET'; + } + + public function getIsOptions() + { + return $this->getMethod() === 'OPTIONS'; + } + + public function getIsHead() + { + return $this->getMethod() === 'HEAD'; + } + + public function getIsPost() + { + return $this->getMethod() === 'POST'; + } + + public function getIsDelete() + { + return $this->getMethod() === 'DELETE'; + } + + public function getIsPut() + { + return $this->getMethod() === 'PUT'; + } + + public function getIsPatch() + { + return $this->getMethod() === 'PATCH'; + } + public function getIsAjax() { return isset($this->swooleRequest->header["x-requested-with"]) && $this->swooleRequest->header["x-requested-with"] === 'XMLHttpRequest'; } - //待带修改 public function getIsPjax() { - return $this->getIsAjax() && !empty($this->swooleRequest->header["x-requested-with"]); + return $this->getIsAjax() && !empty($this->swooleRequest->header["x-pjax"]); } public function getIsFlash() @@ -101,6 +144,11 @@ public function getRawBody() return $this->_rawBody; } + public function setRawBody($rawBody) + { + $this->_rawBody = $rawBody; + } + private $_bodyParams; public function getBodyParams() @@ -144,6 +192,27 @@ public function getBodyParams() return $this->_bodyParams; } + public function setBodyParams($values) + { + $this->_bodyParams = $values; + } + + public function getBodyParam($name, $defaultValue = null) + { + $params = $this->getBodyParams(); + + return isset($params[$name]) ? $params[$name] : $defaultValue; + } + + public function post($name = null, $defaultValue = null) + { + if ($name === null) { + return $this->getBodyParams(); + } + + return $this->getBodyParam($name, $defaultValue); + } + private $_queryParams; public function getQueryParams() @@ -169,16 +238,32 @@ public function setQueryParams($values) $this->_queryParams = $values; } + public function get($name = null, $defaultValue = null) + { + if ($name === null) { + return $this->getQueryParams(); + } + + return $this->getQueryParam($name, $defaultValue); + } + + public function getQueryParam($name, $defaultValue = null) + { + $params = $this->getQueryParams(); + + return isset($params[$name]) ? $params[$name] : $defaultValue; + } + private $_hostInfo; private $_hostName; - public function getHostInfo() { if ($this->_hostInfo === null) { - $this->_hostInfo = $this->swooleRequest->header['host']; + $secure = $this->getIsSecureConnection(); + $http = $secure ? 'https' : 'http'; + $this->_hostInfo = $http . '://' . $this->swooleRequest->header['host']; } - return $this->_hostInfo; } @@ -188,6 +273,7 @@ public function setHostInfo($value) $this->_hostInfo = $value === null ? null : rtrim($value, '/'); } + public function getHostName() { if ($this->_hostName === null) { @@ -202,7 +288,7 @@ public function getHostName() public function getBaseUrl() { if ($this->_baseUrl === null) { - $this->_baseUrl = ''; + $this->_baseUrl = rtrim(dirname($this->getScriptUrl()), '\\/'); } return $this->_baseUrl; @@ -218,7 +304,7 @@ public function setBaseUrl($value) public function getScriptUrl() { if ($this->_scriptUrl === null) { - $this->scriptUrl = '/'; + $this->_scriptUrl = '/'; } return $this->_scriptUrl; @@ -238,11 +324,7 @@ public function getScriptFile() return $this->_scriptFile; } - if (isset($_SERVER['SCRIPT_FILENAME'])) { - return $_SERVER['SCRIPT_FILENAME']; - } - - throw new InvalidConfigException('Unable to determine the entry script file path.'); + return yii::getAlias("@web"); } public function setScriptFile($value) @@ -334,9 +416,14 @@ public function getQueryString() return isset($this->swooleRequest->server['query_string']) ? $this->swooleRequest->server['query_string'] : ''; } + public function getIsSecureConnection() + { + return false; + } + public function getServerName() { - return $this->swooleRequest->server['server_software']; + return $_SERVER['HOSTNAME']; } public function getServerPort() @@ -346,17 +433,32 @@ public function getServerPort() public function getReferrer() { - return $this->swooleRequest->header["referer"]; + return isset( $this->swooleRequest->header["referer"] ) ? $this->swooleRequest->header["referer"] : null ; } public function getUserAgent() { - return $this->swooleRequest->header['user-agent']; + return isset( $this->swooleRequest->header['user-agent'] ) ? $this->swooleRequest->header['user-agent'] : null; } public function getUserIP() { - return $this->swooleRequest->server['remote_addr']; + return isset( $this->swooleRequest->server['remote_addr'] ) ? $this->swooleRequest->server['remote_addr'] : null; + } + + public function getUserHost() + { + return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null; + } + + public function getAuthUser() + { + return isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; + } + + public function getAuthPassword() + { + return isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; } private $_port; @@ -442,6 +544,74 @@ public function setAcceptableLanguages($value) $this->_languages = $value; } + public function parseAcceptHeader($header) + { + $accepts = []; + foreach (explode(',', $header) as $i => $part) { + $params = preg_split('/\s*;\s*/', trim($part), -1, PREG_SPLIT_NO_EMPTY); + if (empty($params)) { + continue; + } + $values = [ + 'q' => [$i, array_shift($params), 1], + ]; + foreach ($params as $param) { + if (strpos($param, '=') !== false) { + list ($key, $value) = explode('=', $param, 2); + if ($key === 'q') { + $values['q'][2] = (double) $value; + } else { + $values[$key] = $value; + } + } else { + $values[] = $param; + } + } + $accepts[] = $values; + } + + usort($accepts, function ($a, $b) { + $a = $a['q']; // index, name, q + $b = $b['q']; + if ($a[2] > $b[2]) { + return -1; + } + + if ($a[2] < $b[2]) { + return 1; + } + + if ($a[1] === $b[1]) { + return $a[0] > $b[0] ? 1 : -1; + } + + if ($a[1] === '*/*') { + return 1; + } + + if ($b[1] === '*/*') { + return -1; + } + + $wa = $a[1][strlen($a[1]) - 1] === '*'; + $wb = $b[1][strlen($b[1]) - 1] === '*'; + if ($wa xor $wb) { + return $wa ? 1 : -1; + } + + return $a[0] > $b[0] ? 1 : -1; + }); + + $result = []; + foreach ($accepts as $accept) { + $name = $accept['q'][1]; + $accept['q'] = $accept['q'][2]; + $result[$name] = $accept; + } + + return $result; + } + public function getPreferredLanguage(array $languages = []) { if (empty($languages)) { @@ -464,11 +634,7 @@ public function getPreferredLanguage(array $languages = []) return reset($languages); } - /** - * Gets the Etags. - * - * @return array The entity tags - */ + //待修改 public function getETags() { if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { diff --git a/src/web/Response.php b/src/web/Response.php index 578c22e..3f6d67a 100644 --- a/src/web/Response.php +++ b/src/web/Response.php @@ -8,44 +8,269 @@ namespace feehi\web; - use Yii; use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; +use yii\helpers\Inflector; +use yii\helpers\Url; +use yii\helpers\FileHelper; +use yii\helpers\StringHelper; use yii\web\CookieCollection; +use yii\web\HeaderCollection; +use yii\web\HttpException; +use yii\web\RangeNotSatisfiableHttpException; +use yii\web\ResponseFormatterInterface; class Response extends \yii\web\Response { - /* @var $swooleResponse \swoole_http_response */ + /* @var $swooleResponse \swoole_http_response */ public $swooleResponse; - protected function sendHeaders() + const EVENT_BEFORE_SEND = 'beforeSend'; + + const EVENT_AFTER_SEND = 'afterSend'; + + const EVENT_AFTER_PREPARE = 'afterPrepare'; + const FORMAT_RAW = 'raw'; + const FORMAT_HTML = 'html'; + const FORMAT_JSON = 'json'; + const FORMAT_JSONP = 'jsonp'; + const FORMAT_XML = 'xml'; + + + public $format = self::FORMAT_HTML; + + public $acceptMimeType; + + public $acceptParams = []; + + public $formatters = []; + + public $data; + + public $content; + + public $stream; + + public $charset; + + public $statusText = 'OK'; + + public $version; + + public $isSent = false; + + public static $httpStatuses = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 118 => 'Connection timed out', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 210 => 'Content Different', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 310 => 'Too many 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 Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested range unsatisfiable', + 417 => 'Expectation failed', + 418 => 'I\'m a teapot', + 421 => 'Misdirected Request', + 422 => 'Unprocessable entity', + 423 => 'Locked', + 424 => 'Method failure', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 449 => 'Retry With', + 450 => 'Blocked by Windows Parental Controls', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway or Proxy Error', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 507 => 'Insufficient storage', + 508 => 'Loop Detected', + 509 => 'Bandwidth Limit Exceeded', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ]; + + private $_statusCode = 200; + + private $_headers; + + + + public function init() + { + if ($this->version === null) { + $swooleRequest = yii::$app->getRequest()->swooleRequest; + if (isset($swooleRequest->server['server_protocol']) && $swooleRequest->server['server_protocol'] === 'HTTP/1.0') { + $this->version = '1.0'; + } else { + $this->version = '1.1'; + } + } + if ($this->charset === null) { + $this->charset = Yii::$app->charset; + } + $this->formatters = array_merge($this->defaultFormatters(), $this->formatters); + } + + public function getStatusCode() { - if (headers_sent()) { + return $this->_statusCode; + } + + public function setStatusCode($value, $text = null) + { + if ($value === null) { + $value = 200; + } + $this->_statusCode = (int) $value; + if ($this->getIsInvalid()) { + throw new InvalidParamException("The HTTP status code is invalid: $value"); + } + if ($text === null) { + $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : ''; + } else { + $this->statusText = $text; + } + return $this; + } + + public function setStatusCodeByException($e) + { + if ($e instanceof HttpException) { + $this->setStatusCode($e->statusCode); + } else { + $this->setStatusCode(500); + } + return $this; + } + + public function getHeaders() + { + if ($this->_headers === null) { + $this->_headers = new HeaderCollection; + } + return $this->_headers; + } + + public function send() + { + if ($this->isSent) { return; } - $headers = $this->getHeaders(); - if ($headers) { + $this->trigger(self::EVENT_BEFORE_SEND); + $this->prepare(); + $this->trigger(self::EVENT_AFTER_PREPARE); + $this->sendHeaders(); + $this->sendContent(); + $this->trigger(self::EVENT_AFTER_SEND); + $this->isSent = true; + } + + public function clear() + { + $this->_headers = null; + $this->_cookies = null; + $this->_statusCode = 200; + $this->statusText = 'OK'; + $this->data = null; + $this->stream = null; + $this->content = null; + $this->isSent = false; + } + + protected function sendHeaders() + { + /*if (headers_sent()) { + return; + }*/ + $statusCode = $this->getStatusCode(); + $this->swooleResponse->status($statusCode); + if ($this->_headers) { + $headers = $this->getHeaders(); foreach ($headers as $name => $values) { $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); - foreach ($values as $value) { - $this->swooleResponse->header($name, $value, false); - } + // set replace for first occurrence of header but false afterwards to allow multiple + $this->swooleResponse->header($name, end( $values ) ); } } - $statusCode = $this->getStatusCode(); - $this->swooleResponse->status($statusCode); $this->sendCookies(); } + protected function sendCookies() + { + $session = yii::$app->getSession(); + $data = $session->getCookieParams(); + $this->swooleResponse->cookie($session->getName(), $session->getId(), time()+$data['lifetime'], $data['path'], $data['domain'], $data['secure'], $data['httponly']); + if ($this->_cookies === null) { + return; + } + $request = Yii::$app->getRequest(); + if ($request->enableCookieValidation) { + if ($request->cookieValidationKey == '') { + throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.'); + } + $validationKey = $request->cookieValidationKey; + } + foreach ($this->getCookies() as $cookie) { + $value = $cookie->value; + if ($cookie->expire != 1 && isset($validationKey)) { + $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey); + } + $this->swooleResponse->cookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); + } + } + protected function sendContent() { if ($this->stream === null) { - $this->swooleResponse->end($this->content); + $this->swooleResponse->end( $this->content ); + + $session = yii::$app->getSession(); + $session->persist(); return; } - $html = ""; set_time_limit(0); // Reset time limit for big files $chunkSize = 8 * 1024 * 1024; // 8MB per chunk @@ -56,18 +281,226 @@ protected function sendContent() if ($pos + $chunkSize > $end) { $chunkSize = $end - $pos + 1; } - $html .= fread($handle, $chunkSize); + $this->swooleResponse->write( fread($handle, $chunkSize) ); flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. } fclose($handle); + $this->swooleResponse->end(null); } else { while (!feof($this->stream)) { - $html -= fread($this->stream, $chunkSize); + $this->swooleResponse->write( fread($this->stream, $chunkSize) ); flush(); } fclose($this->stream); + $this->swooleResponse->end(null); } - $this->swooleResponse->end($html); + $session = yii::$app->getSession(); + $session->persist(); + } + + public function sendFile($filePath, $attachmentName = null, $options = []) + { + if (!isset($options['mimeType'])) { + $options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath); + } + if ($attachmentName === null) { + $attachmentName = basename($filePath); + } + $handle = fopen($filePath, 'rb'); + $this->sendStreamAsFile($handle, $attachmentName, $options); + + return $this; + } + + public function sendContentAsFile($content, $attachmentName, $options = []) + { + $headers = $this->getHeaders(); + + $contentLength = StringHelper::byteLength($content); + $range = $this->getHttpRange($contentLength); + + if ($range === false) { + $headers->set('Content-Range', "bytes */$contentLength"); + throw new RangeNotSatisfiableHttpException(); + } + + list($begin, $end) = $range; + if ($begin != 0 || $end != $contentLength - 1) { + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $begin-$end/$contentLength"); + $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1); + } else { + $this->setStatusCode(200); + $this->content = $content; + } + + $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream'; + $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1); + + $this->format = self::FORMAT_RAW; + + return $this; + } + + public function sendStreamAsFile($handle, $attachmentName, $options = []) + { + $headers = $this->getHeaders(); + if (isset($options['fileSize'])) { + $fileSize = $options['fileSize']; + } else { + fseek($handle, 0, SEEK_END); + $fileSize = ftell($handle); + } + + $range = $this->getHttpRange($fileSize); + if ($range === false) { + $headers->set('Content-Range', "bytes */$fileSize"); + throw new RangeNotSatisfiableHttpException(); + } + + list($begin, $end) = $range; + if ($begin != 0 || $end != $fileSize - 1) { + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $begin-$end/$fileSize"); + } else { + $this->setStatusCode(200); + } + + $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream'; + $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1); + + $this->format = self::FORMAT_RAW; + $this->stream = [$handle, $begin, $end]; + + return $this; + } + + public function setDownloadHeaders($attachmentName, $mimeType = null, $inline = false, $contentLength = null) + { + $headers = $this->getHeaders(); + + $disposition = $inline ? 'inline' : 'attachment'; + $headers->setDefault('Pragma', 'public') + ->setDefault('Accept-Ranges', 'bytes') + ->setDefault('Expires', '0') + ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName)); + + if ($mimeType !== null) { + $headers->setDefault('Content-Type', $mimeType); + } + + if ($contentLength !== null) { + $headers->setDefault('Content-Length', $contentLength); + } + + return $this; + } + + protected function getHttpRange($fileSize) + { + if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') { + return [0, $fileSize - 1]; + } + if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) { + return false; + } + if ($matches[1] === '') { + $start = $fileSize - $matches[2]; + $end = $fileSize - 1; + } elseif ($matches[2] !== '') { + $start = $matches[1]; + $end = $matches[2]; + if ($end >= $fileSize) { + $end = $fileSize - 1; + } + } else { + $start = $matches[1]; + $end = $fileSize - 1; + } + if ($start < 0 || $start > $end) { + return false; + } else { + return [$start, $end]; + } + } + + public function xSendFile($filePath, $attachmentName = null, $options = []) + { + if ($attachmentName === null) { + $attachmentName = basename($filePath); + } + if (isset($options['mimeType'])) { + $mimeType = $options['mimeType']; + } elseif (($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) { + $mimeType = 'application/octet-stream'; + } + if (isset($options['xHeader'])) { + $xHeader = $options['xHeader']; + } else { + $xHeader = 'X-Sendfile'; + } + + $disposition = empty($options['inline']) ? 'attachment' : 'inline'; + $this->getHeaders() + ->setDefault($xHeader, $filePath) + ->setDefault('Content-Type', $mimeType) + ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName)); + + $this->format = self::FORMAT_RAW; + + return $this; + } + + protected function getDispositionHeaderValue($disposition, $attachmentName) + { + $fallbackName = str_replace('"', '\\"', str_replace(['%', '/', '\\'], '_', Inflector::transliterate($attachmentName, Inflector::TRANSLITERATE_LOOSE))); + $utfName = rawurlencode(str_replace(['%', '/', '\\'], '', $attachmentName)); + + $dispositionHeader = "{$disposition}; filename=\"{$fallbackName}\""; + if ($utfName !== $fallbackName) { + $dispositionHeader .= "; filename*=utf-8''{$utfName}"; + } + return $dispositionHeader; + } + + public function redirect($url, $statusCode = 302, $checkAjax = true) + { + if (is_array($url) && isset($url[0])) { + // ensure the route is absolute + $url[0] = '/' . ltrim($url[0], '/'); + } + $url = Url::to($url); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + + if ($checkAjax) { + if (Yii::$app->getRequest()->getIsAjax()) { + if (Yii::$app->getRequest()->getHeaders()->get('X-Ie-Redirect-Compatibility') !== null && $statusCode === 302) { + // Ajax 302 redirect in IE does not work. Change status code to 200. See https://github.com/yiisoft/yii2/issues/9670 + $statusCode = 200; + } + if (Yii::$app->getRequest()->getIsPjax()) { + $this->getHeaders()->set('X-Pjax-Url', $url); + } else { + $this->getHeaders()->set('X-Redirect', $url); + } + } else { + $this->getHeaders()->set('Location', $url); + } + } else { + $this->getHeaders()->set('Location', $url); + } + + $this->setStatusCode($statusCode); + + return $this; + } + + public function refresh($anchor = '') + { + return $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor); } private $_cookies; @@ -80,25 +513,105 @@ public function getCookies() return $this->_cookies; } - protected function sendCookies() + public function getIsInvalid() { - if ($this->_cookies === null) { + return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600; + } + + public function getIsInformational() + { + return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; + } + + public function getIsSuccessful() + { + return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; + } + + public function getIsRedirection() + { + return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; + } + + public function getIsClientError() + { + return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; + } + + public function getIsServerError() + { + return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; + } + + public function getIsOk() + { + return $this->getStatusCode() == 200; + } + + public function getIsForbidden() + { + return $this->getStatusCode() == 403; + } + + public function getIsNotFound() + { + return $this->getStatusCode() == 404; + } + + public function getIsEmpty() + { + return in_array($this->getStatusCode(), [201, 204, 304]); + } + + protected function defaultFormatters() + { + return [ + self::FORMAT_HTML => 'yii\web\HtmlResponseFormatter', + self::FORMAT_XML => 'yii\web\XmlResponseFormatter', + self::FORMAT_JSON => 'yii\web\JsonResponseFormatter', + self::FORMAT_JSONP => [ + 'class' => 'yii\web\JsonResponseFormatter', + 'useJsonp' => true, + ], + ]; + } + + protected function prepare() + { + if ($this->stream !== null) { return; } - $request = Yii::$app->getRequest(); - if ($request->enableCookieValidation) { - if ($request->cookieValidationKey == '') { - throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.'); + + if (isset($this->formatters[$this->format])) { + $formatter = $this->formatters[$this->format]; + if (!is_object($formatter)) { + $this->formatters[$this->format] = $formatter = Yii::createObject($formatter); } - $validationKey = $request->cookieValidationKey; + if ($formatter instanceof ResponseFormatterInterface) { + $formatter->format($this); + } else { + throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); + } + } elseif ($this->format === self::FORMAT_RAW) { + if ($this->data !== null) { + $this->content = $this->data; + } + } else { + throw new InvalidConfigException("Unsupported response format: {$this->format}"); } - foreach ($this->getCookies() as $cookie) { - $value = $cookie->value; - if ($cookie->expire != 1 && isset($validationKey)) { - $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey); + + if (is_array($this->content)) { + throw new InvalidParamException('Response content must not be an array.'); + } elseif (is_object($this->content)) { + if( in_array($this->getStatusCode(), [301, 302]) ){ + $this->content = null; + }else { + if (method_exists($this->content, '__toString')) { + $this->content = $this->content->__toString(); + } else { + throw new InvalidParamException('Response content must be a string or an object implementing __toString().'); + } } - $this->swooleResponse->cookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); } } - -} \ No newline at end of file +} diff --git a/src/web/Session.php b/src/web/Session.php new file mode 100644 index 0000000..8b6019d --- /dev/null +++ b/src/web/Session.php @@ -0,0 +1,344 @@ + 1400, + 'path' => '/', + 'domain' => '', + 'secure' => false, + 'httponly' => true, + ]; + + private $_prefix = "feehi_"; + + public function init() + { + parent::init(); + if ($this->getIsActive()) { + Yii::warning('Session is already started', __METHOD__); + $this->updateFlashCounters(); + } + } + + public function getSessionFullName() + { + return $this->getSavePath() . $this->_prefix . $this->getId(); + } + + public function persist() + { + $this->open(); + file_put_contents($this->getSessionFullName(), json_encode($_SESSION)); + } + + public function gcSession() + { + $handle = opendir( $this->getSavePath() ); + while (false !== ($file = readdir($handle))) + { + if ($file != "." && $file != ".." && (strpos($file, $this->_prefix) === 0) && is_file($this->getSavePath() . $file)) { + $lastUpdatedAt = filemtime($this->getSavePath() . $file); + if( time() - $lastUpdatedAt > $this->lifeTime ){ + unlink($this->getSavePath() . $file); + } + } + } + } + + public function open() + { + if ($this->getIsActive()) { + return; + } + $file = $this->getSessionFullName(); + if( file_exists($file) && is_file($file) ) { + $data = file_get_contents($file); + $_SESSION = json_decode($data, true); + }else{ + $_SESSION = []; + } + $this->_started = true; + } + + public function getCookieParams() + { + return $this->_cookieParams; + } + + public function setCookieParams(array $config){ + $this->_cookieParams; + } + + public function destroy() + { + if ($this->getIsActive()) { + $_SESSION = []; + } + } + + public function getIsActive() + { + return $this->_started; + } + + private $_hasSessionId; + + public function getHasSessionId() + { + if ($this->_hasSessionId === null) { + $name = $this->getName(); + $request = Yii::$app->getRequest(); + if (!empty($_COOKIE[$name]) && ini_get('session.use_cookies')) { + $this->_hasSessionId = true; + } elseif (!ini_get('session.use_only_cookies') && ini_get('session.use_trans_sid')) { + $this->_hasSessionId = $request->get($name) != ''; + } else { + $this->_hasSessionId = false; + } + } + return $this->_hasSessionId; + } + + public function setHasSessionId($value) + { + $this->_hasSessionId = $value; + } + + public function getId() + { + if( isset($_COOKIE[$this->getName()]) ){ + $id = $_COOKIE[$this->getName()]; + }else{ + $id = uniqid(); + } + return $id; + } + + public function regenerateID($deleteOldSession = false) + { + } + + public function getName() + { + return "feehi_session"; + } + + public function getSavePath() + { + if( strrpos( $this->savePath, '/') !==0 ){ + $this->savePath .= '/'; + } + if( !is_readable($this->savePath) ){ + throw new InvalidConfigException("SESSION saved path {$this->savePath} is not readable"); + } + if( !is_writable($this->savePath) ){ + throw new InvalidConfigException("SESSION saved path {$this->savePath} is not writable"); + } + return $this->savePath; + } + + public function setSavePath($value) + { + $this->savePath = $value; + } + + public function getIterator() + { + $this->open(); + return new SessionIterator(); + } + + public function getCount() + { + $this->open(); + return count($_SESSION); + } + + public function count() + { + return $this->getCount(); + } + + public function get($key, $defaultValue = null) + { + $this->open(); + return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; + } + + public function set($key, $value) + { + $this->open(); + $_SESSION[$key] = $value; + } + + public function remove($key) + { + $this->open(); + if (isset($_SESSION[$key])) { + $value = $_SESSION[$key]; + unset($_SESSION[$key]); + return $value; + } + return null; + } + + public function removeAll() + { + $this->open(); + foreach (array_keys($_SESSION) as $key) { + unset($_SESSION[$key]); + } + } + + public function has($key) + { + $this->open(); + return isset($_SESSION[$key]); + } + + protected function updateFlashCounters() + { + $counters = $this->get($this->flashParam, []); + if (is_array($counters)) { + foreach ($counters as $key => $count) { + if ($count > 0) { + unset($counters[$key], $_SESSION[$key]); + } elseif ($count == 0) { + $counters[$key]++; + } + } + $_SESSION[$this->flashParam] = $counters; + } else { + // fix the unexpected problem that flashParam doesn't return an array + unset($_SESSION[$this->flashParam]); + } + } + + public function getFlash($key, $defaultValue = null, $delete = false) + { + $counters = $this->get($this->flashParam, []); + if (isset($counters[$key])) { + $value = $this->get($key, $defaultValue); + if ($delete) { + $this->removeFlash($key); + } elseif ($counters[$key] < 0) { + // mark for deletion in the next request + $counters[$key] = 1; + $_SESSION[$this->flashParam] = $counters; + } + return $value; + } + return $defaultValue; + } + + public function getAllFlashes($delete = false) + { + $counters = $this->get($this->flashParam, []); + $flashes = []; + foreach (array_keys($counters) as $key) { + if (array_key_exists($key, $_SESSION)) { + $flashes[$key] = $_SESSION[$key]; + if ($delete) { + unset($counters[$key], $_SESSION[$key]); + } elseif ($counters[$key] < 0) { + // mark for deletion in the next request + $counters[$key] = 1; + } + } else { + unset($counters[$key]); + } + } + $_SESSION[$this->flashParam] = $counters; + return $flashes; + } + + public function setFlash($key, $value = true, $removeAfterAccess = true) + { + $counters = $this->get($this->flashParam, []); + $counters[$key] = $removeAfterAccess ? -1 : 0; + $_SESSION[$key] = $value; + $_SESSION[$this->flashParam] = $counters; + } + + public function addFlash($key, $value = true, $removeAfterAccess = true) + { + $counters = $this->get($this->flashParam, []); + $counters[$key] = $removeAfterAccess ? -1 : 0; + $_SESSION[$this->flashParam] = $counters; + if (empty($_SESSION[$key])) { + $_SESSION[$key] = [$value]; + } else { + if (is_array($_SESSION[$key])) { + $_SESSION[$key][] = $value; + } else { + $_SESSION[$key] = [$_SESSION[$key], $value]; + } + } + } + + public function removeFlash($key) + { + $counters = $this->get($this->flashParam, []); + $value = isset($_SESSION[$key], $counters[$key]) ? $_SESSION[$key] : null; + unset($counters[$key], $_SESSION[$key]); + $_SESSION[$this->flashParam] = $counters; + return $value; + } + + public function removeAllFlashes() + { + $counters = $this->get($this->flashParam, []); + foreach (array_keys($counters) as $key) { + unset($_SESSION[$key]); + } + unset($_SESSION[$this->flashParam]); + } + + public function hasFlash($key) + { + return $this->getFlash($key) !== null; + } + + public function offsetExists($offset) + { + $this->open(); + return isset($_SESSION[$offset]); + } + + public function offsetGet($offset) + { + $this->open(); + return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; + } + + public function offsetSet($offset, $item) + { + $this->open(); + $_SESSION[$offset] = $item; + } + + public function offsetUnset($offset) + { + $this->open(); + unset($_SESSION[$offset]); + } +} \ No newline at end of file