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

Give_Email_Access::send_email()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 16
nc 1
nop 2
dl 0
loc 30
rs 8.8571
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 26 and the first side effect is on line 14.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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
Documentation introduced by
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 ) {
0 ignored issues
show
Unused Code introduced by
The parameter $email is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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