1: <?php
2: /**
3: * Handle crunchmail REST API in php
4: *
5: * PHP version 5.5+
6: *
7: * @author Yannick Huerre <dev@sheoak.fr>
8: * @copyright 2015 (c) Oasiswork
9: * @license https://opensource.org/licenses/MIT MIT
10: *
11: * @link https://github.com/crunchmail/crunchmail-client-php
12: * @link http://docs.guzzlephp.org/en/latest/
13: *
14: * @todo check $message->bounces (bounce resource)
15: * @todo check $message->spam (spam resource)
16: * @todo check $message->stats (stat resource)
17: * @todo implements $message->archive (archive_url resource)
18: * @todo implements forbidden resources list for entities
19: */
20:
21: namespace Crunchmail;
22:
23: /**
24: * Crunchmail\Client main class
25: */
26: class Client extends \GuzzleHttp\Client
27: {
28: /**
29: * Allowed paths and mapping to api resource path
30: * ex: $client->recipients will access path /mails
31: *
32: * @var array
33: */
34: public static $paths = [
35: 'recipients' => 'mails',
36: "optouts" => 'opt-outs'
37: ];
38:
39: /**
40: * Plural / Singular names of entites
41: * This is used to generate class name that need singular form
42: *
43: * @var array
44: */
45: public static $entities = [
46: 'categories' => 'category',
47: 'preview' => 'preview',
48: 'lists' => 'contactList'
49: ];
50:
51: /**
52: * @var array
53: */
54: private $config;
55:
56: /**
57: * List of authorized methods on client.
58: * ex: $client->get($url);
59: *
60: * @var array
61: */
62: public static $methods = [
63: 'get',
64: 'delete',
65: 'head',
66: 'options',
67: 'patch',
68: 'post',
69: 'put'
70: //'request' // request is disable for now, not implemented
71: ];
72:
73: /**
74: * Default request format
75: *
76: * @param string
77: */
78: public $format = 'json';
79:
80: /**
81: * Default headers
82: *
83: * @param array
84: */
85: public $headers = [];
86:
87: /**
88: * Initilialize the client, extends guzzle constructor
89: *
90: * @param array $config API configuration
91: *
92: * @return object
93: */
94: public function __construct(array $config = [])
95: {
96: if (!isset($config['base_uri']))
97: {
98: throw new \RuntimeException('base_uri is missing in configuration');
99: }
100:
101: if (!isset($config['token_uri']))
102: {
103: throw new \RuntimeException('token_uri is missing in configuration');
104: }
105:
106: $this->config = $config;
107:
108: return parent::__construct($config);
109: }
110:
111: /**
112: * Create a resource when accessing client properties and returns it
113: *
114: * Example:
115: * $client->messages
116: * $messageEntity->recipients
117: *
118: * @param string $name property
119: *
120: * @return Crunchmail\Resources\GenericResource
121: */
122: public function __get($name)
123: {
124: return $this->$name = $this->createResource($name);
125: }
126:
127: /**
128: * Translate resource to class name, camelcased
129: *
130: * @param string $str resource name
131: *
132: * @return string
133: */
134: private function toCamelCase($str)
135: {
136: return str_replace(' ', '', ucwords(str_replace('_', ' ', $str)));
137: }
138:
139: /**
140: * Create a resource depending on name
141: *
142: * If a specific class exists for this type of ressource (ie:
143: * attachmentResource) then it will be used.
144: *
145: * Forcing an url is usefull when creating a sub-resource from an
146: * entity object, because the base url is then specific
147: *
148: * @param string $name name of the resource (ie: attachments)
149: * @param string $url force an url for the resource
150: *
151: * @return mixed
152: */
153: public function createResource($name, $url = '')
154: {
155: $camelCase = $this->toCamelCase($name);
156:
157: // TODO: find a way to make namespace "use" works with this
158: $classPrefix = '\\Crunchmail\\Resources\\';
159: $className = $classPrefix . $camelCase . 'Resource';
160:
161: if (!class_exists($className))
162: {
163: $className = $classPrefix . 'GenericResource';
164: }
165:
166: return new $className($this, $name, $url);
167: }
168:
169: /**
170: * Convert resource path if map is found, path otherwise
171: *
172: * @param string $path
173: *
174: * @return string
175: */
176: public function mapPath($path)
177: {
178: return isset(self::$paths[$path]) ? self::$paths[$path] : $path;
179: }
180:
181: /**
182: * Request the API with the given method and params.
183: *
184: * This will execute a guzzle call and catch any guzzle exception.
185: * In that case the values must be in the format expected from guzzle
186: *
187: * @param string $method method to test
188: * @param string $url url id
189: * @param array $values data
190: * @param array $filters filters to apply
191: *
192: * @return stdClass
193: *
194: * @link http://docs.guzzlephp.org/en/latest/quickstart.html?highlight=multipart#sending-form-files
195: * @link http://docs.guzzlephp.org/en/latest/request-options.html?highlight=query#query
196: *
197: * @todo Refactor to match guzzle format ( request() )
198: */
199: //public function apiRequest($method, $url = '', $values = [], $filters = [])
200: public function apiRequest($method, $url = '', $values = [], $filters = [])
201: {
202: $parse = parse_url($url);
203:
204: // if url contains a query string, we have to merge it to avoid
205: // any conflict with filters
206: if (isset($parse['query']))
207: {
208: $query = $parse['query'];
209: parse_str($query, $output);
210: $filters = array_merge($filters, $output);
211: }
212:
213: try
214: {
215: // TODO: merge headers or use guzzle default on client?
216: // making the guzzle call, json or multipart
217: $result = $this->$method($url, [
218: $this->format => $values,
219: 'query' => $filters,
220: 'headers' => $this->headers
221: ]);
222: }
223: catch (\Exception $e)
224: {
225: $this->catchGuzzleException($e);
226: }
227:
228: // TODO: handle non JSON response
229: return json_decode((string) $result->getBody());
230: }
231:
232: /**
233: * Return an auth token from credentials
234: *
235: * @param string $identifier login
236: * @param string $password password
237: * @return string
238: */
239: public function getTokenFromCredentials($identifier, $password)
240: {
241: return $this->getToken([
242: 'identifier' => $identifier,
243: 'password' => $password
244: ]);
245: }
246:
247: /**
248: * Return an auth token from given parameters
249: *
250: * @param string $params parameters to post
251: * @return string
252: */
253: public function getToken(array $params = null)
254: {
255: if (is_null($params))
256: {
257: if (!isset($this->config['auth']) || count($this->config['auth']) < 2)
258: {
259: throw new \RuntimeException('auth parameters are missing');
260: }
261:
262: $params = ['api_key' => $this->config['auth'][1] ];
263: }
264:
265: $result = $this->apiRequest('post', $this->config['token_uri'], $params);
266: return isset($result->token) ? $result->token : null;
267: }
268:
269: /**
270: * Catch all guzzle exception types and execute proper action
271: *
272: * @param mixed $e guzzle exception
273: *
274: * @return null
275: */
276: protected function catchGuzzleException($exc)
277: {
278: // not a guzzle exception
279: if (strpos(get_class($exc), 'GuzzleHttp\\') !== 0)
280: {
281: throw $exc;
282: }
283:
284: // guzzle exceptions
285: throw new Exception\ApiException($exc->getMessage(), $exc->getCode(), $exc);
286: }
287: }
288: