From 2645674f7a253a9fed0361e39054d6503d3b5a96 Mon Sep 17 00:00:00 2001 From: Fredrik Sundblom Date: Mon, 24 May 2021 20:22:13 +0200 Subject: [PATCH 1/3] Replaced the internal redirect handling with a Psr7 response interface instead. --- composer.json | 1 + src/Saml2/Auth.php | 34 +++++++++++++++++++++++++--------- src/Saml2/Utils.php | 13 +++++++------ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 42290e8e..5da36d41 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ }, "require": { "php": ">=7.3", + "guzzlehttp/guzzle": "^7.3", "robrichards/xmlseclibs": ">=3.1.1" }, "require-dev": { diff --git a/src/Saml2/Auth.php b/src/Saml2/Auth.php index 70a87152..9a7c8a25 100644 --- a/src/Saml2/Auth.php +++ b/src/Saml2/Auth.php @@ -15,6 +15,7 @@ namespace OneLogin\Saml2; +use Psr\Http\Message\ResponseInterface; use RobRichards\XMLSecLibs\XMLSecurityKey; use Exception; @@ -270,12 +271,16 @@ public function processResponse($requestId = null) * @param callable $cbDeleteSession Callback to be executed to delete session * @param bool $stay True if we want to stay (returns the url string) False to redirect * - * @return string|null + * @return string|ResponseInterface * * @throws Error */ - public function processSLO($keepLocalSession = false, $requestId = null, $retrieveParametersFromServer = false, $cbDeleteSession = null, $stay = false) + public function processSLO($keepLocalSession = false, $requestId = null, $retrieveParametersFromServer = false, $cbDeleteSession = null, $stay = false): ResponseInterface { + if ($stay !== false) { + trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); + } + $this->_errors = array(); $this->_lastError = $this->_lastErrorException = null; if (isset($_GET['SAMLResponse'])) { @@ -352,10 +357,13 @@ public function processSLO($keepLocalSession = false, $requestId = null, $retrie * @param array $parameters Extra parameters to be passed as part of the url * @param bool $stay True if we want to stay (returns the url string) False to redirect * - * @return string|null + * @return string|ResponseInterface */ - public function redirectTo($url = '', array $parameters = array(), $stay = false) + public function redirectTo($url = '', array $parameters = array(), $stay = false): ResponseInterface { + if ($stay !== false) { + trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); + } assert(is_string($url)); if (empty($url) && isset($_REQUEST['RelayState'])) { @@ -533,12 +541,16 @@ public function getAttributeWithFriendlyName($friendlyName) * @param bool $setNameIdPolicy When true the AuthNRequest will set a nameIdPolicy element * @param string $nameIdValueReq Indicates to the IdP the subject that should be authenticated * - * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters + * @return string|ResponseInterface * * @throws Error */ - public function login($returnTo = null, array $parameters = array(), $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true, $nameIdValueReq = null) + public function login($returnTo = null, array $parameters = array(), $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true, $nameIdValueReq = null): ResponseInterface { + if ($stay !== false) { + trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); + } + $authnRequest = $this->buildAuthnRequest($this->_settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq); $this->_lastRequest = $authnRequest->getXML(); @@ -573,12 +585,16 @@ public function login($returnTo = null, array $parameters = array(), $forceAuthn * @param string|null $nameIdFormat The NameID Format will be set in the LogoutRequest. * @param string|null $nameIdNameQualifier The NameID NameQualifier will be set in the LogoutRequest. * - * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters + * @return string|ResponseInterface If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters * * @throws Error */ - public function logout($returnTo = null, array $parameters = array(), $nameId = null, $sessionIndex = null, $stay = false, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null) + public function logout($returnTo = null, array $parameters = array(), $nameId = null, $sessionIndex = null, $stay = false, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null): ResponseInterface { + if ($stay !== false) { + trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); + } + $sloUrl = $this->getSLOurl(); if (empty($sloUrl)) { throw new Error( @@ -670,7 +686,7 @@ public function getLastRequestID() * * @return AuthnRequest The AuthnRequest object */ - public function buildAuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq = null) + public function buildAuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq = null): ResponseInterface { return new AuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq); } diff --git a/src/Saml2/Utils.php b/src/Saml2/Utils.php index 582c117b..f888aa43 100644 --- a/src/Saml2/Utils.php +++ b/src/Saml2/Utils.php @@ -15,6 +15,7 @@ namespace OneLogin\Saml2; +use Psr\Http\Message\ResponseInterface; use RobRichards\XMLSecLibs\XMLSecurityKey; use RobRichards\XMLSecLibs\XMLSecurityDSig; use RobRichards\XMLSecLibs\XMLSecEnc; @@ -303,12 +304,15 @@ public static function getStringBetween($str, $start, $end) * @param array $parameters Extra parameters to be passed as part of the url * @param bool $stay True if we want to stay (returns the url string) False to redirect * - * @return string|null $url + * @return string|ResponseInterface $url * * @throws Error */ - public static function redirect($url, array $parameters = array(), $stay = false) + public static function redirect($url, array $parameters = array(), $stay = false): ResponseInterface { + if ($stay !== false) { + trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); + } assert(is_string($url)); if (substr($url, 0, 1) === '/') { @@ -360,10 +364,7 @@ public static function redirect($url, array $parameters = array(), $stay = false return $url; } - header('Pragma: no-cache'); - header('Cache-Control: no-cache, must-revalidate'); - header('Location: ' . $url); - exit(); + return new \GuzzleHttp\Psr7\Response(302, ['location' => [(string) $url]]); } /** From 4e3c3c6b73b8da16870cceba1e873f5f0788a966 Mon Sep 17 00:00:00 2001 From: Fredrik Sundblom Date: Mon, 24 May 2021 21:06:26 +0200 Subject: [PATCH 2/3] Updated demo1 and demo2 on how to use this library with Psr7 interfaces. --- demo1/attrs.php | 22 ++++++++------- demo1/index.php | 69 +++++++++++++++++++++++++--------------------- demo1/metadata.php | 5 ++-- demo2/consume.php | 29 +++++++++++-------- demo2/index.php | 28 +++++++++++-------- demo2/metadata.php | 5 ++-- demo2/slo.php | 7 +++-- demo2/sso.php | 4 +-- 8 files changed, 93 insertions(+), 76 deletions(-) diff --git a/demo1/attrs.php b/demo1/attrs.php index 9905e61a..d01d45e3 100644 --- a/demo1/attrs.php +++ b/demo1/attrs.php @@ -1,25 +1,27 @@ '; - echo ''; + $html .= 'You have the following attributes:
'; + $html .= '
NameValues
'; foreach ($attributes as $attributeName => $attributeValues) { - echo ''; + $html .= ''; } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; + $html .= '
' . htmlentities($attributeName) . '
    '; foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; + $html .= '
  • ' . htmlentities($attributeValue) . '
  • '; } - echo '
'; + $html .= ''; } else { - echo "

You don't have any attribute

"; + $html .= "

You don't have any attribute

"; } - echo '

Logout

'; + $html .= '

Logout

'; } else { - echo '

Login and access later to this page

'; + $html .= '

Login and access later to this page

'; } + +return new \GuzzleHttp\Psr7\Response(200, [], $html); \ No newline at end of file diff --git a/demo1/index.php b/demo1/index.php index 122ac500..36c80a69 100644 --- a/demo1/index.php +++ b/demo1/index.php @@ -15,21 +15,25 @@ $auth = new Auth($settingsInfo); -if (isset($_GET['sso'])) { - $auth->login(); +/** @var \GuzzleHttp\Psr7\ServerRequest $request */ +$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); +if (isset($request->getQueryParams()['sso'])) { + return $auth->login(); # If AuthNRequest ID need to be saved in order to later validate it, do instead # $ssoBuiltUrl = $auth->login(null, array(), false, false, true); # $_SESSION['AuthNRequestID'] = $auth->getLastRequestID(); - # header('Pragma: no-cache'); - # header('Cache-Control: no-cache, must-revalidate'); - # header('Location: ' . $ssoBuiltUrl); - # exit(); + # return new \GuzzleHttp\Psr7\Response(302, [ + # 'Pragma' => 'no-cache', + # 'Cache-Control' => 'no-cache, must-revalidate', + # 'location' => [(string) $ssoBuiltUrl] + #]); -} else if (isset($_GET['sso2'])) { + +} else if (isset($request->getQueryParams()['sso2'])) { $returnTo = $spBaseUrl.'/demo1/attrs.php'; - $auth->login($returnTo); -} else if (isset($_GET['slo'])) { + return $auth->login($returnTo); +} else if (isset($request->getQueryParams()['slo'])) { $returnTo = null; $paramters = array(); $nameId = null; @@ -54,7 +58,7 @@ $sessionIndex = $_SESSION['samlSessionIndex']; } - $auth->logout($returnTo, $paramters, $nameId, $sessionIndex, false, $nameIdFormat, $nameIdNameQualifier, $nameIdSPNameQualifier); + return $auth->logout($returnTo, $paramters, $nameId, $sessionIndex, false, $nameIdFormat, $nameIdNameQualifier, $nameIdSPNameQualifier); # If LogoutRequest ID need to be saved in order to later validate it, do instead # $sloBuiltUrl = $auth->logout(null, $paramters, $nameId, $sessionIndex, true); @@ -64,7 +68,7 @@ # header('Location: ' . $sloBuiltUrl); # exit(); -} else if (isset($_GET['acs'])) { +} else if (isset($request->getQueryParams()['acs'])) { if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) { $requestID = $_SESSION['AuthNRequestID']; } else { @@ -76,15 +80,15 @@ $errors = $auth->getErrors(); if (!empty($errors)) { - echo '

' . implode(', ', $errors) . '

'; + $html = '

' . implode(', ', $errors) . '

'; if ($auth->getSettings()->isDebugActive()) { - echo '

'.$auth->getLastErrorReason().'

'; + $html .= '

'.$auth->getLastErrorReason().'

'; } + return new \GuzzleHttp\Psr7\Response(500, [], $html); } if (!$auth->isAuthenticated()) { - echo '

Not authenticated

'; - exit(); + return new \GuzzleHttp\Psr7\Response(401, [], '

Not authenticated

'); } $_SESSION['samlUserdata'] = $auth->getAttributes(); @@ -95,10 +99,11 @@ $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); unset($_SESSION['AuthNRequestID']); - if (isset($_POST['RelayState']) && Utils::getSelfURL() != $_POST['RelayState']) { - $auth->redirectTo($_POST['RelayState']); + $relayState = $request->getParsedBody()['RelayState'] ?? null; + if ($relayState !== null && Utils::getSelfURL() !== $relayState) { + return $auth->redirectTo($relayState); } -} else if (isset($_GET['sls'])) { +} else if (isset($request->getQueryParams()['sls'])) { if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { $requestID = $_SESSION['LogoutRequestID']; } else { @@ -108,11 +113,11 @@ $auth->processSLO(false, $requestID); $errors = $auth->getErrors(); if (empty($errors)) { - echo '

Sucessfully logged out

'; + $html = '

Sucessfully logged out

'; } else { - echo '

' . implode(', ', $errors) . '

'; + $html = '

' . implode(', ', $errors) . '

'; if ($auth->getSettings()->isDebugActive()) { - echo '

'.$auth->getLastErrorReason().'

'; + $html .= '

'.$auth->getLastErrorReason().'

'; } } } @@ -120,22 +125,24 @@ if (isset($_SESSION['samlUserdata'])) { if (!empty($_SESSION['samlUserdata'])) { $attributes = $_SESSION['samlUserdata']; - echo 'You have the following attributes:
'; - echo ''; + $html .= 'You have the following attributes:
'; + $html .= '
NameValues
'; foreach ($attributes as $attributeName => $attributeValues) { - echo ''; + $html .= ''; } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; + $html .= '
' . htmlentities($attributeName) . '
    '; foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; + $html .= '
  • ' . htmlentities($attributeValue) . '
  • '; } - echo '
'; + $html .= ''; } else { - echo "

You don't have any attribute

"; + $html .= "

You don't have any attribute

"; } - echo '

Logout

'; + $html .= '

Logout

'; } else { - echo '

Login

'; - echo '

Login and access to attrs.php page

'; + $html .= '

Login

'; + $html .= '

Login and access to attrs.php page

'; } + +return new \GuzzleHttp\Psr7\Response(200, [], $html); \ No newline at end of file diff --git a/demo1/metadata.php b/demo1/metadata.php index 6161dc61..9f6369d6 100644 --- a/demo1/metadata.php +++ b/demo1/metadata.php @@ -19,8 +19,7 @@ $metadata = $settings->getSPMetadata(); $errors = $settings->validateMetadata($metadata); if (empty($errors)) { - header('Content-Type: text/xml'); - echo $metadata; + return new \GuzzleHttp\Psr7\Response(500, ['Content-Type', 'text/xml'], $metadata); } else { throw new Error( 'Invalid SP metadata: '.implode(', ', $errors), @@ -28,5 +27,5 @@ ); } } catch (Exception $e) { - echo $e->getMessage(); + return new \GuzzleHttp\Psr7\Response(500, [], $e->getMessage()); } diff --git a/demo2/consume.php b/demo2/consume.php index 1543735d..9a53bd81 100644 --- a/demo2/consume.php +++ b/demo2/consume.php @@ -12,31 +12,36 @@ use OneLogin\Saml2\Response; use OneLogin\Saml2\Settings; +/** @var \GuzzleHttp\Psr7\ServerRequest $request */ +$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); +$html = ''; + try { - if (isset($_POST['SAMLResponse'])) { + if (isset($request->getParsedBody()['SAMLResponse'])) { $samlSettings = new Settings(); - $samlResponse = new Response($samlSettings, $_POST['SAMLResponse']); + $samlResponse = new Response($samlSettings, $request->getParsedBody()['SAMLResponse']); if ($samlResponse->isValid()) { - echo 'You are: ' . $samlResponse->getNameId() . '
'; + $html .= 'You are: ' . $samlResponse->getNameId() . '
'; $attributes = $samlResponse->getAttributes(); if (!empty($attributes)) { - echo 'You have the following attributes:
'; - echo ''; + $html .= 'You have the following attributes:
'; + $html .= '
NameValues
'; foreach ($attributes as $attributeName => $attributeValues) { - echo ''; + $html .= ''; } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; + $html .= '
' . htmlentities($attributeName) . '
    '; foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; + $html .= '
  • ' . htmlentities($attributeValue) . '
  • '; } - echo '
'; + $html .= ''; } } else { - echo 'Invalid SAML Response'; + $html .= 'Invalid SAML Response'; } } else { - echo 'No SAML Response found in POST.'; + $html .= 'No SAML Response found in POST.'; } + return new \GuzzleHttp\Psr7\Response(200, [], 'Invalid SAML Response: ' . $html); } catch (Exception $e) { - echo 'Invalid SAML Response: ' . $e->getMessage(); + return new \GuzzleHttp\Psr7\Response(400, [], 'Invalid SAML Response: ' . $e->getMessage()); } diff --git a/demo2/index.php b/demo2/index.php index 69a3bf45..32c038ac 100755 --- a/demo2/index.php +++ b/demo2/index.php @@ -16,6 +16,9 @@ use OneLogin\Saml2\Settings; use OneLogin\Saml2\Utils; +/** @var \GuzzleHttp\Psr7\ServerRequest $request */ +$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); + if (!isset($_SESSION['samlUserdata'])) { $settings = new Settings(); $authRequest = new AuthnRequest($settings); @@ -26,27 +29,28 @@ $idpData = $settings->getIdPData(); $ssoUrl = $idpData['singleSignOnService']['url']; - $url = Utils::redirect($ssoUrl, $parameters, true); - - header("Location: $url"); + return Utils::redirect($ssoUrl, $parameters); } else { + $html = ''; if (!empty($_SESSION['samlUserdata'])) { $attributes = $_SESSION['samlUserdata']; - echo 'You have the following attributes:
'; - echo ''; + $html .= 'You have the following attributes:
'; + $html .= '
NameValues
'; foreach ($attributes as $attributeName => $attributeValues) { - echo ''; + $html .= ''; } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; + $html .= '
' . htmlentities($attributeName) . '
    '; foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; + $html .= '
  • ' . htmlentities($attributeValue) . '
  • '; } - echo '
'; + $html .= ''; if (!empty($_SESSION['IdPSessionIndex'])) { - echo '

The SessionIndex of the IdP is: '.$_SESSION['IdPSessionIndex'].'

'; + $html .= '

The SessionIndex of the IdP is: '.$_SESSION['IdPSessionIndex'].'

'; } } else { - echo "

You don't have any attribute

"; + $html .= "

You don't have any attribute

"; } - echo '

Logout

'; + $html .= '

Logout

'; + + return new \GuzzleHttp\Psr7\Response(200, [], $html); } diff --git a/demo2/metadata.php b/demo2/metadata.php index ff972628..0486702f 100644 --- a/demo2/metadata.php +++ b/demo2/metadata.php @@ -11,10 +11,9 @@ use OneLogin\Saml2\Metadata; use OneLogin\Saml2\Settings; -header('Content-Type: text/xml'); - $samlSettings = new Settings(); $sp = $samlSettings->getSPData(); $samlMetadata = Metadata::builder($sp); -echo $samlMetadata; + +return new \GuzzleHttp\Psr7\Response(200, ['Content-Type' => 'text/xml'], $samlMetadata); diff --git a/demo2/slo.php b/demo2/slo.php index 235a20ca..c971b909 100644 --- a/demo2/slo.php +++ b/demo2/slo.php @@ -14,6 +14,9 @@ use OneLogin\Saml2\Settings; use OneLogin\Saml2\Utils; +/** @var \GuzzleHttp\Psr7\ServerRequest $request */ +$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); + $samlSettings = new Settings(); $idpData = $samlSettings->getIdPData(); @@ -33,6 +36,4 @@ $parameters = array('SAMLRequest' => $samlRequest); -$url = Utils::redirect($sloUrl, $parameters, true); - -header("Location: $url"); +return Utils::redirect($sloUrl, $parameters); diff --git a/demo2/sso.php b/demo2/sso.php index 03a726dc..9da902be 100644 --- a/demo2/sso.php +++ b/demo2/sso.php @@ -18,8 +18,8 @@ $auth = new OneLogin\Saml2\Auth(); if (!isset($_SESSION['samlUserdata'])) { - $auth->login(); + return $auth->login(); } else { $indexUrl = str_replace('/sso.php', '/index.php', Utils::getSelfURLNoQuery()); - Utils::redirect($indexUrl); + return Utils::redirect($indexUrl); } From 7c19182f0f247f38eace6b02022c2d38cffb63a3 Mon Sep 17 00:00:00 2001 From: Fredrik Sundblom Date: Mon, 24 May 2021 21:21:15 +0200 Subject: [PATCH 3/3] Removed return type hinting since we still have the return as string functionality --- src/Saml2/Auth.php | 10 +++++----- src/Saml2/Utils.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Saml2/Auth.php b/src/Saml2/Auth.php index 9a7c8a25..b173fd66 100644 --- a/src/Saml2/Auth.php +++ b/src/Saml2/Auth.php @@ -275,7 +275,7 @@ public function processResponse($requestId = null) * * @throws Error */ - public function processSLO($keepLocalSession = false, $requestId = null, $retrieveParametersFromServer = false, $cbDeleteSession = null, $stay = false): ResponseInterface + public function processSLO($keepLocalSession = false, $requestId = null, $retrieveParametersFromServer = false, $cbDeleteSession = null, $stay = false) { if ($stay !== false) { trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); @@ -359,7 +359,7 @@ public function processSLO($keepLocalSession = false, $requestId = null, $retrie * * @return string|ResponseInterface */ - public function redirectTo($url = '', array $parameters = array(), $stay = false): ResponseInterface + public function redirectTo($url = '', array $parameters = array(), $stay = false) { if ($stay !== false) { trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); @@ -545,7 +545,7 @@ public function getAttributeWithFriendlyName($friendlyName) * * @throws Error */ - public function login($returnTo = null, array $parameters = array(), $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true, $nameIdValueReq = null): ResponseInterface + public function login($returnTo = null, array $parameters = array(), $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true, $nameIdValueReq = null) { if ($stay !== false) { trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); @@ -589,7 +589,7 @@ public function login($returnTo = null, array $parameters = array(), $forceAuthn * * @throws Error */ - public function logout($returnTo = null, array $parameters = array(), $nameId = null, $sessionIndex = null, $stay = false, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null): ResponseInterface + public function logout($returnTo = null, array $parameters = array(), $nameId = null, $sessionIndex = null, $stay = false, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null) { if ($stay !== false) { trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE); @@ -686,7 +686,7 @@ public function getLastRequestID() * * @return AuthnRequest The AuthnRequest object */ - public function buildAuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq = null): ResponseInterface + public function buildAuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq = null) { return new AuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq); } diff --git a/src/Saml2/Utils.php b/src/Saml2/Utils.php index f888aa43..91aa0347 100644 --- a/src/Saml2/Utils.php +++ b/src/Saml2/Utils.php @@ -308,7 +308,7 @@ public static function getStringBetween($str, $start, $end) * * @throws Error */ - public static function redirect($url, array $parameters = array(), $stay = false): ResponseInterface + public static function redirect($url, array $parameters = array(), $stay = false) { if ($stay !== false) { trigger_error('stay is deprecated and will be removed in a future release', E_USER_NOTICE);