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 / Controllers / StatementController.php

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

227 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Statement of account for a client.
4 *
5 * @package Easy_Invoice
6 * @subpackage Controllers
7 */
8
9 namespace EasyInvoice\Controllers;
10
11 use EasyInvoice\Services\ClientLedger;
12 use EasyInvoice\Services\PdfRenderer;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Shows and prints what a client owes across every document.
20 *
21 * Why this is not just an invoice list
22 * ------------------------------------
23 * "What do I owe you?" is the most common question a client asks, and an
24 * invoice list cannot answer it: the answer depends on invoices, payments and
25 * credit notes together, in date order, with a running balance. Every
26 * accounting package has this, and it is the thing a bookkeeper reconciles
27 * against at the end of a quarter.
28 *
29 * It is also the cheapest way to get paid. Sending a client one page that shows
30 * three unpaid invoices is more effective than sending three reminders.
31 */
32 class StatementController {
33
34 /** Admin page slug. */
35 const PAGE_SLUG = 'easy-invoice-statement';
36
37 /** admin-post action that serves the PDF. */
38 const ACTION_PDF = 'easy_invoice_statement_pdf';
39
40 /**
41 * Wire it up.
42 *
43 * @return void
44 */
45 public static function init(): void {
46 add_action( 'admin_menu', [ __CLASS__, 'registerPage' ], 99 );
47 add_action( 'easy_invoice_admin_main_content', [ __CLASS__, 'maybeRenderPage' ], 11 );
48 add_action( 'admin_post_' . self::ACTION_PDF, [ __CLASS__, 'handlePdf' ] );
49
50 add_filter( 'easy_invoice_self_rendering_pages', [ __CLASS__, 'claimPage' ] );
51 }
52
53 /**
54 * Tell the dispatcher this page draws itself.
55 *
56 * @param array $slugs Slugs that render themselves.
57 * @return array
58 */
59 public static function claimPage( $slugs ): array {
60 $slugs = is_array( $slugs ) ? $slugs : [];
61 $slugs[] = self::PAGE_SLUG;
62
63 return $slugs;
64 }
65
66 /**
67 * Register the hidden page.
68 *
69 * @return void
70 */
71 public static function registerPage(): void {
72 add_submenu_page(
73 'easy-invoice-hidden',
74 __( 'Statement of Account', 'easy-invoice' ),
75 __( 'Statement', 'easy-invoice' ),
76 (string) apply_filters( 'easy_invoice_menu_capability', 'manage_options', self::PAGE_SLUG ),
77 self::PAGE_SLUG,
78 [ __CLASS__, 'renderShell' ]
79 );
80 }
81
82 /**
83 * Render the admin chrome.
84 *
85 * @return void
86 */
87 public static function renderShell(): void {
88 include EASY_INVOICE_PLUGIN_DIR . 'templates/main-template.php';
89 }
90
91 /**
92 * Render the statement when the shell asks for it.
93 *
94 * @param string $page Page slug.
95 * @return void
96 */
97 public static function maybeRenderPage( $page ): void {
98 // A statement is a read-only view of documents the reader may already
99 // see. Gating it on client *management* locked out the Accountant role,
100 // which is exactly who reconciles against one.
101 if ( self::PAGE_SLUG !== $page || ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
102 return;
103 }
104
105 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only view.
106 $client_id = isset( $_GET['client_id'] ) ? absint( $_GET['client_id'] ) : 0;
107 $from = isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : '';
108 $to = isset( $_GET['to'] ) ? sanitize_text_field( wp_unslash( $_GET['to'] ) ) : '';
109 // phpcs:enable
110
111 $client = $client_id > 0 ? get_userdata( $client_id ) : false;
112
113 if ( ! $client ) {
114 echo '<div class="p-8"><p>' . esc_html__( 'That client does not exist.', 'easy-invoice' ) . '</p></div>';
115 return;
116 }
117
118 $statement = ClientLedger::statement( $client_id, $from, $to );
119
120 include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/statement-page.php';
121 }
122
123 /**
124 * Serve the statement as a PDF.
125 *
126 * @return void
127 */
128 public static function handlePdf(): void {
129 $client_id = isset( $_GET['client_id'] ) ? absint( $_GET['client_id'] ) : 0;
130 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
131
132 if ( ! wp_verify_nonce( $nonce, self::ACTION_PDF . '_' . $client_id ) ) {
133 wp_die( esc_html__( 'Security check failed.', 'easy-invoice' ), '', [ 'response' => 403 ] );
134 }
135
136 if ( ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
137 wp_die( esc_html__( 'You do not have permission to do that.', 'easy-invoice' ), '', [ 'response' => 403 ] );
138 }
139
140 $client = $client_id > 0 ? get_userdata( $client_id ) : false;
141 if ( ! $client ) {
142 wp_die( esc_html__( 'That client does not exist.', 'easy-invoice' ), '', [ 'response' => 404 ] );
143 }
144
145 if ( ! PdfRenderer::isAvailable() ) {
146 wp_die(
147 esc_html__( 'Server-side PDF rendering is unavailable because the PDF library is missing.', 'easy-invoice' ),
148 '',
149 [ 'response' => 501, 'back_link' => true ]
150 );
151 }
152
153 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- covered by the nonce above.
154 $from = isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : '';
155 $to = isset( $_GET['to'] ) ? sanitize_text_field( wp_unslash( $_GET['to'] ) ) : '';
156 // phpcs:enable
157
158 $statement = ClientLedger::statement( $client_id, $from, $to );
159
160 ob_start();
161 include EASY_INVOICE_PLUGIN_DIR . 'templates/pdf/statement.php';
162 $html = (string) ob_get_clean();
163
164 $pdf = PdfRenderer::fromHtml( $html );
165
166 if ( is_wp_error( $pdf ) ) {
167 wp_die( esc_html( $pdf->get_error_message() ), '', [ 'response' => 500, 'back_link' => true ] );
168 }
169
170 $name = sanitize_file_name( 'statement-' . ( $client->display_name ?: $client->user_login ) . '.pdf' );
171
172 if ( ob_get_length() ) {
173 @ob_end_clean(); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
174 }
175
176 nocache_headers();
177 header( 'Content-Type: application/pdf' );
178 header( 'Content-Disposition: attachment; filename="' . $name . '"' );
179 header( 'Content-Length: ' . strlen( $pdf ) );
180 header( 'X-Content-Type-Options: nosniff' );
181
182 echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput -- binary document.
183 exit;
184 }
185
186 /**
187 * URL of the statement screen for a client.
188 *
189 * @param int $client_id Client ID.
190 * @return string
191 */
192 public static function pageUrl( int $client_id ): string {
193 return add_query_arg(
194 [
195 'page' => self::PAGE_SLUG,
196 'client_id' => $client_id,
197 ],
198 admin_url( 'admin.php' )
199 );
200 }
201
202 /**
203 * URL that downloads the statement PDF.
204 *
205 * @param int $client_id Client ID.
206 * @param string $from Start date.
207 * @param string $to End date.
208 * @return string
209 */
210 public static function pdfUrl( int $client_id, string $from = '', string $to = '' ): string {
211 return wp_nonce_url(
212 add_query_arg(
213 array_filter(
214 [
215 'action' => self::ACTION_PDF,
216 'client_id' => $client_id,
217 'from' => $from,
218 'to' => $to,
219 ]
220 ),
221 admin_url( 'admin-post.php' )
222 ),
223 self::ACTION_PDF . '_' . $client_id
224 );
225 }
226 }
227