Completed
Push — whmcs-jetpack-partner-module ( 85a63c...12992f )
by
unknown
07:06
created

jetpack.php ➔ get_authentication_errors_from_response()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 1
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
use WHMCS\Database\Capsule;
4
5
if (!defined("WHMCS")) {
6
    die("This file cannot be accessed directly");
7
}
8
9
10
/**
11
 * A WHMCS module for use by Jetpack hosting partners to provision Jetpack plans.
12
 * The module provides functionality for partner hosts to be able to save their
13
 * client id and secret to request an access token for provisioning plans.
14
 *
15
 * Plans available for provisioning include free, personal, premium and professional
16
 *
17
 * A host has options to either provision(Create == WHMCS equivalent functional term)
18
 * or Cancel(Terminate == WHMCS equivalent functional term) from the WHMCS client area.
19
 *
20
 * Host setup for custom fields is currently required in order to use the module.
21
 *
22
 */
23
24
/**
25
 * Jetpack Meta Data for WHMCS module.
26
 * @return array
27
 */
28
function jetpack_MetaData()
29
{
30
    return [
31
        'DisplayName' => 'Jetpack by Automattic',
32
        'Description' => 'Use this module to provision Jetpack plans with your Jetpack hosting partner account',
33
        'APIVersion' => '1.1',
34
        'RequiresServer' => false,
35
    ];
36
}
37
38
39
/**
40
 * Basic configuration options required for a partner to get
41
 * a Jetpack plan provisioned. Currently a partner client id
42
 * and secret are the only host partner options needed to get
43
 * an access token to provision a Jetpack plan
44
 * @return array
45
 */
46
function jetpack_ConfigOptions()
47
{
48
    return [
49
        'Jetpack Partner Client ID' => [
50
            'Type' => 'text',
51
            'Size' => '256',
52
        ],
53
        'Jetpack Partner Client Secret' => [
54
            'Type' => 'text',
55
            'Size' => '256',
56
        ]
57
    ];
58
}
59
60
61
/**
62
 * Equivalent to /provision. Create a Jetpack plan using
63
 * a Jetpack Hosting partner account. WHMCS expects the string "success"
64
 * to be returned if the process is completed successfully otherwise
65
 * a string can be returned which will be logged as part of the error.
66
 *
67
 * Pre Provisioning Steps:
68
 *  Module requirements are validated and will return an error string
69
 *  if the module was not setup correctly. An error string will also be
70
 *  returned in the even that a request to provision a new returns a
71
 *  4xx or 5xx error.
72
 *
73
 *  An Access token will also be retrieved before trying to provisioning.
74
 *  Errors strings are prefixed with 'JETPACK MODULE' so if getting the access
75
 *  token starts with this
76
 *
77
 * If the response from provisioning does not contain "success" in the message
78
 * consider the provisioning to be incomplete and get a useful error from the response
79
 *
80
 * @param array WHMCS $params
81
 * @return string Either 'success' or an error with what went wrong when provisioning
82
 * @throws Exception An exception is thrown from make_api_request when there is a curl_error or an empty response
83
84
 */
85
function jetpack_CreateAccount(array $params)
86
{
87
88
    $module_errors = validate_required_fields($params);
89
    if ($module_errors !== true) {
90
        return $module_errors;
91
    }
92
93
    $access_token = get_access_token($params);
94
    if (strpos($access_token, 'JETPACK MODULE') === 0) {
95
        return $access_token;
96
    }
97
98
    try {
99
        $provisioning_url = "https://public-api.wordpress.com/rest/v1.3/jpphp/provision";
100
        $stripped_url = preg_replace("(^https?://)", "", $params['customfields']['Site URL']);
101
        $stripped_url = rtrim($stripped_url, '/');
102
103
        $request_data = [
104
            'plan' => strtolower($params['customfields']['Plan']),
105
            'siteurl' => $stripped_url,
106
            'local_user' => $params['customfields']['Local User'],
107
            'force_register' => true,
108
        ];
109
110
        $response = make_api_request($provisioning_url, $access_token, $request_data);
111
        if ($response->success && $response->success == true) {
112
            if ($response->next_url) {
113
                save_provisioning_details($response->next_url, $params);
114
            } elseif (!$response->next_url && $response->auth_required) {
115
                save_provisioning_details($response->next_url, $params, true);
116
            }
117
            return 'success';
118
        } else {
119
            $errors = get_provisioning_errors_from_response($response);
120
            return $errors;
121
        }
122
    } catch (Exception $e) {
123
        logModuleCall('jetpack', __FUNCTION__, $params, $e->getMessage(), $e->getTraceAsString());
124
        return $e->getMessage();
125
    }
126
}
127
128
/**
129
 * Equivalent to partner/cancel. Cancel a Jetpack plan using using a Jetpack Hosting partner account. This has
130
 * the same prerequiste steps as jetpack_createAccount and will return an error string if the module has not been
131
 * setup correctly or there was a failure getting an access token.
132
 *
133
 * The url scheme for the site being cancelled is not necessary when making this request and is stripped along
134
 * with trailing slashes
135
 *
136
 * If the response json does not contain "success" return error strings based on the response properties
137
 *
138
 * @param array WHMCS $params
139
 * @return string Either 'success' or an error with what went wrong when provisioning
140
 * @throws Exception An exception is thrown from make_api_request when there is a curl_error or an empty response
141
 */
142
function jetpack_TerminateAccount(array $params)
143
{
144
    $module_errors = validate_required_fields($params);
145
    if ($module_errors !== true) {
146
        return $module_errors;
147
    }
148
149
    $access_token = get_access_token($params);
150
    if (strpos($access_token, 'JETPACK MODULE') === 0) {
151
        return $access_token;
152
    }
153
154
    try {
155
        $stripped_url = preg_replace("(^https?://)", "", $params['customfields']['Site URL']);
156
        $clean_url = rtrim($stripped_url, '/');
157
        $clean_url = str_replace('/', '::', $clean_url);
158
159
160
        $request_url = 'https://public-api.wordpress.com/rest/v1.3/jpphp/' . $clean_url . '/partner-cancel';
161
        $response = make_api_request($request_url, $access_token);
162
        if ($response->success === true) {
163
            return 'success';
164
        } elseif ($response->success === false) {
165
            return 'JETPACK MODULE: Unable to terminate this Jetpack plan as it has likely already been cancelled';
166
        } else {
167
            $errors = get_cancellation_errors_from_response($response);
168
            return $errors;
169
        }
170
    } catch (Exception $e) {
171
        logModuleCall('jetpack', __FUNCTION__, $params, $e->getMessage(), $e->getTraceAsString());
172
        return $e->getMessage();
173
    }
174
}
175
176
/**
177
 * Get a Jetpack partner access token using the client_id and client secret
178
 * stored when the product was created in the WHMCS product settings. If the
179
 * response does not explicitly contain an access token provisioning or cancellation
180
 * cannot be attempted so return an error string.
181
 *
182
 *
183
 * @param array $params WHMCS params
184
 * @return mixed A string with the access token or an error string beginning with 'JETPACK MODULE' indicating that
185
 * an access token was not retrieved for provisioning or cancelling.
186
 * @throws Exception  An exception is thrown from make_api_request when there is a curl_error or an empty response
187
 */
188
function get_access_token($params)
189
{
190
191
    $oauth_url = "https://public-api.wordpress.com/oauth2/token";
192
193
    $credentials = [
194
        'client_id' => $params['configoption1'],
195
        'client_secret' => $params['configoption2'],
196
        'grant_type' => 'client_credentials',
197
        'scope' => 'jetpack-partner'
198
    ];
199
200
    $response = make_api_request($oauth_url, null, $credentials);
201
202
    if (isset($response->access_token)) {
203
        return $response->access_token;
204
    } else {
205
        $errors = get_authentication_errors_from_response($response);
206
        return $errors;
207
    }
208
}
209
210
211
/**
212
 * Make an API request for authenticating and provisioning or cancelling a Jetpack plan or getting an access token.
213
 * Include the http status in the response.
214
 *
215
 * @param string $url where to make the request to
216
 * @param null $auth access token to make a provisioning or cancellation request
217
 * @param null $data form data for the api request
218
 * @return mixed json_decoded response
219
 * @throws Exception On a curl error or an empty body an Exception will be thrown.
220
 */
221
function make_api_request($url, $auth = null, $data = null)
222
{
223
    if (isset($auth)) {
224
        $auth = "Authorization: Bearer " . $auth;
225
    }
226
227
    $curl = curl_init();
228
    curl_setopt_array($curl, [
229
        CURLOPT_HTTPHEADER => [$auth],
230
        CURLOPT_URL => $url,
231
        CURLOPT_RETURNTRANSFER => true,
232
        CURLOPT_ENCODING => "",
233
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
234
        CURLOPT_POSTFIELDS => $data,
235
        CURLOPT_CUSTOMREQUEST => "POST"
236
    ]);
237
238
    $response = curl_exec($curl);
239
    $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
240
    if (curl_error($curl)) {
241
        throw new Exception('Unable to connect: ' . curl_errno($curl) . ' - ' . curl_error($curl));
242
    } elseif (empty($response)) {
243
        throw new Exception('Empty response');
244
    }
245
246
    $decoded_response = json_decode($response);
247
    $decoded_response->http_status = $http_code;
248
    curl_close($curl);
249
250
    return $decoded_response;
251
}
252
253
/**
254
 * Save the next_url for Jetpack activation/setup to the
255
 * order for the client
256
 *
257
 * @param string $url The next url for activating the Jetpack plan
258
 * @param array $params WHMCS params
259
 * @param bool $pending If the plan is pending domain resolution.
260
 */
261
function save_provisioning_details($url, $params, $pending = false)
262
{
263
    $jetpack_next_url_field = Capsule::table('tblcustomfields')
264
        ->where(['fieldname' => 'jetpack_provisioning_details', 'type' => 'product'])->first();
265
266
    $details = '';
267
    if ($url) {
268
        $details = 'URL to Activate Jetpack: ' . $url;
269
    } elseif ($pending) {
270
        $details = 'The domain did not appear to resolve when provisioning was attempted however a Jetpack plan is 
271
        waiting for ' . $params['customfields']['Site URL'] . '. Once DNS resolves please connect the site via 
272
        the Jetpack Banner in the sites dashboard';
273
    }
274
    Capsule::table('tblcustomfieldsvalues')->where(['fieldid' => $jetpack_next_url_field->id])->update([
275
        'relid' => $params['model']['orderId'], 'value' => $details]);
276
}
277
278
/**
279
 * Validate that the module was correctly set up when the product was
280
 * created by the WHMCS user and that the required Fields/Options for
281
 * being able to provision a Jetpack plan are present. Fields validated are
282
 *  - Allowed Plans from Plan Custom Field
283
 *  - Required Custom Fields
284
 *  - Required Config Options
285
 *
286
 * @param array $params WHMCS params
287
 * @return bool|string An error describing what was not correctly included in the setup of the module
288
 */
289
function validate_required_fields(array $params)
290
{
291
    $allowed_plans = ['free', 'personal', 'premium', 'professional'];
292
    $required_custom_fields = ['Plan', 'Site URL', 'Local User', 'jetpack_provisioning_details'];
293
294
    foreach ($required_custom_fields as $field) {
295
        if (!isset($params['customfields'][$field])) {
296
            return 'JETPACK MODULE: The module does not appear to be setup correctly. The required custom field '
297
                . $field . ' was not setup when the product was created.
298
				Please see the module documentation for more information';
299
        }
300
    }
301
302
    if (!in_array(strtolower($params['customfields']['Plan']), $allowed_plans)) {
303
        return 'JETPACK MODULE: The module does not appear to be setup correctly. ' .
304
            $params['customfields']['Plan'] . ' is not an allowed plan';
305
    }
306
307
    if (!isset($params['configoption1']) || !isset($params['configoption2'])) {
308
        return'JETPACK MODULE: Your credentials for provisioning are not complete. Please see the module documentation
309
        for more information';
310
    }
311
    return true;
312
}
313
314
/**
315
 * If we are attempting to get an access token and this fails parse the response
316
 * http status code and response body and return a useful error from these
317
 * regarding what went wrong.
318
 *
319
 * Return a general error if no other information is available.
320
 *
321
 * @param object $response Response from request for access token
322
 * @return string an error string describing the issue when requesting an access token
323
 */
324
function get_authentication_errors_from_response($response)
325
{
326
    if ($response->http_status == 400 and $response->error_description) {
327
        return 'JETPACK MODULE: There was a problem getting an access token for your Jetpack hosting partner 
328
            account. This usually means the Client Id or Client Secret provided when setting up the module are invalid.
329
            The specific error from the request was ' . $response->error_description;
330
    } elseif ($response->http_status > 500) {
331
        return 'JETPACK MODULE: There was an error communicating with the provisioning server. Please try again later.';
332
    }
333
    return 'JETPACK MODULE: There was an error getting an authentication token. Please contact us for assistance';
334
}
335
336
/**
337
 * If provisioning fails for a Jetpack plan parse the response http status code
338
 * and response body and return a useful error regarding what went wrong. Include
339
 * the response message if there is one.
340
 *
341
 * If the response is a 400 or 403 and there is no message in the response return
342
 * a generic error letting the partner know.
343
 *
344
 * @param object $response Response from the provisioning request
345
 * @return string an error string provided to the partner host describing the issue.
346
 */
347
function get_provisioning_errors_from_response($response)
348
{
349
    if (($response->http_status == 400 || $response->http_status == 403) && $response->message) {
350
        return 'JETPACK MODULE: The following error was returned trying to provision a plan - ' . $response->message;
351
    } elseif ($response->http_status > 500) {
352
        return 'JETPACK MODULE: There was an error communicating with the provisioning server. Please try again later.';
353
    }
354
    return 'JETPACK MODULE: There was an error provisioning the Jetpack plan. Please contact us for assistance.';
355
}
356
357
358
/**
359
 * If termination fails for a Jetpack plan parse the http status code and response body
360
 * and return a useful error message.
361
 *
362
 * @param object $response Response from the provisioning request
363
 * @return string error message for a failed plan cancellation describing the issue.
364
 */
365
function get_cancellation_errors_from_response($response)
366
{
367
    if ($response->http_status == 404) {
368
        return 'JETPACK MODULE: The http response was a 404 which likely means the site url attempting to be cancelled
369
        is invalid';
370
    } elseif ($response->http_status > 500) {
371
        return 'JETPACK MODULE: There was an error communicating with the provisioning server. Please try again later.';
372
    }
373
    return 'JETPACK MODULE: Unable to terminate the plan. Please contact us for assistance';
374
}
375