Completed
Push — issues/611 ( 661115...758b1c )
by Ravinder
21:11
created

includes/class-give-email-access.php (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Email Access
4
 *
5
 * @package     Give
6
 * @subpackage  Classes/Give_Email_Access
7
 * @copyright   Copyright (c) 2016, WordImpress
8
 * @license     https://opensource.org/licenses/gpl-license GNU Public License
9
 * @since       1.4
10
 */
11
12
// Exit if accessed directly.
13
if ( ! defined( 'ABSPATH' ) ) {
14
	exit;
15
}
16
17
/**
18
 * Give_Email_Access class
19
 *
20
 * This class handles email access, allowing donors access to their donation w/o logging in;
21
 *
22
 * Based on the work from Matt Gibbs - https://github.com/FacetWP/edd-no-logins
23
 *
24
 * @since 1.0
25
 */
26
class Give_Email_Access {
27
28
	/**
29
	 * Token exists
30
	 *
31
	 * @since  1.0
32
	 * @access public
33
	 *
34
	 * @var    bool
35
	 */
36
	public $token_exists = false;
37
38
	/**
39
	 * Token email
40
	 *
41
	 * @since  1.0
42
	 * @access public
43
	 *
44
	 * @var    bool
45
	 */
46
	public $token_email = false;
47
48
	/**
49
	 * Token
50
	 *
51
	 * @since  1.0
52
	 * @access public
53
	 *
54
	 * @var    bool
55
	 */
56
	public $token = false;
57
58
	/**
59
	 * Error
60
	 *
61
	 * @since  1.0
62
	 * @access public
63
	 *
64
	 * @var    string
65
	 */
66
	public $error = '';
67
68
	/**
69
	 * Verify throttle
70
	 *
71
	 * @since  1.0
72
	 * @access private
73
	 *
74
	 * @var
75
	 */
76
	private $verify_throttle;
77
78
	/**
79
	 * Verify expiration
80
	 *
81
	 * @since  1.0
82
	 * @access private
83
	 *
84
	 * @var    string
85
	 */
86
	private $token_expiration;
87
88
	/**
89
	 * Class Constructor
90
	 *
91
	 * Set up the Give Email Access Class.
92
	 *
93
	 * @since  1.0
94
	 * @access public
95
	 */
96
	public function __construct() {
97
98
		// get it started
99
		add_action( 'init', array( $this, 'init' ) );
100
	}
101
102
	/**
103
	 * Init
104
	 *
105
	 * Register defaults and filters
106
	 *
107
	 * @since  1.0
108
	 * @access public
109
	 *
110
	 * @return void
111
	 */
112
	public function init() {
113
114
		/**
115
		 * Do NOT pass go if:
116
		 *
117
		 * a. User is logged in
118
		 * b. Email access setting is not enabled
119
		 * c. You're in the admin
120
		 */
121
		if (
122
			is_user_logged_in()
123
			|| ! give_is_setting_enabled( give_get_option( 'email_access' ) )
124
			|| is_admin()
125
		) {
126
			return;
127
		}
128
129
		// Are db columns setup?
130
		$is_setup = give_get_option( 'email_access_installed' );
131
		if ( empty( $is_setup ) ) {
132
			$this->create_columns();
133
		}
134
135
		// Timeouts.
136
		$this->verify_throttle  = apply_filters( 'give_nl_verify_throttle', 300 );
137
		$this->token_expiration = apply_filters( 'give_nl_token_expiration', 7200 );
138
139
		// Setup login.
140
		$this->check_for_token();
141
142
		if ( $this->token_exists ) {
143
			add_filter( 'give_can_view_receipt', '__return_true' );
144
			add_filter( 'give_user_pending_verification', '__return_false' );
145
			add_filter( 'give_get_users_donations_args', array( $this, 'users_donations_args' ) );
146
		}
147
148
	}
149
150
	/**
151
	 * Prevent email spamming.
152
	 *
153
	 * @since  1.0
154
	 * @access public
155
	 *
156
	 * @param  $customer_id string Customer id.
157
	 *
158
	 * @return bool
159
	 */
160
	public function can_send_email( $customer_id ) {
161
		/* @var WPDB $wpdb */
162
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
163
164
		// Prevent multiple emails within X minutes
165
		$throttle = date( 'Y-m-d H:i:s', time() - $this->verify_throttle );
166
167
		// Does a user row exist?
168
		$exists = (int) $wpdb->get_var(
169
			$wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}give_customers WHERE id = %d", $customer_id )
170
		);
171
172
		if ( 0 < $exists ) {
173
			$row_id = (int) $wpdb->get_var(
174
				$wpdb->prepare( "SELECT id FROM {$wpdb->prefix}give_customers WHERE id = %d AND (verify_throttle < %s OR verify_key = '') LIMIT 1", $customer_id, $throttle )
175
			);
176
177
			if ( $row_id < 1 ) {
178
				give_set_error( 'give_email_access_attempts_exhausted', __( 'Please wait a few minutes before requesting a new email access link.', 'give' ) );
179
180
				return false;
181
			}
182
		}
183
184
		return true;
185
	}
186
187
	/**
188
	 * Send the user's token
189
	 *
190
	 * @since  1.0
191
	 * @access public
192
	 *
193
	 * @param  $customer_id string Customer id.
194
	 * @param  $email       string Customer email.
195
	 *
196
	 * @return void
197
	 */
198
	public function send_email( $customer_id, $email ) {
199
200
		$verify_key = wp_generate_password( 20, false );
201
202
		// Generate a new verify key
203
		$this->set_verify_key( $customer_id, $email, $verify_key );
204
205
		// Get the donation history page
206
		$page_id = give_get_option( 'history_page' );
207
208
		$access_url = add_query_arg( array(
209
			'give_nl' => $verify_key,
210
		), get_permalink( $page_id ) );
211
212
		// Nice subject and message.
213
		$subject = apply_filters( 'give_email_access_token_subject', sprintf( __( 'Your Access Link to %s', 'give' ), get_bloginfo( 'name' ) ) );
214
215
		$message = __( 'You or someone in your organization requested an access link be sent to this email address. This is a temporary access link for you to view your donation information. Click on the link below to view:', 'give' ) . "\n\n";
216
		$message .= '<a href="' . esc_url( $access_url ) . '" target="_blank">' . __( 'Access Donation Details &raquo;', 'give' ) . '</a>' . "\n\n";
217
		$message .= "\n\n";
218
		$message .= __( 'Sincerely,', 'give' ) . "\n";
219
		$message .= get_bloginfo( 'name' ) . "\n";
220
221
		$message = apply_filters( 'give_email_access_token_message', $message );
222
223
		// Send the email.
224
		Give()->emails->__set( 'heading', apply_filters( 'give_email_access_token_heading', __( 'Your Access Link', 'give' ) ) );
225
		Give()->emails->send( $email, $subject, $message );
226
227
	}
228
229
	/**
230
	 * Has the user authenticated?
231
	 *
232
	 * @since  1.0
233
	 * @access public
234
	 *
235
	 * @return bool
0 ignored issues
show
Should the return type not be null|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
236
	 */
237
	public function check_for_token() {
238
239
		$token = isset( $_GET['give_nl'] ) ? $_GET['give_nl'] : '';
240
241
		// Check for cookie.
242
		if ( empty( $token ) ) {
243
			$token = isset( $_COOKIE['give_nl'] ) ? $_COOKIE['give_nl'] : '';
244
		}
245
246
		// Must have a token.
247
		if ( ! empty( $token ) ) {
248
249
			if ( ! $this->is_valid_token( $token ) ) {
250
				if ( ! $this->is_valid_verify_key( $token ) ) {
251
					return;
252
				}
253
			}
254
255
			$this->token_exists = true;
256
			// Set cookie.
257
			$lifetime = current_time( 'timestamp' ) + Give()->session->set_expiration_time();
258
			@setcookie( 'give_nl', $token, $lifetime, COOKIEPATH, COOKIE_DOMAIN, false );
259
260
			return true;
261
		}
262
	}
263
264
	/**
265
	 * Is this a valid token?
266
	 *
267
	 * @since  1.0
268
	 * @access public
269
	 *
270
	 * @param  $token string The token.
271
	 *
272
	 * @return bool
273
	 */
274
	public function is_valid_token( $token ) {
275
276
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
277
278
		// Make sure token isn't expired.
279
		$expires = date( 'Y-m-d H:i:s', time() - $this->token_expiration );
280
281
		$email = $wpdb->get_var(
282
			$wpdb->prepare( "SELECT email FROM {$wpdb->prefix}give_customers WHERE token = %s AND verify_throttle >= %s LIMIT 1", $token, $expires )
283
		);
284
285
		if ( ! empty( $email ) ) {
286
			$this->token_email = $email;
287
			$this->token       = $token;
288
			return true;
289
		}
290
291
		// Set error only if email access form isn't being submitted
292
		if ( ! isset( $_POST['give_email'] ) && ! isset( $_POST['_wpnonce'] ) ) {
293
			give_set_error( 'give_email_token_expired', apply_filters( 'give_email_token_expired_message', __( 'Your access token has expired. Please request a new one below:', 'give' ) ) );
294
		}
295
296
		return false;
297
298
	}
299
300
	/**
301
	 * Add the verify key to DB
302
	 *
303
	 * @since  1.0
304
	 * @access public
305
	 *
306
	 * @param  $customer_id string Customer id.
307
	 * @param  $email       string Customer email.
308
	 * @param  $verify_key  string The verification key.
309
	 *
310
	 * @return void
311
	 */
312
	public function set_verify_key( $customer_id, $email, $verify_key ) {
313
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
314
315
		$now = date( 'Y-m-d H:i:s' );
316
317
		// Insert or update?
318
		$row_id = (int) $wpdb->get_var(
319
			$wpdb->prepare( "SELECT id FROM {$wpdb->prefix}give_customers WHERE id = %d LIMIT 1", $customer_id )
320
		);
321
322
		// Update.
323
		if ( ! empty( $row_id ) ) {
324
			$wpdb->query(
325
				$wpdb->prepare( "UPDATE {$wpdb->prefix}give_customers SET verify_key = %s, verify_throttle = %s WHERE id = %d LIMIT 1", $verify_key, $now, $row_id )
326
			);
327
		} // Insert.
328
		else {
329
			$wpdb->query(
330
				$wpdb->prepare( "INSERT INTO {$wpdb->prefix}give_customers ( verify_key, verify_throttle) VALUES (%s, %s)", $verify_key, $now )
331
			);
332
		}
333
	}
334
335
	/**
336
	 * Is this a valid verify key?
337
	 *
338
	 * @since  1.0
339
	 * @access public
340
	 *
341
	 * @param  $token string The token.
342
	 *
343
	 * @return bool
344
	 */
345
	public function is_valid_verify_key( $token ) {
346
		/* @var WPDB $wpdb */
347
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
348
349
		// See if the verify_key exists.
350
		$row = $wpdb->get_row(
351
			$wpdb->prepare( "SELECT id, email FROM {$wpdb->prefix}give_customers WHERE verify_key = %s LIMIT 1", $token )
352
		);
353
354
		$now = date( 'Y-m-d H:i:s' );
355
356
		// Set token and remove verify key.
357
		if ( ! empty( $row ) ) {
358
			$wpdb->query(
359
				$wpdb->prepare( "UPDATE {$wpdb->prefix}give_customers SET verify_key = '', token = %s, verify_throttle = %s WHERE id = %d LIMIT 1", $token, $now, $row->id )
360
			);
361
362
			$this->token_email = $row->email;
363
			$this->token       = $token;
364
365
			return true;
366
		}
367
368
		return false;
369
	}
370
371
	/**
372
	 * Users donations args
373
	 *
374
	 * Force Give to find donations by email, not user ID.
375
	 *
376
	 * @since  1.0
377
	 * @access public
378
	 *
379
	 * @param  $args array User Donations arguments.
380
	 *
381
	 * @return mixed
382
	 */
383
	public function users_donations_args( $args ) {
384
		$args['user'] = $this->token_email;
385
386
		return $args;
387
	}
388
389
	/**
390
	 * Create בolumns
391
	 *
392
	 * Create the necessary columns for email access
393
	 *
394
	 * @since  1.0
395
	 * @access public
396
	 *
397
	 * @return void
398
	 */
399
	public function create_columns() {
400
401
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
402
403
		// Create columns in customers table
404
		$query = $wpdb->query( "ALTER TABLE {$wpdb->prefix}give_customers ADD `token` VARCHAR(255) CHARACTER SET utf8 NOT NULL, ADD `verify_key` VARCHAR(255) CHARACTER SET utf8 NOT NULL AFTER `token`, ADD `verify_throttle` DATETIME NOT NULL AFTER `verify_key`" );
405
406
		// Columns added properly
407
		if ( $query ) {
408
			give_update_option( 'email_access_installed', 1 );
409
		}
410
411
	}
412
413
}
414