PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / trunk
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions vtrunk
260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 120219 120301 All 187 releases
s2member / src / includes / classes / utilities.inc.php

utilities.inc.php in s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions trunk, at src/includes/classes/utilities.inc.php

485 lines 18.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreFile
3 /**
4 * General utilities.
5 *
6 * Copyright: © 2009-2011
7 * {@link http://websharks-inc.com/ WebSharks, Inc.}
8 * (coded in the USA)
9 *
10 * Released under the terms of the GNU General Public License.
11 * You should have received a copy of the GNU General Public License,
12 * along with this software. In the main directory, see: /licensing/
13 * If not, see: {@link http://www.gnu.org/licenses/}.
14 *
15 * @package s2Member\Utilities
16 * @since 3.5
17 */
18 if(!defined('WPINC')) // MUST have WordPress.
19 exit ('Do not access this file directly.');
20
21 if(!class_exists('c_ws_plugin__s2member_utilities'))
22 {
23 /**
24 * General utilities.
25 *
26 * @package s2Member\Utilities
27 * @since 3.5
28 */
29 class c_ws_plugin__s2member_utilities
30 {
31 /**
32 * Evaluates PHP code, and 'returns' output.
33 *
34 * @package s2Member\Utilities
35 * @since 3.5
36 *
37 * @param string $code A string of data, possibly with embedded PHP code.
38 * @param array $vars Variables to put into the scope of `eval()`.
39 *
40 * @return string Output after PHP evaluation.
41 */
42 public static function evl($code = '', $vars = array())
43 {
44 if(is_array($vars) && !empty($vars))
45 extract($vars, EXTR_PREFIX_SAME, '_extract_');
46
47 ob_start(); // Output buffer.
48
49 eval ('?>'.trim($code));
50
51 return ob_get_clean();
52 }
53
54 /**
55 * Buffers (gets) function output.
56 *
57 * A variable length of additional arguments are possible.
58 * Additional parameters get passed into the ``$function``.
59 *
60 * @package s2Member\Utilities
61 * @since 3.5
62 *
63 * @param string $function Name of a function to call upon.
64 *
65 * @return string Output after call to function.
66 * Any output is buffered and returned.
67 */
68 public static function get($function = '')
69 {
70 $args = func_get_args();
71 $function = array_shift($args);
72
73 if(is_string($function) && $function)
74 {
75 ob_start(); // Open output buffer.
76
77 if(is_array($args) && !empty($args))
78 $return = call_user_func_array($function, $args);
79
80 else // There are no additional arguments to pass.
81 $return = call_user_func($function);
82
83 $echo = ob_get_clean(); // Close buffer.
84
85 return !strlen($echo) && strlen($return) ? $return : $echo;
86 }
87 return NULL;
88 }
89
90 /**
91 * Builds a version checksum for this installation.
92 *
93 * @package s2Member\Utilities
94 * @since 3.5
95 *
96 * @return string String with `[version]-[pro version]-[consolidated checksum]`.
97 */
98 public static function ver_checksum()
99 {
100 $checksum = WS_PLUGIN__S2MEMBER_VERSION; // Software version string.
101 $checksum .= (c_ws_plugin__s2member_utils_conds::pro_is_installed()) ? '-'.WS_PLUGIN__S2MEMBER_PRO_VERSION : ''; // Pro version string?
102 $checksum .= '-'.abs(crc32($GLOBALS['WS_PLUGIN__']['s2member']['c']['checksum'].$GLOBALS['WS_PLUGIN__']['s2member']['o']['options_checksum'].$GLOBALS['WS_PLUGIN__']['s2member']['o']['options_version']));
103
104 return $checksum; // (i.e., version-pro version-checksum)
105 }
106
107 /**
108 * String with current time details.
109 *
110 * @package s2Member\Utilities
111 * @since 130210
112 *
113 * @return string String with time representation (in UTC time).
114 */
115 public static function time_details()
116 {
117 $time = time(); // The time at this very moment.
118 $details = date('D M jS, Y', $time).' @ precisely '.date('g:i a e', $time);
119
120 return $details; // Return all details.
121 }
122
123 /**
124 * String with all version details *(for PHP, WordPress, s2Member, and Pro)*.
125 *
126 * @package s2Member\Utilities
127 * @since 3.5
128 *
129 * @return string String with `PHP vX.XX :: WordPress vX.XX :: s2Member vX.XX :: s2Member Pro vX.XX`.
130 */
131 public static function ver_details()
132 {
133 $details = 'PHP v'.PHP_VERSION.' :: WordPress v'.get_bloginfo('version').' :: s2Member v'.WS_PLUGIN__S2MEMBER_VERSION;
134 $details .= (c_ws_plugin__s2member_utils_conds::pro_is_installed()) ? ' :: s2Member Pro v'.WS_PLUGIN__S2MEMBER_PRO_VERSION : '';
135
136 return $details; // Return all details.
137 }
138
139 /**
140 * Generates s2Member Security Badge.
141 *
142 * @package s2Member\Utilities
143 * @since 3.5
144 *
145 * @param string $v A variation number to display. Defaults to `1`.
146 * @param bool $no_cache Defaults to false. If true, the HTML markup will contain query string params that prevent caching.
147 * @param bool $display_on_failure . Defaults to false. True if we need to display the 'NOT yet verified' version inside admin panels.
148 *
149 * @return string HTML markup for display of s2Member Security Badge.
150 */
151 public static function s_badge_gen($v = '1', $no_cache = FALSE, $display_on_failure = FALSE)
152 {
153 if($v && file_exists(($template = dirname(dirname(__FILE__)).'/templates/badges/s-badge.php')))
154 {
155 switch((int)$v) // Width/height based on variation.
156 {
157 case 1: // Variation number 1.
158
159 $width_height_attrs = 'width="200" height="55"';
160 $width_height_styles = 'width:200px; height:55px;';
161
162 break; // Break switch loop.
163
164 case 2: // Variation number 2.
165
166 $width_height_attrs = 'width="180" height="58"';
167 $width_height_styles = 'width:180px; height:58px;';
168
169 break; // Break switch loop.
170
171 case 3: // Variation number 3.
172
173 $width_height_attrs = 'width="80" height="15"';
174 $width_height_styles = 'width:80px; height:15px;';
175
176 break; // Break switch loop.
177
178 default: // Default case handler.
179
180 $width_height_attrs = $width_height_styles = '';
181
182 break; // Break switch loop.
183 }
184 $badge = trim(c_ws_plugin__s2member_utilities::evl(file_get_contents($template)));
185
186 $badge = preg_replace('/%%v%%/i', (string)(int)$v, $badge);
187 $badge = preg_replace('/%%site_url%%/i', urlencode(home_url()), $badge);
188 $badge = preg_replace('/%%no_cache%%/i', $no_cache ? '&amp;no_cache='.urlencode(mt_rand()) : '', $badge);
189 $badge = preg_replace('/%%display_on_failure%%/i', $display_on_failure ? '&amp;display_on_failure=1' : '', $badge);
190 $badge = preg_replace(array('/%%width_height_attrs%%/i', '/%%width_height_styles%%/i'), array($width_height_attrs, $width_height_styles), $badge);
191 }
192 return !empty($badge) ? $badge : ''; // Return Security Badge.
193 }
194
195 /**
196 * Acquires information about memory usage.
197 *
198 * @package s2Member\Utilities
199 * @since 110815
200 *
201 * @return string String with `Memory x MB :: Real Memory x MB :: Peak Memory x MB :: Real Peak Memory x MB`.
202 */
203 public static function mem_details()
204 {
205 $memory = number_format(memory_get_usage() / 1048576, 2, '.', '');
206 $real_memory = number_format(memory_get_usage(TRUE) / 1048576, 2, '.', '');
207 $peak_memory = number_format(memory_get_peak_usage() / 1048576, 2, '.', '');
208 $real_peak_memory = number_format(memory_get_peak_usage(TRUE) / 1048576, 2, '.', '');
209
210 $details = 'Memory '.$memory.' MB :: Real Memory '.$real_memory.' MB :: Peak Memory '.$peak_memory.' MB :: Real Peak Memory '.$real_peak_memory.' MB';
211
212 return $details; // Return all details.
213 }
214
215 /**
216 * Acquires s2Member options for the Main Site of a Multisite Network.
217 *
218 * @package s2Member\Utilities
219 * @since 110912
220 *
221 * @return array Array of s2Member options for the Main Site.
222 */
223 public static function mms_options()
224 {
225 return (is_multisite()) ? (array)get_site_option('ws_plugin__s2member_options') : array();
226 }
227
228 /**
229 * Builds an array of backtrace callers.
230 *
231 * @package s2Member\Utilities
232 * @since 110912
233 *
234 * @param array $debug_backtrace Optional. Defaults to ``debug_backtrace()``.
235 *
236 * @return array Array of backtrace callers (lowercase).
237 */
238 public static function callers($debug_backtrace = NULL)
239 {
240 $callers = array(); // Initialize array.
241 foreach(($debug_backtrace = is_array($debug_backtrace) ? $debug_backtrace : debug_backtrace()) as $caller)
242 if(isset($caller['class'], $caller['function']) || (!isset($caller['class']) && isset($caller['function'])))
243 $callers[] = isset($caller['class']) ? $caller['class'].'::'.$caller['function'] : $caller['function'];
244
245 return array_map('strtolower', array_unique($callers));
246 }
247
248 /**
249 * Sends an email using WordPress' `wp_mail()`, supporting HTML formatting and auto From header.
250 *
251 * Automatically sets Content-Type to `text/html` if HTML markers are found.
252 * If no `From:` header is provided, adds one using s2Member options via `email_config()` filters.
253 *
254 * @since 250605
255 *
256 * @param string|string[] $to Recipient email address(es).
257 * @param string $subject Email subject line.
258 * @param string $message Email body. May contain HTML.
259 * @param string|string[] $headers Optional. Additional headers.
260 * @param string|string[] $attachments Optional. File paths to attach.
261 *
262 * @return bool True if the email was sent successfully, false otherwise.
263 */
264 public static function mail($to, $subject, $message, $headers = '', $attachments = [])
265 {
266 // If From: not already in headers, add it using configured options.
267 if (stripos($headers, 'From:') === false) {
268 c_ws_plugin__s2member_email_configs::email_config(); // Setup From filters
269 $from_email = isset($GLOBALS['WS_PLUGIN__']['s2member']['o']['reg_email_from_email']) ? $GLOBALS['WS_PLUGIN__']['s2member']['o']['reg_email_from_email'] : '';
270 $from_name = isset($GLOBALS['WS_PLUGIN__']['s2member']['o']['reg_email_from_name']) ? str_replace('"', "'", $GLOBALS['WS_PLUGIN__']['s2member']['o']['reg_email_from_name']) : '';
271 $headers .= (empty($headers) ? '' : "\r\n") . 'From: "' . $from_name . '" <' . $from_email . '>';
272 c_ws_plugin__s2member_email_configs::email_config_release(); // Remove filters.
273 }
274
275 // Check HTML tags.
276 $is_html = false;
277 foreach (['</', '/>', '<i', '<b', '<h'] as $tag) {
278 if (strpos($message, $tag) !== false) {
279 $is_html = true;
280 break;
281 }
282 }
283 if ($is_html && strpos($message, '<p') === false && strpos($message, '<br') === false) {
284 $message = wpautop($message);
285 }
286
287 // Add correct content type.
288 $headers .= (empty($headers) ? '' : "\r\n");
289 $headers .= 'Content-Type: ' . ($is_html ? 'text/html' : 'text/plain') . '; charset=UTF-8';
290
291 return wp_mail($to, $subject, $message, $headers, $attachments);
292 }
293
294 /**
295 * Renders an s2Member email template editor (TinyMCE if HTML emails are enabled).
296 * Falls back to a plain <textarea> if HTML emails are disabled.
297 * Displays a soft warning if the database charset may strip emojis.
298 *
299 * @since 250610
300 *
301 * @param string $option_key The s2Member option key (w/o prefix).
302 * @param string $editor_id Optional. The DOM ID for the editor. Defaults to derived from option_key.
303 * @param array $editor_args Optional. Extra arguments for `wp_editor()`.
304 */
305 public static function editor($option_key, $editor_id = '', array $editor_args = [])
306 {
307 $value = isset($GLOBALS['WS_PLUGIN__']['s2member']['o'][$option_key]) ? $GLOBALS['WS_PLUGIN__']['s2member']['o'][$option_key] : '';
308 $editor_id = $editor_id ? $editor_id : strtr('ws_plugin__s2member_' . $option_key, ['_' => '-']);
309
310 // If HTML emails are enabled, use wp_editor()
311 $html_enabled = !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['html_emails_enabled']);
312 if ($html_enabled) {
313 $args = wp_parse_args($editor_args, [
314 'textarea_name' => 'ws_plugin__s2member_' . $option_key,
315 'textarea_rows' => 20,
316 'editor_height' => null,
317 'media_buttons' => false,
318 'teeny' => false,
319 'quicktags' => true,
320 'tinymce' => [
321 'height' => 400,
322 'resize' => true,
323 'wp_autoresize_on' => false,
324 'toolbar1' => 'formatselect | bold italic underline strikethrough | bullist numlist blockquote | alignleft aligncenter alignright | link unlink | undo redo | removeformat',
325 'toolbar2' => '',
326 ],
327 ]);
328 wp_editor($value, $editor_id, $args);
329 } else {
330 echo '<textarea name="ws_plugin__s2member_' . esc_attr($option_key) . '" id="' . esc_attr($editor_id) . '" rows="20">'
331 . format_to_edit($value) . '</textarea><br />' . "\n";
332 }
333
334 // Check charset and warn about emojis.
335 global $wpdb;
336 $collation = $wpdb->get_var(
337 $wpdb->prepare(
338 "SELECT COLLATION_NAME
339 FROM information_schema.COLUMNS
340 WHERE TABLE_SCHEMA = %s
341 AND TABLE_NAME = %s
342 AND COLUMN_NAME = 'option_value'",
343 $wpdb->dbname,
344 $wpdb->options
345 )
346 );
347 $charset = $collation ? strtolower(preg_replace('/_.+$/', '', $collation)) : '';
348 if ($charset && $charset !== 'utf8mb4') {
349 echo '<div class="ws-menu-page-hilite" style="margin-bottom:.2em;">';
350 echo '⚠️ <strong>Note:</strong> Your database uses the <code>' . esc_html($charset) . '</code> charset for WordPress option values. Changes to your email will be lost if you use emojis, as they require the <code>utf8mb4</code> charset.';
351 echo '</div>';
352 }
353 }
354
355 /**
356 * Cancels a subscription using a gateway-aware dispatch routine.
357 *
358 * This is designed for replacement/modification flows where the old subscription must be
359 * canceled after a new checkout begins. Callers should pass the previously captured
360 * subscription details from before proxy/IPN processing updates the user's current profile.
361 *
362 * For PayPal-family subscriptions, this uses captured signup/proxy vars first when available
363 * as a product hint, and then falls back to trying other configured cancellation routines
364 * that are still plausible for the captured subscription context. //260407
365 *
366 * Gateway/product handlers may no longer be configured or loaded by the current installation.
367 * In those cases this routine fails safely and returns `FALSE` instead of calling unavailable code.
368 *
369 * @package s2Member\Utilities
370 * @since 260407
371 *
372 * @param string|bool $subscr_gateway Previously captured `s2member_subscr_gateway`.
373 * @param string|bool $subscr_id Previously captured `s2member_subscr_id`.
374 * @param string|bool $subscr_baid Previously captured `s2member_subscr_baid`.
375 * @param string|bool $subscr_cid Previously captured `s2member_subscr_cid`.
376 * @param array $ipn_signup_vars Previously captured signup/proxy vars for product-specific routing.
377 * @param bool $immediate Optional. If false, cancel at period end where supported.
378 * @param string $reason Optional human-readable cancellation reason for APIs that accept one.
379 *
380 * @return bool True if a cancellation request was accepted; else false.
381 */
382 public static function cancel_gateway_subscription($subscr_gateway = FALSE, $subscr_id = FALSE, $subscr_baid = FALSE, $subscr_cid = FALSE, $ipn_signup_vars = array(), $immediate = TRUE, $reason = '')
383 {
384 $subscr_gateway = strtolower((string)$subscr_gateway);
385 $subscr_id = (string)$subscr_id;
386 $subscr_baid = (string)$subscr_baid;
387 $subscr_cid = (string)$subscr_cid;
388 $ipn_signup_vars = (is_array($ipn_signup_vars)) ? $ipn_signup_vars : array();
389
390 if(!$subscr_gateway || !$subscr_id)
391 return FALSE;
392
393 switch($subscr_gateway)
394 {
395 case 'stripe':
396 if(!$subscr_cid || !class_exists('c_ws_plugin__s2member_pro_stripe_utilities'))
397 return FALSE;
398
399 //260407 Stripe replacement flows need the guard around old-subscription cancellation.
400 c_ws_plugin__s2member_pro_stripe_utilities::set_replacement_cancellation_guard($subscr_id);
401
402 if(!is_object(c_ws_plugin__s2member_pro_stripe_utilities::cancel_customer_subscription($subscr_cid, $subscr_id, !$immediate)))
403 {
404 c_ws_plugin__s2member_pro_stripe_utilities::clear_replacement_cancellation_guard($subscr_id);
405 return FALSE;
406 }
407 return TRUE;
408
409 case 'authnet':
410 if(!class_exists('c_ws_plugin__s2member_pro_authnet_utilities'))
411 return FALSE;
412
413 return (($authnet = c_ws_plugin__s2member_pro_authnet_utilities::authnet_arb_response(array('x_method' => 'cancel', 'x_subscription_id' => $subscr_id))) && empty($authnet['__error'])) ? TRUE : FALSE;
414
415 case 'paypal':
416 $paypal_proxy_use = (!empty($ipn_signup_vars['s2member_paypal_proxy_use'])) ? (string)$ipn_signup_vars['s2member_paypal_proxy_use'] : '';
417 $paypal_invoice = (!empty($ipn_signup_vars['invoice'])) ? (string)$ipn_signup_vars['invoice'] : '';
418 $ppco_hint = ($paypal_proxy_use === 'paypal_checkout' || strpos($paypal_invoice, 's2mpf-') === 0);
419 $can_try_ppco = FALSE;
420 $can_try_standard = FALSE;
421 $can_try_payflow = FALSE;
422
423 $paypal_checkout_creds = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_creds();
424 $can_try_ppco = (!empty($paypal_checkout_creds['client_id']) && !empty($paypal_checkout_creds['secret']));
425
426 $can_try_standard = (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_api_username'])
427 && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_api_password'])
428 && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_api_signature']));
429
430 $can_try_payflow = (!empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_payflow_api_username'])
431 && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_payflow_api_partner'])
432 && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_payflow_api_vendor'])
433 && !empty($GLOBALS['WS_PLUGIN__']['s2member']['o']['paypal_payflow_api_password'])
434 && class_exists('c_ws_plugin__s2member_pro_paypal_utilities'));
435
436 //260819.0543 Use positive product provenance where available; I-* is ambiguous, while Payflow profile IDs use RP/RT prefixes.
437 $payflow_hint = (bool)preg_match('/^R[PT]/i', $subscr_id);
438 if($ppco_hint)
439 $paypal_routes = array('ppco', 'standard', 'payflow');
440 else if($payflow_hint)
441 $paypal_routes = array('payflow', 'standard', 'ppco');
442 else
443 $paypal_routes = array('standard', 'ppco', 'payflow');
444
445 $reason = trim((string)$reason);
446 if(!$reason)
447 $reason = 'Cancelled by replacement checkout.';
448
449 foreach($paypal_routes as $_paypal_route)
450 {
451 switch($_paypal_route)
452 {
453 case 'ppco':
454 if(!$can_try_ppco)
455 break;
456
457 if(($paypal_checkout = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_cancel($subscr_id, $reason))
458 && !empty($paypal_checkout['code']) && ($paypal_checkout['code'] === 204 || ($paypal_checkout['code'] >= 200 && $paypal_checkout['code'] <= 299)))
459 return TRUE;
460 break;
461
462 case 'standard':
463 if(!$can_try_standard)
464 break;
465
466 if(($paypal_standard = c_ws_plugin__s2member_paypal_utilities::paypal_standard_subscription_cancel($subscr_id))
467 && empty($paypal_standard['__error']))
468 return TRUE;
469 break;
470
471 case 'payflow':
472 if(!$can_try_payflow)
473 break;
474
475 if(c_ws_plugin__s2member_pro_paypal_utilities::payflow_cancel_profile($subscr_id, $subscr_baid))
476 return TRUE;
477 break;
478 }
479 }
480 }
481 return FALSE;
482 }
483 }
484 }
485