Issues (850)

Security Analysis    4 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection (1)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (2)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting (1)
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/admin/class-getpaid-admin.php (22 issues)

1
<?php
2
/**
3
 * Contains the admin class.
4
 *
5
 */
6
7
defined( 'ABSPATH' ) || exit;
8
9
/**
10
 * The main admin class.
11
 *
12
 * @since       1.0.19
13
 */
14
class GetPaid_Admin {
15
16
    /**
17
	 * Local path to this plugins admin directory
18
	 *
19
	 * @var         string
20
	 */
21
	public $admin_path;
22
23
	/**
24
	 * Web path to this plugins admin directory
25
	 *
26
	 * @var         string
27
	 */
28
	public $admin_url;
29
30
	/**
31
	 * Reports components.
32
	 *
33
	 * @var GetPaid_Reports
34
	 */
35
    public $reports;
36
37
    /**
38
	 * Class constructor.
39
	 */
40
	public function __construct() {
41
42
        $this->admin_path  = plugin_dir_path( __FILE__ );
43
		$this->admin_url   = plugins_url( '/', __FILE__ );
44
		$this->reports     = new GetPaid_Reports();
45
46
        if ( is_admin() ) {
47
			$this->init_admin_hooks();
48
        }
49
50
    }
51
52
    /**
53
	 * Init action and filter hooks
54
	 *
55
	 */
56
	private function init_admin_hooks() {
57
        add_action( 'admin_enqueue_scripts', array( $this, 'enqeue_scripts' ), 9 );
58
        add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
59
        add_action( 'admin_init', array( $this, 'init_ayecode_connect_helper' ) );
60
        add_action( 'admin_init', array( $this, 'activation_redirect' ) );
61
        add_action( 'admin_init', array( $this, 'maybe_do_admin_action' ) );
62
		add_action( 'admin_notices', array( $this, 'show_notices' ) );
63
		add_action( 'getpaid_authenticated_admin_action_rate_plugin', array( $this, 'redirect_to_wordpress_rating_page' ) );
64
		add_action( 'getpaid_authenticated_admin_action_duplicate_form', array( $this, 'duplicate_payment_form' ) );
65
		add_action( 'getpaid_authenticated_admin_action_reset_form_stats', array( $this, 'reset_form_stats' ) );
66
		add_action( 'getpaid_authenticated_admin_action_duplicate_invoice', array( $this, 'duplicate_invoice' ) );
67
		add_action( 'getpaid_authenticated_admin_action_refund_invoice', array( $this, 'refund_invoice' ) );
68
		add_action( 'getpaid_authenticated_admin_action_send_invoice', array( $this, 'send_customer_invoice' ) );
69
		add_action( 'getpaid_authenticated_admin_action_send_invoice_reminder', array( $this, 'send_customer_payment_reminder' ) );
70
        add_action( 'getpaid_authenticated_admin_action_reset_tax_rates', array( $this, 'admin_reset_tax_rates' ) );
71
		add_action( 'getpaid_authenticated_admin_action_create_missing_pages', array( $this, 'admin_create_missing_pages' ) );
72
		add_action( 'getpaid_authenticated_admin_action_refresh_permalinks', array( $this, 'admin_refresh_permalinks' ) );
73
		add_action( 'getpaid_authenticated_admin_action_create_missing_tables', array( $this, 'admin_create_missing_tables' ) );
74
		add_action( 'getpaid_authenticated_admin_action_migrate_old_invoices', array( $this, 'admin_migrate_old_invoices' ) );
75
		add_action( 'getpaid_authenticated_admin_action_download_customers', array( $this, 'admin_download_customers' ) );
76
		add_action( 'getpaid_authenticated_admin_action_recalculate_discounts', array( $this, 'admin_recalculate_discounts' ) );
77
		add_action( 'getpaid_authenticated_admin_action_install_plugin', array( $this, 'admin_install_plugin' ) );
78
		add_action( 'getpaid_authenticated_admin_action_connect_gateway', array( $this, 'admin_connect_gateway' ) );
79
		add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ) );
80
		do_action( 'getpaid_init_admin_hooks', $this );
81
82
		// Setup/welcome
83
		if ( ! empty( $_GET['page'] ) ) {
84
			switch ( sanitize_text_field( $_GET['page'] ) ) {
85
				case 'gp-setup':
86
					include_once dirname( __FILE__ ) . '/class-getpaid-admin-setup-wizard.php';
87
					break;
88
			}
89
		}
90
91
    }
92
93
    /**
94
	 * Register admin scripts
95
	 *
96
	 */
97
	public function enqeue_scripts() {
98
        global $current_screen, $pagenow;
99
100
		$page    = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
101
		$editing = $pagenow == 'post.php' || $pagenow == 'post-new.php';
102
103
        if ( ! empty( $current_screen->post_type ) ) {
104
			$page = $current_screen->post_type;
105
        }
106
107
        // General styles.
108
        if ( false !== stripos( $page, 'wpi' ) || false !== stripos( $page, 'getpaid' ) || 'gp-setup' == $page || false !== stripos( $page, 'geodir-tickets' ) ) {
109
110
            // Styles.
111
            $version = filemtime( WPINV_PLUGIN_DIR . 'assets/css/admin.css' );
112
            wp_enqueue_style( 'wpinv_admin_style', WPINV_PLUGIN_URL . 'assets/css/admin.css', array( 'wp-color-picker' ), $version );
113
            wp_enqueue_style( 'select2', WPINV_PLUGIN_URL . 'assets/css/select2/select2.min.css', array(), '4.0.13', 'all' );
114
115
            // Scripts.
116
            wp_enqueue_script( 'select2', WPINV_PLUGIN_URL . 'assets/js/select2/select2.full.min.js', array( 'jquery' ), WPINV_VERSION );
117
118
            $version = filemtime( WPINV_PLUGIN_DIR . 'assets/js/admin.js' );
119
            wp_enqueue_script( 'wpinv-admin-script', WPINV_PLUGIN_URL . 'assets/js/admin.js', array( 'jquery', 'wp-color-picker', 'jquery-ui-tooltip' ), $version );
120
            wp_localize_script( 'wpinv-admin-script', 'WPInv_Admin', apply_filters( 'wpinv_admin_js_localize', $this->get_admin_i18() ) );
121
122
        }
123
124
        // Payment form scripts.
125
		if ( 'wpi_payment_form' == $page && $editing ) {
126
            $this->load_payment_form_scripts();
127
        }
128
129
		if ( $page == 'wpinv-subscriptions' ) {
130
			wp_enqueue_script( 'postbox' );
131
		}
132
133
    }
134
135
    /**
136
	 * Returns admin js translations.
137
	 *
138
	 */
139
	protected function get_admin_i18() {
140
        global $post;
141
142
		$date_range = array(
143
			'period' => isset( $_GET['date_range'] ) ? sanitize_text_field( $_GET['date_range'] ) : '7_days',
144
		);
145
146
		if ( $date_range['period'] == 'custom' ) {
147
148
			if ( isset( $_GET['from'] ) ) {
149
				$date_range['after'] = date( 'Y-m-d', strtotime( sanitize_text_field( $_GET['from'] ), current_time( 'timestamp' ) ) - DAY_IN_SECONDS );
150
			}
151
152
			if ( isset( $_GET['to'] ) ) {
153
				$date_range['before'] = date( 'Y-m-d', strtotime( sanitize_text_field( $_GET['to'] ), current_time( 'timestamp' ) ) + DAY_IN_SECONDS );
154
			}
155
}
156
157
        $i18n = array(
158
            'ajax_url'                  => admin_url( 'admin-ajax.php' ),
159
            'post_ID'                   => isset( $post->ID ) ? $post->ID : '',
160
			'wpinv_nonce'               => wp_create_nonce( 'wpinv-nonce' ),
161
			'rest_nonce'                => wp_create_nonce( 'wp_rest' ),
162
			'rest_root'                 => esc_url_raw( rest_url() ),
163
			'date_range'                => $date_range,
164
            'add_invoice_note_nonce'    => wp_create_nonce( 'add-invoice-note' ),
165
            'delete_invoice_note_nonce' => wp_create_nonce( 'delete-invoice-note' ),
166
            'invoice_item_nonce'        => wp_create_nonce( 'invoice-item' ),
167
            'billing_details_nonce'     => wp_create_nonce( 'get-billing-details' ),
168
            'tax'                       => wpinv_tax_amount(),
169
            'discount'                  => 0,
170
			'currency_symbol'           => wpinv_currency_symbol(),
171
			'currency'                  => wpinv_get_currency(),
172
            'currency_pos'              => wpinv_currency_position(),
173
            'thousand_sep'              => wpinv_thousands_separator(),
174
            'decimal_sep'               => wpinv_decimal_separator(),
175
            'decimals'                  => wpinv_decimals(),
176
            'save_invoice'              => __( 'Save Invoice', 'invoicing' ),
177
            'status_publish'            => wpinv_status_nicename( 'publish' ),
178
            'status_pending'            => wpinv_status_nicename( 'wpi-pending' ),
179
            'delete_tax_rate'           => __( 'Are you sure you wish to delete this tax rate?', 'invoicing' ),
180
            'status_pending'            => wpinv_status_nicename( 'wpi-pending' ),
181
            'FillBillingDetails'        => __( 'Fill the user\'s billing information? This will remove any currently entered billing information', 'invoicing' ),
182
            'confirmCalcTotals'         => __( 'Recalculate totals? This will recalculate totals based on the user billing country. If no billing country is set it will use the base country.', 'invoicing' ),
183
            'AreYouSure'                => __( 'Are you sure?', 'invoicing' ),
184
            'errDeleteItem'             => __( 'This item is in use! Before delete this item, you need to delete all the invoice(s) using this item.', 'invoicing' ),
185
            'delete_subscription'       => __( 'Are you sure you want to delete this subscription?', 'invoicing' ),
186
            'action_edit'               => __( 'Edit', 'invoicing' ),
187
            'action_cancel'             => __( 'Cancel', 'invoicing' ),
188
            'item_description'          => __( 'Item Description', 'invoicing' ),
189
            'invoice_description'       => __( 'Invoice Description', 'invoicing' ),
190
            'discount_description'      => __( 'Discount Description', 'invoicing' ),
191
			'searching'                 => __( 'Searching', 'invoicing' ),
192
			'loading'                   => __( 'Loading...', 'invoicing' ),
193
			'search_customers'          => __( 'Enter customer name or email', 'invoicing' ),
194
			'search_items'              => __( 'Enter item name', 'invoicing' ),
195
			'graphs'                    => array_merge( array( 'refunded_fees', 'refunded_items', 'refunded_subtotal', 'refunded_tax' ), array_keys( wpinv_get_report_graphs() ) ),
196
        );
197
198
		if ( ! empty( $post ) && getpaid_is_invoice_post_type( $post->post_type ) ) {
199
200
			$invoice              = new WPInv_Invoice( $post );
201
			$i18n['save_invoice'] = sprintf(
202
				__( 'Save %s', 'invoicing' ),
203
				ucfirst( $invoice->get_invoice_quote_type() )
204
			);
205
206
			$i18n['invoice_description'] = sprintf(
207
				__( '%s Description', 'invoicing' ),
208
				ucfirst( $invoice->get_invoice_quote_type() )
209
			);
210
211
		}
212
		return $i18n;
213
	}
214
215
	/**
216
	 * Change the admin footer text on GetPaid admin pages.
217
	 *
218
	 * @since  2.0.0
219
	 * @param  string $footer_text
220
	 * @return string
221
	 */
222
	public function admin_footer_text( $footer_text ) {
223
		global $current_screen;
224
225
		$page    = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
226
227
        if ( ! empty( $current_screen->post_type ) ) {
228
			$page = $current_screen->post_type;
229
        }
230
231
        // General styles.
232
        if ( apply_filters( 'getpaid_display_admin_footer_text', wpinv_current_user_can_manage_invoicing() ) && false !== stripos( $page, 'wpi' ) ) {
233
234
			// Change the footer text
235
			if ( ! get_user_meta( get_current_user_id(), 'getpaid_admin_footer_text_rated', true ) ) {
236
237
				$rating_url  = esc_url(
238
					wp_nonce_url(
239
						admin_url( 'admin.php?page=wpinv-reports&getpaid-admin-action=rate_plugin' ),
240
						'getpaid-nonce',
241
						'getpaid-nonce'
242
                    )
243
				);
244
245
				$footer_text = sprintf(
246
					/* translators: %s: five stars */
247
					__( 'If you like <strong>GetPaid</strong>, please leave us a %s rating. A huge thanks in advance!', 'invoicing' ),
248
					"<a href='$rating_url'>&#9733;&#9733;&#9733;&#9733;&#9733;</a>"
249
				);
250
251
			} else {
252
253
				$footer_text = sprintf(
254
					/* translators: %s: GetPaid */
255
					__( 'Thank you for using %s!', 'invoicing' ),
256
					"<a href='https://wpgetpaid.com/' target='_blank'><strong>GetPaid</strong></a>"
257
				);
258
259
			}
260
}
261
262
		return $footer_text;
263
	}
264
265
	/**
266
	 * Redirects to wp.org to rate the plugin.
267
	 *
268
	 * @since  2.0.0
269
	 */
270
	public function redirect_to_wordpress_rating_page() {
271
		update_user_meta( get_current_user_id(), 'getpaid_admin_footer_text_rated', 1 );
272
		wp_redirect( 'https://wordpress.org/support/plugin/invoicing/reviews?rate=5#new-post' );
273
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
274
	}
275
276
    /**
277
	 * Loads payment form js.
278
	 *
279
	 */
280
	protected function load_payment_form_scripts() {
281
        global $post;
282
283
        wp_enqueue_script( 'vue', WPINV_PLUGIN_URL . 'assets/js/vue/vue.min.js', array(), WPINV_VERSION );
284
		wp_enqueue_script( 'sortable', WPINV_PLUGIN_URL . 'assets/js/sortable.min.js', array(), WPINV_VERSION );
285
		wp_enqueue_script( 'vue_draggable', WPINV_PLUGIN_URL . 'assets/js/vue/vuedraggable.min.js', array( 'sortable', 'vue' ), WPINV_VERSION );
286
287
		wp_register_script( 'wpinv-admin-payment-form-script', WPINV_PLUGIN_URL . 'assets/js/admin-payment-forms.min.js', array( 'wpinv-admin-script', 'vue_draggable', 'wp-hooks' ), WPINV_VERSION );
288
289
		wp_localize_script(
290
            'wpinv-admin-payment-form-script',
291
            'wpinvPaymentFormAdmin',
292
            array(
293
				'elements'      => wpinv_get_data( 'payment-form-elements' ),
294
				'form_elements' => getpaid_get_payment_form_elements( $post->ID ),
295
				'currency'      => wpinv_currency_symbol(),
296
				'position'      => wpinv_currency_position(),
297
				'decimals'      => (int) wpinv_decimals(),
298
				'thousands_sep' => wpinv_thousands_separator(),
299
				'decimals_sep'  => wpinv_decimal_separator(),
300
				'form_items'    => gepaid_get_form_items( $post->ID ),
301
				'is_default'    => $post->ID == wpinv_get_default_payment_form(),
302
            )
303
        );
304
305
        wp_enqueue_script( 'wpinv-admin-payment-form-script' );
306
307
    }
308
309
    /**
310
	 * Add our classes to admin pages.
311
     *
312
     * @param string $classes
313
     * @return string
314
	 *
315
	 */
316
    public function admin_body_class( $classes ) {
317
		global $pagenow, $post, $current_screen;
318
319
        $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
320
321
        if ( ! empty( $current_screen->post_type ) ) {
322
			$page = $current_screen->post_type;
323
        }
324
325
        if ( false !== stripos( $page, 'wpi' ) ) {
326
            $classes .= ' wpi-' . sanitize_key( $page );
327
        }
328
329
        if ( in_array( $page, wpinv_parse_list( 'wpi_invoice wpi_payment_form wpi_quote' ) ) ) {
330
            $classes .= ' wpinv-cpt wpinv';
331
		}
332
333
		if ( getpaid_is_invoice_post_type( $page ) ) {
334
            $classes .= ' getpaid-is-invoice-cpt';
335
        }
336
337
		return $classes;
338
    }
339
340
    /**
341
	 * Maybe show the AyeCode Connect Notice.
342
	 */
343
	public function init_ayecode_connect_helper() {
344
345
		// Register with the deactivation survey class.
346
		AyeCode_Deactivation_Survey::instance(
347
            array(
348
				'slug'              => 'invoicing',
349
				'version'           => WPINV_VERSION,
350
				'support_url'       => 'https://wpgetpaid.com/support/',
351
				'documentation_url' => 'https://docs.wpgetpaid.com/',
352
				'activated'         => (int) get_option( 'gepaid_installed_on' ),
353
            )
354
        );
355
356
        new AyeCode_Connect_Helper(
357
            array(
358
				'connect_title'     => __( 'WP Invoicing - an AyeCode product!', 'invoicing' ),
359
				'connect_external'  => __( 'Please confirm you wish to connect your site?', 'invoicing' ),
360
				'connect'           => sprintf( __( '<strong>Have a license?</strong> Forget about entering license keys or downloading zip files, connect your site for instant access. %1$slearn more%2$s', 'invoicing' ), "<a href='https://ayecode.io/introducing-ayecode-connect/' target='_blank'>", '</a>' ),
361
				'connect_button'    => __( 'Connect Site', 'invoicing' ),
362
				'connecting_button' => __( 'Connecting...', 'invoicing' ),
363
				'error_localhost'   => __( 'This service will only work with a live domain, not a localhost.', 'invoicing' ),
364
				'error'             => __( 'Something went wrong, please refresh and try again.', 'invoicing' ),
365
            ),
366
            array( 'wpi-addons' )
367
        );
368
369
    }
370
371
	/**
372
	 * Redirect users to settings on activation.
373
	 *
374
	 * @return void
375
	 */
376
	public function activation_redirect() {
377
378
		$redirected = get_option( 'wpinv_redirected_to_settings' );
379
380
		if ( ! empty( $redirected ) || wp_doing_ajax() || ! current_user_can( 'manage_options' ) ) {
381
			return;
382
		}
383
384
		// Bail if activating from network, or bulk
385
		if ( is_network_admin() || isset( $_GET['activate-multi'] ) ) {
386
			return;
387
		}
388
389
	    update_option( 'wpinv_redirected_to_settings', 1 );
390
391
        wp_safe_redirect( admin_url( 'index.php?page=gp-setup' ) );
392
        exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
393
394
	}
395
396
    /**
397
     * Fires an admin action after verifying that a user can fire them.
398
     */
399
    public function maybe_do_admin_action() {
400
        if ( isset( $_REQUEST['getpaid-admin-action'] ) && isset( $_REQUEST['getpaid-nonce'] ) && wp_verify_nonce( $_REQUEST['getpaid-nonce'], 'getpaid-nonce' ) && wpinv_current_user_can( sanitize_text_field( $_REQUEST['getpaid-admin-action'] ), $_REQUEST ) ) {
401
            $key = sanitize_key( $_REQUEST['getpaid-admin-action'] );
402
403
            do_action( "getpaid_authenticated_admin_action_$key", $_REQUEST );
404
        }
405
    }
406
407
	/**
408
     * Duplicate invoice.
409
	 *
410
	 * @param array $args
411
     */
412
    public function duplicate_invoice( $args ) {
413
414
		if ( empty( $args['invoice_id'] ) ) {
415
			return;
416
		}
417
418
		$invoice = new WPInv_Invoice( (int) $args['invoice_id'] );
419
420
		if ( ! $invoice->exists() ) {
421
			return;
422
		}
423
424
		$new_invoice = getpaid_duplicate_invoice( $invoice );
425
		$new_invoice->save();
426
427
		if ( $new_invoice->exists() ) {
428
429
			getpaid_admin()->show_success( __( 'Invoice duplicated successfully.', 'invoicing' ) );
430
431
			wp_safe_redirect(
432
				add_query_arg(
433
					array(
434
						'action' => 'edit',
435
						'post'   => $new_invoice->get_id(),
436
					),
437
					admin_url( 'post.php' )
438
				)
439
			);
440
			exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
441
442
		}
443
444
		getpaid_admin()->show_error( __( 'There was an error duplicating this invoice. Please try again.', 'invoicing' ) );
445
446
	}
447
448
	/**
449
     * Refund an invoice.
450
	 *
451
	 * @param array $args
452
     */
453
    public function refund_invoice( $args ) {
454
455
		if ( empty( $args['invoice_id'] ) ) {
456
			return;
457
		}
458
459
		$invoice = new WPInv_Invoice( (int) $args['invoice_id'] );
460
461
		if ( ! $invoice->exists() || $invoice->is_refunded() ) {
462
			return;
463
		}
464
465
		$invoice->refund();
466
467
		// Refund remotely.
468
		if ( getpaid_payment_gateway_supports( $invoice->get_gateway(), 'refunds' ) && ! empty( $args['getpaid_refund_remote'] ) ) {
469
			do_action( 'getpaid_refund_invoice_remotely', $invoice );
470
		}
471
472
		// Cancel subscriptions.
473
		if ( ! empty( $args['getpaid_cancel_subscription'] ) ) {
474
			$subscriptions = getpaid_get_invoice_subscriptions( $invoice );
475
476
			if ( ! empty( $subscriptions ) ) {
477
				if ( ! is_array( $subscriptions ) ) {
0 ignored issues
show
The condition is_array($subscriptions) is always false.
Loading history...
478
					$subscriptions = array( $subscriptions );
479
				}
480
481
				foreach ( $subscriptions as $subscription ) {
482
					$subscription->cancel();
483
					$invoice->add_system_note(
484
						sprintf(
485
							// translators: %s: subscription ID.
486
							__( 'Subscription #%s cancelled', 'invoicing' ),
487
							$subscription->get_id()
488
						)
489
					);
490
				}
491
			}
492
		}
493
494
		// Add notice.
495
		$this->show_success( __( 'Invoice refunded successfully.', 'invoicing' ) );
496
497
		// Redirect.
498
		wp_safe_redirect(
499
			remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce', 'invoice_id', 'getpaid_cancel_subscription', 'getpaid_refund_remote' ) )
500
		);
501
	}
502
503
	/**
504
     * Sends a payment reminder to a customer.
505
	 *
506
	 * @param array $args
507
     */
508
    public function duplicate_payment_form( $args ) {
509
510
		if ( empty( $args['form_id'] ) ) {
511
			return;
512
		}
513
514
		$form = new GetPaid_Payment_Form( (int) $args['form_id'] );
515
516
		if ( ! $form->exists() ) {
517
			return;
518
		}
519
520
		$new_form = new GetPaid_Payment_Form();
521
		$new_form->set_author( $form->get_author( 'edit' ) );
522
		$new_form->set_name( $form->get_name( 'edit' ) . __( '(copy)', 'invoicing' ) );
523
		$new_form->set_elements( $form->get_elements( 'edit' ) );
524
		$new_form->set_items( $form->get_items( 'edit' ) );
525
		$new_form->save();
526
527
		if ( $new_form->exists() ) {
528
			$this->show_success( __( 'Form duplicated successfully', 'invoicing' ) );
529
			$url = get_edit_post_link( $new_form->get_id(), 'edit' );
530
		} else {
531
			$this->show_error( __( 'Unable to duplicate form', 'invoicing' ) );
532
			$url = remove_query_arg( array( 'getpaid-admin-action', 'form_id', 'getpaid-nonce' ) );
533
		}
534
535
		wp_redirect( $url );
536
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
537
	}
538
539
	/**
540
     * Resets form stats.
541
	 *
542
	 * @param array $args
543
     */
544
    public function reset_form_stats( $args ) {
545
546
		if ( empty( $args['form_id'] ) ) {
547
			return;
548
		}
549
550
		$form = new GetPaid_Payment_Form( (int) $args['form_id'] );
551
552
		if ( ! $form->exists() ) {
553
			return;
554
		}
555
556
		$form->set_earned( 0 );
557
		$form->set_refunded( 0 );
558
		$form->set_cancelled( 0 );
559
		$form->set_failed( 0 );
560
		$form->save();
561
562
		$this->show_success( __( 'Form stats reset successfully', 'invoicing' ) );
563
564
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'form_id', 'getpaid-nonce' ) ) );
565
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
566
	}
567
568
	/**
569
     * Sends a payment reminder to a customer.
570
	 *
571
	 * @param array $args
572
     */
573
    public function send_customer_invoice( $args ) {
574
		getpaid()->get( 'invoice_emails' )->user_invoice( new WPInv_Invoice( $args['invoice_id'] ), true );
575
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce', 'invoice_id' ) ) );
576
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
577
	}
578
579
	/**
580
     * Sends a payment reminder to a customer.
581
	 *
582
	 * @param array $args
583
     */
584
    public function send_customer_payment_reminder( $args ) {
585
		$sent = getpaid()->get( 'invoice_emails' )->force_send_overdue_notice( new WPInv_Invoice( $args['invoice_id'] ) );
586
587
		if ( $sent ) {
588
			$this->show_success( __( 'Payment reminder was successfully sent to the customer', 'invoicing' ) );
589
		} else {
590
			$this->show_error( __( 'Could not sent payment reminder to the customer', 'invoicing' ) );
591
		}
592
593
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce', 'invoice_id' ) ) );
594
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
595
	}
596
597
	/**
598
     * Resets tax rates.
599
	 *
600
     */
601
    public function admin_reset_tax_rates() {
602
603
		update_option( 'wpinv_tax_rates', wpinv_get_data( 'tax-rates' ) );
604
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce' ) ) );
605
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
606
607
	}
608
609
	/**
610
     * Resets admin pages.
611
	 *
612
     */
613
    public function admin_create_missing_pages() {
614
		$installer = new GetPaid_Installer();
615
		$installer->create_pages();
616
		$this->show_success( __( 'GetPaid pages updated.', 'invoicing' ) );
617
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce' ) ) );
618
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
619
	}
620
621
	/**
622
	 * Refreshes the permalinks.
623
	 */
624
	public function admin_refresh_permalinks() {
625
		flush_rewrite_rules();
626
		$this->show_success( __( 'Permalinks refreshed.', 'invoicing' ) );
627
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce' ) ) );
628
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
629
	}
630
631
	/**
632
     * Creates missing admin tables.
633
	 *
634
     */
635
    public function admin_create_missing_tables() {
636
		global $wpdb;
637
638
		GetPaid_Installer::create_db_tables();
639
		GetPaid_Installer::migrate_old_customers();
640
641
		if ( '' !== $wpdb->last_error ) {
642
			$this->show_error( __( 'Your GetPaid tables have been updated:', 'invoicing' ) . ' ' . $wpdb->last_error );
643
		} else {
644
			$this->show_success( __( 'Your GetPaid tables have been updated.', 'invoicing' ) );
645
		}
646
647
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce' ) ) );
648
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
649
	}
650
651
	/**
652
     * Migrates old invoices to the new database tables.
653
	 *
654
     */
655
    public function admin_migrate_old_invoices() {
656
657
		// Migrate the invoices.
658
		$installer = new GetPaid_Installer();
659
		$installer->migrate_old_invoices();
660
661
		// Show an admin message.
662
		$this->show_success( __( 'Your invoices have been migrated.', 'invoicing' ) );
663
664
		// Redirect the admin.
665
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce' ) ) );
666
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
667
668
	}
669
670
	/**
671
     * Download customers.
672
	 *
673
     */
674
    public function admin_download_customers() {
675
676
		$output = fopen( 'php://output', 'w' );
677
678
		if ( false === $output ) {
679
			wp_die( esc_html__( 'Unsupported server', 'invoicing' ), 500 );
680
		}
681
682
		header( 'Content-Type:text/csv' );
683
		header( 'Content-Disposition:attachment;filename=customers.csv' );
684
685
		/** @var GetPaid_Customer[] $customers */
686
		$customers = getpaid_get_customers( array( 'number' => -1 ) );
687
		$columns   = array_keys( GetPaid_Customer_Data_Store::get_database_fields() );
688
689
		// Output the csv column headers.
690
		fputcsv( $output, $columns );
691
692
		// Loop through
693
		foreach ( $customers as $customer ) {
694
695
			$row  = array();
696
697
			foreach ( $columns as $column ) {
698
				$row[]  = (string) maybe_serialize( $customer->get( $column, 'edit' ) );
699
			}
700
701
			fputcsv( $output, $row );
702
		}
703
704
		fclose( $output );
705
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
706
707
	}
708
709
	/**
710
     * Installs a plugin.
711
	 *
712
	 * @param array $data
713
     */
714
    public function admin_install_plugin( $data ) {
715
716
		if ( ! empty( $data['plugins'] ) ) {
717
			include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
718
			wp_cache_flush();
719
720
			foreach ( $data['plugins'] as $slug => $file ) {
721
				$plugin_zip = esc_url( 'https://downloads.wordpress.org/plugin/' . $slug . '.latest-stable.zip' );
722
				$upgrader   = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
723
				$installed  = $upgrader->install( $plugin_zip );
724
725
				if ( ! is_wp_error( $installed ) && $installed ) {
726
					activate_plugin( $file, '', false, true );
727
				} else {
728
					wpinv_error_log( $upgrader->skin->get_upgrade_messages(), false );
0 ignored issues
show
The method get_upgrade_messages() does not exist on WP_Upgrader_Skin. It seems like you code against a sub-type of WP_Upgrader_Skin such as Automatic_Upgrader_Skin. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

728
					wpinv_error_log( $upgrader->skin->/** @scrutinizer ignore-call */ get_upgrade_messages(), false );
Loading history...
$upgrader->skin->get_upgrade_messages() of type string[] is incompatible with the type string expected by parameter $log of wpinv_error_log(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

728
					wpinv_error_log( /** @scrutinizer ignore-type */ $upgrader->skin->get_upgrade_messages(), false );
Loading history...
729
				}
730
}
731
}
732
733
		$redirect = isset( $data['redirect'] ) ? esc_url_raw( $data['redirect'] ) : admin_url( 'plugins.php' );
734
		wp_safe_redirect( $redirect );
735
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
736
737
	}
738
739
	/**
740
     * Connects a gateway.
741
	 *
742
	 * @param array $data
743
     */
744
    public function admin_connect_gateway( $data ) {
745
746
		if ( ! empty( $data['plugin'] ) ) {
747
748
			$gateway     = sanitize_key( $data['plugin'] );
749
			$connect_url = apply_filters( "getpaid_get_{$gateway}_connect_url", false, $data );
750
751
			if ( ! empty( $connect_url ) ) {
752
				wp_redirect( $connect_url );
753
				exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
754
			}
755
756
			if ( 'stripe' == $data['plugin'] ) {
757
				require_once ABSPATH . 'wp-admin/includes/plugin.php';
758
				include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
759
				wp_cache_flush();
760
761
				if ( ! array_key_exists( 'getpaid-stripe-payments/getpaid-stripe-payments.php', get_plugins() ) ) {
762
					$plugin_zip = esc_url( 'https://downloads.wordpress.org/plugin/getpaid-stripe-payments.latest-stable.zip' );
763
					$upgrader   = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
764
					$upgrader->install( $plugin_zip );
765
				}
766
767
				activate_plugin( 'getpaid-stripe-payments/getpaid-stripe-payments.php', '', false, true );
768
			}
769
770
			$connect_url = apply_filters( "getpaid_get_{$gateway}_connect_url", false, $data );
771
			if ( ! empty( $connect_url ) ) {
772
				wp_redirect( $connect_url );
773
				exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
774
			}
775
}
776
777
		$redirect = isset( $data['redirect'] ) ? esc_url_raw( urldecode( $data['redirect'] ) ) : admin_url( 'admin.php?page=wpinv-settings&tab=gateways' );
778
		wp_safe_redirect( $redirect );
779
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
780
781
	}
782
783
	/**
784
     * Recalculates discounts.
785
	 *
786
     */
787
    public function admin_recalculate_discounts() {
788
		global $wpdb;
789
790
		// Fetch all invoices that have discount codes.
791
		$table    = $wpdb->prefix . 'getpaid_invoices';
792
		$invoices = $wpdb->get_col( "SELECT `post_id` FROM `$table` WHERE `discount` = 0 && `discount_code` <> ''" );
793
794
		foreach ( $invoices as $invoice ) {
795
796
			$invoice = new WPInv_Invoice( $invoice );
797
798
			if ( ! $invoice->exists() ) {
799
				continue;
800
			}
801
802
			// Abort if the discount does not exist or does not apply here.
803
			$discount = new WPInv_Discount( $invoice->get_discount_code() );
804
			if ( ! $discount->exists() ) {
805
				continue;
806
			}
807
808
			$invoice->add_discount( getpaid_calculate_invoice_discount( $invoice, $discount ) );
809
			$invoice->recalculate_total();
810
811
			if ( $invoice->get_total_discount() > 0 ) {
812
				$invoice->save();
813
			}
814
}
815
816
		// Show an admin message.
817
		$this->show_success( __( 'Discounts have been recalculated.', 'invoicing' ) );
818
819
		// Redirect the admin.
820
		wp_safe_redirect( remove_query_arg( array( 'getpaid-admin-action', 'getpaid-nonce' ) ) );
821
		exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
822
823
	}
824
825
    /**
826
	 * Returns an array of admin notices.
827
	 *
828
	 * @since       1.0.19
829
     * @return array
830
	 */
831
	public function get_notices() {
832
		$notices = get_option( 'wpinv_admin_notices' );
833
        return is_array( $notices ) ? $notices : array();
834
	}
835
836
	/**
837
	 * Checks if we have any admin notices.
838
	 *
839
	 * @since       2.0.4
840
     * @return array
841
	 */
842
	public function has_notices() {
843
		return count( $this->get_notices() ) > 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return count($this->get_notices()) > 0 returns the type boolean which is incompatible with the documented return type array.
Loading history...
844
	}
845
846
	/**
847
	 * Clears all admin notices
848
	 *
849
	 * @access      public
850
	 * @since       1.0.19
851
	 */
852
	public function clear_notices() {
853
		delete_option( 'wpinv_admin_notices' );
854
	}
855
856
	/**
857
	 * Saves a new admin notice
858
	 *
859
	 * @access      public
860
	 * @since       1.0.19
861
	 */
862
	public function save_notice( $type, $message ) {
863
		$notices = $this->get_notices();
864
865
		if ( empty( $notices[ $type ] ) || ! is_array( $notices[ $type ] ) ) {
866
			$notices[ $type ] = array();
867
		}
868
869
		$notices[ $type ][] = $message;
870
871
		update_option( 'wpinv_admin_notices', $notices );
872
	}
873
874
	/**
875
	 * Displays a success notice
876
	 *
877
	 * @param       string $msg The message to qeue.
878
	 * @access      public
879
	 * @since       1.0.19
880
	 */
881
	public function show_success( $msg ) {
882
		$this->save_notice( 'success', $msg );
883
	}
884
885
	/**
886
	 * Displays a error notice
887
	 *
888
	 * @access      public
889
	 * @param       string $msg The message to qeue.
890
	 * @since       1.0.19
891
	 */
892
	public function show_error( $msg ) {
893
		$this->save_notice( 'error', $msg );
894
	}
895
896
	/**
897
	 * Displays a warning notice
898
	 *
899
	 * @access      public
900
	 * @param       string $msg The message to qeue.
901
	 * @since       1.0.19
902
	 */
903
	public function show_warning( $msg ) {
904
		$this->save_notice( 'warning', $msg );
905
	}
906
907
	/**
908
	 * Displays a info notice
909
	 *
910
	 * @access      public
911
	 * @param       string $msg The message to qeue.
912
	 * @since       1.0.19
913
	 */
914
	public function show_info( $msg ) {
915
		$this->save_notice( 'info', $msg );
916
	}
917
918
	/**
919
	 * Show notices
920
	 *
921
	 * @access      public
922
	 * @since       1.0.19
923
	 */
924
	public function show_notices() {
925
926
        $notices = $this->get_notices();
927
        $this->clear_notices();
928
929
		foreach ( $notices as $type => $messages ) {
930
931
			if ( ! is_array( $messages ) ) {
932
				continue;
933
			}
934
935
            $type  = esc_attr( $type );
936
			foreach ( $messages as $message ) {
937
				echo wp_kses_post( "<div class='notice notice-$type is-dismissible'><p>$message</p></div>" );
938
            }
939
}
940
941
		foreach ( array( 'checkout_page', 'invoice_history_page', 'success_page', 'failure_page', 'invoice_subscription_page' ) as $page ) {
942
943
			if ( ! is_numeric( wpinv_get_option( $page, false ) ) ) {
944
				$url     = wp_nonce_url(
945
					add_query_arg( 'getpaid-admin-action', 'create_missing_pages' ),
946
					'getpaid-nonce',
947
					'getpaid-nonce'
948
				);
949
				$message  = __( 'Some GetPaid pages are missing. To use GetPaid without any issues, click the button below to generate the missing pages.', 'invoicing' );
950
				$message2 = __( 'Generate Pages', 'invoicing' );
951
				echo wp_kses_post( "<div class='notice notice-warning is-dismissible'><p>$message<br><br><a href='$url' class='button button-primary'>$message2</a></p></div>" );
952
				break;
953
			}
954
}
955
956
	}
957
958
}
959