PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
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 / Services / DocumentPage.php

DocumentPage.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at includes/Services/DocumentPage.php

351 lines 15.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Assets for the public invoice / quote page.
4 *
5 * @package Easy_Invoice
6 * @subpackage Services
7 */
8
9 namespace EasyInvoice\Services;
10
11 use EasyInvoice\Constants\PostTypes;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Puts the public document page on WordPress's asset pipeline.
19 *
20 * Why this exists
21 * ---------------
22 * The public invoice and quote pages used to be self-contained HTML documents:
23 * their own <!DOCTYPE>, hand-written <script src> tags, six hundred lines of
24 * inline CSS and JavaScript each, and no wp_head() or wp_footer(). Two things
25 * followed. Anything the plugin *enqueued* for those pages never printed --
26 * the bank-transfer form's submit script was enqueued on every invoice page
27 * and loaded on none of them -- and anything a site added through the normal
28 * hooks (analytics, consent banners, a caching plugin's markers) was absent.
29 *
30 * The page now calls wp_head() and wp_footer() like any other, with its own
31 * stylesheet and script enqueued here.
32 *
33 * Isolation
34 * ---------
35 * A theme's stylesheet is written for the theme's pages, not for a document a
36 * client is about to print, so by default every style and script that is not
37 * ours (or a dependency of ours) is dequeued on these pages. A theme that
38 * wants in -- to supply fonts, say -- returns false from
39 * `easy_invoice_document_isolate_assets`, or whitelists handles through
40 * `easy_invoice_document_allowed_handles`.
41 */
42 class DocumentPage {
43
44 /** Stylesheet handle. */
45 const STYLE = 'easy-invoice-document';
46
47 /** Script handle. */
48 const SCRIPT = 'easy-invoice-document';
49
50 /**
51 * Wire it up.
52 *
53 * @return void
54 */
55 public static function init(): void {
56 add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue' ], 20 );
57 add_action( 'wp_enqueue_scripts', [ __CLASS__, 'isolate' ], PHP_INT_MAX );
58 add_filter( 'body_class', [ __CLASS__, 'bodyClass' ] );
59 add_filter( 'show_admin_bar', [ __CLASS__, 'hideAdminBar' ] );
60 }
61
62 /**
63 * Is the current request a public invoice or quote?
64 *
65 * @return string 'invoice', 'quote' or ''.
66 */
67 public static function currentType(): string {
68 if ( is_admin() || ! is_singular() ) {
69 return '';
70 }
71 if ( is_singular( PostTypes::EASY_INVOICE_POST_TYPE ) ) {
72 return 'invoice';
73 }
74 if ( is_singular( PostTypes::EASY_INVOICE_QUOTE_POST_TYPE ) ) {
75 return 'quote';
76 }
77 return '';
78 }
79
80 /**
81 * Cache-busting version for one of the page's own files: the plugin
82 * version plus the file's modification time, so a changed file is never
83 * served stale from a browser or CDN that keys on the query string.
84 *
85 * @param string $relative Path inside the plugin.
86 * @return string
87 */
88 private static function assetVersion( string $relative ): string {
89 $path = EASY_INVOICE_PLUGIN_DIR . $relative;
90 $mtime = file_exists( $path ) ? (int) filemtime( $path ) : 0;
91 return EASY_INVOICE_VERSION . ( $mtime ? '.' . $mtime : '' );
92 }
93
94 /**
95 * Register and enqueue the page's own assets.
96 *
97 * @return void
98 */
99 public static function enqueue(): void {
100 $type = self::currentType();
101 if ( '' === $type ) {
102 return;
103 }
104
105 $document_id = get_queried_object_id();
106
107 wp_register_style( self::STYLE, EASY_INVOICE_PLUGIN_URL . 'assets/css/document.css', [], self::assetVersion( 'assets/css/document.css' ) );
108 wp_enqueue_style( self::STYLE );
109
110 // Shared UI pieces the admin already ships.
111 wp_register_script( 'easy-invoice-toast', EASY_INVOICE_PLUGIN_URL . 'assets/js/easy-invoice-toast.js', [], EASY_INVOICE_VERSION, true );
112
113 // In-browser PDF: html2canvas + jsPDF capture the page as drawn. The
114 // default for the Download button, and the fallback whenever the
115 // server cannot render.
116 wp_register_script( 'html2canvas', EASY_INVOICE_PLUGIN_URL . 'assets/js/vendors/html2canvas.min.js', [], '1.4.1', true );
117 wp_register_script( 'jspdf', EASY_INVOICE_PLUGIN_URL . 'assets/js/vendors/jspdf.umd.min.js', [], '2.5.1', true );
118 wp_register_script( 'easy-invoice-document-pdf', EASY_INVOICE_PLUGIN_URL . 'assets/js/document-pdf.js', [ 'html2canvas', 'jspdf' ], self::assetVersion( 'assets/js/document-pdf.js' ), true );
119
120 wp_register_script(
121 self::SCRIPT,
122 EASY_INVOICE_PLUGIN_URL . 'assets/js/document.js',
123 [ 'jquery', 'easy-invoice-toast', 'easy-invoice-document-pdf' ],
124 self::assetVersion( 'assets/js/document.js' ),
125 true
126 );
127
128 $token_param = ( 'quote' === $type ) ? 'qk' : 'ik';
129 $token = isset( $_GET[ $token_param ] ) ? sanitize_text_field( wp_unslash( $_GET[ $token_param ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- access token, verified server-side by the endpoints.
130 /** This filter is documented in includes/Controllers/InvoiceController.php */
131 $token = (string) apply_filters( 'easy_invoice_presented_access_token', $token, $type );
132
133 $config = [
134 'type' => $type,
135 'id' => $document_id,
136 'number' => (string) get_post_meta( $document_id, 'quote' === $type ? '_easy_invoice_quote_number' : '_easy_invoice_number', true ),
137 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
138 'accessToken' => $token,
139 'nonces' => [
140 'sendEmail' => wp_create_nonce( 'quote' === $type ? 'easy_invoice_send_quote_email' : 'easy_invoice_send_invoice_email' ),
141 'additionalCss' => wp_create_nonce( 'save_additional_css_nonce' ),
142 'quoteAction' => ( 'quote' === $type ) ? wp_create_nonce( 'easy_invoice_quote_action_' . $document_id ) : '',
143 ],
144 'autoDownload' => isset( $_GET['auto_download_pdf'] ) && '1' === (string) $_GET['auto_download_pdf'], // phpcs:ignore WordPress.Security.NonceVerification.Recommended
145 'pdfMethod' => class_exists( '\\EasyInvoice\\Services\\PdfDownload' ) ? PdfDownload::downloadMethod() : 'browser',
146 'openPayment' => isset( $_GET['pay'] ) && '1' === (string) $_GET['pay'], // phpcs:ignore WordPress.Security.NonceVerification.Recommended
147 'i18n' => [
148 'sendTitle' => __( 'Send by email', 'easy-invoice' ),
149 // Staff send the document to the client; the client sends
150 // themselves a copy — the same endpoint, different question.
151 'sendMessage' => self::sendPrompt( $type, $document_id ),
152 'send' => __( 'Send', 'easy-invoice' ),
153 'sending' => __( 'Sending…', 'easy-invoice' ),
154 'sent' => __( 'Sent.', 'easy-invoice' ),
155 'sendFailed' => __( 'The email could not be sent.', 'easy-invoice' ),
156 'cancel' => __( 'Cancel', 'easy-invoice' ),
157 'close' => __( 'Close', 'easy-invoice' ),
158 'networkError' => __( 'Network error. Please try again.', 'easy-invoice' ),
159 'pdfUnavailable' => __( 'PDF generator not available.', 'easy-invoice' ),
160 'hidePayment' => __( 'Hide payment', 'easy-invoice' ),
161 'acceptTitle' => __( 'Accept this quote?', 'easy-invoice' ),
162 'accept' => __( 'Accept', 'easy-invoice' ),
163 'accepting' => __( 'Accepting…', 'easy-invoice' ),
164 'accepted' => __( 'Quote accepted.', 'easy-invoice' ),
165 'acceptedRedirect' => __( 'Quote accepted. Taking you to the invoice…', 'easy-invoice' ),
166 'declineTitle' => __( 'Decline this quote?', 'easy-invoice' ),
167 'declineMessage' => __( 'Let us know why, so we can put a better proposal together.', 'easy-invoice' ),
168 'declineReason' => __( 'Reason (optional)', 'easy-invoice' ),
169 'decline' => __( 'Decline', 'easy-invoice' ),
170 'declining' => __( 'Declining…', 'easy-invoice' ),
171 'declined' => __( 'Quote declined.', 'easy-invoice' ),
172 'actionFailed' => __( 'That did not work. Please try again.', 'easy-invoice' ),
173 'cssSaved' => __( 'CSS saved.', 'easy-invoice' ),
174 'cssCleared' => __( 'CSS cleared.', 'easy-invoice' ),
175 'cssFailed' => __( 'Could not save the CSS.', 'easy-invoice' ),
176 ],
177 ];
178
179 /**
180 * Filter the configuration handed to the document page script.
181 *
182 * @param array $config Settings and strings.
183 * @param string $type 'invoice' or 'quote'.
184 * @param int $document_id Document post id.
185 */
186 $config = apply_filters( 'easy_invoice_document_script_config', $config, $type, $document_id );
187
188 wp_localize_script( self::SCRIPT, 'easyInvoiceDocument', $config );
189 wp_enqueue_script( self::SCRIPT );
190
191 if ( 'invoice' === $type ) {
192 self::localizePaymentVars( $document_id );
193 }
194 }
195
196 /**
197 * Re-localise the payment script with this invoice's currency and token.
198 *
199 * PaymentController localises `easy_invoice_vars` with the *site*
200 * currency on every front-end page. The bare template used to redefine
201 * the object inline to win that race; now the later localisation wins the
202 * same way, through the pipeline.
203 *
204 * @param int $invoice_id Invoice id.
205 * @return void
206 */
207 /**
208 * The question behind the "Send Email" button.
209 *
210 * @param string $type 'invoice' or 'quote'.
211 * @param int $document_id Post id.
212 * @return string
213 */
214 private static function sendPrompt( string $type, int $document_id ): string {
215 $cap = 'ei_send_invoice'; // the staff "send documents" capability; there is no separate one for quotes
216 $document = 'quote' === $type
217 ? \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->find( $document_id )
218 : \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository()->find( $document_id );
219 $staff = function_exists( 'easy_invoice_user_can' ) && easy_invoice_user_can( $cap );
220 if ( $staff ) {
221 return 'quote' === $type
222 ? __( 'Email this quote to the client now?', 'easy-invoice' )
223 : __( 'Email this invoice to the client now?', 'easy-invoice' );
224 }
225 $email = is_callable( [ $document, 'getCustomerEmail' ] ) ? (string) $document->getCustomerEmail() : '';
226 if ( '' !== $email ) {
227 /* translators: 1: document kind, 2: email address. */
228 return sprintf( __( 'Send a copy of this %1$s to %2$s?', 'easy-invoice' ), 'quote' === $type ? __( 'quote', 'easy-invoice' ) : __( 'invoice', 'easy-invoice' ), $email );
229 }
230 return 'quote' === $type
231 ? __( 'Send yourself a copy of this quote by email?', 'easy-invoice' )
232 : __( 'Send yourself a copy of this invoice by email?', 'easy-invoice' );
233 }
234
235 private static function localizePaymentVars( int $invoice_id ): void {
236 if ( ! wp_script_is( 'easy-invoice-payment', 'registered' ) ) {
237 return;
238 }
239
240 $invoice = new \EasyInvoice\Models\Invoice( get_post( $invoice_id ) );
241 $currency = $invoice->getCurrencyCode();
242 if ( empty( $currency ) || 'global' === $currency ) {
243 $currency = get_option( 'easy_invoice_currency_code', 'USD' );
244 }
245
246 wp_localize_script( 'easy-invoice-payment', 'easy_invoice_vars', [
247 'ajax_url' => admin_url( 'admin-ajax.php' ),
248 'nonce' => wp_create_nonce( 'easy_invoice_payment' ),
249 /** This filter is documented in includes/Controllers/InvoiceController.php */
250 'access_token' => (string) apply_filters( 'easy_invoice_presented_access_token', isset( $_GET['ik'] ) ? sanitize_text_field( wp_unslash( $_GET['ik'] ) ) : '', 'invoice' ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
251 'currency_symbol' => \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol( $currency ),
252 'currency_code' => $currency,
253 ] );
254 }
255
256 /**
257 * Drop every style and script that is not ours.
258 *
259 * @return void
260 */
261 public static function isolate(): void {
262 $type = self::currentType();
263 if ( '' === $type ) {
264 return;
265 }
266
267 /**
268 * Filter whether the theme's assets are kept off the document page.
269 *
270 * @param bool $isolate Default true.
271 * @param string $type 'invoice' or 'quote'.
272 */
273 if ( ! apply_filters( 'easy_invoice_document_isolate_assets', true, $type ) ) {
274 return;
275 }
276
277 $allowed = [
278 'jquery', 'jquery-core', 'jquery-migrate',
279 'wp-i18n', 'wp-hooks', 'wp-polyfill', 'wp-api-fetch', 'wp-url',
280 'admin-bar', 'dashicons',
281 ];
282
283 /**
284 * Filter the handles allowed to load alongside the plugin's own.
285 *
286 * @param array $allowed Handles.
287 * @param string $type 'invoice' or 'quote'.
288 */
289 $allowed = (array) apply_filters( 'easy_invoice_document_allowed_handles', $allowed, $type );
290
291 foreach ( [ wp_styles(), wp_scripts() ] as $collection ) {
292 // What we keep, plus everything it depends on -- a gateway's SDK
293 // (js.stripe.com, Square's web SDK) is a dependency of our own
294 // handler and must survive.
295 $keep = [];
296 foreach ( (array) $collection->queue as $handle ) {
297 if ( in_array( $handle, $allowed, true ) || self::isOurs( $handle ) ) {
298 $keep[] = $handle;
299 }
300 }
301 $collection->all_deps( $keep );
302 $keep = array_unique( array_merge( $keep, (array) $collection->to_do ) );
303 $collection->to_do = [];
304
305 foreach ( (array) $collection->queue as $handle ) {
306 if ( ! in_array( $handle, $keep, true ) ) {
307 $collection->dequeue( $handle );
308 }
309 }
310 }
311 }
312
313 /**
314 * Does a handle belong to Easy Invoice or one of its addons?
315 *
316 * @param string $handle Script or style handle.
317 * @return bool
318 */
319 private static function isOurs( string $handle ): bool {
320 return 0 === strpos( $handle, 'easy-invoice' )
321 || 0 === strpos( $handle, 'easy_invoice' )
322 || in_array( $handle, [ 'html2canvas', 'jspdf', 'stripe-js' ], true );
323 }
324
325 /**
326 * Mark the body so CSS can tell the two documents apart.
327 *
328 * @param array $classes Body classes.
329 * @return array
330 */
331 public static function bodyClass( $classes ): array {
332 $classes = is_array( $classes ) ? $classes : [];
333 $type = self::currentType();
334 if ( '' !== $type ) {
335 $classes[] = 'easy-invoice-document';
336 $classes[] = 'easy-invoice-document--' . $type;
337 }
338 return $classes;
339 }
340
341 /**
342 * No admin bar on a document a client is reading.
343 *
344 * @param bool $show Whether to show it.
345 * @return bool
346 */
347 public static function hideAdminBar( $show ) {
348 return '' === self::currentType() ? $show : false;
349 }
350 }
351