Changeset 3649832
- Timestamp:
- 08/16/2026 02:37:02 PM (2 months ago)
- Location:
- woo-cost-of-shipping/trunk
- Files:
-
- 1 added
- 4 deleted
- 7 edited
-
README.txt (modified) (3 diffs)
-
assets/README.md (deleted)
-
assets/js/woocommerce-cost-of-shipping.js (modified) (1 diff)
-
assets/repo (deleted)
-
includes/README.md (deleted)
-
includes/admin/class-wc-cos-admin-orders.php (modified) (29 diffs)
-
includes/admin/class-wc-cos-safe-domdocument.php (modified) (3 diffs)
-
languages/woo-cost-of-shipping.pot (added)
-
languages/woocommerce-cost-of-shipping.pot (deleted)
-
woo-includes/class-wc-cos-dependencies.php (modified) (1 diff)
-
woo-includes/woo-functions.php (modified) (1 diff)
-
woocommerce-cost-of-shipping.php (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
woo-cost-of-shipping/trunk/README.txt
r3102930 r3649832 1 === WooCommerce Cost of Shipping===1 === Cost of Shipping for WooCommerce === 2 2 Contributors: TheRiteSites 3 Donate link: https://www.theritesites.com/plugins/woo commerce-cost-of-shipping3 Donate link: https://www.theritesites.com/plugins/woo-cost-of-shipping 4 4 Tags: cost of shipping, shipping cost, WooCommerce, net profit, shipstation 5 5 Requires at least: 5.2 6 Tested up to: 6.56 Tested up to: 7.0 7 7 Requires PHP: 7.0 8 8 Stable tag: trunk … … 38 38 39 39 1. Upload the entire `/woo-cost-of-shipping` directory to the `/wp-content/plugins/` directory. 40 2. Activate WooCommerce Cost of Shippingthrough the 'Plugins' menu in WordPress.40 2. Activate Cost of Shipping for WooCommerce through the 'Plugins' menu in WordPress. 41 41 3. Enjoy the easy editting of the cost of shipping in the edit order screen, below the order total. 42 42 … … 59 59 60 60 == Changelog == 61 62 = 1.5.5 = 63 - Changed: display name to "Cost of Shipping for WooCommerce" (was "WooCommerce Cost of Shipping"). Slug and installed directory are unchanged; this is a display-name-only change. 64 - Changed: Plugin URI and Donate link now point at the plugin's real slug instead of the old trademarked path. 65 - Fixed: text domain now matches the plugin slug. 66 - Fixed: PHP 8.1+ deprecation notice on the safe XML parser's method return types. 67 - Fixed: escaped order-ID attribute output and sanitized page-parameter reads flagged by wordpress.org's Plugin Check. 68 - Security: hardened the admin AJAX handlers, the ShipStation import and the XML parser. No vulnerability was found; this is defense in depth. Updating is recommended. 69 - Fixed: the cost of shipping method and change history were not being saved on orders. Existing order data is left as-is; new saves are correct. 70 - Fixed: a possible fatal error when importing WooCommerce Services labels on some stores. 71 - Fixed: a WooCommerce Services import that resolved to exactly 0.00 was reported as a failure. 72 - Fixed: the order screen could show a cost of shipping as saved when the save had not succeeded. 73 - Changed: order-screen strings are now translatable. 61 74 62 75 = 1.5.4 = -
woo-cost-of-shipping/trunk/assets/js/woocommerce-cost-of-shipping.js
r2911313 r3649832 1 window.WCCOS=window.WCCOS||{},function(i,o,t,c){var e={};c.init=function(){c.cache(),c.bindEvents()},c.cache=function(){e.window=t(i),e.body=t(o.body)},c.bindEvents=function(){},t(c.init)}(window,document,jQuery,window.WCCOS),jQuery(document).ready((function(i){"use strict";i("#edit-shipping-cost").on("click",(function(o){o.preventDefault(),i(".edit-shipping-tooltip").toggle(),i("#shipping-cost-input").focus()})),i("#edit-shipping-cancel").on("click",(function(o){o.preventDefault(),i(".edit-shipping-tooltip").toggle()})),i("#edit-shipping-save").on("click",(function(o){o.preventDefault(),i("#edit-shipping-save").attr("disabled",!0),i("#edit-shipping-cancel").attr("disabled",!0);var t={action:"set_shipping_cost",security:WCCOSApi.nonce,post_id:i("#edit-shipping-cost").data("orderid"),shipping_cost:i("#shipping-cost-input").val()};i.post(ajaxurl,t,(function(i){WCCOSApi.debug&&console.log("Shipping cost sent!")})).done((function(){i("#edit-shipping-save").attr("disabled",!1),i("#edit-shipping-cancel").attr("disabled",!1),i(".edit-shipping-tooltip").toggle()})).fail((function(){console.log("Failed to save the shipping cost from manual input.")})).success((function(){WCCOSApi.debug&&console.log("success");var o=parseFloat(i("#shipping-cost-input").val()).toFixed(2);i("#shipping-cost .amount").html('<span class="woocommerce-Price-currencySymbol">'+WCCOSApi.currency+"</span>"+o)})).always((function(){}))})),i(".wc-cos-wcs-import").on("click",(function(o){o.preventDefault(),console.log();var t={action:"set_label_cost_wcs",security:WCCOSApi.nonce,post_id:i(this).data("orderid")};i(".wc-cos-wcs-import").prop("disabled",!0),i(this).html("Loading..."),i.post(ajaxurl,t,(function(i){WCCOSApi.debug&&console.log("Shipping cost being imported!")})).done((function(){i("#wc-cos-protip").remove(),i(".wc-cos-wcs-import").attr("disabled",!1),i(".wc-cos-wcs-import").after('<div id="wc-cos-protip"><p><b>Pro tip:</b> If you purchase your label(s) and then set the order status to complete, the cost of shipping will automatically be imported!</p></div>')})).fail((function(){console.log("failed to import shipping cost from WooCommerce services created label."),i(".wc-cos-wcs-import").html("Retry import")})).success((function(o){WCCOSApi.debug&&console.log("Success"),i(".wc-cos-wcs-import").html("Re-import"),i(".shipping-total .woocommerce-Price-amount").html('<span class="woocommerce-Price-currencySymbol">'+o.data.currency+"</span>"+parseFloat(o.data.cost).toFixed(2))}))}))})); 1 /** 2 * WooCommerce Cost of Shipping 3 * https://www.theritesites.com/plugins/woocomerce-cost-of-shipping 4 * 5 * This plugin can be expanded into net profit reporting by getting the WooCommerce Net Profit plugin 6 * developed by TheRiteSites and found at https://www.theritesites.com/plugins/woocommerce-net-profit 7 * 8 * Licensed under the GPLv2+ license. 9 */ 10 11 window.WCCOS = window.WCCOS || {}; 12 13 ( function( window, document, $, plugin ) { 14 var $c = {}; 15 16 plugin.init = function() { 17 plugin.cache(); 18 plugin.bindEvents(); 19 }; 20 21 plugin.cache = function() { 22 $c.window = $( window ); 23 $c.body = $( document.body ); 24 }; 25 26 plugin.bindEvents = function() { 27 }; 28 29 $( plugin.init ); 30 31 32 }( window, document, jQuery, window.WCCOS ) ); 33 34 jQuery(document).ready(function( $ ) { 35 'use strict'; 36 37 var i18n = ( window.WCCOSApi && WCCOSApi.i18n ) || {}; 38 39 /** 40 * Build the price markup the same way wc_price() does, but from DOM nodes 41 * so neither the currency symbol nor the amount is ever parsed as HTML. 42 */ 43 function renderPrice( $target, currency, amount ) { 44 var value = parseFloat( amount ); 45 if ( isNaN( value ) ) { 46 return; 47 } 48 $target.empty().append( 49 $( '<span/>' ).addClass( 'woocommerce-Price-currencySymbol' ).text( currency ), 50 document.createTextNode( value.toFixed( 2 ) ) 51 ); 52 } 53 54 function debugLog( message ) { 55 if ( window.WCCOSApi && WCCOSApi.debug && window.console ) { 56 console.log( message ); 57 } 58 } 59 60 $('#edit-shipping-cost').on('click', function(e) { 61 e.preventDefault(); 62 $('.edit-shipping-tooltip').toggle(); 63 $('#shipping-cost-input').focus(); 64 }); 65 66 $('#edit-shipping-cancel').on('click', function(e) { 67 e.preventDefault(); 68 $('.edit-shipping-tooltip').toggle(); 69 }); 70 71 $('#edit-shipping-save').on('click', function(e) { 72 e.preventDefault(); 73 74 $('#edit-shipping-save').prop( 'disabled', true ); 75 $('#edit-shipping-cancel').prop( 'disabled', true ); 76 77 var data = { 78 action: 'set_shipping_cost', 79 security: WCCOSApi.nonce, 80 post_id: $('#edit-shipping-cost').data('orderid'), 81 shipping_cost: $('#shipping-cost-input').val() 82 }; 83 84 $.post( ajaxurl, data ) 85 .done(function( response ) { 86 // admin-ajax answers 200 for wp_send_json_error() unless a status is 87 // passed, so a resolved request is not yet a saved value. 88 if ( ! response || ! response.success ) { 89 var message = ( response && response.data && response.data.message ) || i18n.save_failed || 'The cost of shipping could not be saved.'; 90 window.alert( message ); 91 return; 92 } 93 debugLog( 'Shipping cost saved.' ); 94 renderPrice( $('#shipping-cost .amount'), WCCOSApi.currency, $('#shipping-cost-input').val() ); 95 $('.edit-shipping-tooltip').toggle(); 96 }) 97 .fail(function( jqXHR ) { 98 var message = ( jqXHR.responseJSON && jqXHR.responseJSON.data && jqXHR.responseJSON.data.message ) || i18n.save_failed || 'The cost of shipping could not be saved.'; 99 debugLog( 'Failed to save the shipping cost from manual input.' ); 100 window.alert( message ); 101 }) 102 .always(function() { 103 $('#edit-shipping-save').prop( 'disabled', false ); 104 $('#edit-shipping-cancel').prop( 'disabled', false ); 105 }); 106 }); 107 108 $('.wc-cos-wcs-import').on('click', function(e){ 109 e.preventDefault(); 110 var $button = $(this); 111 var data = { 112 action: 'set_label_cost_wcs', 113 security: WCCOSApi.nonce, 114 post_id: $button.data('orderid'), 115 }; 116 117 $('.wc-cos-wcs-import').prop('disabled', true); 118 $button.text( i18n.loading || 'Loading...' ); 119 120 $.post( ajaxurl, data ) 121 .done(function( response ) { 122 if ( ! response || ! response.success ) { 123 $button.text( i18n.retry || 'Retry import' ); 124 return; 125 } 126 $button.text( i18n.reimport || 'Re-import' ); 127 $('#wc-cos-protip').remove(); 128 $button.after( 129 $( '<div id="wc-cos-protip"/>' ).append( 130 $( '<p/>' ).append( 131 $( '<b/>' ).text( 'Pro tip: ' ), 132 document.createTextNode( i18n.protip || 'If you purchase your label(s) and then set the order status to complete, the cost of shipping will automatically be imported!' ) 133 ) 134 ) 135 ); 136 if ( response.data && typeof response.data.cost !== 'undefined' ) { 137 debugLog( 'Shipping cost imported.' ); 138 renderPrice( $('.shipping-total .woocommerce-Price-amount'), response.data.currency, response.data.cost ); 139 } 140 }) 141 .fail(function() { 142 debugLog( 'Failed to import shipping cost from WooCommerce Services created label.' ); 143 $button.text( i18n.retry || 'Retry import' ); 144 }) 145 .always(function() { 146 $('.wc-cos-wcs-import').prop('disabled', false); 147 }); 148 }); 149 150 }); -
woo-cost-of-shipping/trunk/includes/admin/class-wc-cos-admin-orders.php
r3102930 r3649832 1 1 <?php 2 2 /** 3 * WooCommerce Cost of ShippingAdmin Orders3 * Cost of Shipping for WooCommerce Admin Orders 4 4 * 5 5 * This file adds the cost of shipping functionality to the Admin Orders page. … … 27 27 * 28 28 */ 29 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;30 29 31 30 if ( ! defined( 'ABSPATH' ) ) { … … 71 70 72 71 // Adds style sheets for the order edit page 73 add_action( 'admin_ print_styles', array( $this, 'enqueue_styles' ) );72 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) ); 74 73 75 74 // Adds javascript files for the order edit page … … 99 98 100 99 /** 100 * Nonce action shared by this plugin's AJAX handlers. 101 * 102 * Deliberately not 'wp_rest': that nonce is minted for every REST call and 103 * is present on any page that enqueues wp-api, so a purpose-built action 104 * keeps the surface that can drive these handlers as small as possible. 105 * 106 * @since 1.5.5 107 */ 108 const AJAX_NONCE_ACTION = 'wc-cos-ajax'; 109 110 /** 111 * Whether the current admin screen is the single-order edit screen. 112 * 113 * Uses the screen id WooCommerce reports for the current storage mode, so it 114 * is correct for both the shop_order post type and HPOS (wc-orders) without 115 * reading $pagenow or $_GET. 116 * 117 * @since 1.5.5 118 * 119 * @return bool 120 */ 121 protected function is_order_edit_screen() { 122 if ( ! function_exists( 'get_current_screen' ) ) { 123 return false; 124 } 125 $screen = get_current_screen(); 126 if ( ! $screen ) { 127 return false; 128 } 129 return function_exists( 'wc_get_page_screen_id' ) 130 ? wc_get_page_screen_id( 'shop-order' ) === $screen->id 131 : 'shop_order' === $screen->id; 132 } 133 134 /** 101 135 * Function to register and enqueue javascript files for the order admin page 102 * 136 * 103 137 * @since 1.1.0 104 138 */ 105 139 public function enqueue_scripts() { 106 global $pagenow; 107 global $post_type; 108 $admin_page = isset( $_GET['page'] ) ? $_GET['page'] : ''; 109 110 if ( ( $pagenow == 'post.php' && $post_type == 'shop_order' ) || ( $admin_page == 'wc-orders' && $pagenow == 'admin.php' ) ) { 111 112 $js_file = ''; 113 $path = realpath( dirname(__FILE__) . '/../' ); 114 115 if ( file_exists( $path . '/assets/js/woo-cost-of-shipping.min.js' ) && ! ( $this->plugin->__get( 'debug' ) || WP_DEBUG ) ) { 116 $js_file = plugins_url( '/assets/js/woo-cost-of-shipping.min.js', plugin_dir_path(__DIR__) ); 117 } 118 else 119 $js_file = plugins_url( '/assets/js/woocommerce-cost-of-shipping.js', plugin_dir_path(__DIR__) ); 120 121 if ( ! empty( $js_file ) ) { 122 wp_register_script( $this->plugin->basename , $js_file, array( 'jquery' ), $this->plugin->version, false ); 123 124 wp_localize_script( $this->plugin->basename, 'WCCOSApi', array( 125 'nonce' => wp_create_nonce('wp_rest'), 126 'currency' => get_woocommerce_currency_symbol(), 127 'debug' => $this->plugin->__get( 'debug' ) 128 )); 129 wp_enqueue_script( $this->plugin->basename ); 130 } 140 if ( ! $this->is_order_edit_screen() ) { 141 return; 142 } 143 144 $js_file = ''; 145 $path = realpath( dirname(__FILE__) . '/../' ); 146 147 if ( file_exists( $path . '/assets/js/woo-cost-of-shipping.min.js' ) && ! ( $this->plugin->__get( 'debug' ) || WP_DEBUG ) ) { 148 $js_file = plugins_url( '/assets/js/woo-cost-of-shipping.min.js', plugin_dir_path(__DIR__) ); 149 } 150 else 151 $js_file = plugins_url( '/assets/js/woocommerce-cost-of-shipping.js', plugin_dir_path(__DIR__) ); 152 153 if ( ! empty( $js_file ) ) { 154 wp_register_script( $this->plugin->basename , $js_file, array( 'jquery' ), $this->plugin->version, false ); 155 156 wp_localize_script( $this->plugin->basename, 'WCCOSApi', array( 157 'nonce' => wp_create_nonce( self::AJAX_NONCE_ACTION ), 158 'currency' => get_woocommerce_currency_symbol(), 159 'debug' => $this->plugin->__get( 'debug' ), 160 'i18n' => array( 161 'reimport' => __( 'Re-Import', 'woo-cost-of-shipping' ), 162 'retry' => __( 'Retry import', 'woo-cost-of-shipping' ), 163 'loading' => __( 'Loading...', 'woo-cost-of-shipping' ), 164 'save_failed' => __( 'The cost of shipping could not be saved.', 'woo-cost-of-shipping' ), 165 'protip' => __( 'If you purchase your label(s) and then set the order status to complete, the cost of shipping will automatically be imported!', 'woo-cost-of-shipping' ), 166 ), 167 )); 168 wp_enqueue_script( $this->plugin->basename ); 131 169 } 132 170 } … … 134 172 /** 135 173 * Function to register and enqueue CSS files for the order admin page 136 * 174 * 137 175 * @since 1.0.0 138 176 */ 139 177 public function enqueue_styles() { 140 global $pagenow; 141 global $post_type; 142 $admin_page = isset( $_GET['page'] ) ? $_GET['page'] : ''; 143 144 if ( ( $pagenow == 'post.php' && $post_type == 'shop_order' ) || ( $pagenow == 'admin.php' && $admin_page == 'wc-orders' ) ) { 145 146 $cs_file = ''; 147 $path = realpath(dirname(__FILE__) . '/../'); 148 149 if ( file_exists( $path . '/assets/css/woocommerce-cost-of-shipping.min.css' ) && !( $this->plugin->__get( 'debug' ) || WP_DEBUG ) ) { 150 $cs_file = plugins_url( '/assets/css/woocommerce-cost-of-shipping.min.css', plugin_dir_path(__DIR__) ); 151 } 152 else 153 $cs_file = plugins_url( '/assets/css/woocommerce-cost-of-shipping.css', plugin_dir_path(__DIR__) ); 154 wp_enqueue_style( $this->plugin->basename, $cs_file, array(), $this->plugin->version ); 155 } 178 if ( ! $this->is_order_edit_screen() ) { 179 return; 180 } 181 182 $cs_file = ''; 183 $path = realpath(dirname(__FILE__) . '/../'); 184 185 if ( file_exists( $path . '/assets/css/woocommerce-cost-of-shipping.min.css' ) && !( $this->plugin->__get( 'debug' ) || WP_DEBUG ) ) { 186 $cs_file = plugins_url( '/assets/css/woocommerce-cost-of-shipping.min.css', plugin_dir_path(__DIR__) ); 187 } 188 else 189 $cs_file = plugins_url( '/assets/css/woocommerce-cost-of-shipping.css', plugin_dir_path(__DIR__) ); 190 wp_enqueue_style( $this->plugin->basename, $cs_file, array(), $this->plugin->version ); 191 } 192 193 /** 194 * Shared guard for the AJAX handlers: nonce, login, capability, and a 195 * real shop_order behind the submitted id. On any failure it sends the 196 * JSON error itself, with a non-200 status so the front end's fail handler 197 * runs, and exits; otherwise it returns the order. 198 * 199 * @since 1.5.5 200 * 201 * @return WC_Order 202 */ 203 protected function ajax_guard() { 204 if ( ! check_ajax_referer( self::AJAX_NONCE_ACTION, 'security', false ) || ! is_user_logged_in() || ! current_user_can( 'manage_woocommerce' ) ) { 205 wp_send_json_error( array( 'message' => __( 'You are not allowed to do that.', 'woo-cost-of-shipping' ) ), 403 ); 206 } 207 208 $order_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0; 209 $order = $order_id ? wc_get_order( $order_id ) : false; 210 211 // wc_get_order() also returns refunds; only a real order carries a cost of shipping. 212 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) { 213 wp_send_json_error( array( 'message' => __( 'Order not found.', 'woo-cost-of-shipping' ) ), 400 ); 214 } 215 216 return $order; 156 217 } 157 218 … … 167 228 public function add_order_shipping_cost_to_order_note( $order_id, $cos, $method ) { 168 229 $order = wc_get_order( $order_id ); 230 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) { 231 return; 232 } 169 233 170 234 $fcos = wc_price( $cos ); 171 $by = $method == 'manual' ? __('manually', 'woocommerce-cost-of-shipping') : sprintf(__('automatically by %s', 'woocommerce-cost-of-shipping'), $method); 235 // $method has been through a public filter, so escape it before it lands in a note. 236 $by = $method == 'manual' ? __('manually', 'woo-cost-of-shipping') : sprintf(__('automatically by %s', 'woo-cost-of-shipping'), esc_html( $method )); 172 237 // If automatically changed by an integration component, there is a change in verbage. 173 238 // Careful on translation here! 174 $note = sprintf( __('Cost of Shipping was changed to %s %s', 'woo commerce-cost-of-shipping'), $fcos, $by );239 $note = sprintf( __('Cost of Shipping was changed to %s %s', 'woo-cost-of-shipping'), $fcos, $by ); 175 240 176 241 $order->add_order_note( $note ); … … 187 252 */ 188 253 function set_shipping_cost_callback() { 189 190 if ( ! check_ajax_referer( 'wp_rest', 'security' ) || ! is_user_logged_in() || ! current_user_can('manage_woocommerce') ) { 191 wp_send_json_error(); 192 wp_die(); 193 } 194 195 $post_id = intval( sanitize_text_field( $_POST['post_id'] ) ); 196 $cos = floatval( sanitize_text_field( $_POST['shipping_cost'] ) ); 254 255 $order = $this->ajax_guard(); 256 $post_id = $order->get_id(); 257 258 $raw = isset( $_POST['shipping_cost'] ) ? sanitize_text_field( wp_unslash( $_POST['shipping_cost'] ) ) : ''; 259 if ( '' === $raw || ! is_numeric( $raw ) ) { 260 wp_send_json_error( array( 'message' => __( 'Cost of shipping must be a number.', 'woo-cost-of-shipping' ) ), 400 ); 261 } 262 $cos = floatval( $raw ); 263 // The input's min="0" is client-side only; enforce it here too. 264 if ( ! is_finite( $cos ) || $cos < 0 ) { 265 wp_send_json_error( array( 'message' => __( 'Cost of shipping must be zero or more.', 'woo-cost-of-shipping' ) ), 400 ); 266 } 197 267 198 268 do_action( 'wc_cos_before_ajax_set_shipping_cost', $post_id, $cos, 'manual' ); 199 269 200 270 // Returns true on success, false on anything else 201 271 if ( $this->store_cost_of_shipping( $post_id, $cos, 'manual' ) ){ 202 272 203 273 do_action( 'wc_cos_after_ajax_set_shipping_cost', $post_id, $cos, 'manual' ); 204 274 wp_send_json_success(); 205 wp_die(); 206 } 207 wp_send_json_error(); 208 wp_die(); 209 275 } 276 // Not an error the user can act on: the value already matched what was stored. 277 wp_send_json_success( array( 'unchanged' => true ) ); 278 210 279 } 211 280 … … 229 298 } 230 299 if ( true === $this->plugin->__get( 'debug' ) ) { 231 $this->log->add( 'the-rite-sites-profit-plugins', __( 'Function: store_cost_of_shipping', 'woo commerce-cost-of-shipping' ) );232 $this->log->add( 'the-rite-sites-profit-plugins', sprintf( __( ' Order cost of shipping being stored to order id %d', 'woo commerce-cost-of-shipping' ), $order_id ) );233 $this->log->add( 'the-rite-sites-profit-plugins', sprintf( __( ' Cost of shipping: %.2f via method: %s', 'woo commerce-cost-of-shipping' ), $cos, $method ) );300 $this->log->add( 'the-rite-sites-profit-plugins', __( 'Function: store_cost_of_shipping', 'woo-cost-of-shipping' ) ); 301 $this->log->add( 'the-rite-sites-profit-plugins', sprintf( __( ' Order cost of shipping being stored to order id %d', 'woo-cost-of-shipping' ), $order_id ) ); 302 $this->log->add( 'the-rite-sites-profit-plugins', sprintf( __( ' Cost of shipping: %.2f via method: %s', 'woo-cost-of-shipping' ), $cos, $method ) ); 234 303 } 235 304 $cos = apply_filters( 'wc_cos_store_cost_of_shipping', $cos, $order_id ); … … 240 309 // Query wc_order 241 310 $order = wc_get_order( $order_id ); 311 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) { 312 if ( true === $this->plugin->__get( 'debug' ) ) { 313 $this->log->add( 'the-rite-sites-profit-plugins', sprintf( __( ' No order found for id %d, nothing stored.', 'woo-cost-of-shipping' ), $order_id ) ); 314 } 315 return false; 316 } 242 317 243 318 // Store for possible history update before the new store overwrites. … … 256 331 if ( ( float ) $old_cos == ( float ) $cos ) { 257 332 if ( true === $this->plugin->__get( 'debug' ) ) { 258 $this->log->add( 'the-rite-sites-profit-plugins', __( ' No need to update Cost of Shipping, values are equivalent.', 'woo commerce-cost-of-shipping' ) );333 $this->log->add( 'the-rite-sites-profit-plugins', __( ' No need to update Cost of Shipping, values are equivalent.', 'woo-cost-of-shipping' ) ); 259 334 } 260 335 return false; … … 262 337 $order->update_meta_data( '_wc_cost_of_shipping', ( float ) $cos ); 263 338 264 $order->update_meta_data( $order_id, '_wc_cos_method', $method ); 339 // 1.5.0 - 1.5.4 passed ( $order_id, '_wc_cos_method', $method ) here, a leftover 340 // from the update_post_meta() signature, so the method was never stored and a 341 // stray meta row keyed by the order id was written instead. Fixed in 1.5.5. 342 $order->update_meta_data( '_wc_cos_method', $method ); 265 343 266 344 if ( true === $this->plugin->__get( 'debug' ) ) { 267 $this->log->add( 'the-rite-sites-profit-plugins', __( ' Cost of shipping value and method updated.', 'woo commerce-cost-of-shipping' ) );345 $this->log->add( 'the-rite-sites-profit-plugins', __( ' Cost of shipping value and method updated.', 'woo-cost-of-shipping' ) ); 268 346 } 269 347 … … 279 357 280 358 if ( true === $this->plugin->__get( 'debug' ) ) { 281 $this->log->add( 'the-rite-sites-profit-plugins', __( ' Cost of shipping history updated with value and method.', 'woo commerce-cost-of-shipping' ) );359 $this->log->add( 'the-rite-sites-profit-plugins', __( ' Cost of shipping history updated with value and method.', 'woo-cost-of-shipping' ) ); 282 360 } 283 361 } … … 297 375 */ 298 376 public function maybe_save_order_shipping_cost_shipstation( $order, $args ) { 299 300 $xml = $this->get_parsed_xml( $args['xml'] ); 301 377 302 378 if ( is_null( $this->log ) ) { 303 379 $this->log = new WC_Logger(); 304 380 } 305 381 382 if ( ! is_a( $order, 'WC_Order' ) || ! is_array( $args ) || empty( $args['xml'] ) || ! is_string( $args['xml'] ) ) { 383 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration fired without an order or xml payload', 'woo-cost-of-shipping' ) ); 384 return; 385 } 386 387 $xml = $this->get_parsed_xml( $args['xml'] ); 388 306 389 if ( ! isset( $xml ) ) { 307 $this->log->add( 'shipstation', __( 'shipstation integration xml not set', 'woo commerce-cost-of-shipping' ) );308 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration xml not set', 'woo commerce-cost-of-shipping' ) );390 $this->log->add( 'shipstation', __( 'shipstation integration xml not set', 'woo-cost-of-shipping' ) ); 391 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration xml not set', 'woo-cost-of-shipping' ) ); 309 392 } 310 393 elseif ( empty( $xml ) ) { 311 $this->log->add( 'shipstation', __( 'shipstation integration xml is empty', 'woo commerce-cost-of-shipping' ) );312 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration xml is empty', 'woo commerce-cost-of-shipping' ) );394 $this->log->add( 'shipstation', __( 'shipstation integration xml is empty', 'woo-cost-of-shipping' ) ); 395 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration xml is empty', 'woo-cost-of-shipping' ) ); 313 396 } 314 397 elseif ( ! isset( $xml->ShippingCost ) ) { 315 $this->log->add( 'shipstation', __( 'shipstation integration xml does not have shipping amount set', 'woo commerce-cost-of-shipping' ) );316 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration xml does not have shipping amount set', 'woo commerce-cost-of-shipping' ) );398 $this->log->add( 'shipstation', __( 'shipstation integration xml does not have shipping amount set', 'woo-cost-of-shipping' ) ); 399 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration xml does not have shipping amount set', 'woo-cost-of-shipping' ) ); 317 400 } 318 401 else { 319 $shipping_cost = wc_clean( $xml->ShippingCost ); 402 // wc_clean() passes non-scalars through untouched, so cast the 403 // SimpleXMLElement to a string first and then to the float we store. 404 $shipping_cost = floatval( wc_clean( (string) $xml->ShippingCost ) ); 405 if ( ! is_finite( $shipping_cost ) || $shipping_cost < 0 ) { 406 $this->log->add( 'the-rite-sites-profit-plugins', __( 'shipstation integration xml shipping amount is not a valid cost', 'woo-cost-of-shipping' ) ); 407 return; 408 } 320 409 $this->store_cost_of_shipping( $order->get_id(), $shipping_cost, 'shipstation' ); 321 410 $this->log->add( 'shipstation', 'After shipstation automatically stored cost of shipping.'); … … 341 430 if ( true === is_woocommerce_services_active() ) { 342 431 if ( true === $this->plugin->__get( 'debug' ) ) 343 $this->log->add( 'the-rite-sites-profit-plugins', __( 'WooCommerce services is active, retrieving labels.', 'woo commerce-cost-of-shipping' ) );432 $this->log->add( 'the-rite-sites-profit-plugins', __( 'WooCommerce services is active, retrieving labels.', 'woo-cost-of-shipping' ) ); 344 433 $labels = $this->get_wcs_label_order_meta_data( $order_id ); 345 434 $flag = false; … … 353 442 354 443 $order = wc_get_order( $order_id ); 444 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) return false; 355 445 $previous_cos_method = $order->get_meta( '_wc_cos_method', true ); 356 446 if ( false === $flag && ! ( 'wc_services' === $previous_cos_method ) ) return false; … … 361 451 else { 362 452 if ( true === $this->plugin->__get( 'debug' ) ) 363 $this->log->add( 'the-rite-sites-profit-plugins', __( ' WooCommerce services is not active, ignoring trying to save WooCommerce services labels as cost of shipping.', 'woo commerce-cost-of-shipping' ) );453 $this->log->add( 'the-rite-sites-profit-plugins', __( ' WooCommerce services is not active, ignoring trying to save WooCommerce services labels as cost of shipping.', 'woo-cost-of-shipping' ) ); 364 454 } 365 455 return false; … … 374 464 * @return bool 375 465 */ 376 public function should_show_wcs_cos_meta_box() { 377 global $post_type, $pagenow; 378 $admin_page = isset( $_GET['page'] ) ? $_GET['page'] : ''; 379 380 if ( true === is_woocommerce_services_active() ) { 381 $order = wc_get_order(); 382 if ( $order && ( ( 'shop_order' === $post_type && ! ( 'post-new.php' === $pagenow ) || ( $pagenow == 'admin.php' && $admin_page == 'wc-orders' ) ) ) ) { 383 return true; 384 } 385 } 386 return false; 466 public function should_show_wcs_cos_meta_box( $screen_id = '', $post_or_order = null ) { 467 if ( true !== is_woocommerce_services_active() ) { 468 return false; 469 } 470 // add_meta_boxes passes the screen id: 'shop_order' for the post type, 471 // the wc-orders page id under HPOS. wc_get_page_screen_id() returns 472 // whichever is in force, so one comparison covers both storage modes. 473 $order_screen = function_exists( 'wc_get_page_screen_id' ) ? wc_get_page_screen_id( 'shop-order' ) : 'shop_order'; 474 if ( $screen_id !== $order_screen ) { 475 return false; 476 } 477 // The second argument is a WP_Post under the post type and a WC_Order under 478 // HPOS. An order being created is auto-draft in both modes; that is the 479 // add-new case the old $pagenow check excluded. 480 $order = ( $post_or_order instanceof WP_Post ) ? wc_get_order( $post_or_order->ID ) : $post_or_order; 481 return $order && is_a( $order, 'WC_Order' ) && 'auto-draft' !== $order->get_status(); 387 482 } 388 483 … … 398 493 public function display_wcs_cos_meta_box( $post_id ) { 399 494 $order = ( $post_id instanceof WP_Post ) ? wc_get_order( $post_id->ID ) : $post_id; 400 // $order = wc_get_order( $post_id ); 495 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) { 496 return; 497 } 401 498 402 499 $cos = floatval( $order->get_meta( '_wc_cost_of_shipping' ) ); … … 405 502 ?> 406 503 <div class="wc-cos-wrapper"> 407 <button class="wc-cos-wcs-import button-primary" data-orderid="<?php echo $order->get_id() ?>" id="wcs-shipping-import">Import</button>504 <button class="wc-cos-wcs-import button-primary" data-orderid="<?php echo esc_attr( $order->get_id() ); ?>" id="wcs-shipping-import"><?php esc_html_e( 'Import', 'woo-cost-of-shipping' ); ?></button> 408 505 </div> 409 506 <?php … … 412 509 ?> 413 510 <div class="wc-cos-wrapper"> 414 <button class="wc-cos-wcs-import button-secondary" data-orderid="<?php echo $order->get_id() ?>" id="wcs-shipping-re-import">Re-Import</button>511 <button class="wc-cos-wcs-import button-secondary" data-orderid="<?php echo esc_attr( $order->get_id() ); ?>" id="wcs-shipping-re-import"><?php esc_html_e( 'Re-Import', 'woo-cost-of-shipping' ); ?></button> 415 512 </div> 416 513 <?php … … 429 526 */ 430 527 public function set_label_cost_wcs_callback() { 431 if ( ! check_ajax_referer( 'wp_rest', 'security' ) || ! is_user_logged_in() || ! current_user_can( 'manage_woocommerce' ) ) { 432 wp_send_json_error(); 433 wp_die(); 434 } 435 436 $post_id = intval( sanitize_text_field( $_POST['post_id'] ) ); 528 $order = $this->ajax_guard(); 529 $post_id = $order->get_id(); 437 530 438 531 do_action( 'wc_cos_before_ajax_set_label_cost_wcs', $post_id ); 439 440 // Returns true on success, false on anything else 441 if ( $cost = $this->maybe_save_order_shipping_cost_woocommerce_services( $post_id ) ) { 532 533 // Returns the stored cost (which may legitimately be 0.00) on success, false on anything else. 534 $cost = $this->maybe_save_order_shipping_cost_woocommerce_services( $post_id ); 535 if ( false !== $cost ) { 442 536 do_action( 'wc_cos_after_ajax_set_label_cost_wcs', $post_id ); 443 537 wp_send_json_success( array( 'cost' => $cost, 'currency' => get_woocommerce_currency_symbol() ) ); 444 wp_die(); 445 } 446 elseif ( false === $cost ) { 447 do_action( 'wc_cos_after_ajax_no_labels_wcs', $post_id ); 448 wp_send_json_success( array( 'message' => 'No labels exist.' ) ); 449 wp_die(); 450 } 451 wp_send_json_error(); 452 wp_die(); 538 } 539 do_action( 'wc_cos_after_ajax_no_labels_wcs', $post_id ); 540 wp_send_json_success( array( 'message' => __( 'No labels exist.', 'woo-cost-of-shipping' ) ) ); 453 541 } 454 542 … … 462 550 * @param int $post The current post ID. 463 551 */ 464 public function add_wcs_cos_meta_box( $post_type, $post ) {465 if ( $this->should_show_wcs_cos_meta_box( ) ) {466 467 $screen = class_exists( '\Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' ) && wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()468 ? wc_get_page_screen_id( 'shop-order' )469 : 'shop_order';552 public function add_wcs_cos_meta_box( $post_type, $post = null ) { 553 if ( $this->should_show_wcs_cos_meta_box( $post_type, $post ) ) { 554 555 // should_show_wcs_cos_meta_box() has already matched $post_type against 556 // the order screen id for the storage mode in force, so it is the screen. 557 $screen = $post_type; 470 558 471 559 add_meta_box( 472 560 'wc-cos-services-cost-of-shipping', 473 __( 'Import Cost of Shipping', 'woo commerce-cost-of-shipping' ),561 __( 'Import Cost of Shipping', 'woo-cost-of-shipping' ), 474 562 array( $this, 'display_wcs_cos_meta_box' ), 475 563 $screen, … … 481 569 482 570 /** 571 * Attempts to recover a faulty json string field that might contain unescaped quotes, 572 * e.g. "package_name":"12" x 9" box". Escapes any quote inside the value that is not 573 * already escaped and is not the closing quote. 574 * 575 * Called since 1.2.0 but never defined until 1.5.5, so the legacy-string labels path 576 * used to fatal on any store whose wc_connect_labels meta was not valid JSON. 577 * 578 * @since 1.5.5 579 * 580 * @param string $field_name 581 * @param string $json 582 * 583 * @return string 584 */ 585 public function try_recover_invalid_json_string( $field_name, $json ) { 586 $regex = '/"' . preg_quote( $field_name, '/' ) . '":"(.+?)"(?=\s*[,}])/'; 587 preg_match_all( $regex, $json, $match_groups ); 588 if ( 2 === count( $match_groups ) ) { 589 foreach ( $match_groups[ 0 ] as $idx => $match ) { 590 $value = $match_groups[ 1 ][ $idx ]; 591 $escaped_value = preg_replace( '/(?<!\\\)"/', '\\"', $value ); 592 $json = str_replace( '"' . $field_name . '":"' . $value . '"', '"' . $field_name . '":"' . $escaped_value . '"', $json ); 593 } 594 } 595 return $json; 596 } 597 598 /** 483 599 * Attempts to recover faulty json string array fields that might contain strings with unescaped quotes 484 600 * 485 601 * @since 1.2.0 486 * 602 * 487 603 * @param string $field_name 488 604 * @param string $json … … 542 658 public function get_wcs_label_order_meta_data( $order_id ) { 543 659 $order = wc_get_order( ( int ) $order_id ); 660 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) { 661 return array(); 662 } 544 663 $label_data = $order->get_meta( 'wc_connect_labels', true ); 545 664 //return an empty array if the data doesn't exist … … 578 697 579 698 if ( ! $success ) { 580 $this->log->add( 'the-rite-sites-profit-plugins', __( 'wpcom_safe_simplexml_load_string(): Error loading XML string in shipstation xml retrieve', 'woo commerce-cost-of-shipping' ) );699 $this->log->add( 'the-rite-sites-profit-plugins', __( 'wpcom_safe_simplexml_load_string(): Error loading XML string in shipstation xml retrieve', 'woo-cost-of-shipping' ) ); 581 700 return false; 582 701 } 583 702 584 703 if ( isset( $dom->doctype ) ) { 585 $this->log->add( 'the-rite-sites-profit-plugins', __( 'wpcom_safe_simplexml_import_dom(): Unsafe DOCTYPE Detected in shipstation xml retrieve', 'woo commerce-cost-of-shipping' ) );704 $this->log->add( 'the-rite-sites-profit-plugins', __( 'wpcom_safe_simplexml_import_dom(): Unsafe DOCTYPE Detected in shipstation xml retrieve', 'woo-cost-of-shipping' ) ); 586 705 return false; 587 706 } … … 600 719 */ 601 720 public function show_order_shipping_cost( $post_id ) { 602 603 $ajax_nonce = wp_create_nonce( 'edit-shipping-meta-nonce' ); 721 604 722 $order = wc_get_order( $post_id ); 723 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) { 724 return; 725 } 605 726 $cost_of_shipping = floatval( $order->get_meta( '_wc_cost_of_shipping', true ) ); 606 727 $formatted_total = wc_price( $cost_of_shipping ); … … 608 729 <tr> 609 730 610 <td class="total shipping-total"><?php esc_html_e( 'Shipping Cost', 'woo commerce-cost-of-shipping' ); ?>:</td>731 <td class="total shipping-total"><?php esc_html_e( 'Shipping Cost', 'woo-cost-of-shipping' ); ?>:</td> 611 732 612 733 <td width="1%"></td> … … 614 735 <td class="total shipping-total"> 615 736 <span id="shipping-cost"> 616 <?php echo $formatted_total; ?>617 737 <?php echo wp_kses_post( $formatted_total ); ?> 738 618 739 <div class="edit-shipping-tooltip" style="display:none;"> 619 <input id="shipping-cost-input" type="number" step="0.01" min="0" value="<?php echo esc_attr( $cost_of_shipping); ?>" />740 <input id="shipping-cost-input" type="number" step="0.01" min="0" value="<?php echo esc_attr( $cost_of_shipping ); ?>" /> 620 741 <div class="edit-shipping-buttons"> 621 <button id="edit-shipping-cancel" class="button" type="button"> Cancel</button>622 <button id="edit-shipping-save" class="button button-primary" type="button"> Save</button>742 <button id="edit-shipping-cancel" class="button" type="button"><?php esc_html_e( 'Cancel', 'woo-cost-of-shipping' ); ?></button> 743 <button id="edit-shipping-save" class="button button-primary" type="button"><?php esc_html_e( 'Save', 'woo-cost-of-shipping' ); ?></button> 623 744 </div> 624 745 </div> … … 626 747 </td> 627 748 628 <td width="1%"><a data-orderid="<?php echo $post_id; ?>" id="edit-shipping-cost">edit</a></td>749 <td width="1%"><a data-orderid="<?php echo esc_attr( $order->get_id() ); ?>" id="edit-shipping-cost"><?php esc_html_e( 'edit', 'woo-cost-of-shipping' ); ?></a></td> 629 750 630 751 </tr> -
woo-cost-of-shipping/trunk/includes/admin/class-wc-cos-safe-domdocument.php
r1898876 r3649832 19 19 * @return bool|DOMDocument true on success, false on failure. If called statically (E_STRICT error), returns DOMDocument on success. 20 20 */ 21 /** 22 * PHP 8.1 deprecates overriding a built-in method without a compatible 23 * return type. DOMDocument::load() declares `: bool`, but this override 24 * deliberately returns the parsed document or false, and it must keep 25 * working on PHP 7.0 (the plugin's declared floor), where a `: bool` 26 * declaration would be a fatal signature mismatch. 27 * 28 * ReturnTypeWillChange silences the deprecation without changing behaviour. 29 * On PHP 7 the line parses as a comment, so it is safe on both. 30 */ 31 #[\ReturnTypeWillChange] 21 32 public function load( $filename, $options = 0 ) { 22 33 if ( '' === $filename ) { … … 51 62 * @return bool|DOMDocument true on success, false on failure. If called statically (E_STRICT error), returns DOMDocument on success. 52 63 */ 64 /** 65 * PHP 8.1 deprecates overriding a built-in method without a compatible 66 * return type. DOMDocument::loadXML() declares `: bool`, but this override 67 * deliberately returns the parsed document or false, and it must keep 68 * working on PHP 7.0 (the plugin's declared floor), where a `: bool` 69 * declaration would be a fatal signature mismatch. 70 * 71 * ReturnTypeWillChange silences the deprecation without changing behaviour. 72 * On PHP 7 the line parses as a comment, so it is safe on both. 73 */ 74 #[\ReturnTypeWillChange] 53 75 public function loadXML( $source, $options = 0 ) { 54 76 if ( '' === $source ) { … … 62 84 $old = null; 63 85 64 if ( function_exists( 'libxml_disable_entity_loader' ) ) { 86 // PHP 8.0 deprecates libxml_disable_entity_loader() because libxml 2.9+ 87 // no longer loads external entities unless asked to, so the call is 88 // only made on PHP 7, where it is still the guard. 89 if ( PHP_VERSION_ID < 80000 && function_exists( 'libxml_disable_entity_loader' ) ) { 65 90 $old = libxml_disable_entity_loader( true ); 66 91 } 67 92 68 $return = parent::loadXML( $source, $options ); 93 // Never let the parser reach the network for a DTD or entity, whatever 94 // the caller passed. Belt and braces alongside the DOCTYPE bail below. 95 $return = parent::loadXML( $source, $options | LIBXML_NONET ); 69 96 70 97 if ( ! is_null( $old ) ) { -
woo-cost-of-shipping/trunk/woo-includes/class-wc-cos-dependencies.php
r2061967 r3649832 1 1 <?php 2 if ( ! defined( 'ABSPATH' ) ) { 3 exit; 4 } 5 2 6 /** 3 7 * WC Dependency Checker -
woo-cost-of-shipping/trunk/woo-includes/woo-functions.php
r2061967 r3649832 1 1 <?php 2 if ( ! defined( 'ABSPATH' ) ) { 3 exit; 4 } 5 2 6 /** 3 7 * Functions used by plugins -
woo-cost-of-shipping/trunk/woocommerce-cost-of-shipping.php
r3102930 r3649832 1 1 <?php 2 2 /** 3 * Plugin Name: WooCommerce Cost of Shipping4 * Plugin URI: https://www.theritesites.com/plugins/woo commerce-cost-of-shipping3 * Plugin Name: Cost of Shipping for WooCommerce 4 * Plugin URI: https://www.theritesites.com/plugins/woo-cost-of-shipping 5 5 * Description: Allows the association of the cost of shipping to WooCommerce orders 6 * Version: 1.5. 46 * Version: 1.5.5 7 7 * Author: TheRiteSites 8 8 * Author URI: https://www.theritesites.com 9 * Donate link: https://www.theritesites.com/plugins/woo commerce-cost-of-shipping9 * Donate link: https://www.theritesites.com/plugins/woo-cost-of-shipping 10 10 * License: GPL-2.0+ 11 * Text Domain: woo commerce-cost-of-shipping11 * Text Domain: woo-cost-of-shipping 12 12 * Domain Path: /languages 13 * WC tested up to: 8.913 * WC tested up to: 10.9 14 14 * WC requires at least: 3.0 15 15 * Requires at least: 5.2 16 16 * 17 * @link https://www.theritesites.com/plugins/woo commerce-cost-of-shipping17 * @link https://www.theritesites.com/plugins/woo-cost-of-shipping 18 18 * 19 19 * @package WC_COS 20 * @version 1.5. 420 * @version 1.5.5 21 21 */ 22 22 … … 43 43 */ 44 44 45 45 if ( ! defined( 'ABSPATH' ) ) { 46 exit; 47 } 46 48 47 49 if ( ! function_exists( 'is_woocommerce_active' ) ) { … … 62 64 * @since 1.0.0 63 65 */ 64 const VERSION = '1.5. 3';66 const VERSION = '1.5.5'; 65 67 66 68 /** … … 211 213 212 214 // Load translated strings for plugin. 213 load_plugin_textdomain( 'woo commerce-cost-of-shipping', false, dirname( $this->basename ) . '/languages/' );215 load_plugin_textdomain( 'woo-cost-of-shipping', false, dirname( $this->basename ) . '/languages/' ); 214 216 215 217 // Initialize plugin classes. … … 271 273 272 274 if ( ! is_woocommerce_active() ) { 273 array_push( $this->activation_errors, ' WooCommerce is a required plugin for WooCommerce Cost of Shipping!' );275 array_push( $this->activation_errors, 'Cost of Shipping for WooCommerce requires WooCommerce to be installed and active.' ); 274 276 $flag = false; 275 277 } … … 287 289 288 290 // Compile default message. 289 $default_message = sprintf( __( ' WooCommerce Cost of Shipping is missing requirements and has been <a href="%s">deactivated</a>. Please make sure all requirements are available.', 'woocommerce-cost-of-shipping' ), admin_url( 'plugins.php' ) );291 $default_message = sprintf( __( 'Cost of Shipping for WooCommerce is missing requirements and has been <a href="%s">deactivated</a>. Please make sure all requirements are available.', 'woo-cost-of-shipping' ), admin_url( 'plugins.php' ) ); 290 292 291 293 // Default details to null.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)