Completed
Push — whmcs-jetpack-partner-module ( 826d96...3d41d3 )
by
unknown
06:38
created

jetpack.php ➔ jetpack_TerminateAccount()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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