webhooksRepository = $webhooksRepository; $this->merchantDetails = $merchantDetails; $this->merchantRepository = $merchantRepository; $this->refreshToken = $refreshToken; $this->settings = $settings; $this->payPalAuth = $payPalAuth; } /** * give_paypal_commerce_user_onboarded ajax action handler * * @since 2.32.0 Return error response on exception when fetch access token from authorization code. * @since 2.9.0 */ public function onBoardedUserAjaxRequestHandler() { $this->validateAdminRequest(); if (empty($_GET['mode']) || ! in_array($_GET['mode'], ['sandbox', 'live'])) { wp_send_json_error('Must include valid mode'); } $mode = sanitize_text_field(wp_unslash($_GET['mode'])); // Set PayPal client mode. give(PayPalClient::class)->setMode($mode); $partnerLinkInfo = $this->settings->getPartnerLinkDetails(); try { $payPalResponse = $this->payPalAuth->getTokenFromAuthorizationCode( give_clean($_GET['authCode']), give_clean($_GET['sharedId']), $partnerLinkInfo['nonce'] ); } catch (\Exception $exception) { wp_send_json_error(); } $this->settings->updateAccessToken($payPalResponse); // Set cron job to refresh token. $refreshToken = give(RefreshToken::class); $refreshToken->setMode($mode); $refreshToken->registerCronJobToRefreshToken($payPalResponse['expiresIn']); wp_send_json_success(); } /** * This function handle ajax request with give_paypal_commerce_get_partner_url action. * * @since 3.0.0 Add support for accountType. This param is required to get partner link. * @since 2.30.0 Add support for mode param. * @since 2.9.0 */ public function onGetPartnerUrlAjaxRequestHandler() { $this->validateAdminRequest(); if (empty($accountType = $_GET['accountType']) || ! in_array($accountType, ScriptLoader::$accountTypes, true)) { wp_send_json_error('Must include valid account type'); } if (empty($country = $_GET['countryCode']) || ! isset(give_get_country_list()[$country])) { wp_send_json_error('Must include valid 2-character country code'); } if (empty($_GET['mode']) || ! in_array($_GET['mode'], ['sandbox', 'live'])) { wp_send_json_error('Must include valid mode'); } $country = sanitize_text_field(wp_unslash($_GET['countryCode'])); $accountType = sanitize_text_field(wp_unslash($_GET['accountType'])); $mode = sanitize_text_field(wp_unslash($_GET['mode'])); // Generate a unique state token for CSRF protection on PayPal callback. $stateToken = wp_generate_password(32, false); set_transient('give_paypal_onboarding_state_' . $mode, $stateToken, HOUR_IN_SECONDS); $redirectUrl = add_query_arg( [ 'tab' => 'gateways', 'section' => 'paypal', 'group' => 'paypal-commerce', 'mode' => $mode, 'give_paypal_state' => $stateToken, ], admin_url('edit.php?post_type=give_forms&page=give-settings') ); // Set PayPal client mode. give(PayPalClient::class)->setMode($mode); $data = $this->payPalAuth->getSellerPartnerLink($redirectUrl, $accountType); if (! $data) { wp_send_json_error(); } $this->settings->updateAccountCountry($country); $this->settings->updatePartnerLinkDetails($data); wp_send_json_success($data); } /** * give_paypal_commerce_disconnect_account ajax request handler. * * @since 3.16.0 added security nonce check * @since 3.13.0 Add new $keepWebhooks option * @since 2.30.0 Add support for mode param. * @since 2.25.0 Remove merchant seller token. * @since 2.9.0 */ public function removePayPalAccount() { check_ajax_referer( 'give_paypal_commerce_disconnect_account'); if (! current_user_can('manage_give_settings')) { wp_send_json_error(['error' => esc_html__('You are not allowed to perform this action.', 'give')]); } try { $mode = give_clean($_POST['mode']); $keepWebhooks = rest_sanitize_boolean($_POST['keep-webhooks']); $this->webhooksRepository->setMode($mode); $this->merchantRepository->setMode($mode); $this->refreshToken->setMode($mode); $this->settings->setMode($mode); $this->validateAdminRequest(); // Remove the webhook from PayPal if there is one if ( ! $keepWebhooks && $webhookConfig = $this->webhooksRepository->getWebhookConfig()) { $this->webhooksRepository->deleteWebhook($this->merchantDetails->accessToken, $webhookConfig->id); $this->webhooksRepository->deleteWebhookConfig(); } $this->merchantRepository->delete(); $this->merchantRepository->deleteAccountErrors(); $this->merchantRepository->deleteClientToken(); $this->settings->deleteSellerAccessToken(); $this->refreshToken->deleteRefreshTokenCronJob(); wp_send_json_success(); } catch (\Exception $exception) { wp_send_json_error(['error' => $exception->getMessage()]); } } /** * Create order. * * @todo: handle payment create error on frontend. * * @since 3.1.0 Remove unused variable from createOrder argument. * @since 2.9.0 */ public function createOrder() { $this->validateFrontendRequest(); $data = $this->getOrderData(); try { $result = give(PayPalOrder::class)->createOrder($data); wp_send_json_success( [ 'id' => $result, ] ); } catch (\Exception $ex) { wp_send_json_error( [ 'error' => json_decode($ex->getMessage(), true), ] ); } } /** * @since 4.16.7.1 Validate the request through the form layer before building order data. v3 forms must * also send a total at least as large as the amount the form validated; v2 forms are * checked on the final, post-filter amount. * @since 4.14.4 Validate donation amount before creating or updating an order. * @since 4.2.1 Only filter amount for v2 forms. * @since 3.4.2 */ private function getOrderData(): array { $postData = give_clean($_POST); $formId = absint($postData['give-form-id']); $donorAddress = $this->getDonorAddressFromPostedDataForPaypalOrder($postData); $isV3Form = FormUtils::isV3Form($formId); if (!$isV3Form) { $this->skipLegacyCardFieldRequirements(); } $this->validateDonationFormRequest($formId, $postData); if ($isV3Form) { /* * v3 forms send the form's own amount field as "amount" and the total, with fee recovery * already included, as "give-amount". The total is what the donor approves in the PayPal * popup; PayPalCommerce::createPayment() reconciles the order to the validated donation * before capturing, so all this has to guarantee is that the total never drops below the * amount the form just validated. */ $validatedAmount = isset($postData['amount']) ? (float)$postData['amount'] : 0.0; $amount = isset($postData['give-amount']) ? give_clean($postData['give-amount']) : '0.00'; if ($validatedAmount <= 0 || (float)$amount < $validatedAmount) { wp_send_json_error(['error' => __('Invalid donation amount.', 'give')]); } } else { $amount = isset($postData['give-amount']) ? (float)apply_filters( 'give_donation_total', give_maybe_sanitize_amount( $postData['give-amount'], ['currency' => give_get_currency($formId)] ) ) : '0.00'; $this->validateDonationAmount($amount, $formId); } return [ 'formId' => $formId, 'formTitle' => give_payment_gateway_item_title(['post_data' => $postData], 127), 'donationAmount' => $amount, 'payer' => [ 'firstName' => $postData['give_first'], 'lastName' => $postData['give_last'], 'email' => $postData['give_email'], 'address' => $donorAddress, ], ]; } /** * Refuses every request. Both form versions now send their order id with the donation and let * PayPalCommerce::createPayment() capture it, so nothing legitimate captures from the browser. * The endpoint stays registered so anything still calling it receives an error it can report, * rather than an empty response from a missing action. * * @since 4.16.9 Refuse every request; the capture for both form versions happens in PayPalCommerce::createPayment(). * @since 4.16.7.1 Refuse v3 forms; their capture happens in PayPalCommerce::createPayment(). Validate * the posted form before every capture, not only when the amount changed. * @since 4.14.4 Validate donation amount before approving an order. * @since 3.2.0 Discover error by checking capture status. * @since 2.9.0 */ public function approveOrder() { wp_send_json_error( ['error' => __('PayPal orders are captured when the donation is submitted.', 'give')] ); } /** * Refuses every request. The order amount is reconciled against the donation in * PayPalCommerce::createPayment() before the capture, so no form version needs the browser to * change an order's amount. The endpoint stays registered for the same reason approveOrder() * does: a caller gets an error it can report rather than an empty response. * * @since 4.16.9 Refuse every request; the order amount is reconciled in PayPalCommerce::createPayment(). * @since 4.16.7.1 Refuse v3 forms; PayPalCommerce::createPayment() reconciles their order amount. * @since 4.14.4 Validate donation amount before updating an order amount. * @since 3.4.2 */ public function updateOrderAmount() { wp_send_json_error( ['error' => __('PayPal order amounts are reconciled when the donation is submitted.', 'give')] ); } /** * Return on boarding trouble notice. * * @since 2.9.6 */ public function onBoardingTroubleNotice() { if (! current_user_can('manage_give_settings')) { wp_die(); } /* @var AdminSettingFields $adminSettingFields */ $adminSettingFields = give(AdminSettingFields::class); $actionList = sprintf( '
%2$s