├── .gitignore ├── assets ├── images │ └── woo.png └── js │ ├── paddle-bootstrap.js │ └── paddle-helpers.js ├── models ├── api.php ├── checkout.php ├── gateway.php └── settings.php └── paddle-woo-checkout.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /assets/images/woo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasinhayder/paddle-woocommerce-3/d4e21673b98e61215e2ee3b0bb5a298e038423e5/assets/images/woo.png -------------------------------------------------------------------------------- /assets/js/paddle-bootstrap.js: -------------------------------------------------------------------------------- 1 | jQuery(function($) { 2 | 3 | Paddle.Setup({ 4 | vendor: parseInt(paddle_data.vendor), 5 | // debug:true //uncomment for debug mode 6 | }); 7 | 8 | // Can submit from either checkout or order review forms 9 | var form = jQuery('form.checkout, form#order_review'); 10 | 11 | // Intercept form button (Bind to click instead of WC trigger to avoid popup) 12 | jQuery('form.checkout').on('click', ':submit', function(event) { 13 | return !invokeOverlayCheckout(); 14 | }); 15 | // Intercept submit for order review 16 | jQuery('form#order_review').on('submit', function(event) { 17 | return !invokeOverlayCheckout(); 18 | }); 19 | 20 | // Some customers (Inky) have themes where the button is outside the form 21 | jQuery('#checkout_buttons button').on('click', function(event) { 22 | jQuery('form#order_review').submit(); 23 | return false; // Don't fire the submit event twice if the buttons ARE in the form 24 | }); 25 | 26 | // Starts the overlay checkout process (returns false if we can't) 27 | function invokeOverlayCheckout() { 28 | // Check payment method etc 29 | if(isPaddlePaymentMethodSelected()) { 30 | // Need to show spinner before standard checkout as we have to spend time getting the pay URL 31 | Paddle.Spinner.show(); 32 | 33 | getSignedCheckoutUrlViaAjax(); 34 | 35 | // Make sure we don't submit the form normally 36 | return true; 37 | } 38 | 39 | // We didn't fire 40 | return false; 41 | } 42 | 43 | // Gets if the payment method is set to Paddle (in case multiple methods are available) 44 | function isPaddlePaymentMethodSelected () { 45 | if ($('#payment_method_paddle').is(':checked')) { 46 | return true; 47 | } 48 | } 49 | 50 | // Requests the signed checkout link via the Paddle WC plugin 51 | function getSignedCheckoutUrlViaAjax() { 52 | jQuery.ajax({ 53 | dataType: "json", 54 | method: "POST", 55 | url: paddle_data.order_url, 56 | data: form.serializeArray(), 57 | success: function (response) { 58 | // WC will send the error contents in a normal request 59 | if(response.result == "success") { 60 | startPaddleCheckoutOverlay(response.checkout_url, response.email, response.country, response.postcode); 61 | } else { 62 | handleErrorResponse(response); 63 | } 64 | }, 65 | error: function (jqxhr, status) { 66 | // We got a 500 or something if we hit here. Shouldn't normally happen 67 | alert("We were unable to process your order, please try again in a few minutes."); 68 | } 69 | }); 70 | }; 71 | 72 | // Starts the Paddle.js overlay once we have the checkout url. 73 | function startPaddleCheckoutOverlay(checkoutUrl, emailAddress, country, postCode) { 74 | Paddle.Checkout.open({ 75 | email: emailAddress, 76 | country: country, 77 | postcode: postCode, 78 | override: checkoutUrl 79 | }); 80 | }; 81 | 82 | // Shows any errors we encountered 83 | function handleErrorResponse(response) { 84 | // Note: This error handling code is copied from the woocommerce checkout.js file 85 | if (response.reload === 'true') { 86 | window.location.reload(); 87 | return; 88 | } 89 | 90 | // Remove old errors 91 | jQuery( '.woocommerce-error, .woocommerce-message' ).remove(); 92 | // Add new errors 93 | if (response.messages) { 94 | form.prepend(response.messages); 95 | } 96 | 97 | // Cancel processing 98 | form.removeClass('processing').unblock(); 99 | 100 | // Lose focus for all fields 101 | form.find('.input-text, select').blur(); 102 | 103 | // Scroll to top 104 | jQuery('html, body').animate({ 105 | scrollTop: (form.offset().top - 100) 106 | }, 1000 ); 107 | 108 | if (response.nonce) { 109 | form.find('#_wpnonce').val(response.nonce); 110 | } 111 | 112 | // Trigger update in case we need a fresh nonce 113 | if (response.refresh === 'true') { 114 | jQuery('body').trigger('update_checkout'); 115 | } 116 | 117 | // Clear the Paddle spinner manually as we didn't start checkout 118 | Paddle.Spinner.hide(); 119 | }; 120 | 121 | }); -------------------------------------------------------------------------------- /assets/js/paddle-helpers.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function(){ 2 | jQuery('#toggleVendorAccountEntry').click(function(){ 3 | var row = jQuery(this).closest('tr'); 4 | row.next().show(); 5 | row.next().next().show(); 6 | row.hide(); 7 | }); 8 | jQuery('#woocommerce_paddle_paddle_vendor_id').closest('tr').hide(); 9 | jQuery('#woocommerce_paddle_paddle_api_key').closest('tr').hide(); 10 | jQuery('.open_paddle_popup').click(function(event) { 11 | // don't reload admin page when popup is created 12 | event.preventDefault(); 13 | 14 | // open paddle integration popup 15 | window.open(integrationData.url, 'mywindow', 'location=no,status=0,scrollbars=0,width=800,height=600'); 16 | 17 | // handle message sent from popup 18 | window.addEventListener('message', function(e) { 19 | var arrData = e.data.split(" "); 20 | jQuery('#woocommerce_paddle_paddle_vendor_id').val(arrData[0]); 21 | jQuery('#woocommerce_paddle_paddle_api_key').val(arrData[1]); 22 | jQuery('#toggleVendorAccountEntry').click(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /models/api.php: -------------------------------------------------------------------------------- 1 | get_total(); 19 | if($settings->get('vat_included_in_price') != 'yes') { 20 | $order_total -= $order->get_total_tax(); 21 | } 22 | 23 | // Data to be sent to Paddle gateway 24 | $data = array(); 25 | $data['vendor_id'] = $settings->get('paddle_vendor_id'); 26 | $data['vendor_auth_code'] = $settings->get('paddle_api_key'); 27 | $data['prices'] = array(get_woocommerce_currency().':'.$order_total); // Why was tax being removed? 28 | $data['return_url'] = static::get_return_url($order); 29 | $data['title'] = str_replace('{#order}', $order->get_id(), $settings->get('product_name')); 30 | $data['image_url'] = $settings->get('product_icon'); 31 | $data['webhook_url'] = static::get_webhook_url($order->get_id()); 32 | $data['discountable'] = 0; 33 | $data['quantity_variable'] = 0; 34 | $data['customer_email'] = $order->get_billing_email(); 35 | $data['customer_postcode'] = $customer->get_billing_postcode(); 36 | $data['customer_country'] = $customer->get_billing_country(); 37 | 38 | // Add the product name(s) as custom message 39 | if($settings->get('send_names') == 'yes') { 40 | $items = $order->get_items(); 41 | $names = array(); 42 | $passthrough = array(); 43 | foreach($items as $item) { 44 | $names[] = $item['name']; 45 | $passthrough[] = array("products"=>array("id"=>$item['product_id'],"name"=>$item['name'])); //so that you can trace the order history later directly from paddle dashboard 46 | } 47 | $data['custom_message'] = implode(', ', array_unique($names)); 48 | $data['title'] = implode(', ', array_unique($names)); 49 | $data['passthrough'] = base64_encode(json_encode($passthrough)); 50 | } 51 | 52 | // Get pay link from Paddle API 53 | $post_url = Paddle_WC_Settings::PADDLE_ROOT_URL . Paddle_WC_Settings::API_GENERATE_PAY_LINK_URL; 54 | $api_start_time = microtime(true); 55 | $api_call_response = wp_remote_post($post_url, array( 56 | 'method' => 'POST', 57 | 'timeout' => 45, 58 | 'httpversion' => '1.1', 59 | 'blocking' => true, 60 | 'body' => $data, 61 | 'sslverify' => false 62 | ) 63 | ); 64 | $api_duration = (microtime(true) - $api_start_time); 65 | 66 | // Pass back to AJAX response unless error 67 | if (is_wp_error($api_call_response)) { 68 | // We failed to get a response 69 | wc_add_notice( 'Something went wrong getting checkout url. Unable to get API response.', 'error'); 70 | error_log('Paddle error. Unable to get API response. Method: ' . __METHOD__ . ' Error message: ' . $api_call_response->get_error_message()); 71 | return json_encode(array( 72 | 'result' => 'failure', 73 | 'errors' => array('Something went wrong. Unable to get API response.') 74 | )); 75 | } else { 76 | $api_response = json_decode($api_call_response['body']); 77 | if ($api_response && $api_response->success === true) { 78 | // We got a valid response 79 | return json_encode(array( 80 | 'result' => 'success', 81 | 'order_id' => $order->get_id(), 82 | 'checkout_url' => $api_response->response->url, 83 | 'email' => $order->get_billing_email(), 84 | 'country' => $customer->get_billing_country(), 85 | 'postcode' => $customer->get_billing_postcode(), 86 | 'duration_s' => $api_duration 87 | )); 88 | } else { 89 | // We got a response, but it was an error response 90 | wc_add_notice('Something went wrong getting checkout url. Check if gateway is integrated.', 'error'); 91 | if (is_object($api_response)) { 92 | error_log('Paddle error. Error response from API. Method: ' . __METHOD__ . ' Errors: ' . print_r($api_response->error, true)); 93 | } else { 94 | error_log('Paddle error. Error response from API. Method: ' . __METHOD__ . ' Response: ' . print_r($api_call_response, true)); 95 | } 96 | return json_encode(array( 97 | 'result' => 'failure', 98 | 'errors' => array('Something went wrong. Check if Paddle account is properly integrated.') 99 | )); 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Gets the URL we want Paddle to call on payment completion. 106 | * 107 | * @param int $order_id The WC id of the order that will be paid. 108 | */ 109 | private static function get_webhook_url($order_id) { 110 | // Adding index.php makes it work for customers without permalinks, and doesn't seem to affect ones with 111 | return get_bloginfo('url') . '/wc-api/paddle_complete?order_id=' . $order_id; 112 | } 113 | 114 | /** 115 | * Gets the the checkout should return to once it is complete. 116 | * 117 | * @param int $order_id The WC id of the order that will be paid. 118 | */ 119 | private static function get_return_url($order) { 120 | $return_url = $order->get_checkout_order_received_url(); 121 | if (is_ssl() || get_option('woocommerce_force_ssl_checkout') == 'yes' ) { 122 | $return_url = str_replace('http:', 'https:', $return_url); 123 | } 124 | return apply_filters('woocommerce_get_return_url', $return_url); 125 | } 126 | 127 | /** 128 | * Checks the signature from a given webhook so we know it's genuine 129 | * 130 | * @return int Returns 1 if the signature is correct, 0 if it is incorrect, and -1 on error. 131 | */ 132 | public static function check_webhook_signature() { 133 | // Log error if vendor_public_key is not set 134 | $vendor_public_key = Paddle_WC_Settings::instance()->getPaddleVendorKey(); 135 | if (empty($vendor_public_key)) { 136 | error_log('Paddle error. Unable to verify webhook callback - vendor_public_key is not set.'); 137 | return -1; 138 | } 139 | 140 | // Copy get input to separate variable to not modify superglobal array 141 | $webhook_data = $_POST; 142 | foreach ($webhook_data as $k => $v) { 143 | $webhook_data[$k] = stripslashes($v); 144 | } 145 | 146 | // Pop signature from webhook data 147 | $signature = base64_decode($webhook_data['p_signature']); 148 | unset($webhook_data['p_signature']); 149 | 150 | // Check signature and return result 151 | ksort($webhook_data); 152 | $data = serialize($webhook_data); 153 | return openssl_verify($data, $signature, $vendor_public_key, OPENSSL_ALGO_SHA1); 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /models/checkout.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 20 | } 21 | 22 | /** 23 | * Registers the callbacks (WC hooks) that we need to inject Paddle checkout functionality. 24 | */ 25 | public function register_callbacks() { 26 | $this->register_checkout_actions(); 27 | } 28 | 29 | /** 30 | * Registers the callbacks needed to handle the WC checkout. 31 | */ 32 | protected function register_checkout_actions() { 33 | // Inject scripts and CSS we need for checkout 34 | add_action('wp_enqueue_scripts', array($this, 'on_wp_enqueue_scripts')); 35 | 36 | // Add the place order button target url handler 37 | add_action('wc_ajax_paddle_checkout', array($this, 'on_ajax_process_checkout')); 38 | add_action('wc_ajax_nopriv_paddle_checkout', array($this, 'on_ajax_process_checkout')); 39 | // And handle old-version style 40 | add_action('wp_ajax_paddle_checkout', array($this, 'on_ajax_process_checkout')); 41 | add_action('wp_ajax_nopriv_paddle_checkout', array($this, 'on_ajax_process_checkout')); 42 | 43 | // Do the same, but for the order-pay page instead of the checkout page - ie. order already exists 44 | add_action('wc_ajax_paddle_checkout_pay', array($this, 'on_ajax_process_checkout_pay')); 45 | add_action('wc_ajax_nopriv_paddle_checkout_pay', array($this, 'on_ajax_process_checkout_pay')); 46 | add_action('wp_ajax_paddle_checkout_pay', array($this, 'on_ajax_process_checkout_pay')); 47 | add_action('wp_ajax_nopriv_paddle_checkout_pay', array($this, 'on_ajax_process_checkout_pay')); 48 | } 49 | 50 | /** 51 | * Callback when WP is building the list of scripts for the page. 52 | */ 53 | public function on_wp_enqueue_scripts() { 54 | // Inject standard Paddle checkout JS 55 | wp_enqueue_script('paddle-checkout', 'https://cdn.paddle.com/paddle/paddle.js'); 56 | 57 | // Inject our bootstrap JS to intercept the WC button press and invoke standard JS 58 | wp_register_script('paddle-bootstrap', plugins_url('../assets/js/paddle-bootstrap.js', __FILE__), array('jquery'),"3.0.1"); 59 | 60 | // Use wp_localize_script to write JS config that can't be embedded in the script 61 | $endpoint = is_wc_endpoint_url('order-pay') ? 'paddle_checkout_pay' : 'paddle_checkout'; 62 | $paddle_data = array( 63 | 'order_url' => $this->get_ajax_endpoint_path($endpoint), 64 | 'vendor' => $this->settings->get('paddle_vendor_id') 65 | ); 66 | wp_localize_script('paddle-bootstrap', 'paddle_data', $paddle_data); 67 | wp_enqueue_script('paddle-bootstrap'); 68 | } 69 | 70 | /** 71 | * Receives our AJAX callback to process the checkout 72 | */ 73 | public function on_ajax_process_checkout() { 74 | // Invoke our Paddle gateway to call out for the Paddle checkout url and return via JSON 75 | WC()->checkout()->process_checkout(); 76 | } 77 | 78 | /** 79 | * Skip the order creation, and go straight to payment processing 80 | */ 81 | public function on_ajax_process_checkout_pay() { 82 | if (!WC()->session->order_awaiting_payment) { 83 | wc_add_notice('We were unable to process your order, please try again.', 'error'); 84 | ob_start(); 85 | wc_print_notices(); 86 | $messages = ob_get_contents(); 87 | ob_end_clean(); 88 | echo json_encode(array( 89 | 'result' => 'failure', 90 | 'messages' => $messages, 91 | 'errors' => array('We were unable to process your order, please try again.') 92 | )); 93 | exit; 94 | } 95 | 96 | // Need the id of the pre-created order 97 | $order_id = WC()->session->order_awaiting_payment; 98 | // Get the paddle payment gateway - the payment_method should be posted as "paddle" 99 | $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); 100 | $available_gateways['paddle']->process_payment($order_id); 101 | // The process_payment function will exit, so we don't need to return anything here 102 | } 103 | 104 | /** 105 | * Gets the path to be called to invoke the given AJAX endpoint. 106 | * 107 | * @param String $endpoint The endpoint the AJAX request will be calling. 108 | */ 109 | private function get_ajax_endpoint_path($endpoint) { 110 | if(version_compare(WOOCOMMERCE_VERSION, '2.4.0', '>=')) { 111 | // WC AJAX callback (Added in 2.4.0) 112 | $url = parse_url($_SERVER['REQUEST_URI']); 113 | parse_str(isset($url['query']) ? $url['query'] : '', $query); 114 | $query['wc-ajax'] = $endpoint; 115 | $order_url = $url['path'].'?'.http_build_query($query); 116 | } else { 117 | // Older callback (not sure we should care about supporting this old) 118 | $order_url = admin_url('admin-ajax.php?action='.$endpoint); 119 | } 120 | return $order_url; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /models/gateway.php: -------------------------------------------------------------------------------- 1 | paddle_settings = isset($settings) ? $settings : new Paddle_WC_Settings(); 19 | 20 | $this->id = 'paddle'; 21 | $this->method_title = 'Paddle.com Payment Gateway'; 22 | $this->method_description = 'Allow customers to securely checkout with credit cards or PayPal'; 23 | $this->title = 'Paddle Payments'; 24 | $this->description = $this->paddle_settings->get('description'); 25 | $this->icon = apply_filters('wc_paddle_icon', ''); 26 | $this->supports = array('products'); // We only support purchases 27 | $this->has_fields = true; 28 | 29 | // Setup fields used for admin side 30 | $this->init_form_fields(); 31 | // Load settings (we haven't overriden, but must be called in ctor) 32 | $this->init_settings(); 33 | 34 | $this->enabled = $this->paddle_settings->get('enabled'); 35 | 36 | if (is_admin() && $this->enabled == 'yes') { 37 | if(!$this->paddle_settings->currency_supported) { 38 | // Inform users if they are not able to use this plugin due to currency 39 | WC_Admin_Settings::add_error( 40 | 'Paddle does not support your store currency. ' . 41 | "Your store currency is " . get_woocommerce_currency() .", and we only support " . implode(', ', $this->paddle_settings->supported_currencies) 42 | ); 43 | } 44 | // Check we are setup in admin, and display message if not connected 45 | $this->admin_check_connected(); 46 | } 47 | } 48 | 49 | /** 50 | * Registers the callbacks (WC hooks) that we need for the Gateway to function. 51 | */ 52 | public function register_callbacks() { 53 | $this->register_webhook_actions(); 54 | $this->register_admin_actions(); 55 | } 56 | 57 | /** 58 | * Registers the our webhook callbacks to listen to Paddle after payment hooks. 59 | */ 60 | protected function register_webhook_actions() { 61 | // Add the handler for the webhook - register gateway response listener 62 | add_action('woocommerce_api_paddle_complete', array($this, 'on_paddle_payment_webhook_response')); 63 | } 64 | 65 | /** 66 | * Registers the callbacks used by the admin interface. 67 | */ 68 | protected function register_admin_actions() { 69 | if(is_admin()) { // Can skip if not an admin user 70 | // Callback to hit on save 71 | if (version_compare(WOOCOMMERCE_VERSION, '2.0.0', '>=')) { 72 | add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options')); 73 | } else { 74 | add_action('woocommerce_update_options_payment_gateways', array($this, 'process_admin_options')); 75 | } 76 | 77 | // Callback to inject extra JS to admin page 78 | add_action('admin_enqueue_scripts', array($this, 'on_admin_enqueue_scripts')); 79 | } 80 | } 81 | 82 | /** 83 | * Adds the custom scripts used by the admin page. 84 | */ 85 | public function on_admin_enqueue_scripts() { 86 | wp_register_script('paddle-helpers', plugins_url('../assets/js/paddle-helpers.js', __FILE__), array('jquery')); 87 | $integration_url = Paddle_WC_Settings::PADDLE_ROOT_URL . Paddle_WC_Settings::INTEGRATE_URL . '?' . http_build_query(array( 88 | 'app_name' => 'WooCommerce Paddle Payment Gateway', 89 | 'app_description' => 'WooCommerce Paddle Payment Gateway Plugin. Site name: ' . get_bloginfo('name'), 90 | 'app_icon' => plugins_url('../assets/images/woo.png', __FILE__) 91 | )); 92 | wp_localize_script('paddle-helpers', 'integrationData', array('url' => $integration_url)); 93 | if ('woocommerce_page_wc-settings' == get_current_screen()->id) { 94 | wp_enqueue_script('paddle-helpers'); 95 | } 96 | } 97 | 98 | /** 99 | * Check if this gateway can be used. 100 | */ 101 | public function is_available() { 102 | // Parent class checks enabled flag anyway 103 | $is_available = parent::is_available(); 104 | 105 | // Check all required fields set 106 | $is_available = $is_available && 107 | $this->paddle_settings->currency_supported && // Check if WooCoommerce currency is supported by gateway 108 | $this->paddle_settings->is_connected; // Check if gateway is integrated with paddle vendor account 109 | 110 | return $is_available; 111 | } 112 | 113 | /** 114 | * Checks if we are connected, and displays an error message if not. 115 | */ 116 | public function admin_check_connected() { 117 | static $added = false; 118 | if(!$this->paddle_settings->is_connected) { 119 | if($added) return; 120 | WC_Admin_Settings::add_error("You must connect to paddle before the paddle checkout plugin can be used"); 121 | $added = true; 122 | } 123 | } 124 | 125 | /** 126 | * After processing the admin option saving, check if we are connected and display error message if not 127 | */ 128 | public function process_admin_options() { 129 | // Whenever we save, also reset the public key, to force it to be reloaded in case the vendor has changed 130 | update_option('paddle_vendor_public_key', ''); 131 | $result = parent::process_admin_options(); 132 | 133 | $this->admin_check_connected(); 134 | 135 | return $result; 136 | } 137 | 138 | /** 139 | * This function is called by WC when user places order with Paddle chosen as the payment method. 140 | * We actually want to get the payment URL and pass it back client side to the overlay checkout. 141 | * @param int $order_id 142 | * @return mixed 143 | */ 144 | public function process_payment($order_id) { 145 | global $woocommerce; 146 | $order = new WC_Order($order_id); 147 | $pay_url_json = Paddle_WC_API::get_pay_url_for_order($order, $woocommerce->customer, $this->paddle_settings); 148 | 149 | if(wc_notice_count('error') > 0) { 150 | // Errors prevented completion 151 | $result = json_encode(array( 152 | 'result' => 'failure', 153 | 'errors' => WC()->session->get('wc_notices', array()) 154 | )); 155 | } else { 156 | $result = $pay_url_json; 157 | } 158 | 159 | echo $result; 160 | exit; 161 | } 162 | 163 | /** 164 | * Called when we get a webhook response from Paddle to indicate the payment completed. 165 | * 166 | * Returns HTTP 200 if OK, 500 otherwise 167 | */ 168 | public function on_paddle_payment_webhook_response() { 169 | if (Paddle_WC_API::check_webhook_signature()) { 170 | $order_id = $_GET['order_id']; 171 | if (is_numeric($order_id) && (int) $order_id == $order_id) { 172 | $order = new WC_Order($order_id); 173 | if (is_object($order) && $order instanceof WC_Order) { 174 | $order->payment_complete(); 175 | status_header(200); 176 | exit; 177 | } else { 178 | error_log('Paddle error. Unable to complete payment - order ' . $order_id . ' does not exist'); 179 | } 180 | } else { 181 | error_log('Paddle error. Unable to complete payment - order_id is not integer. Got \'' . $order_id . '\'.'); 182 | } 183 | } else { 184 | error_log('Paddle error. Unable to verify webhook callback - bad signature.'); 185 | } 186 | status_header(500); 187 | exit; 188 | } 189 | 190 | /** 191 | * Displays error messages in the admin system (called externally by WC) 192 | */ 193 | public function display_errors() { 194 | foreach ($this->errors as $k => $error) { 195 | WC_Admin_Settings::add_error("Unable to save due to error: " . $error); 196 | unset($this->errors[$k]); 197 | } 198 | } 199 | 200 | /** 201 | * Check that the given url leads to an actual file 202 | */ 203 | protected function url_valid($url) { 204 | $curl = curl_init($url); 205 | curl_setopt($curl, CURLOPT_NOBODY, true); 206 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 207 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); 208 | curl_exec($curl); 209 | $info = curl_getinfo($curl); 210 | curl_close($curl); 211 | return $info['http_code'] == 200; 212 | } 213 | 214 | /** 215 | * Custom validation function to check that a product icon is usable. Called externally by woocommerce 216 | * 217 | * If the value is invalid in some way, it fixes minor issues (e.g. converting http to https) 218 | * 219 | * @param string the name of the field to be validated 220 | * @return string the validated/corrected url 221 | */ 222 | public function validate_product_icon_field($key) { 223 | if (!isset($_POST[$this->plugin_id . $this->id . '_' . $key]) || empty($_POST[$this->plugin_id . $this->id . '_' . $key])) { 224 | return ''; 225 | } 226 | $image_url = $_POST[$this->plugin_id . $this->id . '_' . $key]; 227 | 228 | //If the new url is the same as the old one, AND we know it is valid already (because we are enabled), then skip validation 229 | if($this->get_option($key) == $image_url && $this->enabled == 'yes') return $image_url; 230 | 231 | if (!$this->url_valid($image_url)) { 232 | $this->errors[] = 'Product Icon url is not valid'; 233 | } else if (substr($image_url, 0, 5) != 'https') { 234 | //confirmed that base url is valid; now need to make it secure 235 | $new_url = 'https' . substr($image_url, 4); 236 | if (!$this->url_valid($new_url)) { 237 | //image server does not allow secure connection, so bounce it off our proxy 238 | $vendor_id = $this->getPaddleVendorId(); 239 | $key = $this->getPaddleVendorKey(); 240 | openssl_public_encrypt($image_url, $urlcode, $key); 241 | $new_url = self::IMAGE_BOUNCE_PROXY_URL . $vendor_id . '/' . str_replace(array('+', '/'), array('-', '_'), base64_encode($urlcode)); 242 | WC_Admin_Settings::add_message("Product Icon URL has been converted to use a secure proxy"); 243 | } 244 | $image_url = $new_url; 245 | } 246 | 247 | return $image_url; 248 | } 249 | 250 | /** 251 | * Setup admin fields to be shown in plugin settings. 252 | */ 253 | public function init_form_fields() { 254 | // Note: Not sure I really like this mash of HTML inside the class, but this seems to be pretty 255 | // standard among WC plugins so I'm going with it for consistency. 256 | 257 | if ($this->paddle_settings->is_connected) { 258 | $connection_button = '
Your paddle account has already been connected
' . 259 | 'Reconnect your Paddle Account'; 260 | } else { 261 | $connection_button = 'Connect your Paddle Account'; 262 | } 263 | 264 | $this->form_fields = array( 265 | 'enabled' => array( 266 | 'title' => __('Enable/Disable', 'woocommerce'), 267 | 'type' => 'checkbox', 268 | 'label' => __('Enable', 'woocommerce'), 269 | 'default' => $this->enabled ? 'yes' : 'no' 270 | ), 271 | 'title' => array( 272 | 'title' => __('Title', 'woocommerce'), 273 | 'type' => 'text', 274 | 'description' => __('This controls the title which the user sees during checkout.', 'woocommerce'), 275 | 'default' => __('Paddle', 'woocommerce') 276 | ), 277 | 'description' => array( 278 | 'title' => __('Customer Message', 'woocommerce'), 279 | 'type' => 'textarea', 280 | 'description' => __('This controls the description which the user sees during checkout.', 'woocommerce'), 281 | 'default' => __('Pay using Visa, Mastercard, Amex or PayPal via Paddle', 'woocommerce') 282 | ), 283 | 'paddle_showlink' => array( 284 | 'title' => 'Vendor Account', 285 | 'content' => $connection_button . 'Click here to enter your account details manually
', 286 | 'type' => 'raw', 287 | 'default' => '' 288 | ), 289 | 'paddle_vendor_id' => array( 290 | 'title' => __('Paddle Vendor ID', 'woocommerce'), 291 | 'type' => 'text', 292 | 'description' => __('Click here to integrate Paddle account.', 'woocommerce'), 293 | 'default' => '', 294 | 'row_attributes' => array('style' => 'display:none') 295 | ), 296 | 'paddle_api_key' => array( 297 | 'title' => __('Paddle API Key', 'woocommerce'), 298 | 'type' => 'textarea', 299 | 'description' => __('Click here to integrate Paddle account.', 'woocommerce'), 300 | 'default' => '', 301 | 'row_attributes' => array('style' => 'display:none') 302 | ), 303 | 'product_name' => array( 304 | 'title' => __('Product Name'), 305 | 'description' => __('The name of the product to use in the paddle checkout'), 306 | 'type' => 'text', 307 | 'default' => get_bloginfo('name') . ' Checkout' 308 | ), 309 | 'product_icon' => array( 310 | 'title' => __('Product Icon'), 311 | 'description' => __('The url of the icon to show next to the product name during checkout'), 312 | 'type' => 'text', 313 | 'default' => 'https://s3.amazonaws.com/paddle/default/default_product_icon.png' 314 | ), 315 | 'send_names' => array( 316 | 'title' => __('Send Product Names'), 317 | 'description' => __('Should the names of the product(s) in the cart be shown on the checkout?'), 318 | 'type' => 'checkbox', 319 | 'label' => __('Send Names', 'woocommerce'), 320 | 'default' => $this->enabled ? 'yes' : 'no' 321 | ), 322 | 'vat_included_in_price' => array( 323 | 'title' => __('VAT Included In Price?'), 324 | 'description' => __('This must match your Paddle account settings under VAT/Taxes'), 325 | 'type' => 'checkbox', 326 | 'label' => __('Prices Include VAT', 'woocommerce'), 327 | 'default' => 'yes' 328 | ) 329 | ); 330 | } 331 | 332 | /** 333 | * Custom HTML generate method for inserting raw HTML in the. 334 | * Called externally by WooCommerce based on the type field in $this->form_fields 335 | */ 336 | public function generate_raw_html($key, $data) { 337 | $defaults = array( 338 | 'title' => '', 339 | 'disabled' => false, 340 | 'type' => 'raw', 341 | 'content' => '', 342 | 'desc_tip' => false, 343 | 'label' => $this->plugin_id . $this->id . '_' . $key 344 | ); 345 | 346 | $data = wp_parse_args($data, $defaults); 347 | 348 | ob_start(); 349 | ?> 350 |