/** * Shorthand access to common filter types. */ static function &commonFilters() { static $filters; if (!$filters) { $filters = array('raw' => array('filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_NULL_ON_FAILURE), 'rawS' => array('filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_REQUIRE_SCALAR | FILTER_NULL_ON_FAILURE), 'rawA' => array('filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE), 'boolS' => array('filter' => FILTER_VALIDATE_BOOLEAN, 'flags' => FILTER_REQUIRE_SCALAR | FILTER_NULL_ON_FAILURE), 'intS' => array('filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_SCALAR | FILTER_NULL_ON_FAILURE), 'intA' => array('filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE), 'floatS' => array('filter' => FILTER_VALIDATE_FLOAT, 'flags' => FILTER_REQUIRE_SCALAR | FILTER_NULL_ON_FAILURE), 'floatA' => array('filter' => FILTER_VALIDATE_FLOAT, 'flags' => FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE), 'strS' => array('filter' => FILTER_SANITIZE_STRING, 'flags' => FILTER_REQUIRE_SCALAR | FILTER_NULL_ON_FAILURE | FILTER_FLAG_NO_ENCODE_QUOTES), 'strA' => array('filter' => FILTER_SANITIZE_STRING, 'flags' => FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE | FILTER_FLAG_NO_ENCODE_QUOTES), 'urlS' => array('filter' => FILTER_VALIDATE_URL, 'flags' => FILTER_REQUIRE_SCALAR | FILTER_NULL_ON_FAILURE), 'urlA' => array('filter' => FILTER_VALIDATE_URL, 'flags' => FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE), 'date' => array('filter' => FILTER_CALLBACK, 'flags' => FILTER_NULL_ON_FAILURE, 'options' => '\\core\\Utility::validateDateTime'), 'dateS' => array('filter' => FILTER_CALLBACK, 'flags' => FILTER_REQUIRE_SCALAR | FILTER_NULL_ON_FAILURE, 'options' => '\\core\\Utility::validateDateTime'), 'priceS' => array('filter' => FILTER_CALLBACK, 'options' => function ($input) { return preg_match('/[+-]?\\d+(?:\\.\\d+)?(?:\\:\\w+)?/', trim(Utility::unwrapAssoc($input))) ? $input : null; }), 'regex' => function ($pattern) { return array('filter' => FILTER_CALLBACK, 'options' => function ($input) use($pattern) { return preg_match($pattern, $input) ? $input : null; }); }); } return $filters; }
/** * Send HTTP request to a URL. * * The format is purposedly copied as much as possible from jQuery.ajax() function. * * To initiate multiple requests, pass it as an array and wrapping parameters of * each single request as an array. * * @param {string} $options['url'] Target request url * @param {?string} $options['type'] Request method, defaults to GET. * @param {?array|string} $options['data'] Either raw string or array to be encoded, * to be sent as query string on GET, HEAD, DELETE and OPTION * or message body on POST or PUT. * @param {?array} $options['headers'] Request headers to be sent. * @param {?callable} $options['progress'] Callback function for progress ticks. * function($progress, $current, $maximum); * @param {?callable} $options['success'] Callback function on successful request. * function($responseText, $curlOptions); * @param {?callable} $options['failure'] Callback function on request failure, with curl errors as parameters. * function($errorNumber, $errorMessage, $curlOptions); * @param {?array} $options['__curlOpts'] Curl options to be passed directly to curl_setopt_array. * * @return void */ public static function httpRequest($options) { $options = Utility::wrapAssoc((array) $options); $options = array_map(function (&$option) { if (is_string($option)) { $option = array('url' => $option); } else { if (!@$option['url']) { throw new exceptions\CoreException('No URL set!'); } } // Auto prepend http, default protocol. if (preg_match('/^(\\/\\/)/', $option['url'])) { $option['url'] = "http:" . $option['url']; } $curlOption = array(CURLOPT_URL => $option['url'], CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_CAINFO => null, CURLOPT_CAPATH => null, CURLOPT_FOLLOWLOCATION => true); // Request method: 'GET', 'POST', 'PUT', 'HEAD', 'DELETE' if (!isset($option['type']) && is_array(@$option['data']) || preg_match('/^post$/i', @$option['type'])) { $curlOption[CURLOPT_POST] = true; $curlOption[CURLOPT_CUSTOMREQUEST] = 'POST'; } elseif (preg_match('/^put$/i', @$option['type'])) { if (!@$option['file'] || !is_file($option['file'])) { throw new exceptions\CoreException('Please specify the \'file\' option when using PUT method.'); } $curlOption[CURLOPT_PUT] = true; $curlOption[CURLOPT_CUSTOMREQUEST] = 'PUT'; $curlOption[CURLOPT_UPLOAD] = true; $curlOption[CURLOPT_INFILE] = fopen($option['file'], 'r'); $curlOption[CURLOPT_INFILESIZE] = filesize($option['file']); } elseif (preg_match('/^head$/i', @$option['type'])) { $curlOption[CURLOPT_NOBODY] = true; $curlOption[CURLOPT_CUSTOMREQUEST] = 'HEAD'; } elseif (preg_match('/^delete$/i', @$option['type'])) { $curlOption[CURLOPT_CUSTOMREQUEST] = 'DELETE'; } else { $curlOption[CURLOPT_CUSTOMREQUEST] = 'GET'; } // Query data, applicable for all request methods. if (@$option['data']) { $data = $option['data']; // The data contains traditional file POST value: "@/foo/bar" $hasPostFile = is_array($data) && array_reduce($data, function ($ret, $val) { return $ret || is_a($val, 'CurlFile') || is_string($val) && strpos($val, '@') === 0 && file_exists(Utility::unwrapAssoc(explode(';', substr($val, 1)))); }, false); // Build query regardless if file exists on PHP < 5.2.0, otherwise // only build when there is NOT files to be POSTed. // Skip the whole build if $data is not array or object. if ((version_compare(PHP_VERSION, '5.2.0', '<') || !$hasPostFile) && (is_array($data) || is_object($data))) { $data = http_build_query($data); } if (version_compare(PHP_VERSION, '5.5.0', '>=') && $hasPostFile) { array_walk_recursive($data, function (&$value, $key) { if (is_string($value) && strpos($value, '@') === 0) { @(list($path, $type) = explode(';', substr($value, 1))); if (!$type) { $type = Utility::getInfo($path, FILEINFO_MIME_TYPE); } $value = curl_file_create($path, $type, $key); } }); } if (@$curlOption[CURLOPT_POST] === true) { $curlOption[CURLOPT_POSTFIELDS] = $data; } else { $url =& $curlOption[CURLOPT_URL]; $url .= (strpos($url, '?') === false ? '?' : '&') . $data; } } // HTTP Headers if (isset($option['headers'])) { $curlOption[CURLOPT_HTTPHEADER] =& $option['headers']; } // Data type converting if (isset($option['success'])) { $originalSuccess = @$option['success']; switch (@$option['dataType']) { case 'json': $option['success'] = function ($response, $curlOptions) use($option, $originalSuccess) { $result = @ContentEncoder::json($response); if ($result === false && $response) { Utility::forceInvoke(@$option['failure'], array(3, 'Malformed JSON string returned.', $curlOptions)); } else { Utility::forceInvoke(@$originalSuccess, array($result, $curlOptions)); } }; break; case 'xml': $option['success'] = function ($response, $curlOptions) use($option, $originalSuccess) { try { $result = XMLConverter::fromXML($response); } catch (\Exception $e) { $result = NULL; } if ($result === NULL && $response) { Utility::forceInvoke(@$option['failure'], array(2, 'Malformed XML string returned.', $curlOptions)); } else { Utility::forceInvoke(@$originalSuccess, array($result, $curlOptions)); } }; break; } unset($originalSuccess); } $curlOption['callbacks'] = array_filter(array('progress' => @$option['progress'], 'success' => @$option['success'], 'failure' => @$option['failure'], 'complete' => @$option['complete'])); $curlOption = (array) @$option['__curlOpts'] + $curlOption; if (System::environment() == 'debug') { Log::debug('Net ' . $curlOption[CURLOPT_CUSTOMREQUEST] . ' to ' . $curlOption[CURLOPT_URL], $curlOption); } return $curlOption; }, $options); return self::curlRequest($options); }
public function resolve(Request $request, Response $response) { $path = $this->srcPath . $request->uri('path') . '.url'; // Check if target file is a proxy. if (!is_file($path)) { return; } $cacheTarget = parse_ini_file($path); $cacheTarget = @$cacheTarget['URL']; unset($path); if (!$cacheTarget) { Log::warning('Proxy file has not URL parameter.', array('requestUri' => $request->uri(), 'proxyFile' => $request->uri('path') . '.uri')); $response->status(502); // Bad Gateway return; } /*! Cache Header Notes * * # Cache-Control * [public | private] Cacheable when public, otherwise the client is responsible for caching. * [no-cache( \w+)?] When no fields are specified, the whole thing must revalidate everytime, * otherwise cache it except specified fields. * [no-store] Ignore caching and pipe into output. * [max-age=\d+] Seconds before this cache is meant to expire, this overrides Expires header. * [s-maxage=\d+] Overrides max-age and Expires header, behaves just like max-age. * (This is for CDN and we are using it.) * [must-revalidate] Tells those CDNs which are intended to serve stale contents to revalidate every time. * [proxy-revalidate] Like the "s-" version of max-age, a "must-revalidate" override only for CDN. * [no-transform] Some CDNs will optimize images and other formats, this "opt-out" of it. * * # Expires * RFC timestamp for an absolute cache expiration, overridden by Cache-Control header. * * # ETag * Hash of anything, weak ETags is not supported at this moment. * * # vary * Too much fun inside and we are too serious about caching, ignore this. * * # pragma * This guy is too old to recognize. * [no-cache] Only this is known nowadays and is already succeed by Cache-Control: no-cache. * */ // note; Use "cache-meta://" scheme for header and cache meta info, for performance. // 1. Check if cache exists. $cache = (array) Cache::get("cache-meta://{$cacheTarget}"); // Cache expiration, in seconds. // expires = ( s-maxage || max-age || Expires ); if (@$cache['expires'] && time() > $cache['expires']) { Cache::delete("cache-meta://{$cacheTarget}"); Cache::delete("cache://{$cacheTarget}"); $cache = null; } // - If not exists, make normal request to remote server. // - If exists, make conditional request to remote server. // - Revalidation, we can skip this request and serve the content if false. // revalidates = ( Cache-Control:proxy-revalidate || Cache-Control:must-revalidate ) if (!$cache || @$cache['revalidates']) { $_request = array('uri' => $cacheTarget); if ($cache) { // Last-Modified if (@$cache['headers']['Last-Modified']) { $_request['headers']['If-Modified-Since'] = $cache['Last-Modified']; } // Entity-Tag if (@$cache['headers']['ETag'] && strpos($cache['headers']['ETag'], 'W\\') !== 0) { $_request['headers']['If-None-Match'] = $cache['ETag']; } } else { $cache = array(); } // Make the request $_response = new Response(array('autoOutput' => false)); (new Request($_request))->send(null, $_response); unset($_request); // parse headers into cache settings. if (in_array($_response->status(), array(200, 304))) { $res = preg_split('/\\s*,\\s*/', util::unwrapAssoc($_response->header('Cache-Control'))); $res = array_reduce($res, function ($res, $value) { // todo; Take care of no-cache with field name. if (strpos($value, '=') > 0) { $value = explode('=', $value); $res[$value[0]] = $value[1]; } else { $res[$value] = true; } return $res; }, array()); // private, no-store, no-cache if (@$res['private'] || @$res['no-store'] || @$res['no-cache']) { // note; in case the upstream server change this to uncacheable Cache::delete("cache-meta://{$cacheTarget}"); Cache::delete("cache://{$cacheTarget}"); $_response->clearBody(); } if ($_response->status() == 200 && $_response->body()) { $cache['contents'] = $_response->body(); } // expires = ( s-maxage || max-age || Expires ); if (@$res['s-maxage']) { $cache['expires'] = time() + $res['s-maxage']; } elseif (@$res['max-age']) { $cache['expires'] = time() + $res['max-age']; } else { $res = util::unwrapAssoc($_response->header('Expires')); if ($res) { $cache['expires'] = strtotime($res); } } // revalidates = ( Cache-Control:proxy-revalidate || Cache-Control:must-revalidate ) if (@$res['proxy-revalidate'] || @$res['must-revalidate']) { $cache['revalidates'] = true; } unset($res); } $cache['headers'] = array_map('core\\Utility::unwrapAssoc', $_response->header()); // PHP does not support chunked, skip this one. unset($cache['headers']['Transfer-Encoding']); // note; If cache is to be ignored, the $cacheTarget variable will be already unset(). if (isset($cacheTarget)) { if (@$cache['contents']) { Cache::set("cache://{$cacheTarget}", $cache['contents']); } Cache::set("cache-meta://{$cacheTarget}", array_filter_keys($cache, isNot('contents'))); } unset($_response); } // note; Send cache headers regardless of the request condition. if (@$cache['headers']) { $response->clearHeaders(); foreach ($cache['headers'] as $name => $value) { $response->header($name, $value, true); } unset($name, $value); } // note; Handles conditional request $ch = array_map('core\\Utility::unwrapAssoc', (array) @$cache['headers']); $mtime = @$ch['Last-Modified'] ? strtotime($ch['Last-Modified']) : false; // Request headr: If-Modified-Since if (@$ch['Last-Modified'] && $mtime) { if (strtotime($request->header('If-Modified-Since')) >= $mtime) { return $response->status(304); } } // Request header: If-Range if ($request->header('If-Range')) { // Entity tag if (strpos(substr($request->header('If-Range'), 0, 2), '"') !== false && @$ch['ETag']) { if ($this->compareETags(@$ch['ETag'], $request->header('If-Range'))) { return $this->response()->status(304); } } elseif (strtotime($request->header('If-Range')) === $mtime) { return $this->response()->status(304); } } unset($mtime); // Request header: If-None-Match if (!$request->header('If-Modified-Since') && $request->header('If-None-Match')) { // Exists but not GET or HEAD switch ($request->method()) { case 'get': case 'head': break; default: return $this->response()->status(412); } /*! Note by Vicary @ 24 Jan, 2013 * If-None-Match means 304 when target resources exists. */ if ($request->header('If-None-Match') === '*' && @$ch['ETag']) { return $this->response()->status(304); } if ($this->compareETags(@$ch['ETag'], preg_split('/\\s*,\\s*/', $request->header('If-None-Match')))) { return $this->response()->status(304); } } // Request header: If-Match if (!$request->header('If-Modified-Since') && $request->header('If-Match')) { // Exists but not GET or HEAD switch ($request->method()) { case 'get': case 'head': break; default: return $this->response()->status(412); } if ($request->header('If-Match') === '*' && !@$ch['ETag']) { return $this->response()->status(412); } preg_match_all('/(?:^\\*$|(:?"([^\\*"]+)")(?:\\s*,\\s*(:?"([^\\*"]+)")))$/', $request->header('If-Match'), $eTags); // 412 Precondition Failed when nothing matches. if (@$eTags[1] && !in_array($eTag, (array) $eTags[1])) { return $this->response()->status(412); } } if ($cacheTarget && empty($cache['contents'])) { $cache['contents'] = Cache::get("cache://{$cacheTarget}"); } // Output the cahce content $response->send($cache['contents'], 200); }
/** * Retrieve process related info by specified property $name. * * @param {string} $name Target property in process object, omit this to get the whole object. */ public static function get($name = null) { if (constant('PHP_SAPI') != 'cli' || !function_exists('posix_getppid')) { return null; } $processData =& self::$_processData; if (!$processData) { $processData = Node::get(array(Node::FIELD_COLLECTION => FRAMEWORK_COLLECTION_PROCESS, 'pid' => [getmypid(), posix_getppid()])); $processData = util::unwrapAssoc($processData); } if (is_null($name)) { return $processData; } else { return @$processData[$name]; } }