PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Shortcodes / ShortcodeManager.php

ShortcodeManager.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at includes/Shortcodes/ShortcodeManager.php

247 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shortcode Manager for Easy Invoice Free
4 *
5 * Manages shortcodes for invoice and quote URLs
6 *
7 * @package EasyInvoice
8 * @subpackage Shortcodes
9 * @since 2.0.0
10 */
11
12 namespace EasyInvoice\Shortcodes;
13
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 /**
19 * ShortcodeManager Class
20 */
21 class ShortcodeManager {
22
23 /**
24 * Constructor
25 */
26 public function __construct() {
27 // Register shortcodes
28 add_shortcode('easy_invoice_url', [$this, 'renderInvoiceUrl']);
29 add_shortcode('easy_quote_url', [$this, 'renderQuoteUrl']);
30
31 // Add shortcode info to help tab
32 add_action('admin_head', [$this, 'addShortcodeHelp']);
33 }
34
35 /**
36 * Render invoice URL shortcode
37 *
38 * @param array $atts Shortcode attributes
39 * @return string Rendered shortcode
40 */
41 public function renderInvoiceUrl($atts) {
42 $atts = shortcode_atts([
43 'id' => 0,
44 'number' => '',
45 'text' => '',
46 'class' => 'easy-invoice-url',
47 'target' => '_blank'
48 ], $atts, 'easy_invoice_url');
49
50 // Get invoice by ID or number
51 $invoice = null;
52 if (!empty($atts['id'])) {
53 $invoice = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository()->find($atts['id']);
54 } elseif (!empty($atts['number'])) {
55 $invoice = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository()->findByNumber($atts['number']);
56 }
57
58 if (!$invoice) {
59 return '<span class="easy-invoice-error">' . __('Invoice not found', 'easy-invoice') . '</span>';
60 }
61
62 $url = get_permalink($invoice->getId());
63 // Only use secure link if enabled in settings and available
64 $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes';
65 if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) {
66 $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getInvoiceSecureLinkUrl($invoice->getId());
67 if ($secure_url) {
68 $url = $secure_url;
69 }
70 }
71
72 // SECURITY: the per-invoice access token authorises manual-payment
73 // submission for the named invoice. Two rules govern when it can
74 // be attached to a shortcode-rendered URL:
75 //
76 // 1. NEVER mint a new token from this code path. The shortcode
77 // is callable by anyone able to author rendered content
78 // (Contributors previewing drafts, public template includes),
79 // so auto-minting via invoiceAccessToken() here would let a
80 // low-privileged actor create payment-auth tokens for any
81 // invoice ID they can guess. We read existing tokens only.
82 // 2. Even reading an existing token is gated. Only the admin
83 // (manage_options) or a viewer who already passes the
84 // ownership check (canSubmitPaymentForInvoice — which
85 // itself accepts a token already presented via ?ik=) gets
86 // the keyed URL. Everyone else gets the bare permalink.
87 //
88 // Legitimate flows still work:
89 // * Admin sends invoice email -> EmailManager (server-trusted)
90 // mints + embeds the token in the emailed URL directly.
91 // * Client clicks the emailed link -> arrives with ?ik= in URL
92 // -> canSubmitPaymentForInvoice passes -> shortcode renders
93 // other invoice URLs on the page with the keyed form too.
94 // * Admin embedding [easy_invoice_url] on an admin-context
95 // page sees the keyed URL via the manage_options branch.
96 $invoice_id_int = (int) $invoice->getId();
97 $invoice_access_token = '';
98 if (current_user_can('manage_options')
99 || \EasyInvoice\Controllers\InvoiceController::canSubmitPaymentForInvoice($invoice_id_int, $invoice)) {
100 $invoice_access_token = \EasyInvoice\Controllers\InvoiceController::invoiceAccessTokenIfExists($invoice_id_int);
101 }
102 if ($invoice_access_token !== '' && $url) {
103 $url = add_query_arg('ik', $invoice_access_token, $url);
104 }
105
106 $text = !empty($atts['text']) ? $atts['text'] : $invoice->getNumber();
107 $class = esc_attr($atts['class']);
108 $target = esc_attr($atts['target']);
109
110 return sprintf(
111 '<a href="%s" class="%s" target="%s">%s</a>',
112 esc_url($url),
113 $class,
114 $target,
115 esc_html($text)
116 );
117 }
118
119 /**
120 * Render quote URL shortcode
121 *
122 * @param array $atts Shortcode attributes
123 * @return string Rendered shortcode
124 */
125 public function renderQuoteUrl($atts) {
126 $atts = shortcode_atts([
127 'id' => 0,
128 'number' => '',
129 'text' => '',
130 'class' => 'easy-quote-url',
131 'target' => '_blank'
132 ], $atts, 'easy_quote_url');
133
134 // Get quote by ID or number
135 $quote = null;
136 if (!empty($atts['id'])) {
137 $quote = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->find($atts['id']);
138 } elseif (!empty($atts['number'])) {
139 $quote = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->findByNumber($atts['number']);
140 }
141
142 if (!$quote) {
143 return '<span class="easy-invoice-error">' . __('Quote not found', 'easy-invoice') . '</span>';
144 }
145
146 $url = get_permalink($quote->getId());
147 $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes';
148 if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) {
149 $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getQuoteSecureLinkUrl($quote->getId());
150 if ($secure_url) {
151 $url = $secure_url;
152 }
153 }
154
155 // SECURITY (CVE-2026-9021 + follow-up): the per-quote access
156 // token authorises Accept/Decline. Same two-rule policy as the
157 // invoice shortcode above (see renderInvoiceUrl for full
158 // rationale):
159 //
160 // 1. NEVER mint a new token from the shortcode path — readers
161 // only. Trusted email composition (EmailManager) keeps
162 // using quoteAccessToken() which mints on first send.
163 // 2. Only disclose an existing token to an admin or a viewer
164 // who already passes the ownership check (canActOnQuote).
165 // Other viewers get the bare permalink — they can view
166 // the quote but not Accept/Decline until they arrive via
167 // a legitimately-emailed link.
168 $quote_id_int = (int) $quote->getId();
169 $quote_access_token = '';
170 if (current_user_can('manage_options')
171 || \EasyInvoice\Controllers\QuoteController::canActOnQuote($quote_id_int, $quote)) {
172 $quote_access_token = \EasyInvoice\Controllers\QuoteController::quoteAccessTokenIfExists($quote_id_int);
173 }
174 if ($quote_access_token !== '' && $url) {
175 $url = add_query_arg('qk', $quote_access_token, $url);
176 }
177
178 $text = !empty($atts['text']) ? $atts['text'] : $quote->getNumber();
179 $class = esc_attr($atts['class']);
180 $target = esc_attr($atts['target']);
181
182 return sprintf(
183 '<a href="%s" class="%s" target="%s">%s</a>',
184 esc_url($url),
185 $class,
186 $target,
187 esc_html($text)
188 );
189 }
190
191 /**
192 * Add shortcode help to the help tab
193 */
194 public function addShortcodeHelp() {
195 $screen = get_current_screen();
196
197 if ($screen && ($screen->id === 'easy-invoice_page_easy-invoice-settings' || $screen->id === 'edit-easy_invoice')) {
198 $screen->add_help_tab([
199 'id' => 'easy-invoice-shortcodes',
200 'title' => __('Shortcodes', 'easy-invoice'),
201 'content' => $this->getShortcodeHelpContent()
202 ]);
203 }
204 }
205
206 /**
207 * Get shortcode help content
208 *
209 * @return string Help content
210 */
211 private function getShortcodeHelpContent() {
212 $content = '<h2>' . __('Available Shortcodes', 'easy-invoice') . '</h2>';
213
214 $content .= '<h3><code>[easy_invoice_url]</code></h3>';
215 $content .= '<p>' . __('Displays a link to an invoice.', 'easy-invoice') . '</p>';
216 $content .= '<h4>' . __('Attributes:', 'easy-invoice') . '</h4>';
217 $content .= '<ul>';
218 $content .= '<li><code>id</code> - ' . __('Invoice ID (required if number is not provided)', 'easy-invoice') . '</li>';
219 $content .= '<li><code>number</code> - ' . __('Invoice number (required if id is not provided)', 'easy-invoice') . '</li>';
220 $content .= '<li><code>text</code> - ' . __('Link text (default: invoice number)', 'easy-invoice') . '</li>';
221 $content .= '<li><code>class</code> - ' . __('CSS class for the link (default: easy-invoice-url)', 'easy-invoice') . '</li>';
222 $content .= '<li><code>target</code> - ' . __('Link target (default: _blank)', 'easy-invoice') . '</li>';
223 $content .= '</ul>';
224 $content .= '<h4>' . __('Examples:', 'easy-invoice') . '</h4>';
225 $content .= '<pre><code>[easy_invoice_url id="123" text="View Invoice"]</code></pre>';
226 $content .= '<pre><code>[easy_invoice_url number="INV-001" text="Click here to view"]</code></pre>';
227
228 $content .= '<hr>';
229
230 $content .= '<h3><code>[easy_quote_url]</code></h3>';
231 $content .= '<p>' . __('Displays a link to a quote.', 'easy-invoice') . '</p>';
232 $content .= '<h4>' . __('Attributes:', 'easy-invoice') . '</h4>';
233 $content .= '<ul>';
234 $content .= '<li><code>id</code> - ' . __('Quote ID (required if number is not provided)', 'easy-invoice') . '</li>';
235 $content .= '<li><code>number</code> - ' . __('Quote number (required if id is not provided)', 'easy-invoice') . '</li>';
236 $content .= '<li><code>text</code> - ' . __('Link text (default: quote number)', 'easy-invoice') . '</li>';
237 $content .= '<li><code>class</code> - ' . __('CSS class for the link (default: easy-quote-url)', 'easy-invoice') . '</li>';
238 $content .= '<li><code>target</code> - ' . __('Link target (default: _blank)', 'easy-invoice') . '</li>';
239 $content .= '</ul>';
240 $content .= '<h4>' . __('Examples:', 'easy-invoice') . '</h4>';
241 $content .= '<pre><code>[easy_quote_url id="456" text="View Quote"]</code></pre>';
242 $content .= '<pre><code>[easy_quote_url number="QT-001" text="Click here to view"]</code></pre>';
243
244 return $content;
245 }
246 }
247