Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ composer.lock
# PHPUnit Files
tests/phpunit.xml

/lib/Examples/settings.json
9 changes: 9 additions & 0 deletions lib/Examples/autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

spl_autoload_register(function ($class) {
if (
file_exists($fn = __DIR__.'/../'.str_replace('\\','/',$class).'.php')
) {
require $fn;
}
});
33 changes: 33 additions & 0 deletions lib/Examples/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

include 'autoload.php';

use \Shopify\Api\Client;
use \Shopify\Api\AuthenticationGateway;
use \Shopify\HttpClient\CurlHttpClient;
use \Shopify\Redirector\HeaderRedirector;

$settings=json_decode(file_get_contents('settings.json'));

if (!empty($settings->shopName) && !empty($_GET['code'])) {
Client::doValidateSignature($settings->clientSecret, $_GET) or die('Signature validation failed');
$auth = new AuthenticationGateway(new CurlHttpClient(null, false, false), new HeaderRedirector());
$token = $auth->forShopName($settings->shopName)
->usingClientId($settings->clientId)
->usingClientSecret($settings->clientSecret)
->toExchange($_GET['code']);
if ($token) {
$settings->accessToken = $token;
file_put_contents('settings.json', json_encode($settings, JSON_PRETTY_PRINT));
HeaderRedirector::go($settings->redirectUri);
} else {
die('toExchange failed');
}
}

if (empty($settings->accessToken)) die('not authenticated, use install.php first');

$client = new Client(new CurlHttpClient(null, false, false), $settings);
$result = $client->get('/admin/pages.json');
var_dump($result);

22 changes: 22 additions & 0 deletions lib/Examples/install.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

if (empty($_GET['shopName'])) die('shopName not provided');

include 'autoload.php';

use \Shopify\Api\AuthenticationGateway;
use \Shopify\HttpClient\CurlHttpClient;
use \Shopify\Redirector\HeaderRedirector;

$settings=json_decode(file_get_contents('settings.json'));
$settings->shopName = $_GET['shopName'];
$settings->redirectUri = "http://{$_SERVER['HTTP_HOST']}".dirname($_SERVER['REQUEST_URI']).'/';

file_put_contents('settings.json', json_encode($settings, JSON_PRETTY_PRINT));

$auth = new AuthenticationGateway(new CurlHttpClient(null, false, false), new HeaderRedirector());
$auth->forShopName($settings->shopName)
->usingClientId($settings->clientId)
->withScope($settings->permissions)
->andReturningTo($settings->redirectUri)
->initiateLogin();
22 changes: 22 additions & 0 deletions lib/Examples/settings.json.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"clientId": "YOUR APP API KEY",
"clientSecret": "YOUR APP API SECRET",
"permissions": [
"read_content",
"write_content",
"read_themes",
"write_themes",
"read_products",
"write_products",
"read_customers",
"write_customers",
"read_orders",
"write_orders",
"read_script_tags",
"write_script_tags",
"read_fulfillments",
"write_fulfillments",
"read_shipping",
"write_shipping"
]
}
9 changes: 5 additions & 4 deletions lib/Shopify/Api/AuthenticationGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,17 @@ public function toExchange($temporaryToken)
'code' => $temporaryToken,
);

$response = json_decode($this->httpClient->post(
$response = $this->httpClient->post(
$this->getAccessUri(),
$request
));
);

$response_obj = json_decode($response);
if (isset($response->error)) {
throw new \RuntimeException($response->error);
throw new \RuntimeException($response_obj->error);
}

return isset($response->access_token) ? $response->access_token : null;
return isset($response_obj->access_token) ? $response_obj->access_token : null;

}

Expand Down
66 changes: 50 additions & 16 deletions lib/Shopify/Api/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ class Client
/**
* initialize the API client
* @param HttpClient $client
* @param $options
*/
public function __construct(HttpClient $client)
public function __construct(HttpClient $client, $options=null)
{
$this->httpClient = $client;
if (is_object($options)) foreach (get_object_vars($this) as $key=>$value) {
if (isset($options->$key)) $this->$key = $options->$key;
}
}

/**
Expand Down Expand Up @@ -123,10 +127,15 @@ public function delete($resource, array $data = array())
/**
* generate the signature as required by shopify
* @param array $params
* @param bool $hmac
* @return string
*/
public function generateSignature(array $params)
public function generateSignature(array $params, $hmac=true)
{
return self::doGenerateSignature($this->getClientSecret(), $params, $hmac);
}

public static function doGenerateSignature($secret, array $params, $hmac=true) {

// Collect the URL parameters into an array of elements of the format
// "$parameter_name=$parameter_value"
Expand All @@ -137,14 +146,29 @@ public function generateSignature(array $params)
$calculated[] = $key . "=" . $value;
}

// Sort the key/value pairs in the array
sort($calculated);
if ($hmac)
{
// Sort the key/value pairs in the array
asort($calculated);

// Join the array elements into a string
$calculated = implode('', $calculated);
// Join the array elements into a string
$calculated = implode('&', $calculated);

// Final calculated_signature to compare against
return hash_hmac('sha256', $calculated, $secret);
}
else
{
// note: md5 validation has been deprecated
// Sort the key/value pairs in the array
sort($calculated);

// Final calculated_signature to compare against
return md5($this->getClientSecret() . $calculated);
// Join the array elements into a string
$calculated = implode('', $calculated);

// Final calculated_signature to compare against
return md5($secret . $calculated);
}

}

Expand All @@ -154,16 +178,26 @@ public function generateSignature(array $params)
*/
public function validateSignature(array $params)
{

$this->assertRequestParamIsNotNull(
$params, 'signature', 'Expected signature in query params'
);

if (empty($params['hmac']) && empty($params['signature'])) {
$this->assertRequestParamIsNotNull(
$params, 'signature', 'Expected signature in query params'
);
}
return self::doValidateSignature($this->getClientSecret(), $params);
}
public static function doValidateSignature($secret, array $params)
{
if (isset($params['hmac'])) {
$signature = $params['hmac'];
unset($params['signature']);
unset($params['hmac']);
return self::doGenerateSignature($secret, $params, true) === $signature;
}
$signature = $params['signature'];
unset($params['signature']);

return $this->generateSignature($params) === $signature;

return
self::doGenerateSignature($secret, $params, true) === $signature ||
self::doGenerateSignature($secret, $params, false) === $signature;
}

/**
Expand Down
10 changes: 6 additions & 4 deletions lib/Shopify/HttpClient/CurlHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class CurlHttpClient extends HttpClientAdapter
* set to false to stop cURL from verifying the peer's certificate
* @var boolean
*/
protected $verifyPeer = true;
protected $verifyPeer;

/**
* set to 1 to check the existence of a common name in the SSL peer
Expand All @@ -22,7 +22,7 @@ class CurlHttpClient extends HttpClientAdapter
* be kept at 2 (default value).
* @var integer
*/
protected $verifyHost = 2;
protected $verifyHost;

/**
* The name of a file holding one or more certificates to verify
Expand All @@ -42,11 +42,14 @@ class CurlHttpClient extends HttpClientAdapter
*
*
* @param string $certificatePath
* @param bool $verifyPeer
*/
public function __construct($certificatePath = null)
public function __construct($certificatePath = null, $verifyPeer=true, $verifyHost=2)
{

$this->certificatePath = $certificatePath;
$this->verifyPeer = $verifyPeer;
$this->verifyHost = $verifyHost;
$this->headers = array();

}
Expand Down Expand Up @@ -162,7 +165,6 @@ protected function initCurlHandler($uri)
if ($this->verifyPeer === false) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
} else {

// @see http://curl.haxx.se/docs/caextract.html

if (!file_exists($this->certificatePath)) {
Expand Down
6 changes: 6 additions & 0 deletions lib/Shopify/Redirector/HeaderRedirector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ class HeaderRedirector implements \Shopify\Redirector

public function redirect($uri)
{
self::go($uri);
}

public static function go($uri)
{
header('Location: ' . $uri);
exit(0);

}



}
9 changes: 9 additions & 0 deletions tests/Shopify/Api/Tests/AuthenticationGatewayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@
class AuthenticationGatewayTest extends \PHPUnit_Framework_TestCase
{

/**
* @var \Shopify\Api\AuthenticationGateway
*/
protected $authenticate;

/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $httpClient;

/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $redirector;

public function setUp()
Expand Down
69 changes: 48 additions & 21 deletions tests/Shopify/Api/Tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,32 @@
class ClientTest extends \PHPUnit_Framework_TestCase
{

/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $httpClient;

/**
* @var \Shopify\Api\Client
*/
protected $api;

public $shopName;
public $sharedSecret;
public $accessToken;
public $shopUri;

public function setUp()
{

$this->shopName = 'mycoolshop';
$this->clientSecret = 'ABC123XYZ';
$this->permanentAccessToken = '0987654321';
$this->sharedSecret = 'ABC123XYZ';
$this->accessToken = '0987654321';
$this->shopUri = "https://{$this->shopName}.myshopify.com";

$this->httpClient = $this->getMock('Shopify\HttpClient');

$this->api = new \Shopify\Api\Client($this->httpClient);
$this->api->setShopName($this->shopName);
$this->api->setClientSecret($this->clientSecret);
$this->api->setAccessToken($this->permanentAccessToken);

$this->api = new \Shopify\Api\Client($this->httpClient, $this);
}

public function testGetRequest()
Expand Down Expand Up @@ -66,26 +77,42 @@ public function testPostRequest()

}

public function testRequestValidation()
{

$this->api->setClientSecret('hush');
public function getValidationData() {
return [
['hush','31b9fcfbd98a3650b8523bcc92f8c5d2',[
'code' => "a94a110d86d2452eb3e2af4cfb8a3828",
'shop' => "some-shop.myshopify.com",
'timestamp' => "1337178173", // 2012-05-16 14:22:53
], false],
['hush','6e39a2ea9e497af6cb806720da1f1bf3',[
'code'=>'a94a110d86d2452eb3e2af4cfb8a3828',
'hmac'=>'2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2',
'shop'=>'some-shop.myshopify.com',
'timestamp'=>'1337178173',
], false],
['hush','2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2',[
'code'=>'a94a110d86d2452eb3e2af4cfb8a3828',
'shop'=>'some-shop.myshopify.com',
'timestamp'=>'1337178173',
], true]

];
}

$signature = "31b9fcfbd98a3650b8523bcc92f8c5d2";
/**
* @dataProvider getValidationData
*/
public function testRequestValidation($secret, $signature, $params, $hmac)
{

// Assume we have the query parameters in a hash
$params = array(
'shop' => "some-shop.myshopify.com",
'code' => "a94a110d86d2452eb3e2af4cfb8a3828",
'timestamp' => "1337178173", // 2012-05-16 14:22:53
);
$this->api->setClientSecret($secret);

$this->assertEquals($signature, $this->api->generateSignature($params));
$this->assertEquals($signature, $this->api->generateSignature($params, $hmac));

$paramsWithSignature = $params;
$paramsWithSignature['signature'] = $signature;
$paramsWithSignature[$hmac?'hmac':'signature'] = $signature;

$this->assertTrue($this->api->validateSignature($paramsWithSignature));
$this->assertTrue($this->api->validateSignature($paramsWithSignature), "{$signature} failed");

// request is older than 1 day, expect false
$this->assertFalse($this->api->isValidRequest($paramsWithSignature));
Expand Down