I'm trying to connect to the LivePerson Engagement History API and I'm running into an issue that I believe is related to the signature being generated.
First off, the API already provides the necessary consumer key, consumer secret, access token, and token secret. So I don't have to go through the process of retrieving those. In order to access their API I just have to provide the auth header. I've mocked everything up using Postman and it all works correctly. The issue is when I try to generate my own timestamp/nonce/signature in my class.
Here's the method from my class that sends the cURL request:
private function execute($options = array())
{
if (!isset($options['url'])) {
return;
}
$ch = curl_init($options['url']);
$method = (isset($options['method'])) ? $options['method'] : 'GET';
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if (isset($options['auth']) && $options['auth']) {
$timestamp = round(microtime(true) * 1000);
$nonce = $this->getNonce(11);
$version = "1.0";
$signatureMethod = "HMAC-SHA1";
$signature = $this->generateSignature($options, $timestamp, $nonce, $signatureMethod, $version);
$authHeader = "Authorization: OAuth oauth_consumer_key=\"{$this->consumerKey}\",oauth_token=\"{$this->accessToken}\",oauth_signature_method=\"{$signatureMethod}\",oauth_timestamp=\"{$timestamp}\",oauth_nonce=\"{$nonce}\",oauth_version=\"{$version}\",oauth_signature=\"{$signature}\"";
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
$authHeader,
"Content-Type: application/json"
));
}
if (isset($options['body']) && !empty($options['body'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($options['body']));
}
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
The getNonce method I copied pretty much directly from https://github.com/BaglerIT/OAuthSimple/blob/master/src/OAuthSimple.php.
Here's the method I've written to generate the signature (which has been cobbled together from various SO posts and other sources):
protected function generateSignature($request, $timestamp, $nonce, $signatureMethod, $version)
{
$base = $request['method'] . "&" . rawurlencode($request['url']) . "&"
. rawurlencode("oauth_consumer_key=" . rawurlencode($this->consumerKey)
. "&oauth_nonce=" . rawurlencode($nonce)
. "&oauth_signature_method=" . rawurlencode($signatureMethod)
. "&oauth_timestamp=" . $timestamp
. "&oauth_version=" . $version);
$key = rawurlencode($this->consumerSecret) . '&' . rawurlencode($this->tokenSecret);
$signature = base64_encode(hash_hmac('sha1', $base, $key, true));
return $signature;
}
I can actually copy and paste the authorization header from Postman into my $authHeader variable, and replace everything except the timestamp/nonce/signature, and it works.
The response I'm getting from their server right now is [code] => 0005 but I can't find anything in their docs about response codes.
Edit: I had missed looking at the response header - the exact error is invalid signature.
There are 2 things I changed to get this to work.
I was missing the oauth_token when creating the base string for the signature
According to the OAuth Core 1.0 documentation, "Parameters are sorted by name, using lexicographical byte value ordering."
So I ended up re-ordering the parameters to be alphabetical. Here's what the code for generating the base string ended up looking like:
$base = $request['method'] . "&" . rawurlencode($request['url']) . "&"
. rawurlencode("oauth_consumer_key=" . rawurlencode($this->consumerKey)
. "&oauth_nonce=" . rawurlencode($nonce)
. "&oauth_signature_method=" . rawurlencode($signatureMethod)
. "&oauth_timestamp=" . rawurlencode($timestamp)
. "&oauth_token=" . rawurlencode($this->accessToken)
. "&oauth_version=" . rawurlencode($version));
I also re-ordered the params in the auth header to match the order of the base string:
$authHeader = "Authorization: OAuth oauth_consumer_key=\"{$this->consumerKey}\",oauth_nonce=\"{$nonce}\",oauth_signature_method=\"{$signatureMethod}\",oauth_timestamp=\"{$timestamp}\",oauth_token=\"{$this->accessToken}\",oauth_version=\"{$version}\",oauth_signature=\"{$signature}\"";
$base = $request['method']
. '&' . rawurlencode($request['url'])
. '&' . rawurlencode('oauth_consumer_key=' . $this->consumerKey)
. rawurlencode('&oauth_nonce=' . $nonce)
. rawurlencode('&oauth_signature_method=' . $signatureMethod)
. rawurlencode('&oauth_timestamp=' . $timestamp)
. rawurlencode('&oauth_version=' . $version)
. rawurlencode('&' . http_build_query($data));
$key = rawurlencode($this->consumerSecret) . '&';
$signature = rawurlencode(base64_encode(hash_hmac('SHA1', $base, $key, true)));
If you do a POST, make sure to include your posted data, otherwise the signature will not validate.
CURLOPT_HTTPHEADER => array(
"authorization: OAuth oauth_consumer_key=\"{$consumerKey}\",oauth_signature_method=\"{$signatureMethod}\",oauth_timestamp=\"{$timestamp}\",oauth_nonce=\"{$nonce}\",oauth_version=\"{$version}\",oauth_signature=\"{$oauthSignature}\"",
"content-type: application/x-www-form-urlencoded",
),
And the header should be as above
This fixed version worked for me :
function generateOauthSignature($method, $url, $consumerKey, $nonce, $signatureMethod, $timestamp, $version, $consumerSecret, $tokenSecret, $tokenValue, $extraParams = array())
{
$base = strtoupper($method) . "&" . rawurlencode($url) . "&"
. rawurlencode("oauth_consumer_key=" . $consumerKey
. "&oauth_nonce=" . $nonce
. "&oauth_signature_method=" . $signatureMethod
. "&oauth_timestamp=" . $timestamp
. "&oauth_token=" . $tokenValue
. "&oauth_version=" . $version);
if (!empty($extraParams)) {
$base .= rawurlencode("&" . http_build_query($extraParams));
}
$key = rawurlencode($consumerSecret) . '&' . rawurlencode($tokenSecret);
$signature = base64_encode(hash_hmac('sha1', $base, $key, true));
return rawurlencode($signature);
}
The following twitter thread helped me : https://twittercommunity.com/t/how-to-generate-oauth-signature-when-post-json-body-in-php/87581
I was also struggling with the proper setup of the OAuth 1 signature and had a lot of failed attempts. After TGA's hint to have a look how it's done with Twitter, I found out that there is an existing class which may be used out-of-the-box:
TwitterAPIExchange.php from the repository https://github.com/J7mbo/twitter-api-php.
Even if is called "Twitter...", it may also be used for other OAuth1 APIs. Calls will look like this:
$settings = array(
'oauth_access_token' => TOKEN,
'oauth_access_token_secret' => TOKEN_SECRET,
'consumer_key' => CONSUMER_KEY,
'consumer_secret' => CONSUMER_SECRET
);
$url = "https://api-url.com/api/v4/users/0451432/";
$requestMethod = 'POST';
$postfields = array(
'groupIds' => '23,24,25',
);
$twitter = new TwitterAPIExchange($settings);
return $twitter->buildOauth($url, $requestMethod)
->setPostfields($postfields)
->performRequest();
It works perfect for me.
This version matches the OAuth PECL library's function so you no longer need it.
public static function oauth_get_sbs(
$requestMethod,
$requestURL,
$request_parameters
): string
{
return $requestMethod . "&" . rawurlencode($requestURL) . "&"
. rawurlencode("oauth_consumer_key=" . rawurlencode($request_parameters['oauth_consumer_key'])
. "&oauth_nonce=" . rawurlencode($request_parameters['oauth_nonce'])
. "&oauth_signature_method=" . rawurlencode($request_parameters['oauth_signature_method'])
. "&oauth_timestamp=" . $request_parameters['oauth_timestamp']
. "&oauth_token=" . $request_parameters['oauth_token']
. "&oauth_version=" . $request_parameters['oauth_version']);
}
Related
enter code hereI just started working with OAuth1 API's calling from php using GuzzleHttp\Client.
In postman it's working fine.In netsuite log file I can see only required parameters is missing. I'm not understanding where I'm going wrong. whenever I'm calling the API I'm getting response as
#message: """
Client error: `GET https://7085372.suitetalk.api.netsuite.com/services/rest/record/v1/customer/` resulted in a `400 Bad Request` response:
{"type":"https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1","title":"Bad Request","status":400,"o:errorD (truncated...)
"""
#code: 400
php code
$realm = 'xxxxxxx';
$consumer_key = 'xxxxxxxxx';
$oauth_token = 'xxxxxxxxxxxxx';
$oauth_signature_method = 'HMAC-SHA256';
$oauth_version = '1.0';
$consumer_secret = 'xxxxxxxxxxxxx';
$token_secrect = 'xxxxxxxxxxxxx';
$timeStamp = Carbon::now()->timestamp;
$oauth_none = Str::random(11);
$base = 'GET' . "&" . urlencode('https://7085372.suitetalk.api.netsuite.com/services/rest/record/v1/customer/') . "&" . urlencode("oauth_consumer_key=" . urlencode($consumer_key) . "&oauth_nonce=" . urlencode($oauth_none) . "&oauth_signature_method=" . urlencode($oauth_signature_method) . "&oauth_timestamp=" . urlencode($timeStamp) . "&oauth_token=" . urlencode($oauth_token) . "&oauth_version=" . urlencode($oauth_version) . "&realm=" . urlencode($realm));
$key = urlencode($consumer_secret) . "&" . urlencode($token_secrect);
$oauth_signature = base64_encode(hash_hmac('sha256', $base, $key, true));
$authorization = 'OAuth oauth_consumer_key=' . $consumer_key . ',oauth_nonce=' . $oauth_none . ',oauth_signature_method=' . $oauth_signature_method . ',oauth_timestamp=' . $timeStamp . ',oauth_token=' . $oauth_token . ',oauth_version=' . $oauth_version . ',realm=' . $realm . ',oauth_signature=' . $oauth_signature . '';
try {
$client = new Client();
$headers = [
'Authorization' => $authorization,
'Content-Type' => 'application/json',
'Cookie' => 'NS_ROUTING_VERSION=LAGGING'
];
$body = '';
$request = new Request('GET', 'https://7085372.suitetalk.api.netsuite.com/services/rest/record/v1/customer/', $headers, $body);
$res = $client->sendAsync($request)->wait();
dd('here', $res, $res->getBody());
} catch (RequestException $e) {
dd($e->getResponse(), $e);
}
Postman Collection
In this line:
$base = 'GET' . "&" . urlencode('https://7085372.suitetalk.api.netsuite.com/services/rest/record/v1/customer/') . "&" . urlencode("oauth_consumer_key=" . urlencode($consumer_key) . "&oauth_nonce=" . urlencode($oauth_none) . "&oauth_signature_method=" . urlencode($oauth_signature_method) . "&oauth_timestamp=" . urlencode($timeStamp) . "&oauth_token=" . urlencode($oauth_token) . "&oauth_version=" . urlencode($oauth_version) . "&realm=" . urlencode($realm));
From urlencode("oauth_consumer_key=" . to "&realm=" . urlencode($realm));, you are url_encoding twice, and you are url_encoding = symbols. So your $authorization string is absolutely malformed, and it results in missing parameters.
This can occurs when you write such a long line, and it's difficult to see. It's better to write it down this way:
$base = 'GET&' . urlencode('https://7085372.suitetalk.api.netsuite.com/services/rest/record/v1/customer/')
. '&oauth_consumer_key=' . urlencode($consumer_key)
. '&oauth_nonce=' . urlencode($oauth_none)
. '&oauth_signature_method=' . urlencode($oauth_signature_method)
. '&oauth_timestamp=' . urlencode($timeStamp)
. '&oauth_token=' . urlencode($oauth_token)
. '&oauth_version=' . urlencode($oauth_version)
. '&realm=' . urlencode($realm);
But it's even better to use an array and http_build_query() function, this way:
$url = 'https://7085372.suitetalk.api.netsuite.com/services/rest/record/v1/customer/';
$data = array(
'oauth_consumer_key' => $consumer_key,
'oauth_nonce' => $oauth_none,
'oauth_signature_method' => $oauth_signature_method,
'oauth_timestamp' => $timeStamp,
'oauth_token' => $oauth_token,
'oauth_version' => $oauth_version,
'realm' => $realm,
);
$base = 'GET&' . urlencode($url) . '&' . http_build_query($data);
Note.- Also, it's a good idea to use a variable for the url, since you are using it more than 1 time.
However, maybe there are more issues left. I'm not sure if you are are doing well the request.
And it's a good idea to have a function to generate OAuth signature.
Here you are a remaking of your code:
$url = 'https://7085372.suitetalk.api.netsuite.com/services/rest/record/v1/customer/';
$consumer_secret = 'xxxxxxxxxx';
$token_secret = 'xxxxxxxxxx';
$data = array(
'oauth_consumer_key' => 'xxxxxxxxxx',
'oauth_nonce' => Str::random(11),
'oauth_signature_method' => 'HMAC-SHA256',
'oauth_timestamp' => Carbon::now()->timestamp,
'oauth_token' => 'xxxxxxxxxx',
'oauth_version' => '1.0',
'realm' => 'xxxxxxxxxx',
);
$oauth_signature = generateOauthSignature(
'GET',
$url,
$data['oauth_consumer_key'],
$data['oauth_nonce'],
$data['oauth_signature_method'],
$data['oauth_timestamp'],
$data['outh_version'],
$consumer_secret,
$token_secret,
$data['oauth_token'],
array('realm' => $data['realm']),
);
$authorization = 'OAuth ';
foreach ($data as $key => $val) {
$authorization .= ',' . $key . '=' . $val;
}
$authorization .= ',oauth_signature=' . $oauth_signature;
try {
$client = new Client();
$headers = [
'Authorization' => $authorization,
'Content-Type' => 'application/json',
'Cookie' => 'NS_ROUTING_VERSION=LAGGING'
];
$body = '';
$request = new Request('GET', $url, $headers, $body);
$res = $client->sendAsync($request)->wait();
dd('here', $res, $res->getBody());
} catch (RequestException $e) {
dd($e->getResponse(), $e);
}
function generateOauthSignature($method, $url, $consumerKey, $nonce, $signatureMethod, $timestamp, $version, $consumerSecret, $tokenSecret, $tokenValue, $extraParams = array())
{
$base = strtoupper($method) . "&" . rawurlencode($url) . "&"
. rawurlencode("oauth_consumer_key=" . $consumerKey
. "&oauth_nonce=" . $nonce
. "&oauth_signature_method=" . $signatureMethod
. "&oauth_timestamp=" . $timestamp
. "&oauth_token=" . $tokenValue
. "&oauth_version=" . $version);
if (!empty($extraParams)) {
$base .= rawurlencode("&" . http_build_query($extraParams));
}
$key = rawurlencode($consumerSecret) . '&' . rawurlencode($tokenSecret);
$signature = base64_encode(hash_hmac('sha1', $base, $key, true));
return rawurlencode($signature);
}
I am working on a small PHP script which calls an API. This API uses OAuth 1.0 authorization. Hence I need to create the signature and request-header. I am pretty sure my signature-generation method and the generation of the request-header work well as I have done the same in C# (where the everything works) and the signature as well as the header look quite the same.
Generating signature:
function generateSignature($method,$url,$consumerKey,$consumerSecret,$tokenValue,$tokenSecret,$timestamp, $nonce, $signatureMethod, $version)
{
$base = $method . "&" . rawurlencode($url) . "&"
. rawurlencode("oauth_consumer_key=" . rawurlencode($consumerKey)
. "&oauth_nonce=" . rawurlencode($nonce)
. "&oauth_signature_method=" . rawurlencode($signatureMethod)
. "&oauth_timestamp=" . rawurlencode($timestamp)
. "&oauth_token=" . rawurlencode($tokenValue)
. "&oauth_version=" . rawurlencode($version));
$key = rawurlencode($consumerSecret) . '&' . rawurlencode($tokenSecret);
$signature = base64_encode(hash_hmac('sha1', $base, $key, true));
return $signature;
}
Generating authorization header:
$authHeader = "OAuth realm="."\"\","."
oauth_consumer_key=\"{$consumerKey}\",
oauth_nonce=\"{$nonce}\",
oauth_signature_method=\"{$signatureMethod}\",
oauth_timestamp=\"{$timestamp}\",
oauth_token=\"{$tokenValue}\",
oauth_version=\"{$version}\",
oauth_signature=\"{$signature}\"";
I got stuck at the point of passing the authorization header over to the API I am calling.
Setting the authorization:
$header = array('Content-Type: application/x-www-form-urlencoded');
curl_setopt($ch, CURLOPT_HTTPHEADER,$header);
curl_setopt($ch, CURLOPT_POSTFIELDS,urlencode($authHeader));
Finally, when I make the request I get the response, that a parameter is missing.
I am trying to construct a custom upload script that uploads to a defined flickr account, I'm looking for something other than phpflickr (which uses the deprecated version of the flickr api). I know how to authorize flickr calls in the new method and have successfully used at least half-a-dozen of their methods but I'm failing to understand how to upload (mainly due to the fact of very limited documentation).
Here is my authorization call that I'm using:
<?php
$url = "format=" . $this->format;
$url .= "&method=" . $method;
$url .= "&nojsoncallback=1";
$url .= "&oauth_consumer_key=" . $this->flickr_key;
$url .= "&oauth_nonce=" . $this->nonce;
$url .= "&oauth_signature_method=" . $this->sig_method;
$url .= "&oauth_timestamp=" . $this->timestamp;
$url .= "&oauth_token=" . $access_token;
$url .= "&oauth_version=1.0";
$url .= "&photoset_id=" . $photoset_id;
$baseurl = "GET&" . urlencode( $flickr_upload_call ) . "&" . urlencode( $url );
$hashkey = $this->flickr_secret . "&" . $access_token_secret;
$oauth_signature = base64_encode( hash_hmac( 'sha1', $baseurl, $hashkey, true ));
$url_parameters = array(
'method' =>$method,
'oauth_consumer_key' =>$this->flickr_key,
'photoset_id' =>$photoset_id,
'format' =>$this->format,
'nojsoncallback' =>'1',
'oauth_nonce' =>$this->nonce,
'oauth_timestamp' =>$this->timestamp,
'oauth_signature_method'=>$this->sig_method,
'oauth_version' =>'1.0',
'oauth_token' =>$access_token,
'oauth_signature' =>$oauth_signature
);
/* Now that we have encoded the parameters for our ouath_signature
* and have reformated them for the url we need to send... we must
* re-urlencode them too. */
$parameters_string = "";
foreach ( $url_parameters as $key=>$value )
$parameters_string .= "$key=" . urlencode( $value ) . "&";
$url = $flickr_api_call . "?" . $parameters_string;
So, what I need to know is how to change this to allow for the flickr upload api to accept it; any ways I can do this better, or how can I use what is in phpflickr and convert it to the new method to suit my needs?
I am able to get access_token for multiple permissions like emails, contacts, docs, etc. using oAuth 2.0. I have access_token
I got contacts using the following code.
$url = 'https://www.google.com/m8/feeds/contacts/default/full?max- results='.$max_results.'&oauth_token='.$access_token;
$response_contacts= curl_get_file_contents($url);
Now i want to get users Emails using this access_token.
i used this url . but it gives 401 unauthorized Error
$url = 'https://mail.google.com/mail/feed/atom&oauth_token='.$access_token;
$response_emails= curl_get_file_contents($url);
please guide me how can i get emails using access_token.
I've seen references to the Gmail feed using oauth_token as a request parameter. However, once I used the OAuth Playground I discovered that you need to pass your OAuth information as an Authorization header, as you'll see below.
<?php
$now = time();
$consumer = ...; // your own value here
$secret = ...; // your own value here
$nonce = ...; // same value you've been using
$algo = "sha1";
$sigmeth = "HMAC-SHA1";
$av = "1.0";
$scope = "https://mail.google.com/mail/feed/atom";
$path = $scope;
$auth = ...; // an object containing outputs of OAuthGetAccessToken
$args = "oauth_consumer_key=" . urlencode($consumer) .
"&oauth_nonce=" . urlencode($nonce) .
"&oauth_signature_method=" . urlencode($sigmeth) .
"&oauth_timestamp=" . urlencode($now) .
"&oauth_token=" . urlencode($auth->oauth_token) .
"&oauth_version=" . urlencode($av);
$base = "GET&" . urlencode($path) . "&" . urlencode($args);
$sig = base64_encode(hash_hmac($algo, $base,
"{$secret}&{$auth->oauth_token_secret}", true));
$url = $path . "?oauth_signature=" . urlencode($sig) . "&" . $args;
// Create a stream
$opts = array(
"http" => array(
"method" => "GET",
"header" => "Authorization: OAuth " .
"oauth_version=\"{$av}\", " .
"oauth_nonce=\"{$nonce}\", " .
"oauth_timestamp=\"{$now}\", " .
"oauth_consumer_key=\"{$consumer}\", " .
"oauth_token=\"{$auth->oauth_token}\", " .
"oauth_signature_method=\"{$sigmeth}\", " .
"oauth_signature=\"{$sig}\"\r\n"
)
);
$context = stream_context_create($opts);
$out = file_get_contents($path, false, $context);
?>
I am facing some problem in generating oauth_signature for flickr api. Can you please look into this and advise me what I am doing wrong?
// p.s. I am sharing my Flickr key and secret as I will change them when I will start production development.
Code
/* PHP code */
$NONCE=base64_decode(rand());
$TIMESTAMP= gmdate('U');
$SECRET="39b4f5fd592ede81";
$KEY="1bab082052d7cf8b3aa9e2bc92882ac0";
$CONSUMER_SECRET= $SECRET. "&";
$url_1 = "http://www.flickr.com/services/oauth/request_token?";
$url_1 = urlencode($url_1);
$url_2 = "oauth_callback=http%3A%2F%2Flocalhost%2FFlickr%2Flogin.php&oauth_consumer_key=". $KEY;
$url_2 .="&oauth_nonce=". $NONCE. "&oauth_signature_method=HMAC-SHA1&oauth_timestamp=". $TIMESTAMP. "&oauth_version=1.0";
// generate signature
$BASE_STRING ="";
$BASE_STRING .= "GET&". urlencode($url_1). urlencode($url_2);
$API_SIG= base64_encode(hash_hmac("sha1",$BASE_STRING,$CONSUMER_SECRET, true) );
// url generate
$url="http://www.flickr.com/services/oauth/request_token?oauth_callback=http://localhost/Flickr/login.php&oauth_consumer_key=". $KEY;
$url.="&oauth_nonce=". $NONCE. "&oauth_timestamp=". $TIMESTAMP. "&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_signature=". $API_SIG;
// calling
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_REFERER, "http://www.example.org/yay.htm");
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla Firefox/3.0");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data= curl_exec($ch);
echo $data;
curl_close($ch);
This worked for me, hopefully it helps someone else...
<?php
$consumerKey = 'your_Flickr_key';
$consumerSecret = 'your_Flickr_secret';
$requestTokenUrl = "https://www.flickr.com/services/oauth/request_token";
$oauthTimestamp = time();
$nonce = md5(mt_rand());
$oauthSignatureMethod = "HMAC-SHA1";
$oauthVersion = "1.0";
$sigBase = "GET&" . rawurlencode($requestTokenUrl) . "&"
. rawurlencode("oauth_consumer_key=" . rawurlencode($consumerKey)
. "&oauth_nonce=" . rawurlencode($nonce)
. "&oauth_signature_method=" . rawurlencode($oauthSignatureMethod)
. "&oauth_timestamp=" . $oauthTimestamp
. "&oauth_version=" . $oauthVersion);
$sigKey = $consumerSecret . "&";
$oauthSig = base64_encode(hash_hmac("sha1", $sigBase, $sigKey, true));
$requestUrl = $requestTokenUrl . "?"
. "oauth_consumer_key=" . rawurlencode($consumerKey)
. "&oauth_nonce=" . rawurlencode($nonce)
. "&oauth_signature_method=" . rawurlencode($oauthSignatureMethod)
. "&oauth_timestamp=" . rawurlencode($oauthTimestamp)
. "&oauth_version=" . rawurlencode($oauthVersion)
. "&oauth_signature=" . rawurlencode($oauthSig);
$response = file_get_contents($requestUrl);
var_export($response);
I am answering my own question. Thanks to Sam Judson: http://www.wackylabs.net
I removed base64_decode() from generating random numbers and it just did worked.