Provide missing default attachment list for Files transactions
[phabricator.git] / src / aphront / AphrontRequest.php
blobf35f3f12792c0c66b953b9fcf848da285a8de590
1 <?php
3 /**
4 * @task data Accessing Request Data
5 * @task cookie Managing Cookies
6 * @task cluster Working With a Phabricator Cluster
7 */
8 final class AphrontRequest extends Phobject {
10 // NOTE: These magic request-type parameters are automatically included in
11 // certain requests (e.g., by phabricator_form(), JX.Request,
12 // JX.Workflow, and ConduitClient) and help us figure out what sort of
13 // response the client expects.
15 const TYPE_AJAX = '__ajax__';
16 const TYPE_FORM = '__form__';
17 const TYPE_CONDUIT = '__conduit__';
18 const TYPE_WORKFLOW = '__wflow__';
19 const TYPE_CONTINUE = '__continue__';
20 const TYPE_PREVIEW = '__preview__';
21 const TYPE_HISEC = '__hisec__';
22 const TYPE_QUICKSAND = '__quicksand__';
24 private $host;
25 private $path;
26 private $requestData;
27 private $user;
28 private $applicationConfiguration;
29 private $site;
30 private $controller;
31 private $uriData = array();
32 private $cookiePrefix;
33 private $submitKey;
35 public function __construct($host, $path) {
36 $this->host = $host;
37 $this->path = $path;
40 public function setURIMap(array $uri_data) {
41 $this->uriData = $uri_data;
42 return $this;
45 public function getURIMap() {
46 return $this->uriData;
49 public function getURIData($key, $default = null) {
50 return idx($this->uriData, $key, $default);
53 /**
54 * Read line range parameter data from the request.
56 * Applications like Paste, Diffusion, and Harbormaster use "$12-14" in the
57 * URI to allow users to link to particular lines.
59 * @param string URI data key to pull line range information from.
60 * @param int|null Maximum length of the range.
61 * @return null|pair<int, int> Null, or beginning and end of the range.
63 public function getURILineRange($key, $limit) {
64 $range = $this->getURIData($key);
65 return self::parseURILineRange($range, $limit);
68 public static function parseURILineRange($range, $limit) {
69 if (!strlen($range)) {
70 return null;
73 $range = explode('-', $range, 2);
75 foreach ($range as $key => $value) {
76 $value = (int)$value;
77 if (!$value) {
78 // If either value is "0", discard the range.
79 return null;
81 $range[$key] = $value;
84 // If the range is like "$10", treat it like "$10-10".
85 if (count($range) == 1) {
86 $range[] = head($range);
89 // If the range is "$7-5", treat it like "$5-7".
90 if ($range[1] < $range[0]) {
91 $range = array_reverse($range);
94 // If the user specified something like "$1-999999999" and we have a limit,
95 // clamp it to a more reasonable range.
96 if ($limit !== null) {
97 if ($range[1] - $range[0] > $limit) {
98 $range[1] = $range[0] + $limit;
102 return $range;
105 public function setApplicationConfiguration(
106 $application_configuration) {
107 $this->applicationConfiguration = $application_configuration;
108 return $this;
111 public function getApplicationConfiguration() {
112 return $this->applicationConfiguration;
115 public function setPath($path) {
116 $this->path = $path;
117 return $this;
120 public function getPath() {
121 return $this->path;
124 public function getHost() {
125 // The "Host" header may include a port number, or may be a malicious
126 // header in the form "realdomain.com:ignored@evil.com". Invoke the full
127 // parser to extract the real domain correctly. See here for coverage of
128 // a similar issue in Django:
130 // https://www.djangoproject.com/weblog/2012/oct/17/security/
131 $uri = new PhutilURI('http://'.$this->host);
132 return $uri->getDomain();
135 public function setSite(AphrontSite $site) {
136 $this->site = $site;
137 return $this;
140 public function getSite() {
141 return $this->site;
144 public function setController(AphrontController $controller) {
145 $this->controller = $controller;
146 return $this;
149 public function getController() {
150 return $this->controller;
154 /* -( Accessing Request Data )--------------------------------------------- */
158 * @task data
160 public function setRequestData(array $request_data) {
161 $this->requestData = $request_data;
162 return $this;
167 * @task data
169 public function getRequestData() {
170 return $this->requestData;
175 * @task data
177 public function getInt($name, $default = null) {
178 if (isset($this->requestData[$name])) {
179 // Converting from array to int is "undefined". Don't rely on whatever
180 // PHP decides to do.
181 if (is_array($this->requestData[$name])) {
182 return $default;
184 return (int)$this->requestData[$name];
185 } else {
186 return $default;
192 * @task data
194 public function getBool($name, $default = null) {
195 if (isset($this->requestData[$name])) {
196 if ($this->requestData[$name] === 'true') {
197 return true;
198 } else if ($this->requestData[$name] === 'false') {
199 return false;
200 } else {
201 return (bool)$this->requestData[$name];
203 } else {
204 return $default;
210 * @task data
212 public function getStr($name, $default = null) {
213 if (isset($this->requestData[$name])) {
214 $str = (string)$this->requestData[$name];
215 // Normalize newline craziness.
216 $str = str_replace(
217 array("\r\n", "\r"),
218 array("\n", "\n"),
219 $str);
220 return $str;
221 } else {
222 return $default;
228 * @task data
230 public function getJSONMap($name, $default = array()) {
231 if (!isset($this->requestData[$name])) {
232 return $default;
235 $raw_data = phutil_string_cast($this->requestData[$name]);
236 $raw_data = trim($raw_data);
237 if (!strlen($raw_data)) {
238 return $default;
241 if ($raw_data[0] !== '{') {
242 throw new Exception(
243 pht(
244 'Request parameter "%s" is not formatted properly. Expected a '.
245 'JSON object, but value does not start with "{".',
246 $name));
249 try {
250 $json_object = phutil_json_decode($raw_data);
251 } catch (PhutilJSONParserException $ex) {
252 throw new Exception(
253 pht(
254 'Request parameter "%s" is not formatted properly. Expected a '.
255 'JSON object, but encountered a syntax error: %s.',
256 $name,
257 $ex->getMessage()));
260 return $json_object;
265 * @task data
267 public function getArr($name, $default = array()) {
268 if (isset($this->requestData[$name]) &&
269 is_array($this->requestData[$name])) {
270 return $this->requestData[$name];
271 } else {
272 return $default;
278 * @task data
280 public function getStrList($name, $default = array()) {
281 if (!isset($this->requestData[$name])) {
282 return $default;
284 $list = $this->getStr($name);
285 $list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
286 return $list;
291 * @task data
293 public function getExists($name) {
294 return array_key_exists($name, $this->requestData);
297 public function getFileExists($name) {
298 return isset($_FILES[$name]) &&
299 (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
302 public function isHTTPGet() {
303 return ($_SERVER['REQUEST_METHOD'] == 'GET');
306 public function isHTTPPost() {
307 return ($_SERVER['REQUEST_METHOD'] == 'POST');
310 public function isAjax() {
311 return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
314 public function isWorkflow() {
315 return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
318 public function isQuicksand() {
319 return $this->getExists(self::TYPE_QUICKSAND);
322 public function isConduit() {
323 return $this->getExists(self::TYPE_CONDUIT);
326 public static function getCSRFTokenName() {
327 return '__csrf__';
330 public static function getCSRFHeaderName() {
331 return 'X-Phabricator-Csrf';
334 public static function getViaHeaderName() {
335 return 'X-Phabricator-Via';
338 public function validateCSRF() {
339 $token_name = self::getCSRFTokenName();
340 $token = $this->getStr($token_name);
342 // No token in the request, check the HTTP header which is added for Ajax
343 // requests.
344 if (empty($token)) {
345 $token = self::getHTTPHeader(self::getCSRFHeaderName());
348 $valid = $this->getUser()->validateCSRFToken($token);
349 if (!$valid) {
351 // Add some diagnostic details so we can figure out if some CSRF issues
352 // are JS problems or people accessing Ajax URIs directly with their
353 // browsers.
354 $info = array();
356 $info[] = pht(
357 'You are trying to save some data to permanent storage, but the '.
358 'request your browser made included an incorrect token. Reload the '.
359 'page and try again. You may need to clear your cookies.');
361 if ($this->isAjax()) {
362 $info[] = pht('This was an Ajax request.');
363 } else {
364 $info[] = pht('This was a Web request.');
367 if ($token) {
368 $info[] = pht('This request had an invalid CSRF token.');
369 } else {
370 $info[] = pht('This request had no CSRF token.');
373 // Give a more detailed explanation of how to avoid the exception
374 // in developer mode.
375 if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
376 // TODO: Clean this up, see T1921.
377 $info[] = pht(
378 "To avoid this error, use %s to construct forms. If you are already ".
379 "using %s, make sure the form 'action' uses a relative URI (i.e., ".
380 "begins with a '%s'). Forms using absolute URIs do not include CSRF ".
381 "tokens, to prevent leaking tokens to external sites.\n\n".
382 "If this page performs writes which do not require CSRF protection ".
383 "(usually, filling caches or logging), you can use %s to ".
384 "temporarily bypass CSRF protection while writing. You should use ".
385 "this only for writes which can not be protected with normal CSRF ".
386 "mechanisms.\n\n".
387 "Some UI elements (like %s) also have methods which will allow you ".
388 "to render links as forms (like %s).",
389 'phabricator_form()',
390 'phabricator_form()',
391 '/',
392 'AphrontWriteGuard::beginScopedUnguardedWrites()',
393 'PhabricatorActionListView',
394 'setRenderAsForm(true)');
397 $message = implode("\n", $info);
399 // This should only be able to happen if you load a form, pull your
400 // internet for 6 hours, and then reconnect and immediately submit,
401 // but give the user some indication of what happened since the workflow
402 // is incredibly confusing otherwise.
403 throw new AphrontMalformedRequestException(
404 pht('Invalid Request (CSRF)'),
405 $message,
406 true);
409 return true;
412 public function isFormPost() {
413 $post = $this->getExists(self::TYPE_FORM) &&
414 !$this->getExists(self::TYPE_HISEC) &&
415 $this->isHTTPPost();
417 if (!$post) {
418 return false;
421 return $this->validateCSRF();
424 public function hasCSRF() {
425 try {
426 $this->validateCSRF();
427 return true;
428 } catch (AphrontMalformedRequestException $ex) {
429 return false;
433 public function isFormOrHisecPost() {
434 $post = $this->getExists(self::TYPE_FORM) &&
435 $this->isHTTPPost();
437 if (!$post) {
438 return false;
441 return $this->validateCSRF();
445 public function setCookiePrefix($prefix) {
446 $this->cookiePrefix = $prefix;
447 return $this;
450 private function getPrefixedCookieName($name) {
451 if (strlen($this->cookiePrefix)) {
452 return $this->cookiePrefix.'_'.$name;
453 } else {
454 return $name;
458 public function getCookie($name, $default = null) {
459 $name = $this->getPrefixedCookieName($name);
460 $value = idx($_COOKIE, $name, $default);
462 // Internally, PHP deletes cookies by setting them to the value 'deleted'
463 // with an expiration date in the past.
465 // At least in Safari, the browser may send this cookie anyway in some
466 // circumstances. After logging out, the 302'd GET to /login/ consistently
467 // includes deleted cookies on my local install. If a cookie value is
468 // literally 'deleted', pretend it does not exist.
470 if ($value === 'deleted') {
471 return null;
474 return $value;
477 public function clearCookie($name) {
478 $this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
479 unset($_COOKIE[$name]);
483 * Get the domain which cookies should be set on for this request, or null
484 * if the request does not correspond to a valid cookie domain.
486 * @return PhutilURI|null Domain URI, or null if no valid domain exists.
488 * @task cookie
490 private function getCookieDomainURI() {
491 if (PhabricatorEnv::getEnvConfig('security.require-https') &&
492 !$this->isHTTPS()) {
493 return null;
496 $host = $this->getHost();
498 // If there's no base domain configured, just use whatever the request
499 // domain is. This makes setup easier, and we'll tell administrators to
500 // configure a base domain during the setup process.
501 $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
502 if (!strlen($base_uri)) {
503 return new PhutilURI('http://'.$host.'/');
506 $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
507 $allowed_uris = array_merge(
508 array($base_uri),
509 $alternates);
511 foreach ($allowed_uris as $allowed_uri) {
512 $uri = new PhutilURI($allowed_uri);
513 if ($uri->getDomain() == $host) {
514 return $uri;
518 return null;
522 * Determine if security policy rules will allow cookies to be set when
523 * responding to the request.
525 * @return bool True if setCookie() will succeed. If this method returns
526 * false, setCookie() will throw.
528 * @task cookie
530 public function canSetCookies() {
531 return (bool)$this->getCookieDomainURI();
536 * Set a cookie which does not expire for a long time.
538 * To set a temporary cookie, see @{method:setTemporaryCookie}.
540 * @param string Cookie name.
541 * @param string Cookie value.
542 * @return this
543 * @task cookie
545 public function setCookie($name, $value) {
546 $far_future = time() + (60 * 60 * 24 * 365 * 5);
547 return $this->setCookieWithExpiration($name, $value, $far_future);
552 * Set a cookie which expires soon.
554 * To set a durable cookie, see @{method:setCookie}.
556 * @param string Cookie name.
557 * @param string Cookie value.
558 * @return this
559 * @task cookie
561 public function setTemporaryCookie($name, $value) {
562 return $this->setCookieWithExpiration($name, $value, 0);
567 * Set a cookie with a given expiration policy.
569 * @param string Cookie name.
570 * @param string Cookie value.
571 * @param int Epoch timestamp for cookie expiration.
572 * @return this
573 * @task cookie
575 private function setCookieWithExpiration(
576 $name,
577 $value,
578 $expire) {
580 $is_secure = false;
582 $base_domain_uri = $this->getCookieDomainURI();
583 if (!$base_domain_uri) {
584 $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
585 $accessed_as = $this->getHost();
587 throw new AphrontMalformedRequestException(
588 pht('Bad Host Header'),
589 pht(
590 'This server is configured as "%s", but you are using the domain '.
591 'name "%s" to access a page which is trying to set a cookie. '.
592 'Access this service on the configured primary domain or a '.
593 'configured alternate domain. Cookies will not be set on other '.
594 'domains for security reasons.',
595 $configured_as,
596 $accessed_as),
597 true);
600 $base_domain = $base_domain_uri->getDomain();
601 $is_secure = ($base_domain_uri->getProtocol() == 'https');
603 $name = $this->getPrefixedCookieName($name);
605 if (php_sapi_name() == 'cli') {
606 // Do nothing, to avoid triggering "Cannot modify header information"
607 // warnings.
609 // TODO: This is effectively a test for whether we're running in a unit
610 // test or not. Move this actual call to HTTPSink?
611 } else {
612 setcookie(
613 $name,
614 $value,
615 $expire,
616 $path = '/',
617 $base_domain,
618 $is_secure,
619 $http_only = true);
622 $_COOKIE[$name] = $value;
624 return $this;
627 public function setUser($user) {
628 $this->user = $user;
629 return $this;
632 public function getUser() {
633 return $this->user;
636 public function getViewer() {
637 return $this->user;
640 public function getRequestURI() {
641 $uri_path = phutil_escape_uri($this->getPath());
642 $uri_query = idx($_SERVER, 'QUERY_STRING', '');
644 return id(new PhutilURI($uri_path.'?'.$uri_query))
645 ->removeQueryParam('__path__');
648 public function getAbsoluteRequestURI() {
649 $uri = $this->getRequestURI();
650 $uri->setDomain($this->getHost());
652 if ($this->isHTTPS()) {
653 $protocol = 'https';
654 } else {
655 $protocol = 'http';
658 $uri->setProtocol($protocol);
660 // If the request used a nonstandard port, preserve it while building the
661 // absolute URI.
663 // First, get the default port for the request protocol.
664 $default_port = id(new PhutilURI($protocol.'://example.com/'))
665 ->getPortWithProtocolDefault();
667 // NOTE: See note in getHost() about malicious "Host" headers. This
668 // construction defuses some obscure potential attacks.
669 $port = id(new PhutilURI($protocol.'://'.$this->host))
670 ->getPort();
672 if (($port !== null) && ($port !== $default_port)) {
673 $uri->setPort($port);
676 return $uri;
679 public function isDialogFormPost() {
680 return $this->isFormPost() && $this->getStr('__dialog__');
683 public function getRemoteAddress() {
684 $address = PhabricatorEnv::getRemoteAddress();
686 if (!$address) {
687 return null;
690 return $address->getAddress();
693 public function isHTTPS() {
694 if (empty($_SERVER['HTTPS'])) {
695 return false;
697 if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
698 return false;
700 return true;
703 public function isContinueRequest() {
704 return $this->isFormOrHisecPost() && $this->getStr('__continue__');
707 public function isPreviewRequest() {
708 return $this->isFormPost() && $this->getStr('__preview__');
712 * Get application request parameters in a flattened form suitable for
713 * inclusion in an HTTP request, excluding parameters with special meanings.
714 * This is primarily useful if you want to ask the user for more input and
715 * then resubmit their request.
717 * @return dict<string, string> Original request parameters.
719 public function getPassthroughRequestParameters($include_quicksand = false) {
720 return self::flattenData(
721 $this->getPassthroughRequestData($include_quicksand));
725 * Get request data other than "magic" parameters.
727 * @return dict<string, wild> Request data, with magic filtered out.
729 public function getPassthroughRequestData($include_quicksand = false) {
730 $data = $this->getRequestData();
732 // Remove magic parameters like __dialog__ and __ajax__.
733 foreach ($data as $key => $value) {
734 if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
735 continue;
737 if (!strncmp($key, '__', 2)) {
738 unset($data[$key]);
742 return $data;
747 * Flatten an array of key-value pairs (possibly including arrays as values)
748 * into a list of key-value pairs suitable for submitting via HTTP request
749 * (with arrays flattened).
751 * @param dict<string, wild> Data to flatten.
752 * @return dict<string, string> Flat data suitable for inclusion in an HTTP
753 * request.
755 public static function flattenData(array $data) {
756 $result = array();
757 foreach ($data as $key => $value) {
758 if (is_array($value)) {
759 foreach (self::flattenData($value) as $fkey => $fvalue) {
760 $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
761 $result[$key.$fkey] = $fvalue;
763 } else {
764 $result[$key] = (string)$value;
768 ksort($result);
770 return $result;
775 * Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
777 * This function accepts a canonical header name, like `"Accept-Encoding"`,
778 * and looks up the appropriate value in `$_SERVER` (in this case,
779 * `"HTTP_ACCEPT_ENCODING"`).
781 * @param string Canonical header name, like `"Accept-Encoding"`.
782 * @param wild Default value to return if header is not present.
783 * @param array? Read this instead of `$_SERVER`.
784 * @return string|wild Header value if present, or `$default` if not.
786 public static function getHTTPHeader($name, $default = null, $data = null) {
787 // PHP mangles HTTP headers by uppercasing them and replacing hyphens with
788 // underscores, then prepending 'HTTP_'.
789 $php_index = strtoupper($name);
790 $php_index = str_replace('-', '_', $php_index);
792 $try_names = array();
794 $try_names[] = 'HTTP_'.$php_index;
795 if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
796 // These headers may be available under alternate names. See
797 // http://www.php.net/manual/en/reserved.variables.server.php#110763
798 $try_names[] = $php_index;
801 if ($data === null) {
802 $data = $_SERVER;
805 foreach ($try_names as $try_name) {
806 if (array_key_exists($try_name, $data)) {
807 return $data[$try_name];
811 return $default;
815 /* -( Working With a Phabricator Cluster )--------------------------------- */
819 * Is this a proxied request originating from within the Phabricator cluster?
821 * IMPORTANT: This means the request is dangerous!
823 * These requests are **more dangerous** than normal requests (they can not
824 * be safely proxied, because proxying them may cause a loop). Cluster
825 * requests are not guaranteed to come from a trusted source, and should
826 * never be treated as safer than normal requests. They are strictly less
827 * safe.
829 public function isProxiedClusterRequest() {
830 return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
835 * Build a new @{class:HTTPSFuture} which proxies this request to another
836 * node in the cluster.
838 * IMPORTANT: This is very dangerous!
840 * The future forwards authentication information present in the request.
841 * Proxied requests must only be sent to trusted hosts. (We attempt to
842 * enforce this.)
844 * This is not a general-purpose proxying method; it is a specialized
845 * method with niche applications and severe security implications.
847 * @param string URI identifying the host we are proxying the request to.
848 * @return HTTPSFuture New proxy future.
850 * @phutil-external-symbol class PhabricatorStartup
852 public function newClusterProxyFuture($uri) {
853 $uri = new PhutilURI($uri);
855 $domain = $uri->getDomain();
856 $ip = gethostbyname($domain);
857 if (!$ip) {
858 throw new Exception(
859 pht(
860 'Unable to resolve domain "%s"!',
861 $domain));
864 if (!PhabricatorEnv::isClusterAddress($ip)) {
865 throw new Exception(
866 pht(
867 'Refusing to proxy a request to IP address ("%s") which is not '.
868 'in the cluster address block (this address was derived by '.
869 'resolving the domain "%s").',
870 $ip,
871 $domain));
874 $uri->setPath($this->getPath());
875 $uri->removeAllQueryParams();
876 foreach (self::flattenData($_GET) as $query_key => $query_value) {
877 $uri->appendQueryParam($query_key, $query_value);
880 $input = PhabricatorStartup::getRawInput();
882 $future = id(new HTTPSFuture($uri))
883 ->addHeader('Host', self::getHost())
884 ->addHeader('X-Phabricator-Cluster', true)
885 ->setMethod($_SERVER['REQUEST_METHOD'])
886 ->write($input);
888 if (isset($_SERVER['PHP_AUTH_USER'])) {
889 $future->setHTTPBasicAuthCredentials(
890 $_SERVER['PHP_AUTH_USER'],
891 new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
894 $headers = array();
895 $seen = array();
897 // NOTE: apache_request_headers() might provide a nicer way to do this,
898 // but isn't available under FCGI until PHP 5.4.0.
899 foreach ($_SERVER as $key => $value) {
900 if (!preg_match('/^HTTP_/', $key)) {
901 continue;
904 // Unmangle the header as best we can.
905 $key = substr($key, strlen('HTTP_'));
906 $key = str_replace('_', ' ', $key);
907 $key = strtolower($key);
908 $key = ucwords($key);
909 $key = str_replace(' ', '-', $key);
911 // By default, do not forward headers.
912 $should_forward = false;
914 // Forward "X-Hgarg-..." headers.
915 if (preg_match('/^X-Hgarg-/', $key)) {
916 $should_forward = true;
919 if ($should_forward) {
920 $headers[] = array($key, $value);
921 $seen[$key] = true;
925 // In some situations, this may not be mapped into the HTTP_X constants.
926 // CONTENT_LENGTH is similarly affected, but we trust cURL to take care
927 // of that if it matters, since we're handing off a request body.
928 if (empty($seen['Content-Type'])) {
929 if (isset($_SERVER['CONTENT_TYPE'])) {
930 $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
934 foreach ($headers as $header) {
935 list($key, $value) = $header;
936 switch ($key) {
937 case 'Host':
938 case 'Authorization':
939 // Don't forward these headers, we've already handled them elsewhere.
940 unset($headers[$key]);
941 break;
942 default:
943 break;
947 foreach ($headers as $header) {
948 list($key, $value) = $header;
949 $future->addHeader($key, $value);
952 return $future;
955 public function updateEphemeralCookies() {
956 $submit_cookie = PhabricatorCookies::COOKIE_SUBMIT;
958 $submit_key = $this->getCookie($submit_cookie);
959 if (strlen($submit_key)) {
960 $this->clearCookie($submit_cookie);
961 $this->submitKey = $submit_key;
966 public function getSubmitKey() {
967 return $this->submitKey;