<?php

if ( class_exists( 'WP_REST_Controller' ) ) {
	class PMPro_REST_API_Routes extends WP_REST_Controller {
		
		public function pmpro_rest_api_register_routes() {

			// ================ DEPRECATED ================ //
			$namespace = 'wp/v2';
			register_rest_route( $namespace, '/users/(?P<id>\d+)'.'/pmpro_membership_level' , 
			array(
				array(
					'methods'         => WP_REST_Server::READABLE,
					'callback'        => array( $this, 'pmpro_rest_api_get_membership_level_for_user' ),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
			),));
			
			register_rest_route( $namespace, '/posts/(?P<post_id>\d+)'.'/user_id/(?P<user_id>\d+)/pmpro_has_membership_access' , 
			array(
				array(
					'methods'         => WP_REST_Server::READABLE,
					'callback'        => array( $this, 'pmpro_rest_api_get_has_membership_access' ),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
			),));
			// ================================================  //

			$pmpro_namespace = 'pmpro/v1';

			/**
			 * Get user access for a specific post.
			 * @since 2.3
			 * Example: https://example.com/wp-json/pmpro/v1/has_membership_access
			 */
			register_rest_route( $pmpro_namespace, '/has_membership_access',
			array(
				array(
					'methods'  => WP_REST_Server::READABLE,
					'callback' => array( $this, 'pmpro_rest_api_get_has_membership_access'),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				)));

			/**
			 * Get a membership level for a user.
			 * @since 2.3
			 * Example: https://example.com/wp-json/pmpro/v1/get_membership_level_for_user
			 */
			 register_rest_route( $pmpro_namespace, '/get_membership_level_for_user', 
			 array(
				 array(
					 'methods'         => WP_REST_Server::READABLE,
					 'callback'        => array( $this, 'pmpro_rest_api_get_membership_level_for_user' ),
					 'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
			 ),));

			 /**
			 * Get a membership level for a user.
			 * @since 2.3
			 * Example: https://example.com/wp-json/pmpro/v1/get_membership_levels_for_user
			 */
			 register_rest_route( $pmpro_namespace, '/get_membership_levels_for_user', 
			 array(
				 array(
					 'methods'         => WP_REST_Server::READABLE,
					 'callback'        => array( $this, 'pmpro_rest_api_get_membership_levels_for_user' ),
					 'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
			 ),));


		/**
		 * Change a user's membership level. This also supports to cancel a membership if you pass through 0.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/change_membership_level
		 */
		register_rest_route( $pmpro_namespace, '/change_membership_level',
			array(
				array(
					'methods'	=> 'POST,PUT,PATCH',
					'callback'	=> array( $this, 'pmpro_rest_api_change_membership_level' ),
					'args'	=> array(
						'user_id' => array(),
						'level_id' => array(),
						'email' => array(),
						'first_name' => array(),
						'last_name' => array(),
						'user_url' => array(),
						'user_login' => array(),
						'description' => array(),
						'create_user' => array(),
					),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				)			
			)
		);

		/**
		 * Cancel a membership level.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/cancel_membership_level
		 */
		register_rest_route( $pmpro_namespace, '/cancel_membership_level',
			array(
				array(
					'methods'	=> 'POST,PUT,PATCH',
					'callback'	=> array( $this, 'pmpro_rest_api_cancel_membership_level' ),
					'args'	=> array(
						'user_id' => array(),
						'level_id' => array()
					),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				)			
			)
		);

		/**
		 * Delete/Retrieve/Update/Create a Membership Level.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/membership_level
		 */
		register_rest_route( $pmpro_namespace, '/membership_level' , 
		array(
			array(
				'methods'         => WP_REST_Server::READABLE,
				'callback'        => array( $this, 'pmpro_rest_api_get_membership_level' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
			),
			array(
				'methods'         => 'POST,PUT,PATCH',
				'callback'        => array( $this, 'pmpro_rest_api_set_membership_level' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
			),
			array(
				'methods' 		=> 'DELETE',
				'callback'        => array( $this, 'pmpro_rest_api_delete_membership_level' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
			)
		));

		/**
		 * Get all membership levels.
		 * @since 3.0
		 * Example: https://example.com/wp-json/pmpro/v1/membership_levels
		 */
		register_rest_route( $pmpro_namespace, '/membership_levels',
			array(
				array(
					'methods'         => WP_REST_Server::READABLE,
					'callback'        => array( $this, 'pmpro_rest_api_get_membership_levels' ),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				),
			)
		);

		/**
		 * Create/Retrieve discount code.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/discount_code
		 */
		register_rest_route( $pmpro_namespace, '/discount_code', 
		array(
			array(
				'methods' => WP_REST_Server::READABLE,
				'callback' => array( $this, 'pmpro_rest_api_get_discount_code' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' )
			),
			array(
				'methods' => 'POST,PUT,PATCH',
				'callback' => array( $this, 'pmpro_rest_api_set_discount_code' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' )
			),
		));

		/**
		 * Retrieve an order.
		 * @since 2.8
		 * Example: https://example.com/wp-json/pmpro/v1/order
		 */
		register_rest_route( $pmpro_namespace, '/order', 
		array(
			array(
				'methods' => WP_REST_Server::READABLE,
				'callback' => array( $this, 'pmpro_rest_api_get_order' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' )
			),
		));
		add_filter( 'pmpro_rest_api_permissions', array( $this, 'pmpro_rest_api_permissions_get_order' ), 10, 2 );

		/**
		 * Get membership level after checkout options are applied.
		 * @since 2.4
		 * Example: https://example.com/wp-json/pmpro/v1/checkout_level
		 */
		register_rest_route( $pmpro_namespace, '/checkout_level', 
		array(
			array(
				'methods' => WP_REST_Server::READABLE,
				'callback' => array( $this, 'pmpro_rest_api_get_checkout_level' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' )
			),
		));

		/**
		 * Get membership levels after checkout options are applied.
		 * Example: https://example.com/wp-json/pmpro/v1/checkout_levels
		 * @deprecated 3.0
		 */
		register_rest_route( $pmpro_namespace, '/checkout_levels', 
		array(
			array(
				'methods' => WP_REST_Server::READABLE,
				'callback' => array( $this, 'pmpro_rest_api_get_checkout_levels' ),
				'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' )
			),
		));

		/**
		 * Authentication route for Zapier integration.
		 *
		 * Used to do authentication when connecting Zapier to PMPro.
		 *
		 * @since 2.6.0
		 */
		register_rest_route( $pmpro_namespace, '/me', 
			array(
				array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array( $this, 'validate_me' ),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' )
				),
			)
		);

		/**
		 * Get the last couple of membership levels/members.
		 *
		 * @since 2.6.0
		 */	
		register_rest_route( $pmpro_namespace, '/recent_memberships',
			array(
				array(
					'methods'  => WP_REST_Server::READABLE,
					'callback' => array( $this, 'pmpro_rest_api_recent_memberships' ),
					'args'	=> array(
						'status' => array(),
					),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				)
		));

		register_rest_route( $pmpro_namespace, '/recent_orders',
			array(
				array(
					'methods'  => WP_REST_Server::READABLE,
					'callback' => array( $this, 'pmpro_rest_api_recent_orders' ),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				)
			)
		);

		/**
		 * Get the IDs that a post has been restricted to.
		 * @since 3.0
		 * Example: https://example.com/wp-json/pmpro/v1/post_restrictions
		 */
		register_rest_route( $pmpro_namespace, '/post_restrictions',
			array(
				array(
					'methods'  => WP_REST_Server::READABLE,
					'callback' => array( $this, 'pmpro_rest_api_get_post_restrictions' ),
					'args'     => array(
						'post_id' => array(),
					),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				),
				array(
					'methods'  => WP_REST_Server::EDITABLE,
					'args'     => array(
						'post_id' => array(),
						'level_ids' => array(),
					),
					'callback' => array( $this, 'pmpro_rest_api_set_post_restrictions' ),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				)
			)
		);

		/**
		 * Start an export.
		 *
		 * Route:
		 * - POST /pmpro/v1/export/start
		 *
		 * Params:
		 * - type: string (e.g., 'members', 'orders') (required)
		 * - l: level filter (optional)
		 * - s: search (optional)
		 * - export_id: for status (optional)
		 *
		 * Example:.../wp-json/pmpro/v1/export/start
		 *
		 * @since 3.7
		 */
		register_rest_route( $pmpro_namespace, '/export/start',
			array(
				array(
					'methods'  => WP_REST_Server::EDITABLE,
					'callback' => function( $request ) {
						$params = $request->get_params();
						$type   = isset( $params['type'] ) ? sanitize_text_field( $params['type'] ) : '';
						if ( empty( $type ) ) {
							return new WP_REST_Response( array( 'error' => 'Missing export type.' ), 400 );
						}
						$force_async = ! empty( $params['force_async'] ) ? (bool) $params['force_async'] : false;
						$exports = PMPro_Exports::instance();
						$result  = $exports->start_export( $type, $params, $force_async );

						// Trigger async queue processing if this was an async export.
						if ( ! empty( $result['export_id'] ) ) {
							PMPro_Action_Scheduler::dispatch_queue();
						}

						$status_code = isset( $result['error'] ) ? 400 : 200;
						return new WP_REST_Response( $result, $status_code );
					},
					'permission_callback' => function( $request ) {
						$params  = $request->get_params();
						$type    = isset( $params['type'] ) ? sanitize_text_field( $params['type'] ) : '';
						$exports = PMPro_Exports::instance();
						if ( empty( $type ) ) {
							return current_user_can( 'manage_options' ) || current_user_can( 'pmpro_memberslistcsv' ) || current_user_can( 'pmpro_orderscsv' );
						}
						return $exports->user_can_export( $type );
					},
				)
			)
		);

		/**
		 * Get the progress status of an export.
		 *
		 * Route:
		 * - GET  /pmpro/v1/exports/status
		 *
		 * Example: .../wp-json/pmpro/v1/export/status?type=members&export_id=12345
		 *
		 * @since 3.7
		 */
		register_rest_route( $pmpro_namespace, '/export/status',
			array(
				array(
					'methods'  => WP_REST_Server::READABLE,
					'callback' => function( $request ) {
						$params    = $request->get_params();
						$type      = isset( $params['type'] ) ? sanitize_text_field( $params['type'] ) : '';
						$export_id = isset( $params['export_id'] ) ? sanitize_text_field( $params['export_id'] ) : '';
						if ( empty( $type ) ) {
							return new WP_REST_Response( array( 'error' => 'Missing export type.' ), 400 );
						}
						$exports = PMPro_Exports::instance();
						$result  = $exports->get_status( $type, $export_id );
						// Never respond with 404 for logical export errors; keep 200 and surface error message.
						if ( isset( $result['error'] ) ) {
							$result['http_status'] = 200;
						}
						return new WP_REST_Response( $result, 200 );
					},
					'permission_callback' => function( $request ) {
						$params  = $request->get_params();
						$type    = isset( $params['type'] ) ? sanitize_text_field( $params['type'] ) : '';
						$exports = PMPro_Exports::instance();
						if ( empty( $type ) ) {
							return current_user_can( 'manage_options' ) || current_user_can( 'pmpro_memberslistcsv' ) || current_user_can( 'pmpro_orderscsv' );
						}
						return $exports->user_can_export( $type );
					},
				)
			)
		);

		/**
		 * Get email log details.
		 * @since 3.7
		 * Example: https://example.com/wp-json/pmpro/v1/email_log_popup
		 */
		register_rest_route( $pmpro_namespace, '/email_log_popup',
			array(
				array(
					'methods'  => WP_REST_Server::READABLE,
					'callback' => array( $this, 'pmpro_rest_api_get_email_log_popup_contents' ),
					'args'     => array(
						'log_id' => array(
							'required' => true,
							'type' => 'integer',
							'sanitize_callback' => 'absint',
						),
					),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				),
			)
		);

		/**
		 * Get search results for the quick search bar.
		 *
		 * @since 3.7
		 *
		 * Example: https://example.com/wp-json/pmpro/v1/quick_search
		 */
		register_rest_route( $pmpro_namespace, '/quick_search',
			array(
				array(
					'methods'  => WP_REST_Server::READABLE,
					'callback' => array( $this, 'pmpro_rest_api_quick_search' ),
					'args'     => array(
						'search' => array(),
						'type'   => array(),
					),
					'permission_callback' => array( $this, 'pmpro_rest_api_get_permissions_check' ),
				)
			)
		);
		}
		
		/**
		 * Get user's membership level.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/get_membership_level_for_user?user_id=1
		 */
		function pmpro_rest_api_get_membership_level_for_user($request) {
			$params = $request->get_params();
			
			$user_id = isset( $params['user_id'] ) ? intval( $params['user_id'] ) : null;

			// Param id was used instead (old style endpoint).
			if ( empty( $user_id ) && !empty( $params['id'] ) ) {
				$user_id = intval( $params['id'] );
			}
			
			// Query by email.
			if ( empty( $user_id ) && !empty( $params['email'] ) ) {
				$user = get_user_by( 'email', sanitize_email( $params['email'] ) );
				$user_id = $user->ID;
			}
			
			if ( ! empty( $user_id ) ) {
				$level = pmpro_getMembershipLevelForUser( $user_id );
			} else {
				$level = false;
			}

			return new WP_REST_Response( $level, 200 );
		}

		/**
		 * Get user's membership levels. (MMPU)
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/get_membership_levels_for_user?user_id=1
		 */
		 function pmpro_rest_api_get_membership_levels_for_user($request) {
			$params = $request->get_params();
			
			$user_id = isset( $params['user_id'] ) ? intval( $params['user_id'] ) : null;

			// Param id was used instead.
			if ( empty( $user_id ) && !empty( $params['id'] ) ) {
				$user_id = intval( $params['id'] );
			}

			// Param email was used instead.
			if ( empty( $user_id ) && !empty( $params['email'] ) ) {
				$user = get_user_by( 'email', sanitize_email( $params['email'] ) );
				$user_id = $user->ID;
			}
			
			if ( ! empty( $user_id ) ) {
				$levels = pmpro_getMembershipLevelsForUser( $user_id );
			} else {
				$levels = false;
			}	

			return new WP_REST_Response( $levels, 200 );
		}
		
		/**
		 * Get user's access status for a specific post.
		 * @since 2.3
		 * Example: https://example.com/wp-json/wp/v2/posts/58/user_id/2/pmpro_has_membership_access
		 * Example: https://example.com/wp-json/pmpro/v1/has_membership_access?post_id=58&user_id=2
		 */
		function pmpro_rest_api_get_has_membership_access($request) {
			$params = $request->get_params();
			$post_id = isset( $params['post_id'] ) ? intval( $params['post_id'] ) : null;
			$user_id = isset( $params['user_id'] ) ? intval( $params['user_id'] ) : null;

			if ( empty( $user_id ) ) {
				// see if they sent an email
				if ( ! empty( $params['email'] ) ) {
					$user = get_user_by( 'email', sanitize_email( $params['email'] ) );
					$user_id = $user->ID;
				} else {
					return new WP_REST_Response( 'No user information passed through.', 404 );
				}
			}
			
			if ( ! empty( $user_id ) ) {
				$has_access = pmpro_has_membership_access( $post_id, $user_id );
			} else {
				// No good user, so say no.
				// Technically this will make public posts look restricted.
				$has_access = false;
			}
			
			return new WP_REST_Response( $has_access, 200 );
		}

		/**
		 * Change a user's membership level.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/change_membership_level
		 */
		function pmpro_rest_api_change_membership_level( $request ) {
			$params = $request->get_params();
			$user_id = isset( $params['user_id'] ) ? (int) $params['user_id'] : null;
			$level_id = isset( $params['level_id'] ) ? (int) $params['level_id'] : null;
			$email = isset( $params['email'] ) ? sanitize_email( $params['email'] ) : null;
			$first_name = isset( $params['first_name'] ) ? sanitize_text_field( $params['first_name'] ) : '';
			$last_name = isset( $params['last_name'] ) ? sanitize_text_field( $params['last_name'] ) : '';
			$username = isset( $params['user_login'] ) ? sanitize_text_field( $params['user_login'] ) : $email;
			$user_url = isset( $params['user_url'] ) ? sanitize_text_field( $params['user_url'] ) : '';
			$description = isset( $params['description'] ) ? sanitize_textarea_field( $params['description'] ) : '';
			$create_user = isset( $params['create_user'] ) ? filter_var( $params['create_user'], FILTER_VALIDATE_BOOLEAN ) : false;
			$response_type = isset( $params['response_type'] ) ? sanitize_text_field( $params['response_type'] ) : null;

			if ( empty( $user_id ) ) {

				// see if they sent an email
				if ( ! empty( $email ) ) {

					$user = get_user_by( 'email', $email );
					
					// Assume the user doesn't already exist.
					if ( $create_user && ! $user ) {

						/**
						 * Filter the user data arguments for wp_insert_user when creating the user via the REST API.
						 * @since 2.7.4
						 * @param array $user_data An associative array with user arguments when creating the user. See https://developer.wordpress.org/reference/functions/wp_insert_user/ for reference.
						 */
						$user_data = apply_filters( 'pmpro_api_new_user_array', array(
								'user_pass' => wp_generate_password(),
								'user_email' => $email,
								'user_login' => $username,
								'first_name' => $first_name,
								'last_name' => $last_name,
								'user_url' => $user_url,
								'description' => $description
							)
						);

						$user_id = wp_insert_user( $user_data );
						
						if ( is_wp_error( $user_id ) ) {
							$error = $user_id->get_error_message();
							return new WP_REST_Response( $error, 500 ); // Assume it failed and return a 500 error occurred like core WordPress.
						}
						
						pmpro_maybe_send_wp_new_user_notification( $user_id, $level_id );
					} else {
						$user_id = $user->ID;
					}
					
				} else {

					if ( 'json' === $response_type ) {
						wp_send_json_error( array( 'email' => $email, 'error' => 'No user information passed through.' ) );
					}
					return new WP_REST_Response( 'No user information passed through.', 404 );
				}
			}

			if ( ! function_exists( 'pmpro_changeMembershipLevel' ) ) {
				if ( 'json' === $response_type ) {
					wp_send_json_error( array( 'email' => $email, 'error' => 'Paid Memberships Pro function not found.' ) );
				}
				return new WP_REST_Response( 'Paid Memberships Pro function not found.', 404 );
			}

			// Make sure we have a user_id by now.
			if ( empty( $user_id ) ) {
				if ( 'json' === $response_type ) {
					wp_send_json_error( array( 'email' => $email, 'error' => 'No user found with that email address. Try the create_user parameter.' ) );
				}				
				return new WP_REST_Response( 'No user found with that email address. Try the create_user parameter.', 404 );
			}
			
			/**
			 * Filter to allow admin levels to be changed via the REST API or Zapier application.
			 * Defaults to false to prevent admin users from having their level changed via API.
			 * @since 2.7.4
			 * @param boolean $can_change_admin_users Should API calls change admin account membership levels.
			 */
			$can_change_admin_users = apply_filters( 'pmpro_api_change_membership_level_for_admin_users', false );
			if ( ! $can_change_admin_users && user_can( $user_id, 'manage_options' ) ) {
				if ( 'json' === $response_type ) {
					wp_send_json_error( array( 'email' => $email, 'error' => 'Sorry, you are not allowed to edit admin accounts.' ) );
				}
				return new WP_REST_Response( 'Sorry, you are not allowed to edit admin accounts.', 403 );
			}
						
			$response = pmpro_changeMembershipLevel( $level_id, $user_id );
						
			if ( 'json' === $response_type ) {
				wp_send_json_success( array( 'user_id' => $user_id, 'level_changed' => $level_id, 'response' => $response, 'status' => 200 ) );
			}
			return new WP_REST_Response( $response, 200 );
		}

		/**
		 * Cancel a user's membership level.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/cancel_membership_level
		 */
		function pmpro_rest_api_cancel_membership_level( $request ) {
			$params = $request->get_params();
			$user_id = isset( $params['user_id'] ) ? intval( $params['user_id'] ) : null;
			$level_id = isset( $params['level_id'] ) ? intval( $params['level_id'] ) : null;
			$email = isset( $params['email'] ) ? sanitize_email( $params['email'] ) : null;
			$response_type = isset( $params['response_type'] ) ? sanitize_text_field( $params['response_type'] ) : null;

			if ( empty( $user_id ) ) {
				// see if they sent an email
				if ( ! empty( $email ) ) {
					$user = get_user_by( 'email', $email );
					$user_id = $user->ID;
				} else {
					if ( 'json' === $response_type ) {
						wp_send_json_error( array( 'email' => $email ) );
					}

					return new WP_REST_Response( 'No user information passed through.', 404 );
				}
			}
			
			if ( empty( $level_id ) ) {
				if ( 'json' === $response_type ) {
					wp_send_json_error( array( 'email' => $email ) );
				}

				return new WP_REST_Response( 'No membership level ID data.', 400 );
			}

			if ( ! function_exists( 'pmpro_cancelMembershipLevel' ) ) {
				if ( 'json' === $response_type ) {
					wp_send_json_error( array( 'email' => $email ) );
				}

				return new WP_REST_Response( 'Paid Memberships Pro function not found.', 404 );
			}
			
			if ( ! empty( $user_id ) ) {
				$response = pmpro_cancelMembershipLevel( $level_id, $user_id, 'inactive' );
			} else {
				$response = false;
			}

			if ( 'json' === $response_type ) {
				wp_send_json_success( array( 'email' => $email ) );
			}

			return new WP_REST_Response( $response, 200 );
		}
		
		/**
		 * Endpoint to get membership level data
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/membership_level/
		 */
		function pmpro_rest_api_get_membership_level( $request ) {

			if ( ! class_exists( 'PMPro_Membership_Level' ) ) {
				return new WP_REST_Response( 'Paid Memberships Pro level class not found.', 404 );
			}

			$params = $request->get_params();
			$id = isset( $params['id'] ) ? intval( $params['id'] ) : null;
			$response_type = isset( $params['response_type'] ) ? sanitize_text_field( $params['response_type'] ) : false;

			if ( empty( $id ) ) {
				return new WP_REST_Response( 'ID not passed through', 400 );
			}

			$level = new PMPro_Membership_Level( $id );

			// Hide confirmation message if not an admin or member.
			if ( ! empty( $level->confirmation ) 
				 && ! pmpro_hasMembershipLevel( $id )
				 && ! current_user_can( 'pmpro_edit_members' ) ) {
					 $level->confirmation = '';
			}

			if ( 'json' === $response_type ) {
				wp_send_json_success( array( 'level' => $level ) );
			}

			return new WP_REST_Response( $level, 200 );
		}

		/**
		 * Create/Update a Membership Level
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/membership_level/
		 */
		function pmpro_rest_api_set_membership_level( $request ) {

			if ( ! class_exists( 'PMPro_Membership_Level' ) ) {
				return new WP_REST_Response( 'Paid Memberships Pro level class not found.', 404 );
			}

			$params = $request->get_params();
			$method = $request->get_method();

			$id = isset( $params['id'] ) ? intval( $params['id'] ) : '';
			$response_type = isset( $params['response_type'] ) ? sanitize_text_field( $params['response_type'] ) : null;

			// Pass through an ID only for PUT/PATCH methods. POST treats it as a brand new level.
			if ( ! empty( $id ) && ( $method === 'PUT' || $method === 'PATCH' ) ) {
				$level = new PMPro_Membership_Level( $id );
			} elseif ( empty( $id ) && ( $method === 'PUT' || $method === 'PATCH' ) ) {
				return false; // Error trying to update
			} elseif ( $method === 'POST' ) {
				$level = new PMPro_Membership_Level();
			}

			$name = isset( $params['name'] ) ? sanitize_text_field( $params['name'] ) : $level->name;
			$description = isset( $params['description'] ) ? sanitize_text_field( $params['description'] ) : $level->description;
			$confirmation = isset( $params['confirmation'] ) ? sanitize_text_field( $params['confirmation'] ) : $level->confirmation;
			$initial_payment = isset( $params['initial_payment'] ) ? floatval( $params['initial_payment'] ) : $level->initial_payment;
			$billing_amount = isset( $params['billing_amount'] ) ? floatval( $params['billing_amount'] ) : $level->billing_amount;
			$cycle_number = isset( $params['cycle_number'] ) ? intval( $params['cycle_number'] ) : $level->cycle_number;
			$cycle_period = isset( $params['cycle_period'] ) ? pmpro_sanitize_period( $params['cycle_period'] ) : $level->cycle_period;
			$billing_limit = isset( $params['billing_limit'] ) ? sanitize_text_field( $params['billing_limit'] ) : $level->billing_limit;
			$trial_amount = isset( $params['trial_amount'] ) ? floatval( $params['trial_amount'] ) : $level->trial_amount;
			$trial_limit = isset( $params['trial_limit'] ) ? floatval( $params['trial_limit'] ) : $level->trial_limit;
			$allow_signups = isset( $params['allow_signups'] ) ? intval( $params['allow_signups'] ) : $level->allow_signups;
			$expiration_number = isset( $params['expiration_number'] ) ? intval( $params['expiration_number'] ) : $level->expiration_number;
			$expiration_period = isset( $params['expiration_period'] ) ? intval( $params['expiration_period'] ) : $level->expiration_period;
			$categories = isset( $params['categories'] ) ? PMPro_REST_API_Routes::pmpro_rest_api_convert_to_array( sanitize_text_field( $params['categories'] ) ) : $level->categories;
			
			// Set Level Object and save it.
			$level->name = $name;
			$level->description = $description;
			$level->confirmation = $confirmation;
			$level->initial_payment = $initial_payment;
			$level->billing_amount = $billing_amount;
			$level->cycle_number = $cycle_number;
			$level->billing_limit = $billing_limit;
			$level->trial_amount = $trial_amount;
			$level->allow_signups = $allow_signups;
			$level->expiration_number = $expiration_number;
			$level->expiration_period = $expiration_period;
			$level->categories = $categories;
			$level->save();	

			if ( 'json' === $response_type ) {
				wp_send_json_success( array( "level" => $level ) );
			}

			return new WP_REST_Response( $level, 200 );

		}

		/**
		 * Delete membership level and remove users from level. (And cancel their subscription.)
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/membership_level/
		 */
		function pmpro_rest_api_delete_membership_level( $request ) {

			if ( ! class_exists( 'PMPro_Membership_Level' ) ) {
				return new WP_REST_Response( 'Paid Memberships Pro level class not found.', 404 );
			}

			$params = $request->get_params();
			$id = isset( $params['id'] ) ? intval( $params['id'] ) : '';

			if ( empty( $id ) ) {
				return new WP_REST_Response( 'ID not passed through.', 400 );
			}
			
			$level = new PMPro_Membership_Level( $id );

			return new WP_REST_Response( $level->delete(), 200 );
		}

		/**
		 * Get all membership levels.
		 * @since 3.0
		 * Example: https://example.com/wp-json/pmpro/v1/membership_levels
		 */
		function pmpro_rest_api_get_membership_levels( $request ) {
			
			if ( ! class_exists( 'PMPro_Membership_Level' ) ) {
				return new WP_REST_Response( 'Paid Memberships Pro level class not found.', 404 );
			}

			$params = $request->get_params();
			$response_type = isset( $params['response_type'] ) ? sanitize_text_field( $params['response_type'] ) : null;

			$levels = pmpro_getAllLevels( true, true );

			// Hide confirmation message if not an admin or member.
			if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_membershiplevels' ) ) {
				foreach ( $levels as $level ) {
					if ( ! pmpro_hasMembershipLevel( $level->id ) ) {
						$level->confirmation = '';
					}
				}
			}

			if ( 'json' === $response_type ) {
				wp_send_json_success( array( "levels" => $levels ) );
			}

			return new WP_REST_Response( $levels, 200 );
		}

		/**
		 * Get a discount code
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/discount_code
		 */
		function pmpro_rest_api_get_discount_code( $request ) {
			if ( ! class_exists( 'PMPro_Discount_Code' ) ) {
				return new WP_REST_Response( 'Paid Memberships Pro discount code class not found.', 404 );
			}

			$params = $request->get_params();
			$code = isset( $params['code'] ) ? sanitize_text_field( $params['code'] ) : null;

			if ( empty( $code ) ) {
				return new WP_REST_Response( 'No discount code sent.', 400 );
			}

			return new WP_REST_Response( new PMPro_Discount_Code( $code ), 200 );
					
		}

		/**
		 * Create/update a discount code.
		 * @since 2.3
		 * Example: https://example.com/wp-json/pmpro/v1/discount_code
		 */
		function pmpro_rest_api_set_discount_code( $request ) {

			if ( ! class_exists( 'PMPro_Discount_Code' ) ) {
				return new WP_REST_Response( 'Paid Memberships Pro discount code class not found.', 404 );
			}

			$params = $request->get_params();
			$method = $request->get_method();
			$code = isset( $params['code'] ) ? sanitize_text_field( $params['code'] ) : '';
			$uses = isset( $params['uses'] ) ? intval( $params['uses'] ) : '';
			$starts = isset( $params['starts'] ) ? sanitize_text_field( $params['starts'] ) : '';
			$expires = isset( $params['expires'] ) ? sanitize_text_field( $params['expires'] ) : '';
			$levels = isset( $params['levels'] ) ? sanitize_text_field( $params['levels'] ) : null;

			if ( ! empty( $levels ) ) {
				$levels = json_decode( $levels, true );

				if ( is_array( $levels ) ) {
					$levels_array = array();
					foreach( $levels as $level ){
						$levels_array[$level['level']] = array(
							'initial_payment' => isset( $level['initial_payment'] ) ? $level['initial_payment'] : null,
							'billing_amount' => isset( $level['billing_amount'] ) ? $level['billing_amount'] : null,
							'cycle_number' => isset( $level['cycle_number'] ) ? $level['cycle_number'] : null,
							'cycle_period' => isset( $level['cycle_period'] ) ? $level['cycle_period'] : null,
							'billing_limit' => isset( $level['cycle_period'] ) ? $level['cycle_period'] : null,
							'custom_trial' => isset( $level['custom_trial'] ) ? $level['custom_trial'] : null,
							'trial_amount' => isset( $level['trial_amount'] ) ? $level['trial_amount'] : null,
							'trial_limit' => isset( $level['trial_limit'] ) ? $level['trial_limit'] : null,
							'expiration_number' => isset( $level['expiration_number'] ) ?  : null,
							'expiration_period' => isset( $level['expiration_period'] ) ?  : null
						);
					}
				}
			}
			
			$discount_code = new PMPro_Discount_Code();

			// See if code already exists when POSTING.
			if ( $method == 'POST' && ! empty( $code ) ) {
				// See if discount code exists.
				if ( is_numeric( $code ) ) {
					$discount_code->get_discount_code_by_id( $code );
				} else {
					$discount_code->get_discount_code_by_code( $code );
				}

				if ( ! empty( $discount_code->id ) ) {
					return new WP_REST_Response( 'Discount code already exists.', 400 );
				}
			}

			$discount_code->code = $code;
			$discount_code->starts = $starts;
			$discount_code->expires = $expires;
			$discount_code->uses = $uses;
			$discount_code->levels = !empty( $levels_array ) ? $levels_array : $levels;
			$discount_code->save();

			return new WP_REST_Response( $discount_code, 200 );
		}

		/**
		 * Get an order.
		 * @since 2.8
		 * Example: https://example.com/wp-json/pmpro/v1/order
		 */
		function pmpro_rest_api_get_order( $request ) {
			if ( ! class_exists( 'MemberOrder' ) ) {
				return new WP_REST_Response( 'Paid Memberships Pro order class not found.', 404 );
			}

			$params = $request->get_params();
			$code   = isset( $params['code'] ) ? sanitize_text_field( $params['code'] ) : null;

			if ( empty( $code ) ) {
				return new WP_REST_Response( 'No order code sent.', 400 );
			}

			// Build the order object to return.
			// Need to do this because the order object now has private properties.
			$order = new MemberOrder( $code );
			$order_data_to_return = new stdClass();
			if ( ! empty( $order->id ) ) {
				$order_data_to_return->id = $order->id;
				$order_data_to_return->code = $order->code;
				$order_data_to_return->user_id = $order->user_id;
				$order_data_to_return->membership_id = $order->membership_id;
				$order_data_to_return->billing = $order->billing;
				$order_data_to_return->subtotal = $order->subtotal;
				$order_data_to_return->tax = $order->tax;
				$order_data_to_return->total = $order->total;
				$order_data_to_return->payment_type = $order->payment_type;
				$order_data_to_return->cardtype = $order->cardtype;
				$order_data_to_return->accountnumber = $order->accountnumber;
				$order_data_to_return->expirationmonth = $order->expirationmonth;
				$order_data_to_return->expirationyear = $order->expirationyear;
				$order_data_to_return->status = $order->status;
				$order_data_to_return->gateway = $order->gateway;
				$order_data_to_return->gateway_environment = $order->gateway_environment;
				$order_data_to_return->payment_transaction_id = $order->payment_transaction_id;
				$order_data_to_return->subscription_transaction_id = $order->subscription_transaction_id;
				$order_data_to_return->timestamp = $order->timestamp;
				$order_data_to_return->affiliate_id = $order->affiliate_id;
				$order_data_to_return->affiliate_subid = $order->affiliate_subid;
				$order_data_to_return->notes = $order->notes;
				$order_data_to_return->checkout_id = $order->checkout_id;
			}

			return new WP_REST_Response( $order_data_to_return, 200 );
		}

		/**
		 * Make sure that users can GET their own orders.
		 * @since 2.8
		 */
		function pmpro_rest_api_permissions_get_order( $permission, $request ) {
			$method = $request->get_method();
			$route  = $request->get_route();

			// Check if the user does not have access but is trying to get an order.
			if ( ! $permission && 'GET' === $method && '/pmpro/v1/order' === $route ) {
				// Only a logged-in user can own an order, so bail early for anonymous requests.
				if ( ! is_user_logged_in() ) {
					return $permission;
				}

				// Check if the order belongs to the user.
				$params = $request->get_params();
				$code   = isset( $params['code'] ) ? sanitize_text_field( $params['code'] ) : null;

				if ( ! empty( $code ) ) {
					$order = new MemberOrder( $code );

					if ( ! empty( $order->id ) && (int) $order->user_id === get_current_user_id() ) {
						return true;
					}
				}
			}
			return $permission;
		}

		/**
		 * Get a membership level at checkout.
		 *
		 * @since 2.4
		 * Example: https://example.com/wp-json/pmpro/v1/checkout_level
		 */
		function pmpro_rest_api_get_checkout_level( $request ) {
			$params = $request->get_params();

			if ( isset( $params['pmpro_level' ] ) ) {
				$level_id = intval( $params['pmpro_level'] );
			} elseif ( isset( $params['level_id'] ) ) {
				$level_id = intval( $params['level_id'] );
			} elseif ( isset( $params['level'] ) ) {
				$level_id = intval( $params['level'] );
			}

			if ( empty( $level_id ) ) {
				return new WP_REST_Response( 'No level found.', 400 );
			}

			if ( isset( $params['pmpro_discount_code'] ) ) {
				$discount_code = sanitize_text_field( $params['pmpro_discount_code'] );
			} elseif ( isset( $params['discount_code'] ) ) {
				$discount_code = sanitize_text_field( $params['discount_code'] );
			} else {
				$discount_code = null;
			}

			$checkout_level = pmpro_getLevelAtCheckout( $level_id, $discount_code );

			if ( ! empty( $checkout_level ) ) {
				// Hide confirmation message if not an admin or member.
				if ( ! empty( $checkout_level->confirmation ) 
					&& ! pmpro_hasMembershipLevel( $level_id )
					&& ! current_user_can( 'pmpro_edit_members' ) ) {
						$checkout_level->confirmation = '';
				}

				// Also add a formatted version of the initial payment.
				$checkout_level->initial_payment_formatted = pmpro_formatPrice( $checkout_level->initial_payment );
			}

			return new WP_REST_Response( $checkout_level );
		}

		/**
		 * Get membership levels at checkout.
		 * Example: https://example.com/wp-json/pmpro/v1/checkout_levels
		 *
		 * @deprecated 3.0 Use the /pmpro/v1/checkout_level endpoint instead.
		 */
		function pmpro_rest_api_get_checkout_levels( $request ) {
			$params = $request->get_params();

			global $pmpro_checkout_level_ids;
			if ( ! empty( $pmpro_checkout_level_ids ) ) {
				// MMPU Compatibility...
				$level_ids = $pmpro_checkout_level_ids;
			} elseif ( isset( $params['pmpro_level' ] ) ) {
				$level_ids = explode( '+', intval( $params['pmpro_level'] ) );
			}elseif ( isset( $params['level_id'] ) ) {
				$level_ids = explode( '+', intval( $params['level_id'] ) );
			} elseif ( isset( $params['level'] ) ) {
				$level_ids = explode( '+', intval( $params['level'] ) );
			}

			if ( empty( $level_ids ) ) {
				return new WP_REST_Response( 'No levels found.', 400 );
			}
			$discount_code = isset( $params['discount_code'] ) ? sanitize_text_field( $params['discount_code'] ) : null;

			$r = array();
			$r['initial_payment'] = 0.00;
			foreach ( $level_ids as $level_id ) {
				$r[ $level_id ] = pmpro_getLevelAtCheckout( $level_id, $discount_code );
				
				// Increment the total initial_paymnent.
				if ( ! empty( $r[ $level_id ]->initial_payment ) ) {
					$r['initial_payment'] += floatval( $r[ $level_id ]->initial_payment );
				}

				// Hide confirmation message if not an admin or member.
				if ( ! empty( $r[ $level_id ]->confirmation ) 
						&& ! pmpro_hasMembershipLevel( $level_id )
						&& ! current_user_can( 'pmpro_membershiplevels' ) ) {				
					$r[ $level_id ]->confirmation = '';					
				}
			}
			$r['initial_payment_formatted'] = pmpro_formatPrice( $r['initial_payment'] );
			return new WP_REST_Response( $r );
		}


		/// ZAPIER TRIGGERS
		/**
		 * Handle authentication testing for the Zapier API.
		 *
		 * @since 2.6.0
		 *
		 * @param WP_REST_Request $request The REST request.
		 */
		public function validate_me( $request ) {

			$params = $request->get_params();

			if ( is_user_logged_in() ) {
			  $me = wp_get_current_user()->display_name;
			} else {
				$me = false;
			}
		
			wp_send_json_success( array( 'username' => $me ) );
		}

		/**
		 * Handle requests for the list of recent memberships.
		 *
		 * @since 2.6.0
		 *
		 * @param WP_REST_Request $request The REST request.
		 *
		 * @return WP_REST_Response The REST response.
		 */
		public function pmpro_rest_api_recent_memberships( $request ) {
			$params = $request->get_params();
			if ( isset($params['limit']) ) {
				$limit = intval( $params['limit'] );
			} else {
				$limit = 1;
			}
			/**
			 * Allow filtering the total number of recent members to show in the /recent_memberships PMPro endpoint.
			 *
			 * @param int $limit The total number of recent members to show.
			 */
			$limit = apply_filters( 'pmpro_trigger_recent_members_limit', $limit );
			
			if ( empty( $params['level_status'] ) ) {
				$level_status = [ 'active' ];
			} else {
				$level_status = sanitize_text_field( trim( $params['level_status'] ) );

				// Force it into an array so we can implode it in the query itself.
				$level_status = explode( ',', $level_status );
			}

			// Set up values to prepare.
			$prepare   = $level_status;
			$prepare[] = $limit;

			// Set up the placeholders we want to use.
			$level_status_placeholders = implode( ', ', array_fill( 0, count( $level_status ), '%s' ) );

			// Grab the useful information.
			global $wpdb;

			$sql = "
				SELECT
					`mu`.`user_id` AS `id`,
					`u`.`user_email`,
					`u`.`user_nicename`,
					`umf`.`meta_value` AS first_name,
					`uml`.`meta_value` AS last_name,
					`mu`.`membership_id`,
					`ml`.`name` as membership_name,
					`mu`.`status`,
					`mu`.`modified`
				FROM `{$wpdb->pmpro_memberships_users}` AS `mu`
				LEFT JOIN `{$wpdb->users}` AS `u`
					ON `mu`.`user_id` = `u`.`id`
				LEFT JOIN `{$wpdb->pmpro_membership_levels}` AS `ml`
					ON `ml`.`id` = `mu`.`membership_id`
				LEFT JOIN `{$wpdb->usermeta}` AS `umf`
					ON `umf`.`meta_key` = 'first_name'
					AND `umf`.`user_id` = `u`.`id`
				LEFT JOIN `{$wpdb->usermeta}` AS `uml`
					ON `uml`.`meta_key` = 'last_name'
					AND `uml`.`user_id` = `u`.`id`
				WHERE
					`mu`.`status` IN ( {$level_status_placeholders} ) 
				ORDER BY
					`mu`.`modified` DESC
				LIMIT %d
			";

			$results = $wpdb->get_results( $wpdb->prepare( $sql, $prepare ) );

			// Let's format the date to ISO8601
			$results[0]->modified = pmpro_format_date_iso8601( $results[0]->modified );

			return new WP_REST_Response( $results, 200 );

		}

		/**
		 * Handle requests for the list of recent orders.
		 *
		 * @since 2.6.0
		 *
		 * @param WP_REST_Request $request The REST request.
		 *
		 * @return WP_REST_Response The REST response.
		 */
		public function pmpro_rest_api_recent_orders( $request ) {
			$params = $request->get_params();
			
			if ( isset($params['limit']) ) {
				$orders_limit = intval( $params['limit'] );
			} else {
				$orders_limit = 1;
			}

			$limit = apply_filters( 'pmpro_trigger_recent_orders_limit', $orders_limit );

			global $wpdb;

			$sql = "
				SELECT
					`o`.`id`,
					`o`.`code`,
					`u`.`ID` AS `user_id`,
					`u`.`user_email`,
					`u`.`user_nicename`,
					`umf`.`meta_value` AS first_name,
					`uml`.`meta_value` AS last_name,
					`o`.`membership_id`,
					`o`.`billing_name`,
					`o`.`billing_street`,
					`o`.`billing_city`,
					`o`.`billing_state`,
					`o`.`billing_zip`,
					`o`.`billing_country`,
					`o`.`billing_phone`,
					`o`.`subtotal`,
					`o`.`tax`,
					`o`.`total`,
					`o`.`status`,
					`o`.`gateway`,
					`o`.`gateway_environment`,
					`o`.`timestamp`
				FROM `{$wpdb->pmpro_membership_orders}` AS `o`
				LEFT JOIN `{$wpdb->users}` AS `u`
					ON `o`.`user_id` = `u`.`ID`
				LEFT JOIN `{$wpdb->usermeta}` AS `umf`
					ON `umf`.`meta_key` = 'first_name'
					AND `umf`.`user_id` = `u`.`ID`
				LEFT JOIN `{$wpdb->usermeta}` AS `uml`
					ON `uml`.`meta_key` = 'last_name'
					AND `uml`.`user_id` = `u`.`ID`
				ORDER BY
					`o`.`timestamp` DESC
				LIMIT %d
			";
			
			$results = $wpdb->get_results( $wpdb->prepare( $sql, $limit ) );

			$results[0]->timestamp = pmpro_format_date_iso8601( $results[0]->timestamp );

			return new WP_REST_Response( $results, 200 );
		}

		/**
		 * Get the membership level IDs restricting a given post.
		 *
		 * @since 3.0
		 *
		 * @param WP_REST_Request $request The REST request.
		 *
		 * @return WP_REST_Response The REST response.
		 */
		public function pmpro_rest_api_get_post_restrictions( $request ) {
			global $wpdb;

			$params = $request->get_params();

			$post_id = isset( $params['post_id'] ) ? intval( $params['post_id'] ) : null;

			if ( empty( $post_id ) ) {
				return new WP_REST_Response( array( 'error' => 'No post ID provided.' ), 400 );
			}

			$level_ids = $wpdb->get_results( $wpdb->prepare( "SELECT `membership_id` FROM `{$wpdb->pmpro_memberships_pages}` WHERE `page_id` = %d", $post_id ) );

			return new WP_REST_Response( $level_ids, 200 );
		}

		/**
		 * Set the membership level IDs restricting a given post.
		 *
		 * @since 3.0
		 *
		 * @param WP_REST_Request $request The REST request.
		 * @return WP_REST_Response The REST response.
		 */
		public function pmpro_rest_api_set_post_restrictions( $request ) {
			global $wpdb;

			$params = $request->get_params();

			$post_id = isset( $params['post_id'] ) ? intval( $params['post_id'] ) : null;
			$level_ids = isset( $params['level_ids'] ) ? array_map( 'intval', $params['level_ids'] ) : null;

			if ( empty( $post_id ) ) {
				return new WP_REST_Response( array( 'error' => 'No post ID provided.' ), 400 );
			}

			if ( ! is_array( $level_ids ) ) {
				return new WP_REST_Response( array( 'error' => 'No level IDs provided.' ), 400 );
			}

			pmpro_update_post_level_restrictions( $post_id, $level_ids );

			return new WP_REST_Response( array( 'success' => $level_ids ), 200 );
		}

		/**
		 * Get email log details.
		 *
		 * @since 3.7
		 *
		 * @param WP_REST_Request $request The REST request.
		 * @return WP_REST_Response The REST response.
		 */
		public function pmpro_rest_api_get_email_log_popup_contents( $request ) {
			global $wpdb;

			$params = $request->get_params();
			$log_id = isset( $params['log_id'] ) ? intval( $params['log_id'] ) : null;

			if ( empty( $log_id ) ) {
				return new WP_REST_Response( array( 'error' => 'No log ID provided.' ), 400 );
			}

			// Get the email log from the database
			$log = $wpdb->get_row( $wpdb->prepare(
				"SELECT * FROM {$wpdb->pmpro_email_log} WHERE id = %d",
				$log_id
			) );

			if ( ! $log ) {
				return new WP_REST_Response( array( 'error' => 'Email log not found.' ), 404 );
			}

			// Use helper function to render the email log details
			$html = pmpro_render_email_log_details( $log );

			return new WP_REST_Response( array( 'html' => $html ), 200 );
		}

		/**
		 * Get the quick search results for a search string.
		 *
		 * @since 3.7
		 *
		 * @param WP_REST_Request $request The REST request.
		 * @return WP_REST_Response The REST response.
		 */
		public function pmpro_rest_api_quick_search( $request ) {
			global $wpdb;

			$params = $request->get_params();

			$search_string = isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : null;

			if ( empty( $search_string ) ) {
				return new WP_REST_Response( array( 'error' => 'No search string provided.' ), 400 );
			}

			// Centralized limits.
			$limit_default  = 5;  // used when searching across "all"
			$limit_specific = 10; // used when a specific type is selected

			// Determine search type.
			$type = isset( $params['type'] ) ? sanitize_text_field( $params['type'] ) : 'all';
			$allowed_types = array( 'all', 'users', 'subscriptions', 'orders', 'reports', 'levels', 'discounts', 'settings', 'documentation' );
			if ( ! in_array( $type, $allowed_types, true ) ) {
				$type = 'all';
			}
			$limit = ( 'all' === $type ) ? $limit_default : $limit_specific;

			// Build an array of results to return. Each result will be a link.
			$results = array();

			// Search Users.
			if ( ( 'all' === $type || 'users' === $type ) && ( current_user_can( pmpro_get_edit_member_capability() ) || current_user_can( 'manage_options' ) ) ) {
				// If we're specifically searching for users and there is a colon in the query, we are going to search by meta fields.
				if ( strpos( $search_string, ':' ) !== false && 'users' === $type ) {
					list( $meta_key, $meta_value ) = explode( ':', $search_string, 2 );
					$meta_key = sanitize_text_field( trim( $meta_key ) );
					$meta_value = sanitize_text_field( trim( $meta_value ) );

					/**
					 * Limit the meta_key lookup to custom profile fields.
					 *
					 * Keys that start with an underscore are WP/plugin-internal by
					 * convention, and a handful of well-known keys hold authentication
					 * or capability data that the quick search isn't meant to surface.
					 *
					 * @since 3.7.2
					 *
					 * @param array $blocklist List of meta_key names (lowercase) to skip.
					 */
					$meta_key_blocklist = apply_filters( 'pmpro_rest_api_quick_search_meta_key_blocklist', array(
						'session_tokens',
						'wp_capabilities',
						'wp_user_level',
						'user_activation_key',
					) );

					$meta_key_is_internal = (
						'' === $meta_key
						|| '_' === $meta_key[0]
						|| in_array( strtolower( $meta_key ), $meta_key_blocklist, true )
					);

					if ( $meta_key_is_internal ) {
						$user_ids = array();
					} else {
						$user_ids = $wpdb->get_col( $wpdb->prepare(
							"SELECT user_id FROM {$wpdb->usermeta}
							WHERE meta_key = %s
							AND meta_value LIKE %s
							LIMIT %d",
							$meta_key,
							$meta_value . '%',
							$limit
						) );
					}
					$users = array();
					foreach( $user_ids as $user_id ) {
						$user = get_userdata( $user_id );
						if ( $user ) {
							$users[] = $user;
						}
					}
				} else {
					// Normal user search by ID, login, or email.
					$users = $wpdb->get_results( $wpdb->prepare(
						"SELECT * FROM {$wpdb->users}
						WHERE ID = %d
						OR user_login LIKE %s
						OR user_email LIKE %s
						ORDER BY display_name ASC
						LIMIT %d",
						is_numeric( $search_string ) ? $search_string : -1,
						$search_string . '%',
						$search_string . '%',
						$limit
					) );
				}
				$results['users'] = array(
					'label' => esc_html__( 'Users', 'paid-memberships-pro' ),
					'items' => array_map(
						function($user) {
							return array(
								'url'   => esc_url( add_query_arg( array( 'page' => 'pmpro-member', 'user_id' => $user->ID ), admin_url( 'admin.php' ) ) ),
								'label' => sprintf( '<strong>%s</strong> (%s)', esc_html( $user->display_name ), esc_html( $user->user_email ) ),
								'icon'  => 'dashicons-admin-users'
							);
						}, $users
					)
				);
			}

			// Search Subscriptions.
			if ( ( 'all' === $type || 'subscriptions' === $type ) && ( current_user_can( pmpro_get_edit_member_capability() ) || current_user_can( 'manage_options' ) ) ) {
				// If we're only searching subscriptions, allow loose matches.
				if ( 'subscriptions' === $type ) {
					$subscriptions = $wpdb->get_results( $wpdb->prepare(
						"SELECT id, subscription_transaction_id, user_id FROM {$wpdb->pmpro_subscriptions}
						WHERE id = %d
						OR subscription_transaction_id LIKE %s
						LIMIT %d",
						is_numeric( $search_string ) ? $search_string : -1,
						$search_string . '%',
						$limit
					) );
				} else {
					// When searching all, only allow exact matches.
					$subscriptions = $wpdb->get_results( $wpdb->prepare(
						"SELECT id, subscription_transaction_id, user_id FROM {$wpdb->pmpro_subscriptions}
						WHERE id = %d
						OR subscription_transaction_id = %s
						LIMIT %d",
						is_numeric( $search_string ) ? $search_string : -1,
						$search_string,
						$limit
					) );
				}
				$results['subscriptions'] = array(
					'label' => esc_html__( 'Subscriptions', 'paid-memberships-pro' ),
					'items' => array_map(
						function($subscription) {
							$user = get_userdata( $subscription->user_id );
							$label = $user ? sprintf( '<strong>%s</strong> (%s)', esc_html( $subscription->subscription_transaction_id ), esc_html( $user->display_name ) ) : sprintf( '<strong>%s</strong>', esc_html( $subscription->subscription_transaction_id ) );
							return array(
								'url'   => esc_url( add_query_arg( array( 'page' => 'pmpro-subscriptions', 'id' => $subscription->id ), admin_url( 'admin.php' ) ) ),
								'label' => $label,
								'icon'  => 'dashicons-update'
							);
						}, $subscriptions
					)
				);
			}

			// Search Orders.
			if ( ( 'all' === $type || 'orders' === $type ) && ( current_user_can( 'pmpro_orders') || current_user_can( 'manage_options' ) ) ) {
				// If we're only searching orders, allow loose matches.
				if ( 'orders' === $type ) {
					$orders = $wpdb->get_results( $wpdb->prepare(
						"SELECT id, code, user_id FROM {$wpdb->pmpro_membership_orders}
						WHERE id = %d
						OR code LIKE %s
						OR payment_transaction_id LIKE %s
						LIMIT %d",
						is_numeric( $search_string ) ? $search_string : -1,
						$search_string . '%',
						$search_string . '%',
						$limit
					) );
				} else {
					// When searching all, only allow exact matches.
					$orders = $wpdb->get_results( $wpdb->prepare(
						"SELECT id, code, user_id FROM {$wpdb->pmpro_membership_orders}
						WHERE id = %d
						OR code = %s
						OR payment_transaction_id = %s
						LIMIT %d",
						is_numeric( $search_string ) ? $search_string : -1,
						$search_string,
						$search_string,
						$limit
					) );
				}
				$results['orders'] = array(
					'label' => esc_html__( 'Orders', 'paid-memberships-pro' ),
					'items' => array_map(
						function($order) {
							$user = get_userdata( $order->user_id );
							$label = $user ? sprintf( '<strong>%s</strong> (%s)', esc_html( $order->code ), esc_html( $user->display_name ) ) : sprintf( '<strong>%s</strong>', esc_html( $order->code ) );
							return array(
								'url'   => esc_url( add_query_arg( array( 'page' => 'pmpro-orders', 'id' => $order->id ), admin_url( 'admin.php' ) ) ),
								'label' => $label,
								'icon'  => 'dashicons-money-alt'
							);
						}, $orders
					)
				);
			}

			// Search reports pages.
			if ( ( 'all' === $type || 'reports' === $type ) && ( current_user_can( 'pmpro_reports' ) || current_user_can( 'manage_options' ) ) ) {
				global $pmpro_reports;
				$results['reports'] = array(
					'label' => esc_html__( 'Reports', 'paid-memberships-pro' ),
					'items' => array_filter(
						array_map(
							function($report_slug, $report_name) use ($search_string) {
								if ( stripos( $report_name, $search_string ) === 0 ) {
									return array(
										'url'   => esc_url( admin_url( 'admin.php?page=pmpro-reports&report=' . $report_slug ) ),
										'label' => sprintf( '<strong>%s</strong>', esc_html( $report_name ) ),
										'icon'  => 'dashicons-chart-line'
									);
								}
							}, array_keys($pmpro_reports), $pmpro_reports
						)
					)
				);
			}

			// Search Levels.
			if ( ( 'all' === $type || 'levels' === $type ) && ( current_user_can( 'pmpro_membershiplevels' ) || current_user_can( 'manage_options' ) ) ) {
				$levels = $wpdb->get_results( $wpdb->prepare(
					"SELECT id, name FROM {$wpdb->pmpro_membership_levels}
					WHERE id = %d
					OR name LIKE %s
					LIMIT %d",
					is_numeric( $search_string ) ? $search_string : -1,
					$search_string . '%',
					$limit
				) );
				$results['levels'] = array(
					'label' => esc_html__( 'Membership Levels', 'paid-memberships-pro' ),
					'items' => array_map(
						function($level) {
							return array(
								'url'   => esc_url( add_query_arg( array( 'page' => 'pmpro-membershiplevels', 'edit' => $level->id ), admin_url( 'admin.php' ) ) ),
								'label' => sprintf( '<strong>%s</strong>', esc_html( $level->name ) ),
								'icon'  => 'dashicons-groups'
							);
						}, $levels
					)
				);
			}

			// Search Discount Codes.
			if ( ( 'all' === $type || 'discounts' === $type ) && ( current_user_can( 'pmpro_discountcodes' ) || current_user_can( 'manage_options' ) ) ) {
				$discounts = $wpdb->get_results( $wpdb->prepare(
					"SELECT id, code FROM {$wpdb->pmpro_discount_codes}
					WHERE id = %d
					OR code LIKE %s
					LIMIT %d",
					is_numeric( $search_string ) ? $search_string : -1,
					$search_string . '%',
					$limit
				) );
				$results['discounts'] = array(
					'label' => esc_html__( 'Discount Codes', 'paid-memberships-pro' ),
					'items' => array_map(
						function($discount) {
							return array(
								'url'   => esc_url( add_query_arg( array( 'page' => 'pmpro-discountcodes', 'edit' => $discount->id ), admin_url( 'admin.php' ) ) ),
								'label' => sprintf( '<strong>%s</strong>', esc_html( $discount->code ) ),
								'icon'  => 'dashicons-tag'
							);
						}, $discounts
					)
				);
			}

			// Search Settings Pages.
			// Note: We are using the paid-memberships-pro text domain here to match the settings page names that are actually being used.
			if ( 'all' === $type || 'settings' === $type ) {
				$settings_pages = array(
					__( 'Levels', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-membershiplevels',
						'permission' => 'pmpro_membershiplevels',
					),
					__( 'Discount Codes', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-discountcodes',
						'permission' => 'pmpro_discountcodes',
					),
					__( 'Pages', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-pagesettings',
						'permission' => 'pmpro_pagesettings',
					),
					__( 'Payments', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-paymentsettings',
						'permission' => 'pmpro_paymentsettings',
					),
					__( 'Security', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-securitysettings',
						'permission' => 'pmpro_securitysettings',
					),
					__( 'Email', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-emailsettings',
						'permission' => 'pmpro_emailsettings',
					),
					__( 'Email Templates', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-emailtemplates',
						'permission' => 'pmpro_emailsettings',
					),
					__( 'User Fields', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-userfields',
						'permission' => 'pmpro_userfields',
					),
					__( 'Design', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-designsettings',
						'permission' => 'pmpro_designsettings',
					),
					__( 'Advanced', 'paid-memberships-pro' ) => array(
						'slug' => 'pmpro-advancedsettings',
						'permission' => 'pmpro_advancedsettings',
					),
				);
				// Add payment gateways to settings pages.
				$gateways = pmpro_gateways();
				foreach( $gateways as $gateway => $nice_name ) {
					$settings_pages[ $nice_name ] = array(
						'slug' => 'pmpro-paymentsettings&edit_gateway=' . urlencode( $gateway ),
						'permission' => 'pmpro_paymentsettings',
						'label' => esc_html__( 'Payments', 'paid-memberships-pro' ) . ' > ' . esc_html( $nice_name ),
					);
				}
				// Add email templates to settings pages.
				$email_templates = PMPro_Email_Template::get_all_email_templates();
				foreach( $email_templates as $template_slug => $template_class ) {
					$template_name = $template_class::get_template_name();
					$settings_pages[ $template_name ] = array(
						'slug' => 'pmpro-emailtemplates&edit=' . urlencode( $template_slug ),
						'permission' => 'pmpro_emailsettings',
						'label' => esc_html__( 'Email Templates', 'paid-memberships-pro' ) . ' > ' . esc_html( $template_name ),
					);
				}
				$results['settings'] = array(
					'label' => esc_html__( 'Settings', 'paid-memberships-pro' ),
					'items' => array_filter(
						array_map(
							function($name, $info) use ($search_string) {
								// Check if $name starts with the query and user has permission.
								if ( stripos( $name, $search_string ) === 0 && ( current_user_can( $info['permission'] ) || current_user_can( 'manage_options' ) ) ) {
									return array(
										'url'   => esc_url( admin_url( 'admin.php?page=' . $info['slug'] ) ),
										'label' => sprintf( '<strong>%s</strong>', esc_html( isset( $info['label'] ) ? $info['label'] : $name ) ),
										'icon'  => 'dashicons-admin-generic'
									);
								}
							}, array_keys($settings_pages), $settings_pages
						)
					)
				);
			}

			// Search paidmembershipspro.com.
			if ( 'all' === $type || 'documentation' === $type ) {
				$docs = array(
					array(
						'label' => 'Caching',
						'url'   => 'https://www.paidmembershipspro.com/documentation/advanced/caching/',
						'tags'  => array( 'cache', 'caching', 'performance' ),
					),
					array(
						'label' => 'Hosting Recommendations',
						'url'   => 'https://www.paidmembershipspro.com/recommendations-web-hosting-support-pmpro/',
						'tags'  => array( 'host', 'hosting', 'performance' ),
					),
				);
				$results['documentation'] = array(
					'label' => esc_html__( 'Documentation', 'paid-memberships-pro' ),
					'items' => array_merge(
						array_filter(
							array_map(
								function($doc) use ($search_string) {
									foreach ( $doc['tags'] as $tag ) {
										if ( stripos( $tag, $search_string ) === 0 ) {
											return array(
												'url'   => esc_url( $doc['url'] ),
												'label' => sprintf( '<strong>%s</strong>', esc_html( $doc['label'] ) ),
												'icon'  => 'dashicons-book',
												'new_tab' => true,
											);
										}
									}
								}, $docs
							)
						),
					)
				);

				// For documentation, show one fewer results plus the "search website" link.
				$results['documentation']['items'] = array_slice( $results['documentation']['items'], 0, $limit - 1 );
				$results['documentation']['items'][] = array(
					'url'   => esc_url( 'https://www.paidmembershipspro.com/?s=' . urlencode( $search_string ) ),
					'label' => sprintf( '<strong>%s</strong>', esc_html__( 'Search Paid Memberships Pro Website', 'paid-memberships-pro' ) ),
					'icon'  => 'dashicons-search',
					'new_tab' => true,
				);
			}

			// Build HTML results.
			$html_results = '<table class="widefat striped">';
			foreach ( $results as $section_slug => $section ) {
				if ( empty( $section['items'] ) ) {
					continue;
				}

				if ( 'all' === $type ) {
					$html_results .= '<tbody><tr><th scope="rowgroup" colspan="2">' . esc_html( $section['label'] ) . '</th></tr>';
				}

				// Limit to the max number of items.
				$items_to_show = array_slice( $section['items'], 0, $limit );

				foreach ( $items_to_show as $item ) {
					$icon_html = '';
					if ( ! empty( $item['icon'] ) ) {
						$icon_html = '<span class="dashicons ' . esc_attr( $item['icon'] ) . '" aria-hidden="true"></span>';
					}
					$html_results .= '<tr><td>' . $icon_html . '</td><td><a href="' . esc_url( $item['url'] ) . '" ' . ( ! empty( $item['new_tab'] ) ? 'target="_blank"' : '' ) . '>' . wp_kses_post( $item['label'] ) . '</a></td></tr>';
				}

				$html_results .= '</tbody>';
			}

			// If we don't have any results.
			if ( $html_results === '<table class="widefat striped">' ) {
				$html_results .= '<tbody><tr><td colspan="2">' . esc_html__( 'No results found.', 'paid-memberships-pro' ) . '</td></tr></tbody>';
			}

			$html_results .= '</table>';

			// Return results.
			wp_send_json_success( $html_results );
		}

		/**
		 * Default permissions check for endpoints/routes.
		 * Defaults to 'subscriber' for all GET requests and 
		 * 'administrator' for any other type of request.
		 *
		 * @since 2.3
		 * @since 2.12.6 Now allowing arrays in $route_caps so you can have a different permission per HTTP method.
		 */
		 function pmpro_rest_api_get_permissions_check( $request ) {

			$method = $request->get_method();
			$route = $request->get_route();

			// Default to requiring pmpro_edit_members capability.
			// NOTE: This basically means that anyone with the pmpro_edit_members capability could potentially do anything made available through the API in this file.
			$permission = current_user_can( 'pmpro_edit_members' );

			// Check other caps for some routes.
			$route_caps = array(
				'/pmpro/v1/has_membership_access' => 'pmpro_edit_members',
				'/pmpro/v1/get_membership_level_for_user' => 'pmpro_edit_members',
				'/pmpro/v1/get_membership_levels_for_user' => 'pmpro_edit_members',
				'/pmpro/v1/change_membership_level' => 'pmpro_edit_members',
				'/pmpro/v1/cancel_membership_level' => 'pmpro_edit_members',
				'/pmpro/v1/membership_level' => array(
					'GET' => true,
					'POST' => 'pmpro_membershiplevels',
					'PUT' => 'pmpro_membershiplevels',
					'PATCH' => 'pmpro_membershiplevels',
					'DELETE' => 'pmpro_membershiplevels',
				),
				'/pmpro/v1/membership_levels' => true,
				'/pmpro/v1/discount_code' => 'pmpro_discountcodes',
				'/pmpro/v1/order' => 'pmpro_orders',
				'/pmpro/v1/checkout_level' => true,
				'/pmpro/v1/checkout_levels' => true,
				'/pmpro/v1/me' => true,
				'/pmpro/v1/recent_memberships' => 'pmpro_edit_members',
				'/pmpro/v1/recent_orders' => 'pmpro_orders',
				'/pmpro/v1/post_restrictions' => array(
					'capability' => 'edit_post',
					'request_param' => 'post_id',
				),
				'/pmpro/v1/email_log_popup' => 'pmpro_reports',
				'/pmpro/v1/quick_search' => true, // Permissions will be checked per result type.
			);
			$route_caps = apply_filters( 'pmpro_rest_api_route_capabilities', $route_caps, $request );
			
			// Check if we have a specific permission to check for this route/method.
			if ( isset( $route_caps[$route] ) ) {
				// Find the permission to check.
				if ( is_array ( $route_caps[$route] ) && isset( $route_caps[$route][$method] ) ) {
					// Different permission for this method, use it.
					$permission_to_check = $route_caps[$route][$method];
				} elseif ( is_array( $route_caps[$route] ) && ! isset( $route_caps[$route]['capability'] ) ) {
					// No permission for this method, default to false.
					// Skipping if there is a capability set as this is likely an array with a specific post/user ID to check a capability for.
					$permission_to_check = false;
				} else {
					// Same permission for all methods, use it.
					$permission_to_check = $route_caps[$route];
				}

				// Check the permission.
				if ( $permission_to_check === true || $permission_to_check === false ) {
					// For true or false, just pass it along.
					$permission = $permission_to_check;
				} elseif ( is_array( $permission_to_check ) && isset( $permission_to_check['capability'] ) && isset( $permission_to_check['request_param'] ) ) {
					// Check if the current user has this capability.
					// This is used in cases like edit_post where we need to check a specific post ID.
					$permission = current_user_can( $permission_to_check['capability'], $request->get_param( $permission_to_check['request_param'] ) );
				} else {
					// Check if the current user has this capability.
					$permission = current_user_can( $permission_to_check );
				}
			}

			// Is the request method allowed? We disable DELETE by default.
			if ( ! in_array( $method, pmpro_get_rest_api_methods( $route ) ) ) {
				$permission = false;
			}

			$permission = apply_filters( 'pmpro_rest_api_permissions', $permission, $request );
			return $permission;
		}

		/** 
		 *	Helper function to convert comma separated items to an array.
		 * @since 2.3
		 */
		function pmpro_rest_api_convert_to_array( $string ) {
			return explode( ',', $string );
		}
	} // End of class

	/**
	 * Register the routes for Paid Memberships Pro.
	 * @since 2.3
	 */
	function pmpro_rest_api_register_custom_routes() {
		$pmpro_rest_api_routes = new PMPro_REST_API_Routes;
		$pmpro_rest_api_routes->pmpro_rest_api_register_routes();
	}

	add_action( 'rest_api_init', 'pmpro_rest_api_register_custom_routes', 5 );
}


/**
 * Get the allowed methods for PMPro REST API endpoints.
 * To enable DELETE, hook into this filter.
 * @since 2.3
 */
function pmpro_get_rest_api_methods( $route = NULL ) {
	$methods = array( 'GET', 'POST', 'PUT', 'PATCH' );
	$methods = apply_filters( 'pmpro_rest_api_methods', $methods, $route );
	return $methods;
}
