Completed
Push — whmcs-jetpack-partner-module ( dd8634...4aa387 )
by
unknown
06:43
created

jetpack.php ➔ get_provisioning_errors_from_response()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 3
nop 1
dl 0
loc 9
rs 9.6111
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
79
 *
80
 * @param array $params
81
 * @return string Either 'success' or an error with what went wrong when provisioning
82
 * @throws Exception
83
84
 */
85
function jetpack_CreateAccount(array $params)
86
{
87
88
    $module_errors = validate_required_fields($params);
89
    if ($module_errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $module_errors of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 $params
139
 * @return string
140
 * @throws Exception
141
 */
142
function jetpack_TerminateAccount(array $params)
143
{
144
    $module_errors = validate_required_fields($params);
145
    if ($module_errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $module_errors of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 = str_replace('/', '::', $stripped_url);
157
        $clean_url = rtrim($clean_url, '/');
158
159
        $request_url = 'https://public-api.wordpress.com/rest/v1.3/jpphp/' . $clean_url . '/partner-cancel';
160
        $response = make_api_request($request_url, $access_token);
161
        if ($response->success === true) {
162
            return 'success';
163
        } elseif ($response->success === false) {
164
            return 'JETPACK MODULE: Unable to terminate this Jetpack plan as it has likely already been cancelled';
165
        } else {
166
            $errors = get_cancellation_errors_from_response();
167
            return $errors;
168
        }
169
    } catch (Exception $e) {
170
        logModuleCall('jetpack', __FUNCTION__, $params, $e->getMessage(), $e->getTraceAsString());
171
        return $e->getMessage();
172
    }
173
}
174
175
/**
176
 * Get a Jetpack partner access token using the client_id and client secret
177
 * stored when the product was created in the WHMCS product settings. If the
178
 * response does not explicitly contain an access token provisioning or cancellation
179
 * cannot be attempted so return an error string.
180
 *
181
 *
182
 * @param $params
183
 * @return mixed
184
 * @throws Exception
185
 */
186
function get_access_token($params)
187
{
188
189
    $oauth_url = "https://public-api.wordpress.com/oauth2/token";
190
191
    $credentials = [
192
        'client_id' => $params['configoption1'],
193
        'client_secret' => $params['configoption2'],
194
        'grant_type' => 'client_credentials',
195
        'scope' => 'jetpack-partner'
196
    ];
197
198
    $response = make_api_request($oauth_url, null, $credentials);
199
200
    if (isset($response->access_token)) {
201
        return $response->access_token;
202
    } else {
203
        return 'JETPACK MODULE: There was a problem getting an access token for your Jetpack hosting partner 
204
            account. This usually means the Client Id or Client Secret provided when setting up the module are invalid';
205
    }
206
}
207
208
209
/**
210
 * Make an API request for authenticating and provisioning or cancelling a Jetpack plan or getting an access token.
211
 * Include the http status in the response.
212
 *
213
 * @param string $url where to make the request to
214
 * @param null $auth access token to make a provisioning or cancellation request
215
 * @param null $data form data for the api request
216
 * @return mixed json_decoded response
217
 * @throws Exception On a curl error or an empty body an Exception will be thrown.
218
 */
219
function make_api_request($url, $auth = null, $data = null)
220
{
221
    if (isset($auth)) {
222
        $auth = "Authorization: Bearer " . $auth;
223
    }
224
225
    $curl = curl_init();
226
    curl_setopt_array($curl, [
227
        CURLOPT_HTTPHEADER => [$auth],
228
        CURLOPT_URL => $url,
229
        CURLOPT_RETURNTRANSFER => true,
230
        CURLOPT_ENCODING => "",
231
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
232
        CURLOPT_POSTFIELDS => $data,
233
        CURLOPT_CUSTOMREQUEST => "POST"
234
    ]);
235
236
    $response = curl_exec($curl);
237
    $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
238
    if (curl_error($curl)) {
239
        throw new Exception('Unable to connect: ' . curl_errno($curl) . ' - ' . curl_error($curl));
240
    } elseif (empty($response)) {
241
        throw new Exception('Empty response');
242
    }
243
244
    $decoded_response = json_decode($response);
245
    $decoded_response->http_status = $http_code;
246
    curl_close($curl);
247
248
    return $decoded_response;
249
}
250
251
/**
252
 * Save the next_url for Jetpack activation/setup to the
253
 * order for the client
254
 *
255
 * @param string $url The next url for activating the Jetpack plan
256
 * @param array $params WHMCS params
257
 * @param bool $pending If the plan is pending domain resolution.
258
 */
259
function save_provisioning_details($url, $params, $pending = false)
260
{
261
    $jetpack_next_url_field = Capsule::table('tblcustomfields')
262
        ->where(['fieldname' => 'jetpack_provisioning_details', 'type' => 'product'])->first();
263
264
    $details = '';
265
    if ($url) {
266
        $details = 'URL to Activate Jetpack: ' . $url;
267
    } elseif ($pending) {
268
        $details = 'The domain did not appear to resolve when provisioning was attempted however a Jetpack plan is 
269
        waiting for ' . $params['customfields']['Site URL'] . '. Once DNS resolves please connect the site via 
270
        the Jetpack Banner in the sites dashboard';
271
    }
272
    Capsule::table('tblcustomfieldsvalues')->where(['fieldid' => $jetpack_next_url_field->id])->update([
273
        'relid' => $params['model']['orderId'], 'value' => $details]);
274
}
275
276
/**
277
 * Validate that the module was correctly set up when the product was
278
 * created by the WHMCS user and that the required Fields/Options for
279
 * being able to provision a Jetpack plan are present. Fields validated are
280
 *  - Allowed Plans from Plan Custom Field
281
 *  - Required Custom Fields
282
 *  - Required Config Options
283
 *
284
 * @param array $params WHMCS params
285
 * @return string An error describing what was not correctly included in the setup of the module
286
 */
287
function validate_required_fields(array $params)
288
{
289
    $allowed_plans = ['free', 'personal', 'premium', 'professional'];
290
    $required_custom_fields = ['Plan', 'Site URL', 'Local User', 'jetpack_provisioning_details'];
291
292
    foreach ($required_custom_fields as $field) {
293
        if (!isset($params['customfields'][$field])) {
294
            return 'JETPACK MODULE: The module does not appear to be setup correctly. The required custom field '
295
                . $field . ' was not setup when the product was created.
296
				Please see the module documentation for more information';
297
        }
298
    }
299
300
    if (!in_array(strtolower($params['customfields']['Plan']), $allowed_plans)) {
301
        return 'JETPACK MODULE: The module does not appear to be setup correctly. ' .
302
            $params['customfields']['Plan'] . ' is not an allowed plan';
303
    }
304
305
    if (!isset($params['configoption1']) || !isset($params['configoption2'])) {
306
        return'JETPACK MODULE: Your credentials for provisioning are not complete. Please see the module documentation
307
        for more information';
308
    }
309
}
310
311
/**
312
 * If provisioning fails for a Jetpack plan parse the response http status code
313
 * and response body and return a useful error regarding what went wrong. Include
314
 * the response message if there is one.
315
 *
316
 * If the response is a 400 or 403 and there is no message in the response return
317
 * a generic error letting the partner know.
318
 *
319
 * @param object $response Response from the provisioning request
320
 * @return string an error string provided to the partner host describing the issue.
321
 */
322
function get_provisioning_errors_from_response($response)
323
{
324
    if (($response->http_status == 400 || $response->http_status == 403) && $response->message) {
325
        return 'JETPACK MODULE: The following error was returned trying to provision a plan - ' . $response->message;
326
    } elseif ($response->http_status > 500) {
327
        return 'JETPACK MODULE: There was an error communicating with the provisioning server. Please try again later.';
328
    }
329
    return 'JETPACK MODULE: There was an error provisioning the Jetpack plan. Please contact us for assistance.';
330
}
331
332
333
/**
334
 * If termination fails for a Jetpack plan parse the http status code and response body
335
 * and return a useful error message.
336
 *
337
 * @return string error message for a failed plan cancellation describing the issue.
338
 */
339
function get_cancellation_errors_from_response()
340
{
341
    return 'JETPACK MODULE: Unable to terminate the plan. Please contact us for assistance';
342
}
343