From 0faa1987195bc3ef14bf0430fe97fb0ac7a43297 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 22 Oct 2020 10:19:08 +0200 Subject: initial import into repository dedicated to only contain the Taler WooCommerce plugin --- LICENSE | 339 ++++++++++++++ assets/images/taler.png | Bin 0 -> 88528 bytes class-wc-gnutaler-gateway.php | 1033 +++++++++++++++++++++++++++++++++++++++++ readme.txt | 52 +++ 4 files changed, 1424 insertions(+) create mode 100644 LICENSE create mode 100644 assets/images/taler.png create mode 100644 class-wc-gnutaler-gateway.php create mode 100644 readme.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/assets/images/taler.png b/assets/images/taler.png new file mode 100644 index 0000000..badd21e Binary files /dev/null and b/assets/images/taler.png differ diff --git a/class-wc-gnutaler-gateway.php b/class-wc-gnutaler-gateway.php new file mode 100644 index 0000000..e9c01f5 --- /dev/null +++ b/class-wc-gnutaler-gateway.php @@ -0,0 +1,1033 @@ +. +*/ + +/** + * Which version of the Taler merchant protocol is implemented + * by this implementation? Used to determine compatibility. + */ +define( 'GNU_TALER_MERCHANT_PROTOCOL_CURRENT', 1 ); + +/** + * How many merchant protocol versions are we backwards compatible with? + */ +define( 'GNU_TALER_MERCHANT_PROTOCOL_AGE', 0 ); + +require_once ABSPATH . 'wp-admin/includes/plugin.php'; + +// Exit if accessed directly. +if ( ! defined( 'ABSPATH' ) ) { + exit(); +} + +/* + * This action hook registers our PHP class as a WooCommerce payment gateway + */ + +/** + * Adds the GNU Taler payment method to the other payment gateways. + * + * @param array $gateways all the payment gateways. + * @return array + */ +function gnutaler_add_gateway_class( $gateways ) { + $gateways[] = 'WC_GNUTaler_Gateway'; + return $gateways; +} + +add_filter( 'woocommerce_payment_gateways', 'gnutaler_add_gateway_class' ); + +/** + * The class itself, please note that it is inside plugins_loaded action hook + */ +add_action( 'plugins_loaded', 'gnutaler_init_gateway_class' ); + + +/** + * Wrapper for the GNU Taler payment method class. Sets up internationalization. + */ +function gnutaler_init_gateway_class() { + // Setup textdomain for gettext style translations. + $plugin_rel_path = basename( dirname( __FILE__ ) ) . '/languages'; + load_plugin_textdomain( 'gnutaler', false, $plugin_rel_path ); + + // Check if WooCommerce is active, if not then deactivate and show error message. + if ( ! in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) ) { + deactivate_plugins( plugin_basename( __FILE__ ) ); + wp_die( + sprintf( + wp_kses( + /* translators: argument is the link to the plugin page */ + __( + 'GNU Taler requires WooCommerce plugin to work. Please activate it or install it from here.

Back to the WordPress Plugins page.', + 'gnutaler' + ), + array( + 'strong' => array(), + 'a' => array( + 'href' => array(), + '_target' => array(), + ), + ) + ), + esc_url( get_admin_url( null, 'plugins.php' ) ) + ) + ); + } + + /** + * GNU Taler Payment Gateway class. + * + * Handles the payments from the Woocommerce Webshop and sends the transactions to the GNU Taler Backend and the GNU Taler Wallet. + */ + class WC_GNUTaler_Gateway extends WC_Payment_Gateway { + + /** + * Cached handle to logging class. + * + * @var Plugin loggger. + */ + private static $logger = false; + + /** + * True if logging is enabled in our configuration. + * + * @var Is logging enabled? + */ + private static $log_enabled = false; + + /** + * Class constructor + */ + public function __construct() { + $this->id = 'gnutaler'; // Payment gateway plugin ID. + $this->logger = new WC_logger( $this->id ); // Setup logging. + $this->icon = plugins_url( '/assets/images/taler.png', __FILE__ ); + // We cannot use custom fields to show the QR code / do the wallet integration as WC doesn't give us the order_id at that time. Bummer. + $this->has_fields = false; + // The following texts will be displayed on the payment plugins settings page. + $this->method_title = 'GNU Taler'; + $this->method_description = __( 'This plugin enables payments via the GNU Taler payment system', 'gnutaler' ); + + // This gateway can support refunds, saved payment methods. + $this->supports = array( + 'products', + 'refunds', + ); + + // Setup logging. + $this->debug = 'yes' === $this->get_option( 'debug', 'no' ); + self::$log_enabled = $this->debug; + + // Setup 'form_fields'. + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'gnutaler' ), + 'label' => __( 'Enable GNU Taler Gateway', 'gnutaler' ), + 'type' => 'checkbox', + 'description' => '', + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'gnutaler' ), + 'type' => 'text', + 'description' => __( 'This is what the customer will see when choosing payment methods.', 'gnutaler' ), + 'default' => 'GNU Taler', + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __( 'Description', 'gnutaler' ), + 'type' => 'textarea', + 'description' => __( 'This controls the description for the payment option which the customer sees during checkout.', 'gnutaler' ), + 'default' => __( 'Pay with GNU Taler', 'gnutaler' ), + ), + 'gnu_taler_backend_url' => array( + 'title' => __( 'Taler backend URL', 'gnutaler' ), + 'type' => 'text', + 'description' => __( 'Set the URL of the Taler backend. (Example: https://backend.demo.taler.net/)', 'gnutaler' ), + 'default' => 'http://backend.demo.taler.net/instances/pos/', + ), + 'GNU_Taler_Backend_API_Key' => array( + 'title' => __( 'Taler Backend API Key', 'gnutaler' ), + 'type' => 'text', + 'description' => __( 'Enter your API key to authenticate with the Taler backend.', 'gnutaler' ), + 'default' => 'Sandbox ApiKey', + ), + 'Order_text' => array( + 'title' => __( 'Summary Text of the Order', 'gnutaler' ), + 'type' => 'text', + 'description' => __( 'Set the text the customer will see when confirming payment. #%%s will be substituted with the order number. (Example: MyShop #%%s)', 'gnutaler' ), + 'default' => 'WooTalerShop #%s', + ), + 'GNU_Taler_refund_delay' => array( + 'title' => __( 'How long should refunds be possible', 'gnutaler' ), + 'type' => 'number', + 'description' => __( 'Set the number of days a customer has to request a refund', 'gnutaler' ), + 'default' => '14', + ), + 'debug' => array( + 'title' => __( 'Debug Log', 'woocommerce' ), + 'label' => __( 'Enable logging', 'woocommerce' ), + 'description' => sprintf( + /* translators: placeholder will be replaced with the path to the log file */ + __( 'Log GNU Taler events inside %s.', 'gnutaler' ), + '' . WC_Log_Handler_File::get_log_file_path( 'gnutaler' ) . '' + ), + 'type' => 'checkbox', + 'default' => 'no', + ), + ); + + // Load the settings. + $this->init_settings(); + $this->title = $this->get_option( 'title' ); + $this->description = $this->get_option( 'description' ); + $this->enabled = $this->get_option( 'enabled' ); + $this->gnu_taler_backend_url = $this->get_option( 'gnu_taler_backend_url' ); + // Remove trailing '/', we add one always ourselves... + if ( substr( $this->gnu_taler_backend_url, -1 ) === '/' ) { + $this->gnu_taler_backend_url = substr( $this->gnu_taler_backend_url, 0, -1 ); + } + + // Make transaction ID a link. We use the public version + // here, as a user clicking on the link could not supply + // the authorization header. + // See also: https://woocommerce.wordpress.com/2014/08/05/wc-2-2-payment-gateways-adding-refund-support-and-transaction-ids/. + $this->view_transaction_url = $this->gnu_taler_backend_url . '/orders/%s'; + + // Register handler for the fulfillment URL. + $hname = 'woocommerce_api_' . strtolower( get_class( $this ) ); + add_action( + $hname, + array( &$this, 'fulfillment_url_handler' ) + ); + + // This action hook saves the settings. + add_action( + 'woocommerce_update_options_payment_gateways_' . $this->id, + array( $this, 'process_admin_options' ) + ); + + // Modify WC canonical refund e-mail notifications to add link to order status page. + // (according to https://www.businessbloomer.com/woocommerce-add-extra-content-order-email/). + add_action( + 'woocommerce_email_before_order_table', + array( $this, 'add_content_refund_email' ), + 20, + 4 + ); + } + + /** + * Called when WC sends out the e-mail notification for refunds. + * Adds a Taler-specific notice for where to click to obtain + * the refund. + * + * @param WC_Order $wc_order The order. + * @param bool $sent_to_admin Not well documented by WooCommerce. + * @param string $plain_text The plain text of the email. + * @param string $email Target email address. + */ + public function add_content_refund_email( $wc_order, $sent_to_admin, $plain_text, $email ) { + if ( 'customer_refunded_order' === $email->id ) { + $backend_url = $this->gnu_taler_backend_url; + $wc_order_id = $wc_order->get_order_key() . '-' . $wc_order->get_order_number(); + $refund_url = $wc_order->get_meta( 'GNU_TALER_REFUND_URL' ); + echo sprintf( + /* translators: placeholder will be replaced with the refund URL */ + esc_html( __( 'Refund granted. Visit %1$s to obtain the refund.', 'gnutaler' ) ), + esc_url( $refund_url ) + ); + } + } + + /** + * Processes and saves options. + * If there is an error thrown, will continue to save and validate fields, but + * will leave the erroring field out. + * + * @return bool was anything saved? + */ + public function process_admin_options() { + $saved = parent::process_admin_options(); + + // Maybe clear logs. + if ( 'yes' !== $this->get_option( 'debug', 'no' ) ) { + if ( empty( self::$log ) ) { + self::$log = wc_get_logger(); + } + self::$log->clear( 'gnutaler' ); + } + + return $saved; + } + + /** + * Required function to add the fields we want to show in the + * payment method selection dialog. We show none. + */ + public function payment_fields() { + // We do not have any. + } + + /** + * Callback is called, when the user goes to the fulfillment URL. + * + * We check that the payment actually was made, and update WC accordingly. + * If the order ID is unknown and/or the payment did not succeed, we + * redirect to the home page and/or the user's order page (for logged in users). + */ + public function fulfillment_url_handler(): void { + global $woocommerce; + + // We intentionally do NOT verify the nonce here, as this page + // should work even if the deep link is shared with other users + // or even non-users. + // phpcs:disable WordPress.Security.NonceVerification + if ( ! isset( $_GET['order_id'] ) ) { + $this->debug( __( "Lacking 'order_id', forwarding user to neutral page", 'gnutaler' ) ); + if ( is_user_logged_in() ) { + wp_safe_redirect( get_home_url() . wc_get_page_permalink( 'myaccount' ) ); + } else { + wp_safe_redirect( get_home_url() . wc_get_page_permalink( 'shop' ) ); + } + exit; + } + + // Gets the order id from the fulfillment url. + $taler_order_id = sanitize_text_field( wp_unslash( $_GET['order_id'] ) ); + // phpcs:enable + $order_id_array = explode( '-', $taler_order_id ); + $order_id_name = $order_id_array[0]; + $order_id = $order_id_array[1]; + $wc_order = wc_get_order( $order_id ); + $backend_url = $this->gnu_taler_backend_url; + + $payment_confirmation = $this->call_api( + 'GET', + $backend_url . '/private/orders/' . $taler_order_id, + false + ); + $payment_body = $payment_confirmation['message']; + $payment_http_status = $payment_confirmation['http_code']; + + switch ( $payment_http_status ) { + case 200: + // Here we check what kind of http code came back from the backend. + $merchant_order_status_response = json_decode( + $payment_body, + $assoc = true + ); + if ( ! $merchant_order_status_response ) { + wc_add_notice( + __( 'Payment error:', 'gnutaler' ) . + __( 'backend did not respond', 'gnutaler' ) + ); + $this->notice( __( 'Payment failed: no reply from Taler backend', 'gnutaler' ) ); + wp_safe_redirect( $this->get_return_url( $order_id ) ); + exit; + } + if ( 'paid' === $merchant_order_status_response['order_status'] ) { + $this->notice( __( 'Payment succeeded and the user was forwarded to the order confirmed page', 'gnutaler' ) ); + // Completes the order, storing a transaction ID. + $wc_order->payment_complete( $taler_order_id ); + // Empties the shopping cart. + WC()->cart->empty_cart(); + } else { + wc_add_notice( + __( 'Payment error:', 'gnutaler' ) . + __( 'backend did not confirm payment', 'gnutaler' ) + ); + $this->notice( __( 'Backend did not confirm payment', 'gnutaler' ) ); + } + wp_safe_redirect( $this->get_return_url( $wc_order ) ); + exit; + default: + $this->error( + __( 'An error occurred during the second request to the GNU Taler backend: ', 'gnutaler' ) + . $payment_http_status . ' - ' . $payment_body + ); + wc_add_notice( __( 'Payment error:', 'gnutaler' ) . $payment_http_status . ' - ' . $payment_body ); + wp_safe_redirect( $this->get_return_url( $order_id ) ); + break; + } + $cart_url = $woocommerce->cart->wc_get_cart_url(); + if ( is_set( $cart_url ) ) { + wp_safe_redirect( get_home_url() . $cart_url ); + } else { + wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); + } + exit; + } + + /** + * Sends a request to a url via HTTP. + * + * Sends a request to a GNU Taler Backend over HTTP and returns the result. + * The request can be sent as POST or GET. PATCH is not supported. + * + * @param string $method POST or GET supported only. Thanks WordPress. + * @param string $url URL for the request to make to the GNU Taler Backend. + * @param string $body The content of the request (for POST). + * + * @return array The return array will either have the successful return value or a detailed error message. + */ + private function call_api( $method, $url, $body ): array { + $apikey = $this->get_option( 'GNU_Taler_Backend_API_Key' ); + $args = array( + 'timeout' => 30, // In seconds. + 'redirection' => 2, // How often. + 'httpversion' => '1.1', // Taler will support. + 'user-agent' => '', // Minimize information leakage. + 'blocking' => true, // We do nothing without it. + 'headers' => array( + 'Authorization: ' . $apikey, + ), + 'decompress' => true, + 'limit_response_size' => 1024 * 1024, // More than enough. + ); + if ( $body ) { + $args['body'] = wp_json_encode( $body, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES, 16 ); + $args['headers'][] = 'Content-type: application/json'; + $args['compress'] = true; + } + $this->debug( 'Issuing HTTP ' . $method . ' request to ' . $url . ' with options ' . wp_json_encode( $args ) . ' and body ' . $body ); + + switch ( $method ) { + case 'POST': + $response = wp_remote_post( $url, $args ); + break; + case 'GET': + $response = wp_remote_get( $url, $args ); + break; + default: + $this->debug( 'HTTP method ' . $method . ' not supported' ); + return null; + } + if ( is_wp_error( $response ) ) { + $error_code = $response->get_error_code(); + $error_data = $response->get_error_data( $error_code ); + $this->warning( + sprintf( + /* translators: first placeholder is the error code, second the error data */ + __( 'HTTP failure %1$s with data %2$s', 'gnutaler' ), + $error_code, + $error_data + ) + ); + + return array( + 'http_code' => 0, + 'message' => $error_code, + ); + } + $http_code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + $this->debug( + sprintf( + /* translators: first placeholder is the HTTP status code, second the body of the HTTP reply */ + __( 'HTTP status %1$s with response body %2$s', 'gnutaler' ), + $http_code, + $body + ) + ); + return array( + 'http_code' => $http_code, + 'message' => $body, + ); + } + + /** + * Checks if the configuration is valid. Used by WC to redirect + * the admin to the setup dialog if they enable the backend and + * it is not properly configured. + */ + public function needs_setup() { + $backend_url = $this->gnu_taler_backend_url; + return ! $this->verify_backend_url( + $backend_url, + null + ); + } + + /** + * Verifying if the url to the backend given in the plugin options is valid or not. + * + * @param string $url URL to the backend. + * @param string $ecurrency expected currency of the order. + * + * @return bool - Returns if valid or not. + */ + private function verify_backend_url( $url, $ecurrency ): bool { + $config = $this->call_api( 'GET', $url . '/config', false ); + $config_http_status = $config['http_code']; + $config_body = $config['message']; + switch ( $config_http_status ) { + case 200: + $info = json_decode( $config_body, $assoc = true ); + if ( ! $info ) { + $this->error( + sprintf( + /* translators: placeholder will be replaced with the URL of the backend */ + __( '/config response of backend at %s did not decode as JSON', 'gnutaler' ), + $url + ) + ); + return false; + } + $version = $info['version']; + if ( ! $version ) { + $this->error( + sprintf( + /* translators: placeholder will be replaced with the URL of the backend */ + __( "No 'version' field in /config reply from Taler backend at %s", 'gnutaler' ), + $url + ) + ); + return false; + } + $ver = explode( ':', $version, 3 ); + $current = $ver[0]; + $revision = $ver[1]; + $age = $ver[2]; + if ( ( ! is_numeric( $current ) ) + || ( ! is_numeric( $revision ) ) + || ( ! is_numeric( $age ) ) + ) { + $this->error( + sprintf( + /* translators: placeholder will be replaced with the (malformed) version number */ + __( "/config response at backend malformed: '%s' is not a valid version", 'gnutaler' ), + $version + ) + ); + return false; + } + if ( GNU_TALER_MERCHANT_PROTOCOL_CURRENT < $current - $age ) { + // Our implementation is too old! + $this->error( + sprintf( + /* translators: placeholder will be replaced with the version number */ + __( 'Backend protocol version %s is too new: please update the GNU Taler plugin', 'gnutaler' ), + $version + ) + ); + return false; + } + if ( GNU_TALER_MERCHANT_PROTOCOL_CURRENT - GNU_TALER_MERCHANT_PROTOCOL_AGE > $current ) { + // Merchant implementation is too old! + $this->error( + sprintf( + /* translators: placeholder will be replaced with the version number */ + __( 'Backend protocol version %s unsupported: please update the backend', 'gnutaler' ), + $version + ) + ); + return false; + } + $currency = $info['currency']; + if ( ( ! is_null( $ecurrency ) ) && ( 0 !== strcasecmp( $currency, $ecurrency ) ) ) { + $this->error( + sprintf( + /* translators: first placeholder is the Taler backend currency, second the expected currency from WooCommerce */ + __( 'Backend currency %1$s does not match order currency %2$s', 'gnutaler' ), + $currency, + $ecurrency + ) + ); + return false; + } + $this->debug( + sprintf( + /* translators: placeholder will be replaced with the URL of the backend */ + __( '/config check for Taler backend at %s succeeded', 'gnutaler' ), + $url + ) + ); + return true; + default: + $this->error( + sprintf( + /* translators: placeholder will be replaced with the HTTP status code returned by the backend */ + __( 'Backend failed /config request with unexpected HTTP status %s', 'gnutaler' ), + $config_http_status + ) + ); + return false; + } + } + + /** + * Processes the payment after the checkout + * + * If the payment process finished successfully the user is being redirected to its GNU Taler Wallet. + * If an error occurs it returns void and throws an error. + * + * @param string $order_id ID of the order to get the Order from the WooCommerce Webshop. + * + * @return array|void - Array with result => success and redirection url otherwise it returns void. + */ + public function process_payment( $order_id ) { + // We need the order ID to get any order detailes. + $wc_order = wc_get_order( $order_id ); + + // Gets the url of the backend from the WooCommerce Settings. + $backend_url = $this->gnu_taler_backend_url; + + // Log entry that the customer started the payment process. + $this->info( __( 'User started the payment process with GNU Taler.', 'gnutaler' ) ); + + if ( ! $this->verify_backend_url( $backend_url, $wc_order->get_currency() ) ) { + wc_add_notice( __( 'Something went wrong please contact the system administrator of the webshop and send the following error: GNU Taler backend URL invalid', 'gnutaler' ), 'error' ); + $this->error( __( 'Checkout process failed: Invalid backend url.', 'gnutaler' ) ); + return; + } + $order_json = $this->convert_to_checkout_json( $order_id ); + + $this->info( __( 'Sending POST /private/orders request send to Taler backend', 'gnutaler' ) ); + $order_confirmation = $this->call_api( + 'POST', + $backend_url . '/private/orders', + $order_json + ); + $order_body = $order_confirmation['message']; + $order_http_status = $order_confirmation['http_code']; + switch ( $order_http_status ) { + case 200: + $post_order_response = json_decode( $order_body, $assoc = true ); + if ( ! $post_order_response ) { + $this->error( + __( 'POST /private/orders request to Taler backend returned 200 OK, but not a JSON body: ', 'gnutaler' ) + . $order_body + ); + wc_add_notice( __( 'Malformed response from Taler backend. Please contact the system administrator.' ) ); + $wc_order->set_status( 'cancelled' ); + return; + } + $taler_order_id = $post_order_response ['order_id']; + $taler_order_token = $post_order_response ['token']; + if ( ! $taler_order_id ) { + $this->error( + __( 'Response to POST /private/orders request to Taler backend lacks order_id field: ', 'gnutaler' ) + . $order_body + ); + wc_add_notice( __( 'Malformed response from Taler backend. Please contact the system administrator.', 'gnutaler' ) ); + $wc_order->set_status( 'cancelled' ); + return; + } + $this->info( __( 'POST /private/orders successful. Redirecting user to Taler Backend.', 'gnutaler' ) ); + return array( + 'result' => 'success', + 'redirect' => $backend_url . '/orders/' . $taler_order_id . '?token=' . $taler_order_token, + ); + case 404: + $post_order_error = json_decode( $order_body, $assoc = true ); + if ( ! $post_order_error ) { + $this->error( + __( 'POST /private/orders request to Taler backend returned 404, but not a JSON body: ', 'gnutaler' ) + . $order_body + ); + wc_add_notice( __( 'Malformed response from Taler backend. Please contact the system administrator.', 'gnutaler' ) ); + $wc_order->set_status( 'cancelled' ); + return; + } + $this->error( + __( 'POST /private/orders request to Taler backend failed: ', 'gnutaler' ) + . $post_order_error['code'] . '(' + . $order_http_status . '): ' . $order_body + ); + wc_add_notice( __( 'Taler backend not configured correctly. Please contact the system administrator.', 'gnutaler' ) ); + $wc_order->set_status( 'cancelled' ); + return; + case 410: + // We don't use inventory management via GNU Taler's backend, so this error should never apply. + // Handle with 'default' case below. + default: + $this->error( + __( 'POST /private/orders request to Taler backend failed: ', 'gnutaler' ) + . $post_order_error['code'] . '(' + . $order_http_status . '): ' + . $order_body + ); + wc_add_notice( __( 'Unexpected problem with the Taler backend. Please contact the system administrator.', 'gnutaler' ) ); + $wc_order->set_status( 'cancelled' ); + return; + } + } + + /** + * Converts the order into a JSON format that can be send to the GNU Taler Backend. + * + * @param string $order_id ID of the order to get the Order from the WooCommerce Webshop. + * + * @return array - return the JSON Format. + */ + public function convert_to_checkout_json( $order_id ): array { + $wc_order = wc_get_order( $order_id ); + $wc_order_total_amount = $wc_order->get_total(); + $wc_order_currency = $wc_order->get_currency(); + $wc_cart = WC()->cart->get_cart(); + $wc_order_id = $wc_order->get_order_key() . '-' . $wc_order->get_order_number(); + $wc_order_products_array = $this->mutate_products_to_json_format( $wc_cart, $wc_order_currency ); + $refund_delay = $this->get_option( 'GNU_Taler_refund_delay' ); + $order_json = array( + 'order' => array( + 'amount' => $wc_order_currency . ':' . $wc_order_total_amount, + 'summary' => sprintf( + $this->get_option( 'Order_text' ), + $wc_order->get_order_number() + ), + // NOTE: This interacts with the 'add_action' call + // to invoke the 'fulfillment_url_handler' when the + // user goes to this URL! + 'fulfillment_url' => get_home_url() + . '/?wc-api=' + . strtolower( get_class( $this ) ) + . '&order_id=' + . $wc_order_id, + 'order_id' => $wc_order_id, + 'products' => $wc_order_products_array, + 'delivery_location' => $this->mutate_shipping_information_to_json_format( $wc_order ), + ), + ); + if ( isset( $refund_delay ) ) { + $order_json['refund_delay'] = array( + 'd_ms' => 1000 * 60 * 60 * 24 * intval( $refund_delay ), + ); + } + return $order_json; + } + + /** + * Mutates the products in the cart into a format which can be included in a JSON file. + * + * @param WC_Cart $wc_cart The content of the WooCommerce Cart. + * @param string $wc_order_currency The currency the WooCommerce Webshop uses. + * + * @return array - Returns an array of products. + */ + private function mutate_products_to_json_format( $wc_cart, $wc_order_currency ): array { + $wc_order_products_array = array(); + foreach ( $wc_cart as $product ) { + $wc_order_products_array[] = array( + 'description' => $product['data']->get_title(), + 'quantity' => $product['quantity'], + 'price' => $wc_order_currency . ':' . $product['data']->get_price(), + 'product_id' => strval( $product['data']->get_id() ), + ); + } + return $wc_order_products_array; + } + + /** + * Processes the refund transaction if requested by the system administrator of the webshop + * + * If the refund request is finished successfully it returns an refund url, which can be send to the customer to finish the refund transaction. + * If an error it will throw a WP_Error message and inform the system administrator. + * + * @param WC_Order $wc_order The WooCommerce order object we are processing. + * + * @return array + */ + private function mutate_shipping_information_to_json_format( $wc_order ): array { + $whitechar_encounter = false; + $shipping_address_street = ''; + $shipping_address_street_nr = ''; + + $store_address = $wc_order->get_shipping_address_1(); + $store_address_inverted = strrev( $store_address ); + $store_address_array = str_split( $store_address_inverted ); + + // Split the address into street and street number. + foreach ( $store_address_array as $char ) { + if ( ! $whitechar_encounter ) { + $shipping_address_street .= $char; + } elseif ( ctype_space( $char ) ) { + $whitechar_encounter = true; + } else { + $shipping_address_street .= $char; + } + } + $ret = array( + 'country' => $wc_order->get_shipping_country(), + 'country_subdivision' => $wc_order->get_shipping_state(), + 'town' => $wc_order->get_shipping_city(), + 'post_code' => $wc_order->get_shipping_postcode(), + 'street' => $shipping_address_street, + 'building_number' => $shipping_address_street_nr, + ); + if ( null !== $wc_order->get_shipping_address_2() ) { + $address_lines = array( + $wc_order->get_shipping_address_1(), + $wc_order->get_shipping_address_2(), + ); + $ret['address_lines'] = $address_lines; + } + return $ret; + } + + /** + * Processes the refund transaction if requested by the system administrator of the webshop + * + * If the refund request is finished successfully it returns an refund url, which can be send to the customer to finish the refund transaction. + * If an error it will throw a WP_Error message and inform the system administrator. + * + * @param string $order_id Order id for logging. + * @param string $amount Amount that is requested to be refunded. + * @param string $reason Reason for the refund request. + * + * @return bool|WP_Error - Returns true or throws an WP_Error message in case of error. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $wc_order = wc_get_order( $order_id ); + + $this->info( + sprintf( + /* translators: first placeholder is the numeric amount, second the currency, and third the reason for the refund */ + __( 'Refund process started with the refunded amount %1$s %2$s and the reason %3$s.' ), + $amount, + $wc_order->get_currency(), + $reason + ) + ); + + // Gets the url of the backend from the WooCommerce Settings. + $backend_url = $this->gnu_taler_backend_url; + + // Get the current status of the order. + $wc_order_status = $wc_order->get_status(); + + // Checks if current status is already set as paid. + if ( ! ( 'processing' === $wc_order_status + || 'on hold' === $wc_order_status + || 'completed' === $wc_order_status ) + ) { + $this->error( __( 'The status of the order does not allow a refund', 'gnutaler' ) ); + return new WP_Error( 'error', __( 'The status of the order does not allow for a refund.', 'gnutaler' ) ); + } + + $refund_request = array( + 'refund' => $wc_order->get_currency() . ':' . $amount, + 'reason' => $reason, + ); + $wc_order_id = $wc_order->get_order_key() . '-' . $wc_order->get_order_number(); + $refund_result = $this->call_api( + 'POST', + $backend_url . '/private/orders/' . $wc_order_id . '/refund', + $refund_request + ); + + $refund_http_status = $refund_result['http_code']; + $refund_body = $refund_result['message']; + switch ( $refund_http_status ) { + case 200: + $refund_response = json_decode( $refund_body, $assoc = true ); + if ( ! $refund_response ) { + $this->error( __( 'Malformed 200 response from Taler backend: not even in JSON', 'gnutaler' ) ); + return new WP_Error( 'error', __( 'Malformed response from Taler backend', 'gnutaler' ) ); + } + $refund_uri = $refund_response['taler_refund_uri']; + $h_contract = $refund_response['h_contract']; + if ( ( ! $refund_uri ) || ( ! $h_contract ) ) { + $this->error( __( 'Malformed 200 response from Taler backend: lacks taler_refund_uri', 'gnutaler' ) ); + return new WP_Error( 'error', __( 'Malformed response from Taler backend', 'gnutaler' ) ); + } + $refund_url = $backend_url + . '/orders/' + . $wc_order_id + . '?h_contract=' + . $h_contract; + $wc_order->add_meta_data( 'GNU_TALER_REFUND_URL', $refund_url ); + $wc_order->update_status( 'refunded' ); + $this->debug( + sprintf( + /* translators: argument is the Taler refund URI */ + + __( 'Received refund URI %s from Taler backend', 'gnutaler' ), + $refund_uri + ) + ); + $this->notice( + sprintf( + /* translators: argument is the Taler refund URL */ + __( 'The user must visit %s to obtain the refund', 'gnutaler' ), + $refund_url + ) + ); + return true; + case 403: + return new WP_Error( + 'error', + __( 'Refunds are disabled for this order. Check the refund_delay option for the Taler payment plugin.', 'gnutaler' ) + ); + case 404: + $refund_error = json_decode( $refund_body, $assoc = true ); + if ( ! $refund_error ) { + return new WP_Error( + 'error', + sprintf( + /* translators: argument is the HTTP status returned by the backend */ + __( 'Unexpected failure %s without Taler error code from Taler backend', 'gnutaler' ), + $refund_http_status + ) + ); + } + $ec = $refund_error['code']; + switch ( $ec ) { + case 2000: // TALER_EC_INSTANCE_UNKNOWN! + return new WP_Error( + 'error', + __( 'Instance unknown reported by Taler backend', 'gnutaler' ) + ); + case 2601: // TALER_EC_REFUND_ORDER_ID_UNKNOWN! + return new WP_Error( + 'error', + __( 'Order unknown reported by Taler backend', 'gnutaler' ) + ); + default: + return new WP_Error( + 'error', + sprintf( + /* translators: placeholder will be replaced with the numeric GNU Taler error code */ + __( 'Unexpected error %s reported by Taler backend', 'gnutaler' ), + $ec + ) + ); + } + // This line is unreachable. + case 409: + return new WP_Error( + 'error', + __( 'Requested refund amount exceeds original payment. This is not allowed!', 'gnutaler' ) + ); + case 410: + return new WP_Error( + 'error', + __( 'Wire transfer already happened. It is too late for a refund with Taler!', 'gnutaler' ) + ); + default: + $refund_error = json_decode( $refund_body, $assoc = true ); + if ( ! $refund_error ) { + $ec = $refund_error['code']; + } else { + $ec = 0; + } + return new WP_Error( + 'error', + sprintf( + /* translators: first placeholder is the HTTP status code, second the numeric GNU Taler error code */ + __( 'Unexpected failure %1$s/%2$s from Taler backend', 'gnutaler' ), + $refund_http_status, + $ec + ) + ); + } + } + + /** + * Log $msg for debugging + * + * @param string $msg message to log. + */ + private function debug( $msg ) : void { + $this->log( 'debug', $msg ); + } + + /** + * Log $msg as a informational + * + * @param string $msg message to log. + */ + private function info( $msg ) : void { + $this->log( 'info', $msg ); + } + + /** + * Log $msg as a notice + * + * @param string $msg message to log. + */ + private function notice( $msg ) : void { + $this->log( 'notice', $msg ); + } + + /** + * Log $msg as a warning. + * + * @param string $msg message to log. + */ + private function warning( $msg ) : void { + $this->log( 'warning', $msg ); + } + + /** + * Log $msg as an error + * + * @param string $msg message to log. + */ + private function error( $msg ) : void { + $this->log( 'error', $msg ); + } + + /** + * Log $msg at log $level. + * + * @param string $level log level to use when logging. + * @param string $msg message to log. + */ + private function log( $level, $msg ) { + if ( ! self::$log_enabled ) { + return; + } + if ( function_exists( 'wp_get_current_user()' ) ) { + $user_id = wp_get_current_user(); + if ( ! isset( $user_id ) ) { + $user_id = __( '', 'gnutaler' ); + } + } else { + $user_id = 'Guest'; + } + // We intentionally do NOT verify the nonce here, as logging + // should always work. + // phpcs:disable WordPress.Security.NonceVerification + $order_id = sanitize_text_field( wp_unslash( $_GET['order_id'] ) ); + // phpcs:enable + if ( empty( self::$logger ) ) { + self::$logger = wc_get_logger(); + } + self::$logger->log( $level, $user_id . '-' . $order_id . ': ' . $msg, array( 'source' => 'gnutaler' ) ); + } + + } +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..bb5978a --- /dev/null +++ b/readme.txt @@ -0,0 +1,52 @@ +=== GNU Taler Payment Gateway for WooCommerce === +Contributors: gnutaler +Tags: WooCommerce, e-commerce, Taler, Payment Gateway +Requires at least: 5.1 +Tested up to: 5.5.1 +Stable tag: 5.5 +Requires PHP: 7.2 +License: GNU General Public License v2.0+ +License URI: http://www.gnu.org/licenses/gpl-2.0.html + +Online payment plugin for WooCommerce powered by GNU Taler + +== Description == + +This plugin provides a safe and secure way to pay via the GNU Taler system. The plugin sends a request to, and receives a response from, the GNU Taler Backend. + +Next, the plugin confirms the transaction again and redirects the customer to his own wallet to confirm the transaction. + +The plugin also provides the possibility for the administrator to send the custumer a refund. + +For refunds, the plugin sends a refund request to the GNU Taler back-end and receives a refund-URI. This URI is forwarded to the customer via an e-mail to confirm the refund. + + +== Installation == + +1. Ensure you have latest version of WooCommerce plugin installed +2. Upload (or copy) the plugin directory (`GNU-Taler-Payment-Gateway`) to the `/wp-content/plugins/` directory of your WordPress/WooCommerce site, or install the plugin through the WordPress plugins interface at `/wp-admin/plugins.php`. +3. Activate the GNU Taler Payment Gateway for WooCommerce plugin through the 'Plugins' screen in WordPress. +4. In the WordPress interface, navigate to WooCommerce -> Settings-> Payments tab. Locate the `GNU Taler Gateway` entry and click the `Set up` button to configure the plugin. + +== Frequently Asked Questions == + +*Q: Do I have to have a GNU Taler account to test the plugin?* + +A: Yes, you need to have a "bank" account to withdraw cash. You can setup one easily at: https://bank.demo.taler.net/ + +*Q: Does the customer need to have the GNU Taler Wallet installed to pay with GNU Taler?* + +A: Yes, the customer needs the GNU Taler Wallet. The customer can get a wallet here: https://wallet.taler.net/ + +*Q: Can the plugin work without WooCommerce* + +A: For the plugin to work correctly you need to have the WooCommerce plugin installed on a WordPress site + + +== Changelog == + += 0.8.0 = + +* Adaptations to GNU Taler Merchant API 1:0:0 +* Ensure plugin follows Wordpress guidelines. First upload to WordPress. + -- cgit v1.2.3