Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 1009
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
jpcrm_templating_placeholders
0.00% covered (danger)
0.00%
0 / 1008
0.00% covered (danger)
0.00%
0 / 20
28056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 default_placeholders
0.00% covered (danger)
0.00%
0 / 617
0.00% covered (danger)
0.00%
0 / 1
2
 build_placeholders
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 add_setting_dependent_placeholders
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
12
 strip_inactive_tooling
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
132
 load_from_object_models
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 1
756
 add_available_in
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
 get_placeholders
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 get_placeholders_shorthand
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 get_placeholders_for_tooling
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
342
 get_single_placeholder_info
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 get_generic_replacements
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
2
 replace_single_placeholder
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 replace_placeholders
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
870
 pick_from_replacement_objects
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
272
 owner_from_replacement_objects
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
56
 make_placeholder_str
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 placeholder_selector
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
110
 simplify_placeholders
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 simplify_placeholders_for_wysiwyg
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 * Copyright 2021 Automattic
6 */
7
8defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
9
10/*
11    WIP:
12
13        each placeholder record will have an array such as:
14
15    {placeholder_model}:
16
17        array(
18
19            'description'     => 'Contact ID',
20            'origin'          => 'Contact Object model',
21            'replace_str'     => '##CONTACT-ID##',
22            'aliases'         => array('##CID##'),
23            'associated_type' => ZBS_TYPE_CONTACT,
24            'expected_format' => 'str' // future proofing
25            'available_in'    => array(
26
27                    // tooling allowed to use this, from:
28                    // if specified, otherwise areas may use type to identify which placeholders are available
29                    'system_email_templates',
30                    'mail_campaigns',
31                    'quote' e.g:
32                        'quote_templates',
33                        'quote_editor',
34                    'invoice' e.g:
35                        'invoice_editor',
36                    'single_email',
37
38            )
39
40        )
41
42*/
43
44/**
45 * jpcrm_templating_placeholders is the placeholder layer in Jetpack CRM 4+
46 *
47 * @author   Woody Hayday <hello@jetpackcrm.com>
48 * @version  4.0
49 * @access   public
50 * @see      https://kb.jetpackcrm.com
51 */
52class jpcrm_templating_placeholders {
53
54        // default set of placeholders, to get amended on init (Custom fields etc.)
55        private $placeholders = array();
56
57    // stores common links e.g. contact fields in quotes
58    // for now these repreresent the 1:1 relational links in the db
59    // later, as the DAL is extended, this could take advantage of
60    // a better mapping integral to the DAL, perhaps extending
61    // the $linkedToObjectTypes system.
62    // for now it's an exception, so hard-typing:
63    private $available_in_links = array(
64
65        'contact' => array(
66            'quote',
67            'invoice',
68            'transaction',
69            'event',
70        ),
71        'company' => array(
72            // 'quote',
73            'invoice',
74            'transaction',
75            'event',
76        ),
77
78    );
79
80    // ===============================================================================
81    // ===========  INIT =============================================================
82    function __construct( $args = array() ) {
83
84        // Build out list of placeholders
85        $this->build_placeholders();
86    }
87    // ===========  / INIT ===========================================================
88    // ===============================================================================
89
90    /**
91     * Fills out default placeholders
92     * This is executed on init because we want to include the full translatable strings
93     * ... which we cannot set as constants because we want the __()
94     *
95     * @return array of all placeholders
96     */
97    private function default_placeholders() {
98
99        $this->placeholders = array(
100
101            // CRM global placeholders, e.g. business name (from settings)
102            'global'      => array(
103
104                'biz-name'           => array(
105
106                    'description'     => __( 'Business name', 'zero-bs-crm' ),
107                    'origin'          => __( 'Global', 'zero-bs-crm' ),
108                    'expected_format' => 'str',
109                    'available_in'    => array(),
110                    'associated_type' => false,
111                    'replace_str'     => '##BIZ-NAME##',
112                    'aliases'         => array( '###BIZNAME###', '##BIZNAME##' ),
113                ),
114
115                'biz-state'          => array(
116
117                    'description'     => __( 'Business state', 'zero-bs-crm' ),
118                    'origin'          => __( 'Global', 'zero-bs-crm' ),
119                    'expected_format' => 'str',
120                    'available_in'    => array(),
121                    'associated_type' => false,
122                    'replace_str'     => '##BIZ-STATE##',
123                    'aliases'         => array( '##BIZSTATE##' ),
124                ),
125
126                'biz-logo'           => array(
127
128                    'description'     => __( 'Business logo', 'zero-bs-crm' ),
129                    'origin'          => __( 'Global', 'zero-bs-crm' ),
130                    'expected_format' => 'html',
131                    'available_in'    => array(),
132                    'associated_type' => false,
133                    'replace_str'     => '##BIZ-LOGO##',
134                ),
135
136                'biz-info'           => array(
137
138                    'description'     => __( 'Table with your business information (HTML)', 'zero-bs-crm' ),
139                    'origin'          => __( 'Global', 'zero-bs-crm' ),
140                    'expected_format' => 'html',
141                    'available_in'    => array(),
142                    'associated_type' => false,
143                    'replace_str'     => '##BIZ-INFO##',
144                    'aliases'         => array( '###BIZINFOTABLE###', '###BIZ-INFO###', '###FOOTERBIZDEETS###', '##INVOICE-BIZ-INFO##' ),
145                ),
146
147                'biz-your-name'      => array(
148
149                    'description'     => __( 'Business: Your Name', 'zero-bs-crm' ),
150                    'origin'          => __( 'Global', 'zero-bs-crm' ),
151                    'expected_format' => 'str',
152                    'available_in'    => array(),
153                    'associated_type' => false,
154                    'replace_str'     => '##BIZ-YOUR-NAME##',
155                    'aliases'         => array(),
156                ),
157
158                'biz-your-email'     => array(
159
160                    'description'     => __( 'Business: Your Email', 'zero-bs-crm' ),
161                    'origin'          => __( 'Global', 'zero-bs-crm' ),
162                    'expected_format' => 'str',
163                    'available_in'    => array(),
164                    'associated_type' => false,
165                    'replace_str'     => '##BIZ-YOUR-EMAIL##',
166                    'aliases'         => array(),
167                ),
168
169                'biz-your-url'       => array(
170
171                    'description'     => __( 'Business: Your URL', 'zero-bs-crm' ),
172                    'origin'          => __( 'Global', 'zero-bs-crm' ),
173                    'expected_format' => 'str',
174                    'available_in'    => array(),
175                    'associated_type' => false,
176                    'replace_str'     => '##BIZ-YOUR-URL##',
177                    'aliases'         => array(),
178                ),
179
180                'biz-extra'          => array(
181
182                    'description'     => __( 'Business: Extra Info', 'zero-bs-crm' ),
183                    'origin'          => __( 'Global', 'zero-bs-crm' ),
184                    'expected_format' => 'str',
185                    'available_in'    => array(),
186                    'associated_type' => false,
187                    'replace_str'     => '##BIZ-EXTRA-INFO##',
188                    'aliases'         => array(),
189                ),
190
191                'social-links'       => array(
192
193                    'description'     => __( 'Social links', 'zero-bs-crm' ),
194                    'origin'          => __( 'Global', 'zero-bs-crm' ),
195                    'expected_format' => 'html',
196                    'available_in'    => array(),
197                    'associated_type' => false,
198                    'replace_str'     => '##SOCIAL-LINKS##',
199                ),
200
201                'unsub-line'         => array(
202
203                    'description'     => __( 'Unsubscribe line', 'zero-bs-crm' ),
204                    'origin'          => __( 'Global', 'zero-bs-crm' ),
205                    'expected_format' => 'html',
206                    'available_in'    => array(),
207                    'associated_type' => false,
208                    'replace_str'     => '##UNSUB-LINE##',
209                    'aliases'         => array( '###UNSUB-LINE###', '###UNSUB###', '###UNSUBSCRIBE###', '###FOOTERUNSUBDEETS###' ),
210                ),
211
212                'powered-by'         => array(
213
214                    'description'     => __( 'Powered by', 'zero-bs-crm' ),
215                    'origin'          => __( 'Global', 'zero-bs-crm' ),
216                    'expected_format' => 'html',
217                    'available_in'    => array(),
218                    'associated_type' => false,
219                    'replace_str'     => '##POWERED-BY##',
220                    'aliases'         => array( '###POWEREDBYDEETS###' ),
221                ),
222
223                'login-link'         => array(
224
225                    'description'     => __( 'CRM login link (HTML)', 'zero-bs-crm' ),
226                    'origin'          => __( 'Global', 'zero-bs-crm' ),
227                    'expected_format' => 'html',
228                    'available_in'    => array(),
229                    'associated_type' => false,
230                    'replace_str'     => '##LOGIN-LINK##',
231                    'aliases'         => array( '###LOGINLINK###' ),
232                ),
233
234                'login-button'       => array(
235
236                    'description'     => __( 'CRM login link button (HTML)', 'zero-bs-crm' ),
237                    'origin'          => __( 'Global', 'zero-bs-crm' ),
238                    'expected_format' => 'html',
239                    'available_in'    => array(),
240                    'associated_type' => false,
241                    'replace_str'     => '##LOGIN-BUTTON##',
242                    'aliases'         => array( '###LOGINBUTTON###' ),
243                ),
244
245                'login-url'          => array(
246
247                    'description'     => __( 'CRM login URL', 'zero-bs-crm' ),
248                    'origin'          => __( 'Global', 'zero-bs-crm' ),
249                    'expected_format' => 'str',
250                    'available_in'    => array(),
251                    'associated_type' => false,
252                    'replace_str'     => '##LOGIN-URL##',
253                    'aliases'         => array( '###LOGINURL###', '###ADMINURL###' ),
254                ),
255
256                'portal-link'        => array(
257
258                    'description'     => __( 'Portal link (HTML)', 'zero-bs-crm' ),
259                    'origin'          => __( 'Global', 'zero-bs-crm' ),
260                    'expected_format' => 'html',
261                    'available_in'    => array(),
262                    'associated_type' => false,
263                    'replace_str'     => '##PORTAL-LINK##',
264                    'aliases'         => array( '###PORTALLINK###' ),
265                ),
266
267                'portal-view-button' => array(
268
269                    'description'     => __( '"View in Portal" button (HTML)', 'zero-bs-crm' ),
270                    'origin'          => __( 'Global', 'zero-bs-crm' ),
271                    'expected_format' => 'html',
272                    'available_in'    => array(),
273                    'associated_type' => false,
274                    'replace_str'     => '##PORTAL-VIEW-BUTTON##',
275                    'aliases'         => array( '###VIEWINPORTAL###' ),
276                ),
277
278                'portal-button'      => array(
279
280                    'description'     => __( 'Portal link button (HTML)', 'zero-bs-crm' ),
281                    'origin'          => __( 'Global', 'zero-bs-crm' ),
282                    'expected_format' => 'html',
283                    'available_in'    => array(),
284                    'associated_type' => false,
285                    'replace_str'     => '##PORTAL-BUTTON##',
286                    'aliases'         => array( '###PORTALBUTTON###' ),
287                ),
288
289                'portal-url'         => array(
290
291                    'description'     => __( 'Portal URL', 'zero-bs-crm' ),
292                    'origin'          => __( 'Global', 'zero-bs-crm' ),
293                    'expected_format' => 'str',
294                    'available_in'    => array(),
295                    'associated_type' => false,
296                    'replace_str'     => '##PORTAL-URL##',
297                    'aliases'         => array( '###PORTALURL###' ),
298                ),
299
300                'css'                => array(
301
302                    'description'     => __( 'CSS (restricted to HTML templates)', 'zero-bs-crm' ),
303                    'origin'          => __( 'Global', 'zero-bs-crm' ),
304                    'expected_format' => 'html',
305                    'available_in'    => array(),
306                    'associated_type' => false,
307                    'replace_str'     => '##CSS##',
308                    'aliases'         => array( '###CSS###' ),
309                ),
310
311                'title'              => array(
312
313                    'description'     => __( 'Generally used to fill HTML title tags', 'zero-bs-crm' ),
314                    'origin'          => __( 'Global', 'zero-bs-crm' ),
315                    'expected_format' => 'str',
316                    'available_in'    => array(),
317                    'associated_type' => false,
318                    'replace_str'     => '##TITLE##',
319                    'aliases'         => array( '###TITLE###' ),
320                ),
321
322                'msg-content'        => array(
323
324                    'description'     => __( 'Message content (restricted to some email templates)', 'zero-bs-crm' ),
325                    'origin'          => __( 'Global', 'zero-bs-crm' ),
326                    'expected_format' => 'html',
327                    'available_in'    => array(),
328                    'associated_type' => false,
329                    'replace_str'     => '##MSG-CONTENT##',
330                    'aliases'         => array( '###MSGCONTENT###' ),
331                ),
332
333                'email'              => array(
334
335                    'description'     => __( 'Email (generally used to insert user email)', 'zero-bs-crm' ),
336                    'origin'          => __( 'Global', 'zero-bs-crm' ),
337                    'expected_format' => 'str',
338                    'available_in'    => array(),
339                    'associated_type' => false,
340                    'replace_str'     => '##EMAIL##',
341                    'aliases'         => array( '###EMAIL###' ),
342                ),
343
344                'password'           => array(
345
346                    'description'     => __( 'Inserts a link where one can reset their password', 'zero-bs-crm' ),
347                    'origin'          => __( 'Global', 'zero-bs-crm' ),
348                    'expected_format' => 'str',
349                    'available_in'    => array(),
350                    'associated_type' => false,
351                    'replace_str'     => '##PASSWORD##',
352                    'aliases'         => array( '##PASSWORD-RESET-LINK##', '###PASSWORD###' ),
353                ),
354
355                'email-from-name'    => array(
356
357                    'description'     => __( '"From" Name when sending email', 'zero-bs-crm' ),
358                    'origin'          => __( 'Global', 'zero-bs-crm' ),
359                    'expected_format' => 'email',
360                    'available_in'    => array(),
361                    'associated_type' => false,
362                    'replace_str'     => '##EMAIL-FROM-NAME##',
363                    'aliases'         => array( '###FROMNAME###' ),
364                ),
365
366            ),
367
368            'contact'     => array(
369
370                'contact-fullname' => array(
371
372                    'description'     => __( 'Contact full name', 'zero-bs-crm' ),
373                    'origin'          => __( 'Contact Information', 'zero-bs-crm' ),
374                    'available_in'    => array(),
375                    'associated_type' => ZBS_TYPE_CONTACT,
376                    'replace_str'     => '##CONTACT-FULLNAME##',
377                    'expected_format' => 'str',
378                    'aliases'         => array( '##CUSTOMERNAME##', '##CUSTOMER-FULLNAME##' ),
379                ),
380
381            ),
382            'company'     => array(),
383            'quote'       => array(
384
385                'quote-content'  => array(
386
387                    'description'     => __( 'Quote content (HTML)', 'zero-bs-crm' ),
388                    'origin'          => __( 'Quote Builder', 'zero-bs-crm' ),
389                    'expected_format' => 'html',
390                    'available_in'    => array(),
391                    'associated_type' => ZBS_TYPE_QUOTE,
392                    'replace_str'     => '##QUOTE-CONTENT##',
393                    'aliases'         => array( '###QUOTECONTENT###' ),
394                ),
395
396                'quote-title'    => array(
397
398                    'description'     => __( 'Quote title', 'zero-bs-crm' ),
399                    'origin'          => __( 'Quote Builder', 'zero-bs-crm' ),
400                    'expected_format' => 'str',
401                    'available_in'    => array(),
402                    'associated_type' => ZBS_TYPE_QUOTE,
403                    'replace_str'     => '##QUOTE-TITLE##',
404                    'aliases'         => array( '###QUOTETITLE###', '##QUOTETITLE##' ),
405                ),
406
407                'quote-value'    => array(
408
409                    'description'     => __( 'Quote value', 'zero-bs-crm' ),
410                    'origin'          => __( 'Quote Builder', 'zero-bs-crm' ),
411                    'expected_format' => 'str',
412                    'available_in'    => array(),
413                    'associated_type' => ZBS_TYPE_QUOTE,
414                    'replace_str'     => '##QUOTE-VALUE##',
415                    'aliases'         => array( '###QUOTEVALUE###', '##QUOTEVALUE##' ),
416                ),
417
418                'quote-date'     => array(
419
420                    'description'     => __( 'Quote date', 'zero-bs-crm' ),
421                    'origin'          => __( 'Quote Builder', 'zero-bs-crm' ),
422                    'expected_format' => 'str',
423                    'available_in'    => array(),
424                    'associated_type' => ZBS_TYPE_QUOTE,
425                    'replace_str'     => '##QUOTE-DATE##',
426                    'aliases'         => array( '###QUOTEDATE###', '##QUOTEDATE##' ),
427                ),
428
429                'quote-url'      => array(
430
431                    'description'     => __( 'Quote URL', 'zero-bs-crm' ),
432                    'origin'          => __( 'Quote Builder', 'zero-bs-crm' ),
433                    'expected_format' => 'str',
434                    'available_in'    => array(),
435                    'associated_type' => ZBS_TYPE_QUOTE,
436                    'replace_str'     => '##QUOTE-URL##',
437                    'aliases'         => array( '###QUOTEURL###' ),
438                ),
439
440                'quote-edit-url' => array(
441
442                    'description'     => __( 'Quote edit URL', 'zero-bs-crm' ),
443                    'origin'          => __( 'Quote Builder', 'zero-bs-crm' ),
444                    'expected_format' => 'str',
445                    'available_in'    => array(),
446                    'associated_type' => ZBS_TYPE_QUOTE,
447                    'replace_str'     => '##QUOTE-EDIT-URL##',
448                    'aliases'         => array( '###QUOTEEDITURL###' ),
449                ),
450
451            ),
452            'invoice'     => array(
453
454                'invoice-title'               => array(
455
456                    'description'     => __( 'Invoice title', 'zero-bs-crm' ),
457                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
458                    'expected_format' => 'str',
459                    'available_in'    => array(),
460                    'associated_type' => ZBS_TYPE_INVOICE,
461                    'replace_str'     => '##INVOICE-TITLE##',
462                    'aliases'         => array( '###INVOICETITLE###' ),
463                ),
464
465                'logo-class'                  => array(
466
467                    'description'     => __( 'Invoice logo CSS class', 'zero-bs-crm' ),
468                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
469                    'expected_format' => 'str',
470                    'available_in'    => array(),
471                    'associated_type' => ZBS_TYPE_INVOICE,
472                    'replace_str'     => '##LOGO-CLASS##',
473                    'aliases'         => array( '###LOGOCLASS###' ),
474                ),
475
476                'logo-url'                    => array(
477
478                    'description'     => __( 'Invoice logo URL', 'zero-bs-crm' ),
479                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
480                    'expected_format' => 'str',
481                    'available_in'    => array(),
482                    'associated_type' => ZBS_TYPE_INVOICE,
483                    'replace_str'     => '##LOGO-URL##',
484                    'aliases'         => array( '###LOGOURL###' ),
485                ),
486
487                'invoice-number'              => array(
488
489                    'description'     => __( 'Invoice number', 'zero-bs-crm' ),
490                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
491                    'expected_format' => 'str',
492                    'available_in'    => array(),
493                    'associated_type' => ZBS_TYPE_INVOICE,
494                    'replace_str'     => '##INVOICE-NUMBER##',
495                    'aliases'         => array( '###INVNOSTR###' ),
496                ),
497
498                'invoice-date'                => array(
499
500                    'description'     => __( 'Invoice date', 'zero-bs-crm' ),
501                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
502                    'expected_format' => 'str',
503                    'available_in'    => array(),
504                    'associated_type' => ZBS_TYPE_INVOICE,
505                    'replace_str'     => '##INVOICE-DATE##',
506                    'aliases'         => array( '###INVDATESTR###' ),
507                ),
508
509                'invoice-id-styles'           => array(
510
511                    'description'     => __( 'Invoice ID styles', 'zero-bs-crm' ),
512                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
513                    'expected_format' => 'str',
514                    'available_in'    => array(),
515                    'associated_type' => ZBS_TYPE_INVOICE,
516                    'replace_str'     => '##INVOICE-ID-STYLES##',
517                    'aliases'         => array( '###INVIDSTYLES###' ),
518                ),
519
520                'invoice-ref'                 => array(
521
522                    'description'     => __( 'Invoice reference', 'zero-bs-crm' ),
523                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
524                    'expected_format' => 'str',
525                    'available_in'    => array(),
526                    'associated_type' => ZBS_TYPE_INVOICE,
527                    'replace_str'     => '##INVOICE-REF##',
528                    'aliases'         => array( '###REF###' ),
529                ),
530
531                'invoice-due-date'            => array(
532
533                    'description'     => __( 'Invoice due date', 'zero-bs-crm' ),
534                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
535                    'expected_format' => 'str',
536                    'available_in'    => array(),
537                    'associated_type' => ZBS_TYPE_INVOICE,
538                    'replace_str'     => '##INVOICE-DUE-DATE##',
539                    'aliases'         => array( '###DUEDATE###' ),
540                ),
541
542                'invoice-biz-class'           => array(
543
544                    'description'     => __( 'CSS class for table with your business info', 'zero-bs-crm' ),
545                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
546                    'expected_format' => 'str',
547                    'available_in'    => array(),
548                    'associated_type' => ZBS_TYPE_INVOICE,
549                    'replace_str'     => '##INVOICE-BIZ-CLASS##',
550                    'aliases'         => array( '###BIZCLASS###' ),
551                ),
552
553                'invoice-customer-info'       => array(
554
555                    'description'     => __( 'Table with assigned contact information (HTML)', 'zero-bs-crm' ),
556                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
557                    'expected_format' => 'html',
558                    'available_in'    => array(),
559                    'associated_type' => ZBS_TYPE_INVOICE,
560                    'replace_str'     => '##INVOICE-CUSTOMER-INFO##',
561                    'aliases'         => array( '###CUSTINFOTABLE###' ),
562                ),
563
564                'invoice-from-name'           => array(
565
566                    'description'     => __( 'Name of company issuing the invoice', 'zero-bs-crm' ),
567                    'origin'          => __( 'Statement Builder', 'zero-bs-crm' ),
568                    'expected_format' => 'str',
569                    'available_in'    => array(),
570                    'associated_type' => ZBS_TYPE_INVOICE,
571                    'replace_str'     => '##INVOICE-FROM-NAME##',
572                    'aliases'         => array(),
573                ),
574
575                'invoice-table-headers'       => array(
576
577                    'description'     => __( 'Table headers for invoice line items (HTML)', 'zero-bs-crm' ),
578                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
579                    'expected_format' => 'html',
580                    'available_in'    => array(),
581                    'associated_type' => ZBS_TYPE_INVOICE,
582                    'replace_str'     => '##INVOICE-TABLE-HEADERS##',
583                    'aliases'         => array( '###TABLEHEADERS###' ),
584                ),
585
586                'invoice-line-items'          => array(
587
588                    'description'     => __( 'Invoice line items (HTML)', 'zero-bs-crm' ),
589                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
590                    'expected_format' => 'html',
591                    'available_in'    => array(),
592                    'associated_type' => ZBS_TYPE_INVOICE,
593                    'replace_str'     => '##INVOICE-LINE-ITEMS##',
594                    'aliases'         => array( '###LINEITEMS###' ),
595                ),
596
597                'invoice-totals-table'        => array(
598
599                    'description'     => __( 'Invoice totals table (HTML)', 'zero-bs-crm' ),
600                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
601                    'expected_format' => 'html',
602                    'available_in'    => array(),
603                    'associated_type' => ZBS_TYPE_INVOICE,
604                    'replace_str'     => '##INVOICE-TOTALS-TABLE##',
605                    'aliases'         => array( '###TOTALSTABLE###' ),
606                ),
607
608                'pre-invoice-payment-details' => array(
609
610                    'description'     => __( 'Text before invoice payment details', 'zero-bs-crm' ),
611                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
612                    'expected_format' => 'html',
613                    'available_in'    => array(),
614                    'associated_type' => ZBS_TYPE_INVOICE,
615                    'replace_str'     => '##PRE-INVOICE-PAYMENT-DETAILS##',
616                    'aliases'         => array( '###PREPARTIALS###' ),
617                ),
618
619                'invoice-payment-details'     => array(
620
621                    'description'     => __( 'Invoice payment details', 'zero-bs-crm' ),
622                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
623                    'expected_format' => 'html',
624                    'available_in'    => array(),
625                    'associated_type' => ZBS_TYPE_INVOICE,
626                    'replace_str'     => '##INVOICE-PAYMENT-DETAILS##',
627                    'aliases'         => array( '###PAYMENTDEETS###', '###PAYDETAILS###' ),
628                ),
629
630                'invoice-partials-table'      => array(
631
632                    'description'     => __( 'Invoice partials table (HTML)', 'zero-bs-crm' ),
633                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
634                    'expected_format' => 'html',
635                    'available_in'    => array(),
636                    'associated_type' => ZBS_TYPE_INVOICE,
637                    'replace_str'     => '##INVOICE-PARTIALS-TABLE##',
638                    'aliases'         => array( '###PARTIALSTABLE###' ),
639                ),
640
641                'invoice-label-inv-number'    => array(
642
643                    'description'     => __( 'Label for invoice number', 'zero-bs-crm' ),
644                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
645                    'expected_format' => 'str',
646                    'available_in'    => array(),
647                    'associated_type' => ZBS_TYPE_INVOICE,
648                    'replace_str'     => '##INVOICE-LABEL-INV-NUMBER##',
649                    'aliases'         => array( '###LANGINVNO###' ),
650                ),
651
652                'invoice-label-inv-date'      => array(
653
654                    'description'     => __( 'Label for invoice date', 'zero-bs-crm' ),
655                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
656                    'expected_format' => 'str',
657                    'available_in'    => array(),
658                    'associated_type' => ZBS_TYPE_INVOICE,
659                    'replace_str'     => '##INVOICE-LABEL-INV-DATE##',
660                    'aliases'         => array( '###LANGINVDATE###' ),
661                ),
662
663                'invoice-label-inv-ref'       => array(
664
665                    'description'     => __( 'Label for invoice reference', 'zero-bs-crm' ),
666                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
667                    'expected_format' => 'str',
668                    'available_in'    => array(),
669                    'associated_type' => ZBS_TYPE_INVOICE,
670                    'replace_str'     => '##INVOICE-LABEL-INV-REF##',
671                    'aliases'         => array( '###LANGINVREF###' ),
672                ),
673
674                'invoice-label-from'          => array(
675
676                    'description'     => __( 'Label for name of company issuing invoice', 'zero-bs-crm' ),
677                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
678                    'expected_format' => 'str',
679                    'available_in'    => array(),
680                    'associated_type' => ZBS_TYPE_INVOICE,
681                    'replace_str'     => '##INVOICE-LABEL-FROM##',
682                    'aliases'         => array( '###LANGFROM###' ),
683                ),
684
685                'invoice-label-to'            => array(
686
687                    'description'     => __( 'Label for name of contact receiving invoice', 'zero-bs-crm' ),
688                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
689                    'expected_format' => 'str',
690                    'available_in'    => array(),
691                    'associated_type' => ZBS_TYPE_INVOICE,
692                    'replace_str'     => '##INVOICE-LABEL-TO##',
693                    'aliases'         => array( '###LANGTO###' ),
694                ),
695
696                'invoice-label-due-date'      => array(
697
698                    'description'     => __( 'Label for invoice due date', 'zero-bs-crm' ),
699                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
700                    'expected_format' => 'str',
701                    'available_in'    => array(),
702                    'associated_type' => ZBS_TYPE_INVOICE,
703                    'replace_str'     => '##INVOICE-LABEL-DUE-DATE##',
704                    'aliases'         => array( '###LANGDUEDATE###' ),
705                ),
706
707                'invoice-label-status'        => array(
708
709                    'description'     => __( 'Label for invoice status', 'zero-bs-crm' ),
710                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
711                    'expected_format' => 'str',
712                    'available_in'    => array(),
713                    'associated_type' => ZBS_TYPE_INVOICE,
714                    'replace_str'     => '##INVOICE-LABEL-STATUS##',
715                    'aliases'         => array(),
716                ),
717
718                'invoice-html-status'         => array(
719
720                    'description'     => __( 'Invoice status (HTML)', 'zero-bs-crm' ),
721                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
722                    'expected_format' => 'html',
723                    'available_in'    => array(),
724                    'associated_type' => ZBS_TYPE_INVOICE,
725                    'replace_str'     => '##INVOICE-HTML-STATUS##',
726                    'aliases'         => array( '###TOPSTATUS###' ),
727                ),
728
729                'invoice-pay-button'          => array(
730
731                    'description'     => __( 'Invoice pay button (HTML)', 'zero-bs-crm' ),
732                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
733                    'expected_format' => 'html',
734                    'available_in'    => array(),
735                    'associated_type' => ZBS_TYPE_INVOICE,
736                    'replace_str'     => '##INVOICE-PAY-BUTTON##',
737                    'aliases'         => array( '###PAYPALBUTTON###' ),
738                ),
739
740                'invoice-pay-thanks'          => array(
741
742                    'description'     => __( 'Invoice payment thanks message (HTML)', 'zero-bs-crm' ),
743                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
744                    'expected_format' => 'html',
745                    'available_in'    => array(),
746                    'associated_type' => ZBS_TYPE_INVOICE,
747                    'replace_str'     => '##INVOICE-PAY-THANKS##',
748                    'aliases'         => array( '###PAYTHANKS###' ),
749                ),
750
751                'invoice-pay-terms'           => array(
752
753                    'description'     => __( 'Invoice payment terms (HTML)', 'zero-bs-crm' ),
754                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
755                    'expected_format' => 'html',
756                    'available_in'    => array(),
757                    'associated_type' => ZBS_TYPE_INVOICE,
758                    'replace_str'     => '##INVOICE-PAY-TERMS##',
759                    'aliases'         => array( '###PAYMENTTERMS###' ),
760                ),
761
762                'invoice-statement-html'      => array(
763
764                    'description'     => __( 'Invoice statement (HTML)', 'zero-bs-crm' ),
765                    'origin'          => __( 'Statement Builder', 'zero-bs-crm' ),
766                    'expected_format' => 'html',
767                    'available_in'    => array(),
768                    'associated_type' => ZBS_TYPE_INVOICE,
769                    'replace_str'     => '##INVOICE-STATEMENT-HTML##',
770                    'aliases'         => array( '###STATEMENTHTML###' ),
771                ),
772
773                'invoice-ref-styles'          => array(
774
775                    'description'     => __( 'CSS attributes applied to invoice reference label', 'zero-bs-crm' ),
776                    'origin'          => __( 'Statement Builder', 'zero-bs-crm' ),
777                    'expected_format' => 'str',
778                    'available_in'    => array(),
779                    'associated_type' => ZBS_TYPE_INVOICE,
780                    'replace_str'     => '##INVOICE-REF-STYLES##',
781                    'aliases'         => array( '###INVREFSTYLES###' ),
782                ),
783
784                'invoice-custom-fields'       => array(
785
786                    'description'     => __( 'Any custom fields associated with invoice (if enabled in Invoice Settings)', 'zero-bs-crm' ),
787                    'origin'          => __( 'Invoice Builder', 'zero-bs-crm' ),
788                    'expected_format' => 'str',
789                    'available_in'    => array(),
790                    'associated_type' => ZBS_TYPE_INVOICE,
791                    'replace_str'     => '##INV-CUSTOM-FIELDS##',
792                    'aliases'         => array(),
793                ),
794
795            ),
796            'transaction' => array(),
797            'event'       => array(
798
799                'task-title'       => array(
800
801                    'description'     => __( 'Task title', 'zero-bs-crm' ),
802                    'origin'          => __( 'Task Scheduler', 'zero-bs-crm' ),
803                    'expected_format' => 'str',
804                    'available_in'    => array(),
805                    'associated_type' => ZBS_TYPE_TASK,
806                    'replace_str'     => '##TASK-TITLE##',
807                    'aliases'         => array( '###EVENTTITLE###', '##EVENT-TITLE##' ),
808                ),
809
810                'task-link'        => array(
811
812                    'description'     => __( 'Task link (HTML)', 'zero-bs-crm' ),
813                    'origin'          => __( 'Task Scheduler', 'zero-bs-crm' ),
814                    'expected_format' => 'html',
815                    'available_in'    => array(),
816                    'associated_type' => ZBS_TYPE_TASK,
817                    'replace_str'     => '##TASK-LINK##',
818                    'aliases'         => array( '###EVENTLINK###' ),
819                ),
820
821                'task-link-button' => array(
822
823                    'description'     => __( 'Task link button (HTML)', 'zero-bs-crm' ),
824                    'origin'          => __( 'Task Scheduler', 'zero-bs-crm' ),
825                    'expected_format' => 'html',
826                    'available_in'    => array(),
827                    'associated_type' => ZBS_TYPE_TASK,
828                    'replace_str'     => '##TASK-LINK-BUTTON##',
829                    'aliases'         => array( '###EVENTLINKBUTTON###' ),
830                ),
831
832                'task-body'        => array(
833
834                    'description'     => __( 'Task content (HTML)', 'zero-bs-crm' ),
835                    'origin'          => __( 'Task Scheduler', 'zero-bs-crm' ),
836                    'expected_format' => 'html',
837                    'available_in'    => array(),
838                    'associated_type' => ZBS_TYPE_TASK,
839                    'replace_str'     => '##TASK-BODY##',
840                    'aliases'         => array( '###EVENTBODY###' ),
841                ),
842
843            ),
844
845            // probably not req. yet:
846            /*
847            'address',
848            'form',
849            'segment',
850            'log',
851            'lineitem',
852            'eventreminder',
853            'quotetemplate',
854            */
855
856        );
857
858        return $this->placeholders;
859    }
860
861    /**
862     * Builds initial list of placeholders using defaults, object-models, custom fields, and filters
863     *
864     * @return array of all placeholders
865     */
866    public function build_placeholders( $include_custom_fields = true ) {
867
868        global $zbs;
869
870        // any hard-typed defaults
871        $placeholders = $this->default_placeholders();
872
873        // load fields from DAL object models
874        $placeholders = $this->load_from_object_models( $placeholders, $include_custom_fields );
875
876        // some backward compat tweaks:
877        if ( isset( $placeholders['quote']['title'] ) ) {
878            $placeholders['quote']['title']['aliases'] = array( '##QUOTE-TITLE##', '##QUOTETITLE##' );
879        }
880        if ( isset( $placeholders['quote']['value'] ) ) {
881            $placeholders['quote']['value']['aliases'] = array( '##QUOTEVALUE##' );
882        }
883        if ( isset( $placeholders['quote']['date'] ) ) {
884            $placeholders['quote']['date']['aliases'] = array( '##QUOTEDATE##' );
885        }
886
887        // add setting dependent placeholders
888        $placeholders = $this->add_setting_dependent_placeholders( $placeholders );
889
890        // here we fill in any 'available-in' crossovers
891        // e.g. you can reference contacts in quotes.
892        $placeholders = $this->add_available_in( $placeholders );
893
894        // filter (to allow extensions to ammend)
895        $this->placeholders = apply_filters( 'jpcrm_templating_placeholders', $placeholders );
896
897        // return
898        return $this->placeholders;
899    }
900
901    /**
902     * Add setting-dependent placeholders
903     *
904     * @return array of all placeholders
905     */
906    public function add_setting_dependent_placeholders( $placeholders = array() ) {
907
908        // remove tooling where inactive modules
909        $placeholders = $this->strip_inactive_tooling( $placeholders );
910
911        // if ownership model enabled, add 'owner' fields
912        // this is simplistic for now and appears outwardly to only encompasses contacts
913        // ... in fact it works for all object types (provided the object is passed to replace_placeholders())
914        // as per #1531 this could later be expanded to encompass all objects, perhaps in sites & teams extension
915        $using_ownership = zeroBSCRM_getSetting( 'perusercustomers' );
916        if ( $using_ownership ) {
917
918            if ( isset( $placeholders['global'] ) ) {
919
920                // note, these are auto-populated using the first `owner` field that our replacement function comes across in $replacement_objects
921                $placeholders['global']['assigned-to-name'] = array(
922
923                    'description'     => __( 'Where available, the display name of the WordPress user who owns the object', 'zero-bs-crm' ),
924                    'origin'          => __( 'Global', 'zero-bs-crm' ),
925                    'expected_format' => 'str',
926                    'available_in'    => array(),
927                    'associated_type' => false,
928                    'replace_str'     => '##ASSIGNED-TO-NAME##',
929                    'aliases'         => array( '##ASSIGNED-TO-SIGNATURE##' ),
930                );
931
932                $placeholders['global']['assigned-to-email'] = array(
933
934                    'description'     => __( 'Where available, the email of the WordPress user who owns the object', 'zero-bs-crm' ),
935                    'origin'          => __( 'Global', 'zero-bs-crm' ),
936                    'expected_format' => 'email',
937                    'available_in'    => array(),
938                    'associated_type' => false,
939                    'replace_str'     => '##ASSIGNED-TO-EMAIL##',
940                    'aliases'         => array(),
941                );
942
943                $placeholders['global']['assigned-to-username'] = array(
944
945                    'description'     => __( 'Where available, the username of the WordPress user who owns the object', 'zero-bs-crm' ),
946                    'origin'          => __( 'Global', 'zero-bs-crm' ),
947                    'expected_format' => 'email',
948                    'available_in'    => array(),
949                    'associated_type' => false,
950                    'replace_str'     => '##ASSIGNED-TO-USERNAME##',
951                    'aliases'         => array(),
952                );
953
954                $placeholders['global']['assigned-to-mob'] = array(
955
956                    'description'     => __( 'Where available, the mobile phone of the WordPress user who owns the object', 'zero-bs-crm' ),
957                    'origin'          => __( 'Global', 'zero-bs-crm' ),
958                    'expected_format' => 'tel',
959                    'available_in'    => array(),
960                    'associated_type' => false,
961                    'replace_str'     => '##ASSIGNED-TO-MOB##',
962                    'aliases'         => array(),
963                );
964
965            }
966        }
967
968        return $placeholders;
969    }
970
971    /**
972     * Remove object types where that module is not active, e.g. invoices
973     * This needs to fire before this->add_available_in in the build queue
974     *
975     * @return array of all placeholders
976     */
977    public function strip_inactive_tooling( $placeholders = array() ) {
978
979        $setting_map = array(
980            'form'        => 'feat_forms',
981            'quote'       => 'feat_quotes',
982            'invoice'     => 'feat_invs',
983            'event'       => 'feat_calendar',
984            'transaction' => 'feat_transactions',
985        );
986
987        // make a quick state list for simplicity
988        $tooling_states = array();
989        foreach ( $setting_map as $tooling_area => $setting_key ) {
990
991            $tooling_states[ $tooling_area ] = ( zeroBSCRM_getSetting( $setting_key ) == '1' ) ? true : false;
992
993            // remove from placeholder list
994            if ( ! $tooling_states[ $tooling_area ] && isset( $placeholders[ $tooling_area ] ) ) {
995
996                // remove
997                unset( $placeholders[ $tooling_area ] );
998            }
999        }
1000
1001        // remove from $available_in_links too
1002        $available_in = array();
1003        foreach ( $this->available_in_links as $object_type_key => $tooling_area_array ) {
1004
1005            if ( ! isset( $tooling_states[ $object_type_key ] ) || $tooling_states[ $object_type_key ] ) {
1006
1007                $tooling_available = array();
1008
1009                foreach ( $tooling_area_array as $tooling_area ) {
1010
1011                    // add back if not in the list
1012                    if ( ! array_key_exists( $tooling_area, $tooling_states ) || $tooling_states[ $tooling_area ] ) {
1013
1014                        $tooling_available[] = $tooling_area;
1015
1016                    }
1017                }
1018
1019                $available_in[ $object_type_key ] = $tooling_available;
1020
1021            }
1022        }
1023        $this->available_in_links = $available_in;
1024
1025        // return
1026        return $placeholders;
1027    }
1028
1029    /**
1030     * Appends to passed placeholder model based on DAL object models
1031     * (optionally) including custom fields where applicable
1032     *
1033     * @param bool include_custom_fields
1034     *
1035     * @return int count
1036     */
1037    private function load_from_object_models(
1038        $placeholders,
1039        $include_custom_fields = false,
1040        $excluded_slugs = array(
1041
1042            // global
1043            'zbs_site',
1044            'zbs_team',
1045            'zbs_owner',
1046            'id_override',
1047            'parent',
1048            'send_attachments',
1049            'hash',
1050
1051            // contact
1052            'alias',
1053
1054            // quote
1055            'template',
1056            'acceptedsigned',
1057            'acceptedip',
1058
1059            // invoice
1060            'pay_via',
1061            'allow_tip',
1062            'allow_partial',
1063            'hash_viewed',
1064            'hash_viewed_count',
1065            'portal_viewed',
1066            'portal_viewed_count',
1067            'pdf_template',
1068            'portal_template',
1069            'email_template',
1070            'invoice_frequency',
1071            'address_to_objtype',
1072
1073            // event
1074            'show_on_cal',
1075            'show_on_portal',
1076            'title',
1077
1078        )
1079    ) {
1080
1081        global $zbs;
1082
1083        // retrieve object types
1084        $object_types         = $zbs->DAL->get_object_types_by_index();
1085        $second_address_label = zeroBSCRM_getSetting( 'secondaddresslabel' );
1086        if ( empty( $second_address_label ) ) {
1087            $second_address_label = __( 'Second Address', 'zero-bs-crm' );
1088        }
1089
1090        // cycle through them, adding where they have $include_in_templating
1091        foreach ( $object_types as $object_type_index => $object_type_key ) {
1092
1093            $object_layer = $zbs->DAL->getObjectLayerByType( $object_type_index );
1094            $object_label = $zbs->DAL->typeStr( $object_type_index );
1095
1096            // if there is an object layer available, and it's included in templating:
1097            if ( is_object( $object_layer ) && $object_layer->is_included_in_templating() ) {
1098
1099                // (optionally) include custom field references
1100                if ( $include_custom_fields ) {
1101
1102                    $object_model = $object_layer->objModelIncCustomFields();
1103
1104                } else {
1105
1106                    $object_model = $object_layer->objModel();
1107
1108                }
1109
1110                // cycle through object model & add fields
1111                foreach ( $object_model as $field_index => $field_info ) {
1112
1113                    // deal with exclusions
1114                    if ( ! in_array( $field_index, $excluded_slugs ) ) {
1115
1116                        // catching legacy secondary address contact field issues
1117                        $secondary_address_array = array( 'secaddr1', 'secaddr2', 'seccity', 'seccounty', 'secpostcode', 'seccountry' );
1118                        if ( 'contact' === $object_type_key && in_array( $field_index, $secondary_address_array, true ) ) {
1119                            $field_index = str_replace( 'sec', 'secaddr_', $field_index );
1120                            $new_key     = $object_type_key . '-' . $field_index;
1121                        } else {
1122                            $new_key = str_replace( '_', '-', $object_type_key . '-' . $field_index );
1123                        }
1124
1125                        $expected_format = '';
1126
1127                        // add if not present
1128                        // e.g. $placeholders['contact']['ID']
1129                        if ( ! isset( $placeholders[ $object_type_key ][ $new_key ] ) ) {
1130
1131                            // prettify these
1132                            $description = $object_label . ' ' . $field_index;
1133                            switch ( $field_index ) {
1134
1135                                case 'created':
1136                                    $description = sprintf( __( 'Date %s was created', 'zero-bs-crm' ), $object_label );
1137                                    break;
1138                                case 'lastupdated':
1139                                    $description = sprintf( __( 'Date %s was last updated', 'zero-bs-crm' ), $object_label );
1140                                    break;
1141                                case 'lastcontacted':
1142                                    $description = sprintf( __( 'Date %s was last contacted', 'zero-bs-crm' ), $object_label );
1143                                    break;
1144                                case 'tw':
1145                                    $description = sprintf( __( 'Twitter handle for %s', 'zero-bs-crm' ), $object_label );
1146                                    break;
1147                                case 'li':
1148                                    $description = sprintf( __( 'LinkedIn handle for %s', 'zero-bs-crm' ), $object_label );
1149                                    break;
1150                                case 'fb':
1151                                    $description = sprintf( __( 'Facebook page ID for %s', 'zero-bs-crm' ), $object_label );
1152                                    break;
1153                                case 'wpid':
1154                                    $description = sprintf( __( 'WordPress ID for %s', 'zero-bs-crm' ), $object_label );
1155                                    break;
1156
1157                            }
1158
1159                            if ( isset( $field_info['label'] ) ) {
1160                                $description = $object_label . ' ' . __( $field_info['label'], 'zero-bs-crm' );
1161                            }
1162
1163                            if ( ! empty( $field_info['area'] ) && $field_info['area'] == 'Second Address' ) {
1164                                $description .= ' (' . esc_html( $second_address_label ) . ')';
1165                            }
1166
1167                            // if it's a custom field we can infer format (and label):
1168                            if ( isset( $field_info['custom-field'] ) && $field_info['custom-field'] == 1 ) {
1169                                $field_info['format'] = $field_info[0];
1170                                if ( $field_info['format'] === 'date' ) {
1171                                    $field_info['format'] = 'uts';
1172                                }
1173                                $description .= ' (' . __( 'Custom Field', 'zero-bs-crm' ) . ')';
1174
1175                            }
1176
1177                            // add {placeholder_model}
1178                            $placeholders[ $object_type_key ][ $new_key ] = array(
1179
1180                                'description'     => $description,
1181                                'origin'          => sprintf( __( '%s object model', 'zero-bs-crm' ), $object_label ),
1182                                'available_in'    => array(),
1183                                'associated_type' => $object_type_index,
1184                                'replace_str'     => '##' . strtoupper( $object_type_key ) . '-' . strtoupper( $field_index ) . '##',
1185                                'expected_format' => $expected_format,
1186
1187                            );
1188
1189                            // trying to future proof, added a few helper attributes:
1190                            if ( isset( $field_info['format'] ) ) {
1191                                $placeholders[ $object_type_key ][ $new_key ]['expected_format'] = $field_info['format'];
1192                                if ( $field_info['format'] === 'uts' && empty( $field_info['autoconvert'] ) ) {
1193                                    $placeholders[ $object_type_key ][ $new_key . '_datetime_str' ] = array(
1194
1195                                        'description'     => $description . ' (' . __( 'DateTime string', 'zero-bs-crm' ) . ')',
1196                                        'origin'          => sprintf( __( '%s object model', 'zero-bs-crm' ), $object_label ),
1197                                        'available_in'    => array(),
1198                                        'associated_type' => $object_type_index,
1199                                        'replace_str'     => '##' . strtoupper( $object_type_key ) . '-' . strtoupper( $field_index ) . '_DATETIME_STR##',
1200                                        'expected_format' => 'str',
1201
1202                                    );
1203                                    $placeholders[ $object_type_key ][ $new_key . '_date_str' ] = array(
1204
1205                                        'description'     => $description . ' (' . __( 'Date string', 'zero-bs-crm' ) . ')',
1206                                        'origin'          => sprintf( __( '%s object model', 'zero-bs-crm' ), $object_label ),
1207                                        'available_in'    => array(),
1208                                        'associated_type' => $object_type_index,
1209                                        'replace_str'     => '##' . strtoupper( $object_type_key ) . '-' . strtoupper( $field_index ) . '_DATE_STR##',
1210                                        'expected_format' => 'str',
1211
1212                                    );
1213                                }
1214                            }
1215                        }
1216                    }
1217                }
1218            }
1219        }
1220
1221        return $placeholders;
1222    }
1223
1224    /**
1225     * Adds `available_in` links to allow tooling to get all applicable fields
1226     * e.g. Contact fields will be available in Quote builder
1227     *
1228     * @param array $placeholders placeholder array
1229     *
1230     * @return array $placeholders placeholder array
1231     */
1232    private function add_available_in( $placeholders ) {
1233
1234        // any to add?
1235        if ( is_array( $this->available_in_links ) ) {
1236
1237            foreach ( $this->available_in_links as $object_type => $tooling_area_array ) {
1238
1239                // $object_type = 'contact', $tooling_area = where to point to
1240                if ( isset( $placeholders[ $object_type ] ) ) {
1241
1242                    foreach ( $placeholders[ $object_type ] as $object_type_placeholder_key => $object_type_placeholder ) {
1243
1244                        // setup if not set
1245                        if ( ! is_array( $placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'] ) ) {
1246
1247                            $placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'] = array();
1248
1249                        }
1250
1251                        // add if not present
1252                        foreach ( $tooling_area_array as $tooling_area ) {
1253
1254                            if ( ! in_array( $tooling_area, $placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'] ) ) {
1255
1256                                $placeholders[ $object_type ][ $object_type_placeholder_key ]['available_in'][] = $tooling_area;
1257
1258                            }
1259                        }
1260                    }
1261                }
1262            }
1263        }
1264
1265        return $placeholders;
1266    }
1267
1268    /**
1269     * Returns full list of viable placeholders
1270     *
1271     * @param bool separate_categories - return in multi-dim array split out by categories
1272     *
1273     * @return array of all placeholders
1274     */
1275    public function get_placeholders( $separate_categories = true ) {
1276
1277        global $zbs;
1278
1279        // if asked to return without categories, do that
1280        if ( ! $separate_categories ) {
1281
1282            $no_cat_list = array();
1283            foreach ( $this->placeholders as $placeholder_group_key => $placeholder_group ) {
1284
1285                foreach ( $placeholder_group as $placeholder_key => $placeholder ) {
1286
1287                    $no_cat_list[ $placeholder['replace_str'] ] = $placeholder;
1288
1289                    // any aliases too
1290                    if ( isset( $placeholder['aliases'] ) && is_array( $placeholder['aliases'] ) ) {
1291
1292                        foreach ( $placeholder['aliases'] as $alias ) {
1293
1294                            $no_cat_list[ $alias ] = $placeholder;
1295
1296                        }
1297                    }
1298                }
1299            }
1300
1301            return $no_cat_list;
1302
1303        }
1304
1305        return $this->placeholders;
1306    }
1307
1308    /**
1309     * Returns flattened list of viable placeholders
1310     *
1311     * @return array of all placeholders
1312     */
1313    public function get_placeholders_shorthand() {
1314
1315        global $zbs;
1316
1317        $shorthand_list = array();
1318        foreach ( $this->placeholders as $placeholder_group_key => $placeholder_group ) {
1319
1320            $placeholder_group_prefix = '';
1321
1322            // all objtypes basically
1323            if ( $zbs->DAL->isValidObjTypeID( $zbs->DAL->objTypeID( $placeholder_group_key ) ) ) {
1324
1325                $placeholder_group_prefix = $placeholder_group_key . '-';
1326
1327            }
1328
1329            foreach ( $placeholder_group as $placeholder_key => $placeholder ) {
1330
1331                $shorthand_list[] = '##' . strtoupper( $placeholder_group_prefix . $placeholder_key ) . '##';
1332
1333                // any aliases too
1334                if ( isset( $placeholder['aliases'] ) && is_array( $placeholder['aliases'] ) ) {
1335
1336                    foreach ( $placeholder['aliases'] as $alias ) {
1337
1338                        $shorthand_list[] = $alias;
1339
1340                    }
1341                }
1342            }
1343        }
1344
1345        return $shorthand_list;
1346    }
1347
1348    /**
1349     * Returns list of viable placeholders for specific tooling/area, or group of areas, or object types, e.g. system emails or contact
1350     *
1351     * @param array|string tooling area, object type, or group of
1352     * @param bool hydrate_aliases - if true Aliases will be included as full records
1353     * @param bool split_by_category - if true return will be split by category as per get_placeholders
1354     *
1355     * @return array placeholders
1356     */
1357    public function get_placeholders_for_tooling( $tooling = array( 'global' ), $hydrate_aliases = false, $split_by_category = false ) {
1358
1359        global $zbs;
1360
1361        $applicable_placeholders = array();
1362
1363        // we allow array or string input here, so if a string, wrap for below
1364        if ( is_string( $tooling ) ) {
1365            $tooling = array( $tooling );
1366        }
1367
1368        // for MVP we let this get the whole lot then filter down, if this proves unperformant we could optimise here
1369        // .. or cache.
1370        $placeholders = $this->get_placeholders();
1371
1372        // cycle through all looking at the `available_in` attribute.
1373        // alternatively if an object type is passed, it'll return all fields for that type
1374        if ( is_array( $placeholders ) ) {
1375
1376            foreach ( $placeholders as $placeholder_group_key => $placeholder_group ) {
1377
1378                $placeholder_group_prefix = '';
1379
1380                // all objtypes basically
1381                if ( $zbs->DAL->isValidObjTypeID( $zbs->DAL->objTypeID( $placeholder_group_key ) ) ) {
1382
1383                    $placeholder_group_prefix = $placeholder_group_key . '-';
1384
1385                }
1386
1387                foreach ( $placeholder_group as $placeholder_key => $placeholder ) {
1388
1389                    // if in object type group:
1390                    if ( in_array( $placeholder_group_key, $tooling )
1391                        ||
1392                        isset( $placeholder['available_in'] ) && count( array_intersect( $tooling, $placeholder['available_in'] ) ) > 0
1393                    ) {
1394
1395                        // here we've flattened the array to actual placeholders (no tooling group)
1396                        // so use the placeholder str as key and add original id to array
1397                        $key = '##' . strtoupper( $placeholder_group_prefix . $placeholder_key ) . '##';
1398                        if ( isset( $placeholder['replace_str'] ) ) {
1399
1400                            // this overrides if set (will always be the same?)
1401                            $key = $placeholder['replace_str'];
1402
1403                        }
1404
1405                        // if return in categories
1406                        if ( $split_by_category ) {
1407
1408                            if ( ! isset( $applicable_placeholders[ $placeholder_group_key ] ) || ! is_array( $applicable_placeholders[ $placeholder_group_key ] ) ) {
1409
1410                                $applicable_placeholders[ $placeholder_group_key ] = array();
1411
1412                            }
1413
1414                            $applicable_placeholders[ $placeholder_group_key ][ $key ] = $placeholder;
1415
1416                            // add original key to arr
1417                            $applicable_placeholders[ $placeholder_group_key ][ $key ]['key'] = $placeholder_key;
1418
1419                        } else {
1420
1421                            $applicable_placeholders[ $key ] = $placeholder;
1422
1423                            // add original key to arr
1424                            $applicable_placeholders[ $key ]['key'] = $placeholder_key;
1425
1426                        }
1427
1428                        // aliases
1429                        if ( $hydrate_aliases && isset( $placeholder['aliases'] ) && is_array( $placeholder['aliases'] ) ) {
1430
1431                            foreach ( $placeholder['aliases'] as $alias ) {
1432
1433                                // if return in categories
1434                                if ( $split_by_category ) {
1435
1436                                    $applicable_placeholders[ $placeholder_group_key ][ $alias ] = $placeholder;
1437
1438                                } else {
1439
1440                                    $applicable_placeholders[ $alias ] = $placeholder;
1441
1442                                }
1443                            }
1444                        }
1445                    }
1446                }
1447            }
1448        }
1449
1450        return $applicable_placeholders;
1451    }
1452
1453    /**
1454     * Returns a single placeholder info array based on a key
1455     *
1456     * @param string placeholder key
1457     *
1458     * @return array placeholder info
1459     */
1460    public function get_single_placeholder_info( $placeholder = '' ) {
1461
1462        // cycle through placeholders and return our match
1463        foreach ( $this->placeholders as $placeholder_area => $placeholders ) {
1464
1465            foreach ( $placeholders as $key => $placeholder_info ) {
1466
1467                if ( $key == $placeholder ) {
1468
1469                    return $placeholder_info;
1470
1471                }
1472            }
1473        }
1474
1475        return false;
1476    }
1477
1478    /*
1479    * Returns a template-friendly placeholder array of generic replacements
1480    */
1481    public function get_generic_replacements() {
1482
1483        global $zbs;
1484
1485        // vars
1486        $login_url      = admin_url( 'admin.php?page=' . $zbs->slugs['dash'] );
1487        $portal_url     = zeroBS_portal_link();
1488        $biz_name       = zeroBSCRM_getSetting( 'businessname' );
1489        $biz_your_name  = zeroBSCRM_getSetting( 'businessyourname' );
1490        $biz_your_email = zeroBSCRM_getSetting( 'businessyouremail' );
1491        $biz_your_url   = zeroBSCRM_getSetting( 'businessyoururl' );
1492        $biz_extra      = zeroBSCRM_getSetting( 'businessextra' );
1493        $biz_info       = zeroBSCRM_invoicing_generateInvPart_bizTable(
1494            array(
1495                'zbs_biz_name'      => $biz_name,
1496                'zbs_biz_yourname'  => $biz_your_name,
1497                'zbs_biz_extra'     => $biz_extra,
1498                'zbs_biz_youremail' => $biz_your_email,
1499                'zbs_biz_yoururl'   => $biz_your_url,
1500                'template'          => 'pdf',
1501            )
1502        );
1503        $social_links   = show_social_links();
1504
1505        // return
1506        return array(
1507
1508            // login
1509            'login-link'      => '<a href="' . $login_url . '">' . __( 'Go to CRM', 'zero-bs-crm' ) . '</a>',
1510            'login-button'    => '<div style="text-align:center;margin:1em;margin-top:2em">' . zeroBSCRM_mailTemplate_emailSafeButton( $login_url, __( 'Go to CRM', 'zero-bs-crm' ) ) . '</div>',
1511            'login-url'       => $login_url,
1512
1513            // portal
1514            'portal-link'     => '<a href="' . $portal_url . '">' . $portal_url . '</a>',
1515            'portal-button'   => '<div style="text-align:center;margin:1em;margin-top:2em">' . zeroBSCRM_mailTemplate_emailSafeButton( $portal_url, __( 'View Portal', 'zero-bs-crm' ) ) . '</div>',
1516            'portal-url'      => $portal_url,
1517
1518            // biz stuff
1519            'biz-name'        => $biz_name,
1520            'biz-your-name'   => $biz_your_name,
1521            'biz-your-email'  => $biz_your_email,
1522            'biz-your-url'    => $biz_your_url,
1523            'biz-info'        => $biz_info,
1524            'biz-extra'       => $biz_extra,
1525            'biz-logo'        => jpcrm_business_logo_img( '150px' ),
1526
1527            // general
1528            'powered-by'      => zeroBSCRM_mailTemplate_poweredByHTML(),
1529            'email-from-name' => zeroBSCRM_mailDelivery_defaultFromname(),
1530            'password'        => '<a href="' . wp_lostpassword_url() . '" title="' . __( 'Lost Password', 'zero-bs-crm' ) . '">' . __( 'Set Your Password', 'zero-bs-crm' ) . '</a>',
1531
1532            // social
1533            'social-links'    => $social_links,
1534        );
1535    }
1536
1537    /**
1538     * Enacts single string replacement on a passed string based on passed tooling/areas
1539     * Note: It's recommended to use this function even though str_replace would be approximate
1540     * - Using this accounts for multiple aliases, e.g. ###MSG-CONTENT### and ##MSG-CONTENT##, and keeps things centralised
1541     *
1542     * @param string string of placeholder to replace, e.g. 'msg-content'
1543     * @param string string to apply replacements to
1544     * @param string string to replace placeholder with
1545     *
1546     * @return string modified string
1547     */
1548    public function replace_single_placeholder( $placeholder_key = '', $replace_with = '', $string = '' ) {
1549
1550        // get info
1551        $placeholder_info = $this->get_single_placeholder_info( $placeholder_key );
1552
1553        // exists?
1554        if ( is_array( $placeholder_info ) ) {
1555
1556            // auto-gen
1557            $main_placeholder_key = $this->make_placeholder_str( $placeholder_key );
1558
1559            // if replace_str is set, use that
1560            if ( isset( $placeholder_info['replace_str'] ) ) {
1561
1562                $main_placeholder_key = $placeholder_info['replace_str'];
1563
1564            }
1565
1566            // replace any and all variants (e.g. aliases)
1567            // we do these first, as often in our case the variants are backward-compat
1568            // and so `###BIZ-INFO###` needs replacing before `##BIZ-INFO##`
1569            if ( isset( $placeholder_info['aliases'] ) && is_array( $placeholder_info['aliases'] ) ) {
1570
1571                $string = str_replace( $placeholder_info['aliases'], $replace_with, $string );
1572
1573            }
1574
1575            // replace
1576            $string = str_replace( $main_placeholder_key, $replace_with, $string );
1577
1578        } else {
1579
1580            // not in index, replace requested placeholder:
1581            $string = str_replace( $this->make_placeholder_str( $placeholder_key ), $replace_with, $string );
1582
1583        }
1584
1585        return $string;
1586    }
1587
1588    /**
1589     * Enacts replacements on a string based on passed tooling/areas
1590     * Note: Using this method allows non-passed values to be emptied,
1591     * e.g. if 'global' tooling method, and no 'unsub-line' value passed,
1592     * any mentions of '##UNSUB-LINE##' will be taken out of $string
1593     *
1594     * @param array|string tooling - tooling area, object type, or group of
1595     * @param string string - to apply replacements to
1596     * @param array replacements - array of replacement values
1597     * @param array|false replacement_objects - an array of objects to replace fields from (e.g. contact) Should be keyed by object type, in the format array[ZBS_TYPE_CONTACT] = contact array
1598     * @param bool retain_unset_placeholders - bool whether or not to retain empty-valued placeholders (e.g. true = leaves ##BIZ-LOGO## in string if no value for 'biz-logo')
1599     *
1600     * @return string modified string
1601     */
1602    public function replace_placeholders(
1603        $tooling = array( 'global' ),
1604        $string = '',
1605        $replacements = array(),
1606        $replacement_objects = false,
1607        $retain_unset_placeholders = false,
1608        $keys_staying_unrendered = array()
1609    ) {
1610
1611        // retrieve replacements for this tooling
1612        $to_replace = $this->get_placeholders_for_tooling( $tooling );
1613
1614        // cycle through replacements and replace where possible
1615        if ( is_array( $to_replace ) ) {
1616
1617            foreach ( $to_replace as $replace_string => $replacement_info ) {
1618
1619                // ##BIZ-STATE## -> biz-state
1620                $key = str_replace( '#', '', strtolower( $replace_string ) );
1621                if ( isset( $replacement_info['key'] ) && ! empty( $replacement_info['key'] ) ) {
1622
1623                    $key = $replacement_info['key'];
1624
1625                }
1626
1627                // attempt to find value in $replacements
1628                $replace_with = '';
1629                if ( isset( $replacements[ $key ] ) ) {
1630
1631                    $replace_with = $replacements[ $key ];
1632
1633                }
1634
1635                // attempt to find value in $replacement_objects
1636                // ... if $replacements[value] not already set (that overrides)
1637                // here $key will be 'contact-prefix'
1638                // .. which would map to $replacement_objects[ZBS_TYPE_CONTACT]['prefix']
1639                if ( empty( $replace_with ) ) {
1640
1641                    // attempt to pluck relative value
1642                    $potential_value = $this->pick_from_replacement_objects( $key, $replacement_objects );
1643
1644                    if ( ! empty( $potential_value ) ) {
1645
1646                        $replace_with = $potential_value;
1647
1648                    }
1649                }
1650
1651                // if $key is 'assigned-to-name', 'assigned-to-email', 'assigned-to-mob'  - seek out an owner.
1652                // ... if $replacements[assigned-to-name] not already set (that overrides)
1653                if ( empty( $replace_with ) && in_array( $key, array( 'assigned-to-name', 'assigned-to-email', 'assigned-to-username', 'assigned-to-mob' ) ) && is_array( $replacement_objects ) ) {
1654
1655                    $owner_value = '';
1656                    $owner_info  = $this->owner_from_replacement_objects( $replacement_objects );
1657
1658                    if ( $owner_info ) {
1659
1660                        switch ( $key ) {
1661
1662                            case 'assigned-to-name':
1663                                $owner_value = $owner_info->display_name;
1664                                break;
1665                            case 'assigned-to-username':
1666                                $owner_value = $owner_info->user_login;
1667                                break;
1668                            case 'assigned-to-email':
1669                                $owner_value = $owner_info->user_email;
1670                                break;
1671                            case 'assigned-to-mob':
1672                                $owner_value = zeroBS_getWPUsersMobile( $owner_info->ID );
1673                                break;
1674
1675                        }
1676
1677                        // got value?
1678                        if ( ! empty( $owner_value ) ) {
1679
1680                            $replace_with = $owner_value;
1681
1682                        }
1683                    }
1684                }
1685
1686                // replace (if not empty + retain_unset_placeholders)
1687                if ( ! $retain_unset_placeholders || ( $retain_unset_placeholders && ! empty( $replace_with ) ) ) {
1688
1689                    // here we hit a problem, we've been wild with our ### use, so now we
1690                    // have a mixture of ## and ### references.
1691                    // ... wherever we have ### we must replace them first,
1692                    // ... to avoid ###A### being pre-replaced by ##A##
1693                    // for now, replacing aliases first solves this.
1694
1695                    // aliases
1696                    if ( isset( $replacement_info['aliases'] ) && is_array( $replacement_info['aliases'] ) ) {
1697
1698                        $string = str_replace( $replacement_info['aliases'], $replace_with, $string );
1699
1700                    }
1701
1702                    // If this is a Quote date key and is not set (Quote accepted or last viewed), let's print out a message saying the quote isn't accepted or viewed.
1703                    if (
1704                        in_array( $key, array( 'quote-accepted', 'quote-lastviewed' ), true ) &&
1705                        ! in_array( $replacement_info['key'], $keys_staying_unrendered, true ) &&
1706                        ( empty( $replace_with ) || jpcrm_date_str_to_uts( $replace_with ) === 0 )
1707                        ) {
1708
1709                        if ( $key === 'quote-accepted' ) {
1710                            $replace_with = __( 'Quote not yet accepted', 'zero-bs-crm' );
1711                            $string       = str_replace( '##QUOTE-ACCEPTED_DATETIME_STR##', $replace_with, $string );
1712                            $string       = str_replace( '##QUOTE-ACCEPTED_DATE_STR##', $replace_with, $string );
1713                        } else {
1714                            $replace_with = __( 'Quote not yet viewed', 'zero-bs-crm' );
1715                            $string       = str_replace( '##QUOTE-LASTVIEWED_DATETIME_STR##', $replace_with, $string );
1716                            $string       = str_replace( '##QUOTE-LASTVIEWED_DATE_STR##', $replace_with, $string );
1717                        }
1718                    }
1719
1720                    // Replace main key.
1721                    if ( empty( $keys_staying_unrendered ) || ! in_array( $replacement_info['key'], $keys_staying_unrendered, true ) ) {
1722
1723                        $string = str_replace( $replace_string, $replace_with, $string );
1724
1725                    }
1726                }
1727            }
1728        }
1729
1730        return $string;
1731    }
1732
1733    /**
1734     * Takes an array of replacement_objects and a target string
1735     * ... and returns a value if it can pick one from replacement_objects
1736     * ... e.g. target_string = 'contact-fname'
1737     * ...      =
1738     * ...      replacement_objects[ZBS_TYPE_CONTACT]['fname']
1739     *
1740     * @param string target string e.g. `contact-fname`
1741     * @param array|bool replacement objects, keyed to object type - (e.g. contact) Should be in the format array[ZBS_TYPE_CONTACT] = contact array
1742     *
1743     * @return string valid placeholder string
1744     */
1745    private function pick_from_replacement_objects( $target_string = '', $replacement_objects = array() ) {
1746
1747        global $zbs;
1748
1749        // got string and replacement objects?
1750        if ( ! empty( $target_string ) && is_array( $replacement_objects ) && count( $replacement_objects ) > 0 ) {
1751
1752            // retrieve object-type from $target_string (where possible)
1753            $target_exploded = explode( '-', $target_string, 2 );
1754            if ( is_array( $target_exploded ) && count( $target_exploded ) > 1 ) {
1755
1756                // convert `contact` to ZBS_TYPE_CONTACT (1)
1757                $object_type_id = $zbs->DAL->objTypeID( $target_exploded[0] );
1758
1759                // validate obj type id and check if it's passed in replacement_objects
1760                if ( $zbs->DAL->isValidObjTypeID( $object_type_id ) && isset( $replacement_objects[ $object_type_id ] ) ) {
1761
1762                    // at this point we turn `contact-fname` into `fname` and we pluck it from `$replacement_objects[ ZBS_TYPE_CONTACT ]['fname']` if it's set
1763                    array_shift( $target_exploded );
1764                    $field_name = strtolower( $target_exploded[0] );
1765
1766                    if ( isset( $replacement_objects[ $object_type_id ][ $field_name ] ) && ! empty( $replacement_objects[ $object_type_id ][ $field_name ] ) ) {
1767
1768                        // successful find
1769                        return $replacement_objects[ $object_type_id ][ $field_name ];
1770
1771                    }
1772
1773                    // check for potential fallback fields
1774
1775                    if ( preg_match( '/_datetime_str$/', $field_name ) ) {
1776
1777                        $potential_uts_field = str_replace( '_datetime_str', '', $field_name );
1778                        if ( isset( $replacement_objects[ $object_type_id ][ $potential_uts_field ] ) ) {
1779                            $potential_uts_value = $replacement_objects[ $object_type_id ][ $potential_uts_field ];
1780                            if ( jpcrm_is_int( $potential_uts_value ) ) {
1781                                return jpcrm_uts_to_datetime_str( $potential_uts_value );
1782                            } else {
1783                                // use original value as fallback
1784                                return $potential_uts_value;
1785                            }
1786                        }
1787                    } elseif ( preg_match( '/_date_str$/', $field_name ) ) {
1788
1789                        $potential_uts_field = str_replace( '_date_str', '', $field_name );
1790                        if ( isset( $replacement_objects[ $object_type_id ][ $potential_uts_field ] ) ) {
1791                            $potential_uts_value = $replacement_objects[ $object_type_id ][ $potential_uts_field ];
1792                            if ( jpcrm_is_int( $potential_uts_value ) ) {
1793                                return jpcrm_uts_to_date_str( $potential_uts_value );
1794                            } else {
1795                                // use original value as fallback
1796                                return $potential_uts_value;
1797                            }
1798                        }
1799                    }
1800                }
1801            }
1802        }
1803
1804        return false;
1805    }
1806
1807    /**
1808     * Takes an array of replacement_objects and returns WordPress user info
1809     * for the first owner it finds in $replacement_objects
1810     *
1811     * @param array|bool replacement objects, keyed to object type - (e.g. contact) Should be in the format array[ZBS_TYPE_CONTACT] = contact array
1812     *
1813     * @return string WordPress user info (get_userdata)
1814     */
1815    private function owner_from_replacement_objects( $replacement_objects = array() ) {
1816
1817        // got string and replacement objects?
1818        if ( is_array( $replacement_objects ) && count( $replacement_objects ) > 0 ) {
1819
1820            foreach ( $replacement_objects as $replacement_obj_type => $replacement_object ) {
1821
1822                // is `owner` set?
1823                if ( isset( $replacement_object['owner'] ) && ! empty( $replacement_object['owner'] ) ) {
1824
1825                    // one of the passed replacement objects has an owner... first passed first served
1826                    $user_info = get_userdata( $replacement_object['owner'] );
1827
1828                    if ( $user_info !== false ) {
1829
1830                        return $user_info;
1831
1832                    }
1833                }
1834            }
1835        }
1836
1837        return false;
1838    }
1839
1840    /**
1841     * Takes a key (e.g. msg-content) and returns in valid placeholder
1842     * format. (e.g. ##MSG-CONTENT##)
1843     *
1844     * @param string placeholder key
1845     *
1846     * @return string valid placeholder string
1847     */
1848    public function make_placeholder_str( $placeholder_key = '' ) {
1849
1850        return '##' . str_replace( '_', '-', trim( strtoupper( $placeholder_key ) ) ) . '##';
1851    }
1852
1853    /**
1854     * Draws WYSIWYG Typeahead (Bloodhound JS)
1855     *
1856     * @param string select - HTML ID to give to the placeholder select
1857     * @param string insert_target_id - HTML ID which the placeholder should insert into when a placeholder is selected
1858     * @param array|string tooling - placeholder array of tooling areas to include, if 'all' string passed, all placeholders will be shown
1859     * @param array return - return or echo?
1860     *
1861     * @return string valid placeholder string
1862     */
1863    public function placeholder_selector( $id = '', $insert_target_id = '', $tooling = array( 'global' ), $return = false, $extra_classes = '' ) {
1864
1865        $placeholder_list = array();
1866
1867        // Simpler <select>
1868        if ( is_array( $tooling ) ) {
1869
1870            // retrieve placeholder list (divided by category)
1871            $placeholder_list = $this->get_placeholders_for_tooling( $tooling, false, true );
1872
1873        } else {
1874
1875            // retrieve placeholder list (divided by category)
1876            $placeholder_list = $this->get_placeholders( true );
1877
1878        }
1879
1880        $html  = '<div class="jpcrm-placeholder-select-wrap ' . $extra_classes . '">';
1881        $html .= '<select id="' . $id . '" data-target="' . $insert_target_id . '" class="jpcrm-placeholder-select ui compact selection dropdown">';
1882
1883            // blank option
1884            $html .= '<option value="-1">' . __( 'Insert Placeholder', 'zero-bs-crm' ) . '</option>';
1885            $html .= '<option value="-1" disabled="disabled">====================</option>';
1886
1887            // cycle through categories of placeholders:
1888        if ( is_array( $placeholder_list ) && count( $placeholder_list ) > 0 ) {
1889
1890            foreach ( $placeholder_list as $placeholder_group_key => $placeholder_group ) {
1891
1892                $html .= '<optgroup label="' . ucwords( $placeholder_group_key ) . '">';
1893
1894                foreach ( $placeholder_group as $placeholder_key => $placeholder_info ) {
1895
1896                    // value
1897                    // if no replace_str attr...
1898                    $placeholder_str = $this->make_placeholder_str( $placeholder_group_key . '-' . $placeholder_key );
1899                    if ( isset( $placeholder_info['replace_str'] ) ) {
1900
1901                        $placeholder_str = $placeholder_info['replace_str'];
1902
1903                    }
1904
1905                    // label
1906                    $placeholder_label = ( isset( $placeholder_info['description'] ) && ! empty( $placeholder_info['description'] ) ) ? $placeholder_info['description'] . ' (' . $placeholder_str . ')' : $placeholder_str;
1907
1908                    $html .= '<option value="' . $placeholder_str . '">' . $placeholder_label . '</option>';
1909
1910                }
1911
1912                $html .= '</optgroup>';
1913
1914            }
1915        } else {
1916
1917            $html .= '<option value="-1">' . __( 'No Placeholders Available', 'zero-bs-crm' ) . '</option>';
1918
1919        }
1920
1921        $html .= '</select><i class="code icon"></i></div>';
1922
1923        if ( ! $return ) {
1924
1925            echo $html;
1926
1927        }
1928
1929        return $html;
1930    }
1931
1932    /**
1933     * Collates and tidies the full output of placeholders into a simpler array (for placeholder selector typeahead primarily)
1934     *
1935     * @param array placeholders - array of placeholders, probably from $this->get_placeholders or $this->get_placeholders_for_tooling (Note this requires non-categorised array)
1936     *
1937     * @return array placeholders - simplified array
1938     */
1939    public function simplify_placeholders( $placeholders = array() ) {
1940
1941        $return = array();
1942
1943        // cycle through and simplify
1944        foreach ( $placeholders as $key => $info ) {
1945
1946            $placeholder = $info;
1947
1948            // unset non-key
1949            unset( $placeholder['available_in'], $placeholder['associated_type'] );
1950
1951            // if return_str not set and key is, add it
1952            if ( ! isset( $placeholder['replace_str'] ) ) {
1953
1954                $placeholder['replace_str'] = $key;
1955
1956            }
1957
1958            $return[] = $placeholder;
1959
1960        }
1961
1962        return $return;
1963    }
1964
1965    /**
1966     * Collates and tidies the full output of placeholders into a simpler array (designed for wysiwyg select insert - e.g. quotebuilder)
1967     *
1968     * @param array placeholders - array of placeholders, probably from $this->get_placeholders or $this->get_placeholders_for_tooling (Note this requires non-categorised array)
1969     *
1970     * @return array placeholders - simplified array (array[{text:desc,value:placeholder}])
1971     */
1972    public function simplify_placeholders_for_wysiwyg( $placeholders = array() ) {
1973
1974        $return = array();
1975
1976        // cycle through and simplify
1977        foreach ( $placeholders as $placeholder_key => $placeholder_info ) {
1978
1979            // label
1980            $placeholder_label = ( isset( $placeholder_info['description'] ) && ! empty( $placeholder_info['description'] ) ) ? $placeholder_info['description'] . ' (' . $placeholder_key . ')' : $placeholder_key;
1981
1982            $return[] = array(
1983                'text'  => $placeholder_label,
1984                'value' => $placeholder_key,
1985            );
1986
1987        }
1988
1989        return $return;
1990    }
1991}