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 / Services / DocumentAttachments.php

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

257 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Files attached to an invoice or quote.
4 *
5 * @package Easy_Invoice
6 * @subpackage Services
7 */
8
9 namespace EasyInvoice\Services;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * A timesheet, a signed contract, a delivery note, the photos of the job:
17 * clients ask for these alongside the invoice, and every SaaS product lets
18 * them travel with it. Here they are media-library files chosen in the
19 * builder, listed on the public page and the PDF, and sent with the email.
20 */
21 class DocumentAttachments {
22
23 const META = '_easy_invoice_attachments';
24
25 /**
26 * Wire it up.
27 *
28 * @return void
29 */
30 public static function init(): void {
31 add_filter( 'easy_invoice_invoice_fields', [ __CLASS__, 'invoiceField' ] );
32 add_filter( 'easy_invoice_quote_form_fields', [ __CLASS__, 'quoteField' ] );
33 add_action( 'admin_enqueue_scripts', [ __CLASS__, 'media' ] );
34 // QuoteRepository sets only the keys it knows; invoices go through
35 // the form processor, which saves every registered field.
36 add_action( 'easy_invoice_quote_set_data_after', [ __CLASS__, 'saveQuote' ], 10, 2 );
37
38 // Inside the design card (part of the PDF capture); the page footer
39 // hook remains for a theme override that predates the in-card hook.
40 add_action( 'easy_invoice_document_body_after', [ __CLASS__, 'renderOnPage' ], 10, 2 );
41 add_action( 'easy_invoice_document_footer', [ __CLASS__, 'renderOnPage' ], 10, 2 );
42 add_action( 'easy_invoice_pdf_after_notes', [ __CLASS__, 'renderOnPdf' ], 20, 2 );
43 add_filter( 'easy_invoice_email_attachments', [ __CLASS__, 'emailAttachments' ], 10, 3 );
44 }
45
46 /**
47 * Attachment ids for a document.
48 *
49 * @param int $document_id Post id.
50 * @return int[]
51 */
52 /**
53 * Meta key for a document: invoices use the plain field name, quotes are
54 * persisted by the Quote model under a _quote_ prefix.
55 *
56 * @param int $document_id Post id.
57 * @return string
58 */
59 public static function metaKey( int $document_id ): string {
60 return \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE === get_post_type( $document_id ) ? '_easy_invoice_quote_attachments' : self::META;
61 }
62
63 public static function ids( int $document_id ): array {
64 $raw = get_post_meta( $document_id, self::metaKey( $document_id ), true );
65 if ( is_array( $raw ) ) {
66 $ids = $raw;
67 } else {
68 $ids = explode( ',', (string) $raw );
69 }
70 $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
71 return array_values( array_filter( $ids, static function ( $id ) {
72 return 'attachment' === get_post_type( $id );
73 } ) );
74 }
75
76 /**
77 * Attachment ids with names, sizes and URLs.
78 *
79 * @param int $document_id Post id.
80 * @return array<int,array{id:int,name:string,size:string,url:string,path:string}>
81 */
82 public static function files( int $document_id ): array {
83 $out = [];
84 foreach ( self::ids( $document_id ) as $id ) {
85 $path = get_attached_file( $id );
86 $out[] = [
87 'id' => $id,
88 'name' => basename( (string) $path ) ?: get_the_title( $id ),
89 'size' => $path && file_exists( $path ) ? size_format( (int) filesize( $path ) ) : '',
90 'url' => (string) wp_get_attachment_url( $id ),
91 'path' => (string) $path,
92 ];
93 }
94 return $out;
95 }
96
97 /* ---- Builder ---------------------------------------------------- */
98
99 private static function field( string $tab ): array {
100 return [
101 'tab_id' => $tab,
102 'name' => 'attachments',
103 'type' => 'attachments',
104 'label' => __( 'Attachments', 'easy-invoice' ),
105 'grid_cols' => 'sm:col-span-6',
106 'section' => 'notes',
107 'order' => 9,
108 'description' => __( 'Files the client can download from the document page and that are sent with the email — a timesheet, a signed contract, delivery photos.', 'easy-invoice' ),
109 ];
110 }
111
112 public static function invoiceField( $fields ) {
113 $fields = is_array( $fields ) ? $fields : [];
114 $fields[] = self::field( 'invoice-tab' );
115 return $fields;
116 }
117
118 public static function quoteField( $fields ) {
119 $fields = is_array( $fields ) ? $fields : [];
120 $fields[] = self::field( 'quote-tab' );
121 return $fields;
122 }
123
124 /**
125 * Persist the field on a quote.
126 *
127 * @param object $quote Quote model.
128 * @param array $data Submitted data.
129 * @return void
130 */
131 public static function saveQuote( $quote, $data ): void {
132 if ( is_array( $data ) && array_key_exists( 'attachments', $data ) && is_callable( [ $quote, 'setMetaData' ] ) ) {
133 $ids = is_array( $data['attachments'] ) ? $data['attachments'] : explode( ',', (string) $data['attachments'] );
134 $quote->setMetaData( '_easy_invoice_attachments', implode( ',', array_filter( array_map( 'intval', $ids ) ) ) );
135 }
136 }
137
138 /**
139 * The media library on the builders.
140 *
141 * @return void
142 */
143 public static function media(): void {
144 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
145 if ( in_array( $page, [ 'easy-invoice-builder', 'easy-invoice-new', 'easy-invoice-quote-builder' ], true ) ) {
146 wp_enqueue_media();
147 }
148 }
149
150 /**
151 * Field markup: chosen files, an Add button, a hidden CSV of ids.
152 *
153 * @param array $field Field config.
154 * @param mixed $document Model or null.
155 * @return string
156 */
157 public static function renderField( array $field, $document = null ): string {
158 $id = $document && is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
159 $files = $id ? self::files( $id ) : [];
160 $ids = implode( ',', array_map( static function ( $f ) { return $f['id']; }, $files ) );
161
162 $html = '<div class="' . esc_attr( $field['grid_cols'] ?? 'sm:col-span-6' ) . '" id="ei-attachments-field">';
163 $html .= '<label class="block text-sm font-medium text-gray-700">' . esc_html( $field['label'] ) . '</label>';
164 $html .= '<input type="hidden" name="attachments" id="ei-attachments-ids" value="' . esc_attr( $ids ) . '">';
165 $html .= '<ul id="ei-attachments-list" class="mt-2 space-y-1 text-sm">';
166 foreach ( $files as $f ) {
167 $html .= '<li class="flex items-center justify-between rounded border border-gray-200 px-3 py-1.5" data-id="' . (int) $f['id'] . '">'
168 . '<span class="truncate"><a href="' . esc_url( $f['url'] ) . '" target="_blank" rel="noopener" class="text-indigo-600">' . esc_html( $f['name'] ) . '</a> <span class="text-gray-400 text-xs">' . esc_html( $f['size'] ) . '</span></span>'
169 . '<button type="button" class="ei-attachment-remove text-xs text-red-600 ml-3">' . esc_html__( 'Remove', 'easy-invoice' ) . '</button></li>';
170 }
171 $html .= '</ul>';
172 $html .= '<button type="button" id="ei-attachments-add" class="mt-2 inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-gray-300 bg-white text-gray-700 hover:bg-gray-50">' . esc_html__( 'Add files', 'easy-invoice' ) . '</button>';
173 if ( ! empty( $field['description'] ) ) {
174 $html .= '<p class="mt-1 text-xs text-gray-500">' . esc_html( $field['description'] ) . '</p>';
175 }
176 $html .= '</div>';
177 $html .= '<script>document.addEventListener("DOMContentLoaded",function(){var w=document.getElementById("ei-attachments-field");if(!w){return;}var ids=document.getElementById("ei-attachments-ids"),list=document.getElementById("ei-attachments-list");function sync(){ids.value=[].map.call(list.querySelectorAll("li"),function(li){return li.getAttribute("data-id");}).join(",");}'
178 . 'list.addEventListener("click",function(e){if(e.target.classList.contains("ei-attachment-remove")){e.target.closest("li").remove();sync();}});'
179 . 'document.getElementById("ei-attachments-add").addEventListener("click",function(){if(!window.wp||!wp.media){return;}var f=wp.media({title:' . wp_json_encode( __( 'Attach files', 'easy-invoice' ) ) . ',multiple:true,button:{text:' . wp_json_encode( __( 'Attach', 'easy-invoice' ) ) . '}});f.on("select",function(){f.state().get("selection").each(function(a){a=a.toJSON();if(list.querySelector("li[data-id=\\""+a.id+"\\"]")){return;}var li=document.createElement("li");li.className="flex items-center justify-between rounded border border-gray-200 px-3 py-1.5";li.setAttribute("data-id",a.id);li.innerHTML="<span class=\\"truncate\\"><a class=\\"text-indigo-600\\" target=\\"_blank\\" rel=\\"noopener\\"></a> <span class=\\"text-gray-400 text-xs\\"></span></span><button type=\\"button\\" class=\\"ei-attachment-remove text-xs text-red-600 ml-3\\">' . esc_js( __( 'Remove', 'easy-invoice' ) ) . '</button>";li.querySelector("a").href=a.url;li.querySelector("a").textContent=a.filename||a.title;li.querySelector("span span").textContent=a.filesizeHumanReadable||"";list.appendChild(li);});sync();});f.open();});});</script>';
180 return $html;
181 }
182
183 /* ---- Output ----------------------------------------------------- */
184
185 /** Public document page: a list under the document. */
186 /** @var array<int,bool> Documents whose list is already printed in this request. */
187 private static $rendered = [];
188
189 public static function renderOnPage( $document, $type ): void {
190 $id = is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
191 if ( ! $id || isset( self::$rendered[ $id ] ) ) {
192 return;
193 }
194 $files = self::files( $id );
195 if ( empty( $files ) ) {
196 return;
197 }
198 self::$rendered[ $id ] = true;
199 echo '<div class="ei-attachments" style="margin:24px 0 0;padding:14px 0 0;border-top:1px solid #e5e7eb;font-size:13px;">';
200 echo '<div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:6px;">' . esc_html__( 'Attachments', 'easy-invoice' ) . '</div><ul style="margin:0;padding:0;list-style:none;">';
201 foreach ( $files as $f ) {
202 echo '<li style="padding:4px 0;"><a href="' . esc_url( $f['url'] ) . '" target="_blank" rel="noopener" style="color:#4f46e5;">' . esc_html( $f['name'] ) . '</a> <span style="color:#9ca3af;font-size:12px;">' . esc_html( $f['size'] ) . '</span></li>';
203 }
204 echo '</ul></div>';
205 }
206
207 /** PDF: names and links. */
208 public static function renderOnPdf( $document, $type ): void {
209 $id = is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
210 // A design-layout PDF already printed the list inside the card.
211 if ( ! $id || isset( self::$rendered[ $id ] ) ) {
212 return;
213 }
214 $files = self::files( $id );
215 if ( empty( $files ) ) {
216 return;
217 }
218 self::$rendered[ $id ] = true;
219 echo '<div style="margin-top:14px;font-size:10px;color:#374151;"><div style="text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-size:9px;margin-bottom:3px;">' . esc_html__( 'Attachments', 'easy-invoice' ) . '</div>';
220 foreach ( $files as $f ) {
221 echo '<div><a href="' . esc_url( $f['url'] ) . '" style="color:#4f46e5;text-decoration:none;">' . esc_html( $f['name'] ) . '</a> <span style="color:#9ca3af;">' . esc_html( $f['size'] ) . '</span></div>';
222 }
223 echo '</div>';
224 }
225
226 /**
227 * Send the files with the document's email.
228 *
229 * @param array $attachments Paths so far.
230 * @param object $document Model.
231 * @param string $type 'invoice' or 'quote'.
232 * @return array
233 */
234 public static function emailAttachments( $attachments, $document, $type ): array {
235 $attachments = is_array( $attachments ) ? $attachments : [];
236 $id = is_callable( [ $document, 'getId' ] ) ? (int) $document->getId() : 0;
237 if ( ! $id ) {
238 return $attachments;
239 }
240 /**
241 * Filter whether attached files are sent with the email (default yes).
242 *
243 * @param bool $send Default true.
244 * @param int $document_id Post id.
245 */
246 if ( ! apply_filters( 'easy_invoice_email_document_attachments', true, $id ) ) {
247 return $attachments;
248 }
249 foreach ( self::files( $id ) as $f ) {
250 if ( '' !== $f['path'] && file_exists( $f['path'] ) ) {
251 $attachments[] = $f['path'];
252 }
253 }
254 return array_values( array_unique( $attachments ) );
255 }
256 }
257