| @@ -18,88 +18,374 @@ | ||
| 18 | 18 | /** |
| 19 | 19 | * Initialize the template loader |
| 20 | 20 | */ |
| 21 | 21 | public function init() { |
| 22 | + // Authorisation runs before anything decides which template to load, and | |
| 23 | + // before any output. `template_redirect` is the right hook because it fires | |
| 24 | + // for every front-end entry point into a document — the pretty permalink, | |
| 25 | + // `?p=<id>`, feeds, embeds, and the `?auto_download_pdf=1` PDF path — so a | |
| 26 | + // single gate covers all of them. | |
| 27 | + add_action('template_redirect', [$this, 'enforceDocumentAccess'], 1); | |
| 28 | + add_action('admin_post_nopriv_' . self::REFRESH_ACTION, [self::class, 'handleLinkRefreshRequest']); | |
| 29 | + add_action('admin_post_' . self::REFRESH_ACTION, [self::class, 'handleLinkRefreshRequest']); | |
| 30 | + | |
| 31 | + // `exclude_from_search` (see EasyInvoice::registerPostTypes) keeps documents | |
| 32 | + // out of site search and search feeds, but it does not stop an explicit | |
| 33 | + // `?post_type=easy_invoice` query, which the theme happily rendered as an | |
| 34 | + // archive listing every invoice title and permalink. `has_archive` is false, | |
| 35 | + // but `publicly_queryable` has to stay true for single permalinks to resolve, | |
| 36 | + // and that is enough for the query to run. | |
| 37 | + add_action('pre_get_posts', [$this, 'blockDocumentArchiveQueries']); | |
| 38 | + | |
| 22 | 39 | add_filter('single_template', [$this, 'loadSingleQuoteTemplate']); |
| 23 | 40 | add_filter('single_template', [$this, 'loadSingleInvoiceTemplate']); |
| 24 | 41 | add_filter('template_include', [$this, 'loadCustomTemplates']); |
| 25 | 42 | } |
| 43 | + | |
| 44 | + /** | |
| 45 | + * Stop invoices and quotes being listed by an archive-style front-end query. | |
| 46 | + * | |
| 47 | + * `?post_type=easy_invoice` (and the quote equivalent, and their feeds) ran a | |
| 48 | + * normal archive query that the active theme rendered as a post list — exposing | |
| 49 | + * every invoice title and permalink to anonymous visitors. The single-document | |
| 50 | + * gate did not apply because those requests are not `is_singular()`. | |
| 51 | + * | |
| 52 | + * Admin queries are untouched: the plugin's own list screens rely on them. | |
| 53 | + * | |
| 54 | + * @param \WP_Query $query | |
| 55 | + * @return void | |
| 56 | + */ | |
| 57 | + public function blockDocumentArchiveQueries($query) { | |
| 58 | + if (is_admin() || !$query instanceof \WP_Query || !$query->is_main_query()) { | |
| 59 | + return; | |
| 60 | + } | |
| 61 | + | |
| 62 | + // Single documents are handled by enforceDocumentAccess(), which knows how to | |
| 63 | + // authorise them. Only listing-style queries are blocked here. | |
| 64 | + if ($query->is_singular()) { | |
| 65 | + return; | |
| 66 | + } | |
| 67 | + | |
| 68 | + $ours = [ | |
| 69 | + \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, | |
| 70 | + \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE, | |
| 71 | + ]; | |
| 72 | + | |
| 73 | + $requested = $query->get('post_type'); | |
| 74 | + if (empty($requested)) { | |
| 75 | + return; | |
| 76 | + } | |
| 77 | + | |
| 78 | + $requested = (array) $requested; | |
| 79 | + if (!array_intersect($requested, $ours)) { | |
| 80 | + return; | |
| 81 | + } | |
| 82 | + | |
| 83 | + $remaining = array_values(array_diff($requested, $ours)); | |
| 84 | + | |
| 85 | + if (!empty($remaining)) { | |
| 86 | + // Mixed query — drop just our types and let the rest run. | |
| 87 | + $query->set('post_type', $remaining); | |
| 88 | + return; | |
| 89 | + } | |
| 90 | + | |
| 91 | + // The query asked for nothing but our documents. Return no results rather | |
| 92 | + // than an empty archive, so the response does not confirm the type exists. | |
| 93 | + $query->set('post__in', [0]); | |
| 94 | + $query->set('posts_per_page', 0); | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * Refuse to render an invoice or quote to a visitor who is not authorised. | |
| 99 | + * | |
| 100 | + * Invoices and quotes are stored with post_status 'publish' regardless of their | |
| 101 | + * workflow status (see Models\Invoice::save() and Models\Quote::save() — the | |
| 102 | + * comment there explains it is done "to ensure proper permalinks"), and both post | |
| 103 | + * types are registered `public` + `publicly_queryable`. Without this gate, any | |
| 104 | + * unauthenticated visitor who guessed or discovered a URL could read the whole | |
| 105 | + * document — customer name, email, address, line items, prices, notes and totals — | |
| 106 | + * including invoices still in Draft. Nothing downstream checked: the single | |
| 107 | + * templates rendered unconditionally, and the template loader keyed only on post | |
| 108 | + * type. | |
| 109 | + * | |
| 110 | + * Authorisation reuses the existing helpers rather than duplicating their rules, | |
| 111 | + * so there is one definition of "may this person see this document": | |
| 112 | + * | |
| 113 | + * - a valid per-document access token (`?ik=` / `?qk=`, compared with | |
| 114 | + * hash_equals) — this is what emailed links carry; | |
| 115 | + * - an administrator; | |
| 116 | + * - the logged-in client the document is bound to. | |
| 117 | + * | |
| 118 | + * Unauthorised requests get a normal 404 rather than an "access denied" page, so | |
| 119 | + * the response does not confirm that a given invoice number exists. | |
| 120 | + * | |
| 121 | + * @return void | |
| 122 | + */ | |
| 123 | + public function enforceDocumentAccess() { | |
| 124 | + if (is_admin() || !is_singular()) { | |
| 125 | + return; | |
| 126 | + } | |
| 127 | + | |
| 128 | + $post = get_queried_object(); | |
| 129 | + if (!$post instanceof \WP_Post) { | |
| 130 | + return; | |
| 131 | + } | |
| 132 | + | |
| 133 | + $invoice_type = \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE; | |
| 134 | + $quote_type = \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE; | |
| 135 | + | |
| 136 | + if ($post->post_type !== $invoice_type && $post->post_type !== $quote_type) { | |
| 137 | + return; | |
| 138 | + } | |
| 139 | + | |
| 140 | + /** | |
| 141 | + * Allow a site to turn the gate off. | |
| 142 | + * | |
| 143 | + * Sites that would rather keep the old open-by-URL behaviour can return | |
| 144 | + * false here, but they are choosing to expose customer data to anyone | |
| 145 | + * holding or guessing a URL. A bare link to an issued document otherwise | |
| 146 | + * shows a page offering to email a fresh keyed link (renderLinkRefreshPage). | |
| 147 | + * | |
| 148 | + * @param bool $enforce Whether to require authorisation. Default true. | |
| 149 | + * @param \WP_Post $post The invoice or quote being requested. | |
| 150 | + */ | |
| 151 | + if (!apply_filters('easy_invoice_require_document_authorisation', true, $post)) { | |
| 152 | + return; | |
| 153 | + } | |
| 154 | + | |
| 155 | + $allowed = false; | |
| 156 | + | |
| 157 | + if ($post->post_type === $invoice_type) { | |
| 158 | + $invoice = null; | |
| 159 | + try { | |
| 160 | + $invoice = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository()->find($post->ID); | |
| 161 | + } catch (\Throwable $e) { | |
| 162 | + $invoice = null; | |
| 163 | + } | |
| 164 | + $allowed = \EasyInvoice\Controllers\InvoiceController::canSubmitPaymentForInvoice((int) $post->ID, $invoice); | |
| 165 | + } else { | |
| 166 | + $quote = null; | |
| 167 | + try { | |
| 168 | + $quote = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->find($post->ID); | |
| 169 | + } catch (\Throwable $e) { | |
| 170 | + $quote = null; | |
| 171 | + } | |
| 172 | + $allowed = \EasyInvoice\Controllers\QuoteController::canActOnQuote((int) $post->ID, $quote); | |
| 173 | + } | |
| 174 | + | |
| 175 | + if ($allowed) { | |
| 176 | + return; | |
| 177 | + } | |
| 178 | + | |
| 179 | + // A bare URL to a real, issued document: most likely a link emailed before | |
| 180 | + // access keys existed. Show nothing of the document, but let the holder | |
| 181 | + // ask for a fresh keyed link to the address the document was issued to. | |
| 182 | + if (self::canOfferLinkRefresh($post)) { | |
| 183 | + self::renderLinkRefreshPage($post); | |
| 184 | + exit; | |
| 185 | + } | |
| 186 | + | |
| 187 | + // Present it as "not found" rather than "forbidden" so the response does not | |
| 188 | + // disclose that this document exists. | |
| 189 | + global $wp_query; | |
| 190 | + $wp_query->set_404(); | |
| 191 | + status_header(404); | |
| 192 | + nocache_headers(); | |
| 193 | + include get_query_template('404'); | |
| 194 | + exit; | |
| 195 | + } | |
| 26 | 196 | |
| 27 | 197 | /** |
| 198 | + * Meta stamped when an email carrying the document's keyed link goes out. Kept | |
| 199 | + * so a site can tell which documents' recipients already hold a keyed link. | |
| 200 | + */ | |
| 201 | + const KEYED_LINK_SENT_META = '_easy_invoice_keyed_link_sent'; | |
| 202 | + | |
| 203 | + /** Action (admin-post, works for anonymous visitors) behind the "send me a fresh link" button. */ | |
| 204 | + const REFRESH_ACTION = 'easy_invoice_request_document_link'; | |
| 205 | + | |
| 206 | + public static function markKeyedLinkSent(int $post_id): void { | |
| 207 | + if ($post_id > 0 && '' === (string) get_post_meta($post_id, self::KEYED_LINK_SENT_META, true)) { | |
| 208 | + update_post_meta($post_id, self::KEYED_LINK_SENT_META, current_time('mysql', true)); | |
| 209 | + } | |
| 210 | + } | |
| 211 | + | |
| 212 | + /** | |
| 213 | + * Only an issued (non-draft, published) document that has an address on file | |
| 214 | + * gets the refresh offer; anything else is a plain 404, so the page cannot be | |
| 215 | + * used to probe which URLs exist beyond what the old behaviour already showed. | |
| 216 | + */ | |
| 217 | + public static function canOfferLinkRefresh(\WP_Post $post): bool { | |
| 218 | + if ('publish' !== $post->post_status) { | |
| 219 | + return false; | |
| 220 | + } | |
| 221 | + $is_invoice = $post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE; | |
| 222 | + $status = strtolower((string) get_post_meta($post->ID, $is_invoice ? '_easy_invoice_status' : '_easy_invoice_quote_status', true)); | |
| 223 | + if ('draft' === $status || '' === $status) { | |
| 224 | + return false; | |
| 225 | + } | |
| 226 | + return '' !== self::recipientAddress($post); | |
| 227 | + } | |
| 228 | + | |
| 229 | + /** The address the document was issued to (never shown to the visitor). */ | |
| 230 | + private static function recipientAddress(\WP_Post $post): string { | |
| 231 | + $is_invoice = $post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE; | |
| 232 | + $email = (string) get_post_meta($post->ID, $is_invoice ? '_easy_invoice_customer_email' : '_easy_invoice_quote_customer_email', true); | |
| 233 | + if ('' === $email) { | |
| 234 | + $client_id = (int) get_post_meta($post->ID, $is_invoice ? '_easy_invoice_client_id' : '_easy_invoice_quote_client_id', true); | |
| 235 | + $user = $client_id > 0 ? get_user_by('id', $client_id) : null; | |
| 236 | + $email = $user ? (string) $user->user_email : ''; | |
| 237 | + } | |
| 238 | + return is_email($email) ? $email : ''; | |
| 239 | + } | |
| 240 | + | |
| 241 | + /** | |
| 242 | + * The page shown instead of the document. Deliberately standalone (no theme, | |
| 243 | + * no document data): a title, one sentence, one button. | |
| 244 | + */ | |
| 245 | + public static function renderLinkRefreshPage(\WP_Post $post, string $state = ''): void { | |
| 246 | + status_header('sent' === $state ? 200 : 403); | |
| 247 | + nocache_headers(); | |
| 248 | + header('X-Robots-Tag: noindex, nofollow'); | |
| 249 | + header('Content-Type: text/html; charset=' . get_option('blog_charset')); | |
| 250 | + $is_invoice = $post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE; | |
| 251 | + $what = $is_invoice ? __('invoice', 'easy-invoice') : __('quote', 'easy-invoice'); | |
| 252 | + $company = (string) get_option('easy_invoice_company_name', get_bloginfo('name')); | |
| 253 | + ?> | |
| 254 | +<!DOCTYPE html> | |
| 255 | +<html <?php language_attributes(); ?>> | |
| 256 | +<head> | |
| 257 | +<meta charset="<?php echo esc_attr(get_option('blog_charset')); ?>"> | |
| 258 | +<meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 259 | +<meta name="robots" content="noindex, nofollow"> | |
| 260 | +<title><?php echo esc_html(sprintf(/* translators: %s: company name */ __('Your %s link', 'easy-invoice'), $company)); ?></title> | |
| 261 | +<style>body{margin:0;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:#1f2937}.ei-box{max-width:480px;margin:12vh auto;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:32px;box-shadow:0 1px 3px rgba(0,0,0,.06)}h1{font-size:20px;margin:0 0 12px}p{line-height:1.6;margin:0 0 16px;color:#4b5563}button{background:#4f46e5;color:#fff;border:0;border-radius:8px;padding:12px 20px;font-size:15px;cursor:pointer}button:hover{background:#4338ca}.ok{color:#065f46;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:8px;padding:12px}.muted{font-size:13px;color:#6b7280}</style> | |
| 262 | +</head> | |
| 263 | +<body> | |
| 264 | +<div class="ei-box"> | |
| 265 | +<?php if ('sent' === $state) : ?> | |
| 266 | + <h1><?php esc_html_e('On its way', 'easy-invoice'); ?></h1> | |
| 267 | + <p class="ok"><?php echo esc_html(sprintf(/* translators: %s: invoice or quote */ __('A fresh link to your %s has been emailed to the address it was issued to. Please check your inbox (and spam folder).', 'easy-invoice'), $what)); ?></p> | |
| 268 | +<?php elseif ('wait' === $state) : ?> | |
| 269 | + <h1><?php esc_html_e('Already sent', 'easy-invoice'); ?></h1> | |
| 270 | + <p><?php esc_html_e('A fresh link was emailed a few minutes ago. Please check your inbox (and spam folder) before requesting another.', 'easy-invoice'); ?></p> | |
| 271 | +<?php else : ?> | |
| 272 | + <h1><?php echo esc_html(sprintf(/* translators: %s: invoice or quote */ __('This %s link has been retired', 'easy-invoice'), $what)); ?></h1> | |
| 273 | + <p><?php echo esc_html(sprintf(/* translators: 1: company name, 2: invoice or quote */ __('%1$s now protects each %2$s with a private link. We can email a new one to the address this %2$s was issued to.', 'easy-invoice'), $company, $what)); ?></p> | |
| 274 | + <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> | |
| 275 | + <input type="hidden" name="action" value="<?php echo esc_attr(self::REFRESH_ACTION); ?>"> | |
| 276 | + <input type="hidden" name="document" value="<?php echo esc_attr((string) $post->ID); ?>"> | |
| 277 | + <input type="hidden" name="check" value="<?php echo esc_attr(self::refreshCheck($post->ID)); ?>"> | |
| 278 | + <input type="text" name="website" value="" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off" aria-hidden="true"> | |
| 279 | + <button type="submit"><?php esc_html_e('Email me a fresh link', 'easy-invoice'); ?></button> | |
| 280 | + </form> | |
| 281 | + <p class="muted" style="margin-top:16px"><?php esc_html_e('The address is not shown here and cannot be changed from this page.', 'easy-invoice'); ?></p> | |
| 282 | +<?php endif; ?> | |
| 283 | +</div> | |
| 284 | +</body> | |
| 285 | +</html> | |
| 286 | + <?php | |
| 287 | + } | |
| 288 | + | |
| 289 | + /** Ties the form to the document id and the current day; not a session nonce (visitors are anonymous). */ | |
| 290 | + private static function refreshCheck(int $post_id): string { | |
| 291 | + return substr(wp_hash('ei-doclink|' . $post_id . '|' . gmdate('Y-m-d')), 0, 20); | |
| 292 | + } | |
| 293 | + | |
| 294 | + /** | |
| 295 | + * Handle the "email me a fresh link" request (admin-post, anonymous allowed). | |
| 296 | + * One send per document per ten minutes, twenty per visitor per hour; the | |
| 297 | + * honeypot field must be empty. Nothing about the document is disclosed either way. | |
| 298 | + */ | |
| 299 | + public static function handleLinkRefreshRequest(): void { | |
| 300 | + $post_id = isset($_POST['document']) ? absint($_POST['document']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- anonymous visitors; keyed by a daily hash + honeypot + rate limits. | |
| 301 | + $check = isset($_POST['check']) ? sanitize_text_field(wp_unslash($_POST['check'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 302 | + $honey = isset($_POST['website']) ? sanitize_text_field(wp_unslash($_POST['website'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 303 | + $post = $post_id ? get_post($post_id) : null; | |
| 304 | + $types = [\EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE]; | |
| 305 | + if (!$post || !in_array($post->post_type, $types, true) || '' !== $honey | |
| 306 | + || !hash_equals(self::refreshCheck($post_id), $check) || !self::canOfferLinkRefresh($post)) { | |
| 307 | + wp_safe_redirect(home_url('/')); | |
| 308 | + exit; | |
| 309 | + } | |
| 310 | + $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; | |
| 311 | + $ip_key = 'ei_doclink_ip_' . md5($ip); | |
| 312 | + $ip_hits = (int) get_transient($ip_key); | |
| 313 | + if ($ip_hits >= 20) { | |
| 314 | + self::renderLinkRefreshPage($post, 'wait'); | |
| 315 | + exit; | |
| 316 | + } | |
| 317 | + set_transient($ip_key, $ip_hits + 1, HOUR_IN_SECONDS); | |
| 318 | + if (get_transient('ei_doclink_doc_' . $post_id)) { | |
| 319 | + self::renderLinkRefreshPage($post, 'wait'); | |
| 320 | + exit; | |
| 321 | + } | |
| 322 | + set_transient('ei_doclink_doc_' . $post_id, 1, 10 * MINUTE_IN_SECONDS); | |
| 323 | + $manager = \EasyInvoice\Services\EmailManager::getInstance(); | |
| 324 | + if ($post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { | |
| 325 | + $doc = \EasyInvoice\Providers\InvoiceServiceProvider::getInvoiceRepository()->find($post_id); | |
| 326 | + if ($doc) { | |
| 327 | + $manager->sendInvoiceEmail($doc, 'new'); | |
| 328 | + } | |
| 329 | + } else { | |
| 330 | + $doc = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository()->find($post_id); | |
| 331 | + if ($doc) { | |
| 332 | + $manager->sendQuoteEmail($doc, 'new'); | |
| 333 | + } | |
| 334 | + } | |
| 335 | + // Same page whether or not the send succeeded: the outcome must not reveal anything. | |
| 336 | + self::renderLinkRefreshPage($post, 'sent'); | |
| 337 | + exit; | |
| 338 | + } | |
| 339 | + | |
| 340 | + /** | |
| 28 | 341 | * Load single quote template |
| 29 | 342 | * |
| 30 | 343 | * @param string $template The template path |
| 31 | 344 | * @return string Modified template path |
| 32 | 345 | */ |
| 346 | + /** | |
| 347 | + * The public document page, theme-overridable. | |
| 348 | + * | |
| 349 | + * Both document types render through templates/document/single.php; a | |
| 350 | + * theme overrides it at {theme}/easy-invoice/document/single.php. | |
| 351 | + * | |
| 352 | + * @return string Absolute path, or '' when even the plugin's copy is gone. | |
| 353 | + */ | |
| 354 | + public static function documentTemplate(): string { | |
| 355 | + return easy_invoice_locate_template('document/single.php'); | |
| 356 | + } | |
| 357 | + | |
| 33 | 358 | public function loadSingleQuoteTemplate($template) { |
| 34 | 359 | global $post; |
| 35 | - | |
| 36 | 360 | if ($post && $post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) { |
| 37 | - $custom_template = EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/single.php'; | |
| 38 | - | |
| 39 | - if (file_exists($custom_template)) { | |
| 361 | + $custom_template = self::documentTemplate(); | |
| 362 | + if ('' !== $custom_template) { | |
| 40 | 363 | return $custom_template; |
| 41 | - } else { | |
| 42 | - // Single quote template not found | |
| 43 | 364 | } |
| 44 | 365 | } |
| 45 | - | |
| 46 | 366 | return $template; |
| 47 | 367 | } |
| 48 | - | |
| 49 | - /** | |
| 50 | - * Load single invoice template | |
| 51 | - * | |
| 52 | - * @param string $template The template path | |
| 53 | - * @return string Modified template path | |
| 54 | - */ | |
| 368 | + | |
| 55 | 369 | public function loadSingleInvoiceTemplate($template) { |
| 56 | 370 | global $post; |
| 57 | - | |
| 58 | 371 | if ($post && $post->post_type === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE) { |
| 59 | - $custom_template = EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/single.php'; | |
| 60 | - | |
| 61 | - if (file_exists($custom_template)) { | |
| 372 | + $custom_template = self::documentTemplate(); | |
| 373 | + if ('' !== $custom_template) { | |
| 62 | 374 | return $custom_template; |
| 63 | - } else { | |
| 64 | - // Single invoice template not found | |
| 65 | 375 | } |
| 66 | 376 | } |
| 67 | - | |
| 68 | 377 | return $template; |
| 69 | 378 | } |
| 70 | - | |
| 71 | - /** | |
| 72 | - * Load custom templates for Easy Invoice pages | |
| 73 | - * | |
| 74 | - * @param string $template The template path | |
| 75 | - * @return string Modified template path | |
| 76 | - */ | |
| 379 | + | |
| 77 | 380 | public function loadCustomTemplates($template) { |
| 78 | - global $post; | |
| 79 | - | |
| 80 | - // Handle single quote pages | |
| 81 | - if (is_singular(\EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE)) { | |
| 82 | - $custom_template = EASY_INVOICE_PLUGIN_DIR . 'templates/quotes/single.php'; | |
| 83 | - | |
| 84 | - | |
| 85 | - if (file_exists($custom_template)) { | |
| 381 | + if (is_singular(\EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) | |
| 382 | + || is_singular(\EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE)) { | |
| 383 | + $custom_template = self::documentTemplate(); | |
| 384 | + if ('' !== $custom_template) { | |
| 86 | 385 | return $custom_template; |
| 87 | - } else { | |
| 88 | - // Single quote template not found | |
| 89 | 386 | } |
| 90 | 387 | } |
| 91 | - | |
| 92 | - // Handle single invoice pages | |
| 93 | - if (is_singular(\EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE)) { | |
| 94 | - $custom_template = EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/single.php'; | |
| 95 | - | |
| 96 | - if (file_exists($custom_template)) { | |
| 97 | - return $custom_template; | |
| 98 | - } else { | |
| 99 | - // Single invoice template not found | |
| 100 | - } | |
| 101 | - } | |
| 102 | - | |
| 388 | + | |
| 103 | 389 | return $template; |
| 104 | 390 | } |
| 105 | -} | |
| 391 | +} | |