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.

api/class-getpaid-rest-crud-controller.php (13 issues)

1
<?php
2
/**
3
 * GetPaid REST CRUD controller class.
4
 *
5
 * Extends the GetPaid_REST_Controller class to provide functionalities for endpoints
6
 * that use our CRUD classes
7
 *
8
 * @version 1.0.19
9
 */
10
11
defined( 'ABSPATH' ) || exit;
12
13
/**
14
 * GetPaid REST CRUD controller class.
15
 *
16
 * @package Invoicing
17
 */
18
class GetPaid_REST_CRUD_Controller extends GetPaid_REST_Controller {
19
20
	/**
21
	 * Contains this controller's class name.
22
	 *
23
	 * @var string
24
	 */
25
	public $crud_class;
26
27
	/**
28
	 * Contains the current CRUD object.
29
	 *
30
	 * @var GetPaid_Data
31
	 */
32
	protected $data_object;
33
34
	/**
35
	 * Registers the routes for the objects of the controller.
36
	 *
37
	 * @since 1.0.19
38
	 *
39
	 * @see register_rest_route()
40
	 */
41
	public function register_namespace_routes( $namespace ) {
42
43
		register_rest_route(
44
			$namespace,
45
			'/' . $this->rest_base,
46
			array(
47
				array(
48
					'methods'             => WP_REST_Server::READABLE,
49
					'callback'            => array( $this, 'get_items' ),
50
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
51
					'args'                => $this->get_collection_params(),
52
				),
53
				array(
54
					'methods'             => WP_REST_Server::CREATABLE,
55
					'callback'            => array( $this, 'create_item' ),
56
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
57
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
58
				),
59
				'schema' => array( $this, 'get_public_item_schema' ),
60
			)
61
		);
62
63
		$get_item_args = array(
64
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
65
		);
66
67
		register_rest_route(
68
			$namespace,
69
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
70
			array(
71
				'args'   => array(
72
					'id' => array(
73
						'description' => __( 'Unique identifier for the object.', 'invoicing' ),
74
						'type'        => 'integer',
75
					),
76
				),
77
				array(
78
					'methods'             => WP_REST_Server::READABLE,
79
					'callback'            => array( $this, 'get_item' ),
80
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
81
					'args'                => $get_item_args,
82
				),
83
				array(
84
					'methods'             => WP_REST_Server::EDITABLE,
85
					'callback'            => array( $this, 'update_item' ),
86
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
87
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
88
				),
89
				array(
90
					'methods'             => WP_REST_Server::DELETABLE,
91
					'callback'            => array( $this, 'delete_item' ),
92
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
93
					'args'                => array(
94
						'force' => array(
95
							'type'        => 'boolean',
96
							'default'     => false,
97
							'description' => __( 'Whether to bypass Trash and force deletion.', 'invoicing' ),
98
						),
99
					),
100
				),
101
				'schema' => array( $this, 'get_public_item_schema' ),
102
			)
103
		);
104
105
	}
106
107
	/**
108
	 * Saves a single object.
109
	 *
110
	 * @param GetPaid_Data $object Object to save.
111
	 * @return WP_Error|GetPaid_Data
112
	 */
113
	protected function save_object( $object ) {
114
		$object->save();
115
116
		if ( ! empty( $object->last_error ) ) {
117
			return new WP_Error( 'rest_cannot_save', $object->last_error, array( 'status' => 400 ) );
118
		}
119
120
		return new $this->crud_class( $object->get_id() );
121
	}
122
123
	/**
124
	 * Retrieves a single object.
125
	 *
126
	 * @since 1.0.13
127
	 *
128
	 * @param int|WP_Post $object_id Supplied ID.
129
	 * @return GetPaid_Data|WP_Error GetPaid_Data object if ID is valid, WP_Error otherwise.
130
	 */
131
	protected function get_object( $object_id ) {
132
133
		// Do we have an object?
134
		if ( empty( $this->crud_class ) || ! class_exists( $this->crud_class ) ) {
135
			return new WP_Error( 'no_crud_class', __( 'You need to specify a CRUD class for this controller', 'invoicing' ) );
136
		}
137
138
		// Fetch the object.
139
		$object = new $this->crud_class( $object_id );
140
		if ( ! empty( $object->last_error ) ) {
141
			return new WP_Error( 'rest_object_invalid_id', $object->last_error, array( 'status' => 404 ) );
142
		}
143
144
		$this->data_object = $object;
145
		return $object->get_id() ? $object : new WP_Error( 'rest_object_invalid_id', __( 'Invalid ID.', 'invoicing' ), array( 'status' => 404 ) );
146
147
	}
148
149
	/**
150
	 * Get a single object.
151
	 *
152
	 * @param WP_REST_Request $request Full details about the request.
153
	 * @return WP_Error|WP_REST_Response
154
	 */
155
	public function get_item( $request ) {
156
157
		// Fetch the item.
158
		$object = $this->get_object( $request['id'] );
159
160
		if ( is_wp_error( $object ) ) {
161
			return $object;
162
		}
163
164
		// Generate a response.
165
		return rest_ensure_response( $this->prepare_item_for_response( $object, $request ) );
0 ignored issues
show
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

165
		return rest_ensure_response( $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request ) );
Loading history...
166
167
	}
168
169
	/**
170
	 * Create a single object.
171
	 *
172
	 * @param WP_REST_Request $request Full details about the request.
173
	 * @return WP_Error|WP_REST_Response
174
	 */
175
	public function create_item( $request ) {
176
177
		// Can not create an existing item.
178
		if ( ! empty( $request['id'] ) ) {
179
			/* translators: %s: post type */
180
			return new WP_Error( "getpaid_rest_{$this->rest_base}_exists", __( 'Cannot create existing resource.', 'invoicing' ), array( 'status' => 400 ) );
181
		}
182
183
		// Generate a GetPaid_Data object from the request.
184
		$object = $this->prepare_item_for_database( $request );
185
		if ( is_wp_error( $object ) ) {
186
			return $object;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $object also could return the type GetPaid_Data which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
187
		}
188
189
		// Save the object.
190
		$object = $this->save_object( $object );
0 ignored issues
show
It seems like $object can also be of type WP_Error; however, parameter $object of GetPaid_REST_CRUD_Controller::save_object() does only seem to accept GetPaid_Data, maybe add an additional type check? ( Ignorable by Annotation )

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

190
		$object = $this->save_object( /** @scrutinizer ignore-type */ $object );
Loading history...
191
		if ( is_wp_error( $object ) ) {
192
			return $object;
193
		}
194
195
		// Save special fields.
196
		$save_special = $this->update_additional_fields_for_object( $object, $request );
197
		if ( is_wp_error( $save_special ) ) {
198
			$object->delete( true );
0 ignored issues
show
The method delete() does not exist on WP_Error. ( Ignorable by Annotation )

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

198
			$object->/** @scrutinizer ignore-call */ 
199
            delete( true );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
199
			return $save_special;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $save_special also could return the type true which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
200
		}
201
202
		$request->set_param( 'context', 'edit' );
203
		$response = $this->prepare_item_for_response( $object, $request );
0 ignored issues
show
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

203
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request );
Loading history...
204
		$response = rest_ensure_response( $response );
205
		$response->set_status( 201 );
206
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) );
0 ignored issues
show
The method get_id() does not exist on WP_Error. ( Ignorable by Annotation )

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

206
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->/** @scrutinizer ignore-call */ get_id() ) ) );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
207
208
		return $response;
209
	}
210
211
	/**
212
	 * Update a single object.
213
	 *
214
	 * @param WP_REST_Request $request Full details about the request.
215
	 * @return WP_Error|WP_REST_Response
216
	 */
217
	public function update_item( $request ) {
218
219
		// Fetch the item.
220
		$object = $this->get_object( $request['id'] );
221
		if ( is_wp_error( $object ) ) {
222
			return $object;
223
		}
224
225
		// Prepare the item for saving.
226
		$object = $this->prepare_item_for_database( $request );
227
		if ( is_wp_error( $object ) ) {
228
			return $object;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $object also could return the type GetPaid_Data which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
229
		}
230
231
		// Save the item.
232
		$object = $this->save_object( $object );
0 ignored issues
show
It seems like $object can also be of type WP_Error; however, parameter $object of GetPaid_REST_CRUD_Controller::save_object() does only seem to accept GetPaid_Data, maybe add an additional type check? ( Ignorable by Annotation )

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

232
		$object = $this->save_object( /** @scrutinizer ignore-type */ $object );
Loading history...
233
		if ( is_wp_error( $object ) ) {
234
			return $object;
235
		}
236
237
		// Save special fields (those added via hooks).
238
		$save_special = $this->update_additional_fields_for_object( $object, $request );
239
		if ( is_wp_error( $save_special ) ) {
240
			return $save_special;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $save_special also could return the type true which is incompatible with the documented return type WP_Error|WP_REST_Response.
Loading history...
241
		}
242
243
		$request->set_param( 'context', 'edit' );
244
		$response = $this->prepare_item_for_response( $object, $request );
0 ignored issues
show
$object of type WP_Error is incompatible with the type GetPaid_Data expected by parameter $object of GetPaid_REST_CRUD_Contro...are_item_for_response(). ( Ignorable by Annotation )

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

244
		$response = $this->prepare_item_for_response( /** @scrutinizer ignore-type */ $object, $request );
Loading history...
245
		return rest_ensure_response( $response );
246
	}
247
248
	/**
249
	 * Prepare links for the request.
250
	 *
251
	 * @param GetPaid_Data    $object GetPaid_Data object.
252
	 * @return array Links for the given object.
253
	 */
254
	protected function prepare_links( $object ) {
255
256
		$links = array(
257
			'self'       => array(
258
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
259
			),
260
			'collection' => array(
261
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
262
			),
263
		);
264
265
		return $links;
266
	}
267
268
	/**
269
	 * Get the query params for collections of attachments.
270
	 *
271
	 * @return array
272
	 */
273
	public function get_collection_params() {
274
		$params = parent::get_collection_params();
275
		$params['context']['default'] = 'view';
276
		return $params;
277
	}
278
279
	/**
280
	 * Only return writable props from schema.
281
	 *
282
	 * @param  array $schema Schema.
283
	 * @return bool
284
	 */
285
	public function filter_writable_props( $schema ) {
286
		return empty( $schema['readonly'] );
287
	}
288
289
	/**
290
	 * Prepare a single object for create or update.
291
	 *
292
	 * @since 1.0.19
293
	 * @param  WP_REST_Request $request Request object.
294
	 * @return GetPaid_Data|WP_Error Data object or WP_Error.
295
	 */
296
	protected function prepare_item_for_database( $request ) {
297
298
		// Do we have an object?
299
		if ( empty( $this->crud_class ) || ! class_exists( $this->crud_class ) ) {
300
			return new WP_Error( 'no_crud_class', __( 'You need to specify a CRUD class for this controller', 'invoicing' ) );
301
		}
302
303
		// Prepare the object.
304
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
305
		$object    = new $this->crud_class( $id );
306
307
		// Abort if an error exists.
308
		if ( ! empty( $object->last_error ) ) {
309
			return new WP_Error( 'invalid_item', $object->last_error );
310
		}
311
312
		$schema    = $this->get_item_schema();
313
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
314
315
		// Handle all writable props.
316
		foreach ( $data_keys as $key ) {
317
			$value = $request[ $key ];
318
319
			if ( ! is_null( $value ) ) {
320
				switch ( $key ) {
321
322
					case 'meta_data':
323
						if ( is_array( $value ) ) {
324
							foreach ( $value as $meta ) {
325
								$object->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
326
							}
327
						}
328
						break;
329
330
					default:
331
						if ( is_callable( array( $object, "set_{$key}" ) ) ) {
332
							$object->{"set_{$key}"}( $value );
333
						}
334
						break;
335
				}
336
			}
337
}
338
339
		// Filters an object before it is inserted via the REST API..
340
		return apply_filters( "getpaid_rest_pre_insert_{$this->rest_base}_object", $object, $request );
341
	}
342
343
	/**
344
	 * Retrieves data from a GetPaid class.
345
	 *
346
	 * @since  1.0.19
347
	 * @param  GetPaid_Meta_Data[]    $meta_data  meta data objects.
348
	 * @return array
349
	 */
350
	protected function prepare_object_meta_data( $meta_data ) {
351
		$meta = array();
352
353
		foreach ( $meta_data as $object ) {
354
			$meta[] = $object->get_data();
355
		}
356
357
		return $meta;
358
	}
359
360
	/**
361
	 * Retrieves invoice items.
362
	 *
363
	 * @since  1.0.19
364
	 * @param  WPInv_Invoice $invoice  Invoice items.
365
	 * @param array            $fields Fields to include.
366
	 * @return array
367
	 */
368
	protected function prepare_invoice_items( $invoice ) {
369
		$items = array();
370
371
		foreach ( $invoice->get_items() as $item ) {
372
373
			$item_data = $item->prepare_data_for_saving();
374
375
			if ( 'amount' == $invoice->get_template() ) {
376
				$item_data['quantity'] = 1;
377
			}
378
379
			$items[] = $item_data;
380
		}
381
382
		return $items;
383
	}
384
385
	/**
386
	 * Retrieves data from a GetPaid class.
387
	 *
388
	 * @since  1.0.19
389
	 * @param  GetPaid_Data    $object  Data object.
390
	 * @param array            $fields Fields to include.
391
	 * @param string           $context either view or edit.
392
	 * @return array
393
	 */
394
	protected function prepare_object_data( $object, $fields, $context = 'view' ) {
395
396
		$data = array();
397
398
		// Handle all writable props.
399
		foreach ( array_keys( $this->get_schema_properties() ) as $key ) {
400
401
			// Abort if it is not included.
402
			if ( ! empty( $fields ) && ! $this->is_field_included( $key, $fields ) ) {
403
				continue;
404
			}
405
406
			// Or this current object does not support the field.
407
			if ( ! $this->object_supports_field( $object, $key ) ) {
408
				continue;
409
			}
410
411
			// Handle meta data.
412
			if ( $key == 'meta_data' ) {
413
				$data['meta_data'] = $this->prepare_object_meta_data( $object->get_meta_data() );
414
				continue;
415
			}
416
417
			// Handle items.
418
			if ( $key == 'items' && is_a( $object, 'WPInv_Invoice' ) ) {
419
				$data['items'] = $this->prepare_invoice_items( $object );
420
				continue;
421
			}
422
423
			// Booleans.
424
			if ( is_callable( array( $object, $key ) ) ) {
425
				$data[ $key ] = $object->$key( $context );
426
				continue;
427
			}
428
429
			// Get object value.
430
			if ( is_callable( array( $object, "get_{$key}" ) ) ) {
431
				$value = $object->{"get_{$key}"}( $context );
432
433
				// If the value is an instance of GetPaid_Data...
434
				if ( is_a( $value, 'GetPaid_Data' ) ) {
435
					$value = $value->get_data( $context );
0 ignored issues
show
The call to GetPaid_Data::get_data() has too many arguments starting with $context. ( Ignorable by Annotation )

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

435
					/** @scrutinizer ignore-call */ 
436
     $value = $value->get_data( $context );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
436
				}
437
438
				// For objects, retrieves it's properties.
439
				$data[ $key ] = is_object( $value ) ? get_object_vars( $value ) : $value;
440
				continue;
441
			}
442
}
443
444
		return $data;
445
	}
446
447
	/**
448
	 * Checks if a key should be included in a response.
449
	 *
450
	 * @since  1.0.19
451
	 * @param  GetPaid_Data $object  Data object.
452
	 * @param  string       $field_key The key to check for.
453
	 * @return bool
454
	 */
455
	public function object_supports_field( $object, $field_key ) {
456
		return apply_filters( 'getpaid_rest_object_supports_key', true, $object, $field_key );
457
	}
458
459
	/**
460
	 * Prepare a single object output for response.
461
	 *
462
	 * @since  1.0.19
463
	 * @param  GetPaid_Data    $object  Data object.
464
	 * @param  WP_REST_Request $request Request object.
465
	 * @return WP_REST_Response
466
	 */
467
	public function prepare_item_for_response( $object, $request ) {
468
		remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 );
469
470
		$this->data_object = $object;
471
472
		// Fetch the fields to include in this response.
473
		$fields = $this->get_fields_for_response( $request );
474
475
		// Prepare object data.
476
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
477
		$data    = $this->prepare_object_data( $object, $fields, $context );
478
		$data    = $this->add_additional_fields_to_object( $data, $request );
479
		$data    = $this->limit_object_to_requested_fields( $data, $fields );
480
		$data    = $this->filter_response_by_context( $data, $context );
481
482
		// Prepare the response.
483
		$response = rest_ensure_response( $data );
484
		$response->add_links( $this->prepare_links( $object, $request ) );
0 ignored issues
show
The call to GetPaid_REST_CRUD_Controller::prepare_links() has too many arguments starting with $request. ( Ignorable by Annotation )

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

484
		$response->add_links( $this->/** @scrutinizer ignore-call */ prepare_links( $object, $request ) );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
485
486
		// Filter item response.
487
		return apply_filters( "getpaid_rest_prepare_{$this->rest_base}_object", $response, $object, $request );
488
	}
489
490
}
491