Skip to content

Commit 3b8150d

Browse files
committed
Fix user agent string to confirm to the client library spec
See https://github.com/Nexmo/client-library-specification/blob/master/SPECIFICATION.md#reporting Instead of having multiple parts delimited with slashes, each name/version pair must be it's own entry, separated with a space. This commit also allows the user to specify an app/name version to help us identify their traffic if required
1 parent ca6b252 commit 3b8150d

File tree

2 files changed

+116
-12
lines changed

2 files changed

+116
-12
lines changed

src/Client.php

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
*/
3939
class Client
4040
{
41-
const VERSION = '1.0.0-beta1';
41+
const VERSION = '1.0.0';
4242

4343
const BASE_API = 'https://api.nexmo.com';
4444
const BASE_REST = 'https://rest.nexmo.com';
@@ -85,6 +85,11 @@ public function __construct(CredentialsInterface $credentials, $options = array(
8585

8686
$this->options = $options;
8787

88+
// If they've provided an app name, validate it
89+
if (isset($options['app'])) {
90+
$this->validateAppOptions($options['app']);
91+
}
92+
8893
$this->setFactory(new MapFactory([
8994
'message' => 'Nexmo\Message\Client',
9095
'verify' => 'Nexmo\Verify\Client',
@@ -319,17 +324,45 @@ public function send(\Psr\Http\Message\RequestInterface $request)
319324
}
320325
}
321326

322-
//set the user-agent
323-
$request = $request->withHeader('user-agent', implode('/', [
324-
'nexmo-php',
325-
self::VERSION,
326-
'PHP-' . implode('.', [PHP_MAJOR_VERSION, PHP_MINOR_VERSION])
327-
]));
327+
// The user agent must be in the following format:
328+
// LIBRARY-NAME/LIBRARY-VERSION LANGUAGE-NAME/LANGUAGE-VERSION [APP-NAME/APP-VERSION]
329+
// See https://github.com/Nexmo/client-library-specification/blob/master/SPECIFICATION.md#reporting
330+
$userAgent = [];
331+
332+
// Library name
333+
$userAgent[] = 'nexmo-php/'.self::VERSION;
334+
335+
// Language name
336+
$userAgent[] = 'php/'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;
337+
338+
// If we have an app set, add that to the UA
339+
if (isset($this->options['app'])) {
340+
$app = $this->options['app'];
341+
$userAgent[] = $app['name'].'/'.$app['version'];
342+
}
343+
344+
// Set the header. Build by joining all the parts we have with a space
345+
$request = $request->withHeader('user-agent', implode(" ", $userAgent));
328346

329347
$response = $this->client->sendRequest($request);
330348
return $response;
331349
}
332350

351+
protected function validateAppOptions($app) {
352+
$disallowedCharacters = ['/', ' ', "\t", "\n"];
353+
foreach (['name', 'version'] as $key) {
354+
if (!isset($app[$key])) {
355+
throw new \InvalidArgumentException('app.'.$key.' has not been set');
356+
}
357+
358+
foreach ($disallowedCharacters as $char) {
359+
if (strpos($app[$key], $char) !== false) {
360+
throw new \InvalidArgumentException('app.'.$key.' cannot contain the '.$char.' character');
361+
}
362+
}
363+
}
364+
}
365+
333366
public function serialize(EntityInterface $entity)
334367
{
335368
if($entity instanceof Verification){

test/ClientTest.php

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,43 @@ public function setUp()
5252
$this->container = new Client\Credentials\Container($this->key_credentials, $this->basic_credentials, $this->signature_credentials);
5353
}
5454

55+
/**
56+
* @dataProvider validateAppNameThrowsProvider
57+
*/
58+
public function testValidateAppNameThrows($name, $version, $field, $invalidCharacter)
59+
{
60+
try {
61+
new Client($this->basic_credentials, [
62+
'app' => [
63+
'name' => $name,
64+
'version' => $version
65+
]
66+
], $this->http);
67+
68+
$this->fail('invalid app details provided, but no exception was thrown');
69+
} catch (\InvalidArgumentException $e) {
70+
$this->assertEquals('app.'.$field.' cannot contain the '.$invalidCharacter.' character', $e->getMessage());
71+
}
72+
73+
}
74+
75+
public function validateAppNameThrowsProvider()
76+
{
77+
$r = [];
78+
79+
$r['/ name'] = ['foo/bar', '1.0', 'name', '/'];
80+
$r['space name'] = ['foo bar', '1.0', 'name', ' '];
81+
$r['tab name'] = ["foo\tbar", '1.0', 'name', "\t"];
82+
$r['newline name'] = ["foo\nbar", '1.0', 'name', "\n"];
83+
84+
$r['/ version'] = ['foobar', '1/0', 'version', '/'];
85+
$r['space version'] = ['foobar', '1 0', 'version', ' '];
86+
$r['tab version'] = ["foobar", "1\t0", 'version', "\t"];
87+
$r['newline version'] = ["foobar", "1\n0", 'version', "\n"];
88+
89+
return $r;
90+
}
91+
5592
public function testBasicCredentialsQuery()
5693
{
5794
$client = new Client($this->basic_credentials, [], $this->http);
@@ -299,10 +336,10 @@ public function testNamespaceFactory()
299336
$this->assertSame($api, $client->sms());
300337
}
301338

302-
public function testUserAgentString()
339+
public function testUserAgentStringAppNotProvided()
303340
{
304341
$version = Client::VERSION;
305-
$php = 'PHP-' . implode('.', [
342+
$php = 'php/' . implode('.', [
306343
PHP_MAJOR_VERSION,
307344
PHP_MINOR_VERSION
308345
]);
@@ -320,15 +357,49 @@ public function testUserAgentString()
320357

321358
//useragent should match the expected format
322359
$agent = $this->http->getRequests()[0]->getHeaderLine('user-agent');
323-
$expected = implode('/', [
324-
'nexmo-php',
325-
$version,
360+
$expected = implode(' ', [
361+
'nexmo-php/'.$version,
326362
$php
327363
]);
328364

329365
$this->assertEquals($expected, $agent);
330366
}
331367

368+
public function testUserAgentStringAppProvided()
369+
{
370+
$version = Client::VERSION;
371+
$php = 'php/' . implode('.', [
372+
PHP_MAJOR_VERSION,
373+
PHP_MINOR_VERSION
374+
]);
375+
376+
//get a mock response to test
377+
$response = new Response();
378+
$response->getBody()->write('test response');
379+
$this->http->addResponse($response);
380+
381+
$client = new Client(new Basic('key', 'secret'), [
382+
'app' => [
383+
'name' => 'TestApp',
384+
'version' => '9.4.5'
385+
]
386+
], $this->http);
387+
$request = $this->getRequest();
388+
389+
//api client should simply pass back the http response
390+
$client->send($request);
391+
392+
//useragent should match the expected format
393+
$agent = $this->http->getRequests()[0]->getHeaderLine('user-agent');
394+
$expected = implode(' ', [
395+
'nexmo-php/'.$version,
396+
$php,
397+
'TestApp/9.4.5'
398+
]);
399+
400+
$this->assertEquals($expected, $agent);
401+
}
402+
332403
public function testSerializationProxiesVerify()
333404
{
334405
$verify = $this->prophesize('Nexmo\Verify\Client');

0 commit comments

Comments
 (0)