Completed
Push — whmcs-jetpack-partner-module ( 12fa09...087d5a )
by
unknown
06:31
created

jetpack.php ➔ jetpack_TerminateAccount()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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