Completed
Push — master ( 24e163...e3aa11 )
by Matt
04:12
created

Client::callApiMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 2
dl 0
loc 8
ccs 6
cts 6
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace Gothick\AkismetClient;
3
use GuzzleHttp\HandlerStack;
4
use GuzzleHttp\Handler\CurlHandler;
5
6
class Client
7
{
8
	const VERB_VERIFY_KEY = 'verify-key';
9
	const VERB_COMMENT_CHECK = 'comment-check';
10
	/**
11
	 * Akismet API key
12
	 *
13
	 * @var string
14
	 */
15
	private $api_key;
16
17
	/**
18
	 * Our Guzzle client.
19
	 * This can be passed in for DI, or if not we'll create a default one ouselves.
20
	 *
21
	 * @var \GuzzleHttp\Client
22
	 */
23
	private $guzzle_client;
24
25
	/**
26
	 * URL of the site using us.
27
	 * Akismet calls this "blog", because WordPress.
28
	 *
29
	 * @var string
30
	 */
31
	private $blog;
32
33
	/**
34
	 * Name of the site using us.
35
	 *
36
	 * @var string
37
	 */
38
	private $app_name;
39
40
	/**
41
	 * Version string of the site using us.
42
	 *
43
	 * @var string
44
	 */
45
	private $app_version;
46
47
	/**
48
	 * Version of this Client.
49
	 * Akismet likes to know. We should bump this every time
50
	 * we package a new version.
51
	 *
52
	 * @var string
53
	 */
54
	const VERSION = '0.1';
55
56
	/**
57
	 * Make an Akismet API client.
58
	 * Typically you'd provide an API key in $api_key, at which point you can make any call. Without the optional 
59
	 * $api_key you're limited to calling verifyApiKey. Once you've verified a key you can call setApiKey() 
60
	 * later and start using the rest of the API.
61
	 *
62
	 * @param string $app_url
63
	 *        	e.g. http://forum.example.com/
64
	 * @param string $app_name
65
	 *        	e.g. phpBB
66
	 * @param string $app_version
67
	 *        	e.g. 3.2.1
68
	 * @param string $api_key
69
	 *        	(optional) Akismet API key
70
	 * @param
71
	 *        	\GuzzleHttp\Client (optional) $guzzle_client. You can inject a mock, or a non-Curl-using Guzzle 
72
	 *        	client here, say. Otherwise we'll just make one.
73
	 * @throws Exception
74
	 */
75 29
	public function __construct($app_url, $app_name, $app_version, $api_key = null, $guzzle_client = null)
76
	{
77 29
		if ((empty($app_url)) || (empty($app_name)) || (empty($app_version)))
78
		{
79 6
			throw new Exception('Must supply app URL, name and version in ' . __METHOD__);
80
		}
81
		// The Akismet API calls it a blog, so keep consistent.
82 23
		$this->blog = $app_url;
83
84 23
		$this->app_name = $app_name;
85 23
		$this->app_version = $app_version;
86 23
		$this->api_key = $api_key;
87
88
		// Our client is passed in, as dependency injection is helpful for 
89
		// testing, but in the normal course of things we'll probably just
90
		// create it ourselves.
91 23
		$this->guzzle_client = $guzzle_client;
92 23
		if (!isset($this->guzzle_client))
93
		{
94 3
			$this->guzzle_client = new \GuzzleHttp\Client();
95
		}
96 23
	}
97
98 17
	private function getStandardHeaders()
99
	{
100
		// I'd use Guzzle middleware for this, as we want to add it on 
101
		// every request, but how do I do that and support dependency 
102
		// injection of our client? You can't add middleware to a 
103
		// Guzzle client after it's been constructed, right?
104
		return array(
105 17
				'User-Agent' => $this->getOurUserAgent()
106
		);
107
	}
108
109 17
	private function getOurUserAgent()
110
	{
111
		// From the docs:
112
		// Setting your user agent If possible, your user agent string should always use the following format: Application Name/Version | Plugin Name/Version
113
		// e.g. WordPress/4.4.1 | Akismet/3.1.7
114
		// TODO: Check this is formatting correctly.
115
		// TODO: Add unit test
116 17
		return "{$this->app_name}/{$this->app_version} | Gothick\\AkismetClient/" . self::VERSION;
117
	}
118
119 3
	public function setApiKey($api_key)
120
	{
121 3
		if (empty($api_key))
122
		{
123 2
			throw new Exception('Must provide an API key in ' . __METHOD__);
124
		}
125 1
		$this->api_key = $api_key;
126 1
	}
127
128 8
	public function verifyKey($api_key = null)
129
	{
130 8
		$key_to_verify = empty($api_key) ? $this->api_key : $api_key;
131
132 8
		if (empty($key_to_verify))
133
		{
134 1
			throw new Exception('Must provide or pre-configure a key in ' . __METHOD__);
135
		}
136
137
		try
138
		{
139
			$params = [
140 7
					"key" => $key_to_verify,
141 7
					"blog" => $this->blog 
142
			];
143 7
			$response = $this->callApiMethod(self::VERB_VERIFY_KEY, $params);
144 1
		} catch (\Exception $e)
145
		{
146
			// Wrap whatever exception we caught up in a new exception of our 
147
			// own type and throw it along up the line.
148 1
			throw new Exception('Unexpected exception in ' . __METHOD__, 0, $e);
149
		}
150 6
		return new VerifyKeyResult($response);
151
	}
152
153
	/**
154
	 * Check a comment for spam.
155
	 * See the Akismet API documentation for full details:
156
	 * https://akismet.com/development/api/#comment-check. 
157
	 * Returns a valid ClientResult object or throws an exception.
158
	 *
159
	 * @param array $params
160
	 *        	User IP, User-Agent, the message, etc. See the Akismet API
161
	 *        	documentation for details.
162
	 * @param array $server_params
163
	 *        	This can just be $_SERVER, if you have access to it
164
	 * @param string $user_role
165
	 *        	If 'administrator', will always pass the check
166
	.*
167
	 */
168 13
	public function commentCheck($params = array(), $server_params = array(), $user_role = 'guest')
169
	{
170
		// According to the Akismet docs, these two (and 'blog', which we have as $this->blog already) are
171
		// the only required parameters. Seems odd, but hey.
172 13
		if (empty($params[ 'user_ip' ]) || empty($params[ 'user_agent' ]))
173
		{
174 2
			throw new Exception(__METHOD__ . ' requires user_ip and user_agent in $params');
175
		}
176
177 11
		$params = array_merge($server_params, $params);
178 11
		$params = array_merge($params, [
179 11
				'blog' => $this->blog,
180 11
				'user_role' => $user_role
181
		]);
182
183
		try
184
		{
185 11
			$response = $this->callApiMethod(self::VERB_COMMENT_CHECK, $params);
186 1
		} catch (\Exception $e)
187
		{
188 1
			throw new Exception('Unexpected exception in ' . __METHOD__, 0, $e);
189
		}
190 10
		return new CommentCheckResult($response);
191
	}
192
193
	/**
194
	 * Call an Akisemet API method.
195
	 * @param string $verb
196
	 * @param array $params
197
	 * @return \GuzzleHttp\Psr7\Response
198
	 */
199 18
	private function callApiMethod($verb, $params)
200
	{
201 18
		return $this->guzzle_client->request(
202 18
				'POST',
203 18
				$this->apiUri($verb),
204
				[
205 17
						'form_params' => $params,
206 17
						'headers' => $this->getStandardHeaders()
207
				]);
208
	}
209
210
	/**
211
	 * Work out the Akismet API URL given the REST verb and our configured key. This would
212
	 * be far less of a pain if Akismet just had you pass the API key as a parameter or 
213
	 * a header. Gawd knows why they change the host for authenticated calls.
214
	 * @param string $verb
215
	 * @throws Exception
216
	 * @return string
217
	 */
218 18
	private function apiUri($verb)
219
	{
220 18
		if ($verb == self::VERB_VERIFY_KEY)
221
		{
222 7
			return "https://rest.akismet.com/1.1/verify-key";
223
		} else
224
		{
225 11
			if (empty($this->api_key))
226
			{
227 1
				throw new Exception("Can't call authenticated method without setting an API key in " . __METHOD__);
228
			}
229 10
			return "https://{$this->api_key}.rest.akismet.com/1.1/$verb";
230
		}
231
	}
232
}