Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 3598
0.00% covered (danger)
0.00%
0 / 154
CRAP
0.00% covered (danger)
0.00%
0 / 1
zeroBSCRM_site
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_team
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_user
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
zbsDAL
0.00% covered (danger)
0.00%
0 / 3579
0.00% covered (danger)
0.00%
0 / 151
1561250
0.00% covered (danger)
0.00%
0 / 1
 reset_mitigation_cache_for_issue_3504
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 mitigation_cache_for_issue_3504_single_setting
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mitigation_cache_for_issue_3504_contains_key
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mitigation_cache_for_issue_3504_contains_all_settings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mitigation_cache_for_issue_3504_set_single_setting
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mitigation_cache_for_issue_3504_set_contains_all
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mitigation_cache_for_issue_3504_all_settings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 postSettingsInit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 field_list_address
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 field_list_address2
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 field_list_address_full
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_object_types_by_index
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getObjectTypesByKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCPTObjectTypesByKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getObjectLayerByType
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
156
 get_object_field_global
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isValidObjTypeID
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 is_valid_obj_status
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 objTypeID
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 objTypeKey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 objFieldVarName
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 objModel
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 cptTaxonomyToObjID
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 listViewSlugFromObjID
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 typeStr
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 typeCPT
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 fields_to_hide_on_frontend
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 specific_obj_type_count_for_assignee
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 checkObjectOwner
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
182
 getObjectOwner
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 setObjectOwner
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
90
 ownershipQueryVars
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 ownershipSQL
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getErrors
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 addError
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 getObjsLinkedToObj
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 1
930
 getObjsLinksLinkedToObj
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 1
870
 getFirstIDLinkedToObj
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
272
 addUpdateObjLink
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 1
380
 addUpdateObjLinks
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 1
650
 deleteObjLinks
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
380
 tidy_objlink
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 setting
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 userSetting
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getSetting
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
600
 getSettings
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
272
 updateSetting
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 updateUserSetting
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 addUpdateSetting
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 1
552
 deleteSetting
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
90
 tidy_setting
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 tidy_settingSingular
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 unpack_setting
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 meta
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getMeta
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
462
 getIDWithMeta
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
272
 updateMeta
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 addUpdateMeta
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 1
650
 deleteMeta
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
240
 deleteMetaByMetaID
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 tidy_meta
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 tidy_metaSingular
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getTag
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
600
 getAllTags
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
272
 addUpdateTag
0.00% covered (danger)
0.00%
0 / 102
0.00% covered (danger)
0.00%
0 / 1
702
 addUpdateObjectTags
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
342
 deleteTag
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
110
 getAllTagStats
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
272
 getTagObjStats
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
272
 tag_slug_exists
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 get_new_tag_slug
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 tidy_tag
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 tidy_tagstat
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getTagsForObjType
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
650
 getTagsForObjID
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
380
 addUpdateTagObjLink
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 1
342
 addUpdateTagObjLinks
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 1
1056
 compile_segments_from_tagIDs
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 deleteTagObjLink
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
210
 deleteTagObjLinks
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
132
 tidy_taglink
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 isActiveCustomField_Contact
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
56
 isActiveCustomField
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
182
 getActiveCustomFields
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
 updateActiveCustomFields
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
 getCustomFieldVal
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
306
 addUpdateCustomField
0.00% covered (danger)
0.00%
0 / 109
0.00% covered (danger)
0.00%
0 / 1
992
 deleteCustomField
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 tidy_customfieldvalSingular
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getExternalSource
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
600
 getExternalSources
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
600
 addUpdateExternalSource
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 1
870
 addUpdateExternalSources
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
132
 deleteExternalSource
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 delete_external_sources
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
132
 tidy_externalsource
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 clean_external_source_domain_string
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 add_origin_prefix
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 remove_origin_prefix
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 hydrate_origin
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getTracking
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
210
 addUpdateTracking
0.00% covered (danger)
0.00%
0 / 110
0.00% covered (danger)
0.00%
0 / 1
306
 deleteTracking
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 tidy_tracking
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getCronLogs
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
272
 addUpdateCronLog
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
182
 deleteCronLog
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 tidy_cronlog
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 truncate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 get_model_field_info
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
72
 does_model_field_exist
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 format_fullname
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 format_name_etc
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 format_address
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
1482
 makeSlug
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 makeSlugCleanStr
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getFieldByWHERE
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
182
 getFieldByID
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
240
 setFieldByID
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
240
 lazyTable
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
182
 lazyTidy
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
182
 lazyTidyGeneric
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 space
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 spaceAnd
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 spaceWhere
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 delimiterIf
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 stripSlashes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decodeIfJSON
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 build_custom_field_order_by_str
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 build_csv
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 buildWhereStr
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 buildWheres
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
600
 build_joins
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 buildSort
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 buildPaging
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 buildWPMetaQueryWhere
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 build_group_concat
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getTypeStr
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
132
 comparison_to_sql_symbol
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 prepare
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 catchSQLError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_cache_var
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 update_cache_var
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 v3templogBacktrace
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getContact
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getContacts
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 addUpdateContact
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 addUpdateContactTags
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 deleteContact
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 segmentBuildDirectOrClause
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 * V1.20
6 *
7 * Copyright 2020 Automattic
8 *
9 * Date: 01/11/16
10 */
11
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14    /*
15    DAL3.0 Notes:
16
17        THIS FILE replaces previous DAL2.php, with the following changes:
18
19        - Originally this was DAL2 1 x class containting methods like:
20            - DAL->getContacts
21        - ... in DAL3 these have moved to:
22            - DAL->contacts->getContacts()
23        - ... for better organisation.
24        - This file, then, basically shuffles original DAL2 out into 4 classes:
25            - DAL, contacts, segments, logs
26            - As DAL, DAL->contacts, DAL->segments, DAL->logs
27        - ... it also adds splat/catcher funcs for OLD references e.g. getContacts,
28        - ... so that they will still function (but log need for replacement)
29
30        ... so it's a kind of chimera DAL2 to bridge the gap between DAL2 + DAL3.
31        ... should be fine to cut the chimera splats after a while though (or even pre v3.0 proper)
32
33
34    */
35
36/*
37======================================================
38    DB GENERIC/OBJ Helpers (not DAL GET etc.)
39    ====================================================== */
40
41    // ===============================================================================
42    // ===========  PERMISSIONS HELPERS  =============================================
43
44    // in time this'll allow multiple 'sites' per install (E.g. site 1 = epic.zbs.com site 2 = zbs.zbs.com)
45    // for now, this is hard-coded to 1
46    // replaces "zeroBSCRM_installSite" from old DAL
47function zeroBSCRM_site() {
48
49    return 1;
50}
51    // in time this'll allow multiple 'team' per site (E.g. branch1,branch2 etc.)
52    // for now, this is hard-coded to 1
53    // replaces "zeroBSCRM_installTeam" from old DAL
54function zeroBSCRM_team() {
55
56    return 1;
57}
58    // active user id - helper func
59    // replaces "zeroBSCRM_currentUserID" from old DAL
60    // can alternatively user $zbs->user()
61function zeroBSCRM_user() {
62
63    return get_current_user_id();
64}
65
66    // =========== /  PERMISSIONS HELPERS  ===========================================
67    // ===============================================================================
68
69    // ===============================================================================
70    // ===========  TYPES ============================================================
71
72define( 'ZBS_TYPE_CONTACT', 1 );
73define( 'ZBS_TYPE_COMPANY', 2 );
74define( 'ZBS_TYPE_QUOTE', 3 );
75define( 'ZBS_TYPE_INVOICE', 4 );
76define( 'ZBS_TYPE_TRANSACTION', 5 );
77define( 'ZBS_TYPE_EVENT', 6 ); // legacy, use ZBS_TYPE_TASK instead
78define( 'ZBS_TYPE_TASK', 6 );
79define( 'ZBS_TYPE_FORM', 7 );
80define( 'ZBS_TYPE_LOG', 8 );
81define( 'ZBS_TYPE_SEGMENT', 9 );
82define( 'ZBS_TYPE_LINEITEM', 10 );
83define( 'ZBS_TYPE_EVENTREMINDER', 11 ); // legacy, use ZBS_TYPE_TASK_REMINDER instead
84define( 'ZBS_TYPE_TASK_REMINDER', 11 );
85define( 'ZBS_TYPE_QUOTETEMPLATE', 12 );
86define( 'ZBS_TYPE_ADDRESS', 13 ); // this is a precursor to v4 where we likely need to split out addresses from current in-object model (included here as custom fields now managed as if obj)
87
88    // =========== /  TYPES  =========================================================
89    // ===============================================================================
90
91/*
92======================================================
93    / DB GENERIC/OBJ Helpers (not DAL GET etc.)
94    ====================================================== */
95
96/**
97 * zbsDAL is the Data Access Layer for ZBS v2.5+
98 *
99 * zbsDAL provides expanded CRUD actions to the
100 * Jetpack CRM generally, and will be initiated globally
101 * like the WordPress $wpdb.
102 *
103 * @author   Woody Hayday <hello@jetpackcrm.com>
104 * @version  2.0
105 * @access   public
106 * @see      https://jetpackcrm.com/kb
107 */
108class zbsDAL {
109
110    public $version = 3.0;
111
112    /*
113    * A general key-value pair store
114    */
115    private $cache = array();
116
117    /**
118     * This is a temporary fix for issue #3504, until we don't change how we load settings this should help to avoid slowing down websites.
119     *
120     * @var array Associative array used for caching settings (key: 'settings') and a flag to indicate if all settings were loaded (key: 'contains_all').
121     */
122    private static $mitigation_cache_for_issue_3504;
123
124    /**
125     * Resets the mitigation cache for issue #3504.
126     *
127     * Initializes the static cache property to its default state, clearing cached settings
128     * and resetting the "contains all" flag to false.
129     *
130     * @return void
131     */
132    public static function reset_mitigation_cache_for_issue_3504() {
133        static::$mitigation_cache_for_issue_3504 = array(
134            'settings'     => array(),
135            'contains_all' => false,
136        );
137    }
138
139    /**
140     * Retrieves a specific setting from the cache.
141     *
142     * @param string $key The setting key.
143     * @return mixed|null The value of the setting or null if not found.
144     */
145    private static function mitigation_cache_for_issue_3504_single_setting( $key ) {
146        return static::$mitigation_cache_for_issue_3504['settings'][ $key ] ?? null;
147    }
148
149    /**
150     * Checks if a specific setting exists in the cache.
151     *
152     * @param string $key The setting key.
153     * @return bool True if the setting exists, false otherwise.
154     */
155    private static function mitigation_cache_for_issue_3504_contains_key( $key ) {
156        return isset( static::$mitigation_cache_for_issue_3504['settings'][ $key ] );
157    }
158
159    /**
160     * Checks if all settings are loaded.
161     *
162     * @return bool True if all settings are loaded, false otherwise.
163     */
164    private static function mitigation_cache_for_issue_3504_contains_all_settings() {
165        return static::$mitigation_cache_for_issue_3504['contains_all'];
166    }
167
168    /**
169     * Sets a specific setting in the cache.
170     *
171     * @param string $key   The setting key.
172     * @param mixed  $value The setting value.
173     * @return void
174     */
175    private static function mitigation_cache_for_issue_3504_set_single_setting( $key, $value ) {
176        static::$mitigation_cache_for_issue_3504['settings'][ $key ] = $value;
177    }
178
179    /**
180     * Sets the "contains all" flag in the cache.
181     *
182     * @param bool $contains_all Whether all settings are loaded.
183     * @return void
184     */
185    private static function mitigation_cache_for_issue_3504_set_contains_all( $contains_all ) {
186        static::$mitigation_cache_for_issue_3504['contains_all'] = $contains_all;
187    }
188
189    /**
190     * Retrieves all cached settings.
191     *
192     * @return array An array of all cached settings.
193     */
194    private static function mitigation_cache_for_issue_3504_all_settings() {
195        return static::$mitigation_cache_for_issue_3504['settings'];
196    }
197
198    // ===============================================================================
199    // ===========  SUB DAL LAYERS  ==================================================
200    // These hold sub-objects, e.g. contact
201    public $contacts           = false;
202    public $segments           = false;
203    public $companies          = false;
204    public $quotes             = false;
205    public $invoices           = false;
206    public $transactions       = false;
207    public $forms              = false;
208    public $events             = false;
209        public $eventreminders = false; // phpcs:ignore Squiz.Commenting.VariableComment.Missing
210    public $logs               = false;
211    public $lineitems          = false;
212    public $quotetemplates     = false;
213    public $addresses          = false;
214
215    // ===========  / SUB DAL LAYERS  ================================================
216    // ===============================================================================
217
218    // ===============================================================================
219    // ===========  OBJECT TYPE & GLOBAL DEFINITIONS    ==============================
220
221    private $typesByID = array(
222
223        ZBS_TYPE_CONTACT       => 'contact',
224        ZBS_TYPE_COMPANY       => 'company',
225        ZBS_TYPE_QUOTE         => 'quote',
226        ZBS_TYPE_INVOICE       => 'invoice',
227        ZBS_TYPE_TRANSACTION   => 'transaction',
228        ZBS_TYPE_TASK          => 'event',
229        ZBS_TYPE_FORM          => 'form',
230        ZBS_TYPE_SEGMENT       => 'segment',
231        ZBS_TYPE_LOG           => 'log',
232        ZBS_TYPE_LINEITEM      => 'lineitem',
233        ZBS_TYPE_TASK_REMINDER => 'eventreminder',
234        ZBS_TYPE_QUOTETEMPLATE => 'quotetemplate',
235        ZBS_TYPE_ADDRESS       => 'address',
236
237    );
238
239    // retrieve via DAL->oldCPT(1)
240    private $typeCPT = array(
241
242        ZBS_TYPE_CONTACT       => 'zerobs_customer',
243        ZBS_TYPE_COMPANY       => 'zerobs_company',
244        ZBS_TYPE_QUOTE         => 'zerobs_quote',
245        ZBS_TYPE_INVOICE       => 'zerobs_invoice',
246        ZBS_TYPE_TRANSACTION   => 'zerobs_transaction',
247        ZBS_TYPE_TASK          => 'zerobs_event',
248        ZBS_TYPE_FORM          => 'zerobs_form',
249        // these never existed:
250        // ZBS_TYPE_SEGMENT => 'zerobs_segment',
251        // ZBS_TYPE_LOG => 'zerobs_log',
252        // ZBS_TYPE_LINEITEM => 'lineitem'
253        // ZBS_TYPE_TASK_REMINDER => 'eventreminder'
254        ZBS_TYPE_QUOTETEMPLATE => 'zerobs_quo_template',
255    );
256
257    /**
258     * Retrieve via DAL->typeStr(1).
259     *
260     * @var array
261     */
262    private $typeNames = array( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
263        ZBS_TYPE_CONTACT       => array( 'Contact', 'Contacts' ),
264        ZBS_TYPE_COMPANY       => array( 'Company', 'Companies' ),
265        ZBS_TYPE_QUOTE         => array( 'Quote', 'Quotes' ),
266        ZBS_TYPE_INVOICE       => array( 'Invoice', 'Invoices' ),
267        ZBS_TYPE_TRANSACTION   => array( 'Transaction', 'Transactions' ),
268        ZBS_TYPE_TASK          => array( 'Task', 'Tasks' ),
269        ZBS_TYPE_FORM          => array( 'Form', 'Forms' ),
270        ZBS_TYPE_SEGMENT       => array( 'Segment', 'Segments' ),
271        ZBS_TYPE_LOG           => array( 'Log', 'Logs' ),
272        ZBS_TYPE_LINEITEM      => array( 'Line Item', 'Line Items' ),
273        ZBS_TYPE_TASK_REMINDER => array( 'Task Reminder', 'Task Reminders' ),
274        ZBS_TYPE_QUOTETEMPLATE => array( 'Quote Template', 'Quote Templates' ),
275        ZBS_TYPE_ADDRESS       => array( 'Address', 'Addresses' ),
276    );
277
278    // List View refs
279    private $listViewRefs = array(
280
281        // each of these is a slug for $zbs->slugs e.g. $zbs->slugs['managecontacts']
282        ZBS_TYPE_CONTACT       => 'managecontacts',
283        ZBS_TYPE_COMPANY       => 'managecompanies',
284        ZBS_TYPE_QUOTE         => 'managequotes',
285        ZBS_TYPE_INVOICE       => 'manageinvoices',
286        ZBS_TYPE_TRANSACTION   => 'managetransactions',
287        ZBS_TYPE_TASK          => 'manage-tasks',
288        ZBS_TYPE_FORM          => 'manageformscrm',
289        ZBS_TYPE_SEGMENT       => 'segments',
290        // no list page ZBS_TYPE_LOG =>             'managecontacts',
291        // no list page ZBS_TYPE_LINEITEM =>        'managecontacts',
292        // no list page ZBS_TYPE_TASK_REMINDER =>   'managecontacts',
293        ZBS_TYPE_QUOTETEMPLATE => 'quote-templates',
294    );
295
296    // field obj models
297    // these match the $globals in fields.php - bit of a legacy chunk tbh, but used throughout
298    private $fieldModelsByID = array(
299
300        ZBS_TYPE_CONTACT     => 'zbsCustomerFields',
301        ZBS_TYPE_COMPANY     => 'zbsCompanyFields',
302        ZBS_TYPE_QUOTE       => 'zbsCustomerQuoteFields',
303        ZBS_TYPE_INVOICE     => 'zbsCustomerInvoiceFields',
304        ZBS_TYPE_TRANSACTION => 'zbsTransactionFields',
305        // ZBS_TYPE_TASK =>           'zbsFormFields',
306        ZBS_TYPE_FORM        => 'zbsFormFields',
307        ZBS_TYPE_ADDRESS     => 'zbsAddressFields',
308
309    );
310
311    // legacy support.
312    private $oldTaxonomies = array(
313
314        'zerobscrm_customertag'    => ZBS_TYPE_CONTACT,
315        'zerobscrm_companytag'     => ZBS_TYPE_COMPANY,
316        'zerobscrm_transactiontag' => ZBS_TYPE_TRANSACTION,
317        'zerobscrm_logtag'         => ZBS_TYPE_LOG,
318
319    );
320
321    // this is a shorthand for grabbing all addr fields
322    private $field_list_address = array(
323
324        'zbsc_addr1',
325        'zbsc_addr2',
326        'zbsc_city',
327        'zbsc_postcode',
328        'zbsc_county',
329        'zbsc_country',
330
331    );
332    private $field_list_address2 = array(
333
334        'zbsc_secaddr1',
335        'zbsc_secaddr2',
336        'zbsc_seccity',
337        'zbsc_secpostcode',
338        'zbsc_seccounty',
339        'zbsc_seccountry',
340
341    );
342    private $field_list_address_full = array(
343
344        'zbsc_addr1',
345        'zbsc_addr2',
346        'zbsc_city',
347        'zbsc_postcode',
348        'zbsc_county',
349        'zbsc_country',
350        'zbsc_secaddr1',
351        'zbsc_secaddr2',
352        'zbsc_seccity',
353        'zbsc_secpostcode',
354        'zbsc_seccounty',
355        'zbsc_seccountry',
356
357    );
358
359    // this stores any insert errors
360    private $errorStack = array();
361
362    /**
363     * Prefix for origin strings which are domains (should mean querying easier later)
364     * Used throughout to specify domain
365     */
366    private $prefix_domain = 'd:';
367
368    // =========== /  OBJECT TYPE & GLOBAL DEFINITIONS   =============================
369    // ===============================================================================
370
371    // ===============================================================================
372    // ===========  INIT =============================================================
373    function __construct( $args = array() ) {
374
375        // init sub-layers:
376        $this->contacts = new zbsDAL_contacts();
377        $this->segments = new zbsDAL_segments();
378
379            $this->companies      = new zbsDAL_companies();
380            $this->quotes         = new zbsDAL_quotes();
381            $this->invoices       = new zbsDAL_invoices();
382            $this->transactions   = new zbsDAL_transactions();
383            $this->forms          = new zbsDAL_forms();
384            $this->events         = new zbsDAL_events();
385            $this->eventreminders = new zbsDAL_eventreminders();
386            $this->logs           = new zbsDAL_logs();
387            $this->lineitems      = new zbsDAL_lineitems();
388            $this->quotetemplates = new zbsDAL_quotetemplates();
389            // Not yet implemented:
390            // $this->addresses = new zbsDAL_addresses;
391
392        // any post-settings-loaded actions
393        add_action( 'after_zerobscrm_settings_preinit', array( $this, 'postSettingsInit' ) );
394    }
395    // ===========  / INIT ===========================================================
396    // ===============================================================================
397
398    /**
399     * Corrects label for 'Company' (could be Organisation) after the settings have loaded.
400     * Clunky workaround for now
401     */
402    public function postSettingsInit() {
403
404        // Correct any labels
405        $this->typeNames[ ZBS_TYPE_COMPANY ] = array( jpcrm_label_company(), jpcrm_label_company( true ) );
406    }
407
408    // ===============================================================================
409    // ===========  HELPER/GET FUNCS =================================================
410
411    public function field_list_address() {
412        return $this->field_list_address; }
413    public function field_list_address2() {
414        return $this->field_list_address2; }
415    public function field_list_address_full() {
416        return $this->field_list_address_full; }
417
418    // returns object types indexed by their global (e.g. ZBS_TYPE_CONTACT = 1)
419    public function get_object_types_by_index() {
420
421        return $this->typesByID;
422    }
423
424    // returns object types indexed by their key (e.g. 'contact' => ZBS_TYPE_CONTACT = 1)
425    public function getObjectTypesByKey() {
426
427        return array_flip( $this->typesByID );
428    }
429
430    public function getCPTObjectTypesByKey() {
431
432        return array_flip( $this->typeCPT );
433    }
434
435    // returns $zbs->DAL->contacts by '1'
436    // for now brutal switch, avoid using anywhere but internally, use proper refs elsewhere
437    // ... avoid use where not essential as does not produce highly readable code.
438    public function getObjectLayerByType( $objTypeID = -1 ) {
439
440        switch ( $objTypeID ) {
441            case ZBS_TYPE_CONTACT:
442                return $this->contacts;
443            break;
444            case ZBS_TYPE_COMPANY:
445                return $this->companies;
446            break;
447            case ZBS_TYPE_QUOTE:
448                return $this->quotes;
449            break;
450            case ZBS_TYPE_INVOICE:
451                return $this->invoices;
452            break;
453            case ZBS_TYPE_TRANSACTION:
454                return $this->transactions;
455            break;
456            case ZBS_TYPE_FORM:
457                return $this->forms;
458            break;
459            case ZBS_TYPE_TASK:
460                return $this->events;
461            break;
462            case ZBS_TYPE_TASK_REMINDER:
463                return $this->eventreminders;
464            break;
465            case ZBS_TYPE_LOG:
466                return $this->logs;
467            break;
468            case ZBS_TYPE_LINEITEM:
469                return $this->lineitems;
470            break;
471            case ZBS_TYPE_QUOTETEMPLATE:
472                return $this->quotetemplates;
473            break;
474            // case ZBS_TYPE_ADDRESS:
475            // return $this->addresses; break;
476        }
477
478        return false;
479    }
480
481    /**
482     * Returns $zbsCustomerFields global from object_type_id
483     * designed as legacy support until we can refactor away from globals (gh-253)
484     *
485     * @param CRM_TYPE int object_type_id - object type ID\
486     *
487     * @return bool|array - object type field global array, or false
488     */
489    public function get_object_field_global( $object_type_id = -1 ) {
490
491        // attempt to retrieve var
492        $global_var_name = $this->objFieldVarName( $object_type_id );
493
494        if ( $global_var_name !== -1 ) {
495
496            return isset( $GLOBALS[ $global_var_name ] ) ? $GLOBALS[ $global_var_name ] : null;
497
498        }
499
500        return false;
501    }
502
503    public function isValidObjTypeID( $objTypeIn = false ) {
504
505        // if it's not an int type, cast it...?
506        if ( ! is_int( $objTypeIn ) ) {
507            $objTypeIn = (int) $objTypeIn;
508        }
509
510        // if bigger than 0 and in our arr as a key, basic check
511        if ( $objTypeIn > 0 && isset( $this->typesByID[ $objTypeIn ] ) ) {
512            return true;
513        }
514
515        return false;
516    }
517
518    /**
519     * Check if a given status is valid for the given object
520     *
521     * @param int    $obj_type_id Object type ID.
522     * @param string $obj_status  Object status string.
523     */
524    public function is_valid_obj_status( $obj_type_id, $obj_status ) {
525        switch ( $obj_type_id ) {
526            case ZBS_TYPE_CONTACT:
527                $valid_statuses = zeroBSCRM_getCustomerStatuses( true );
528                break;
529            case ZBS_TYPE_COMPANY:
530                $valid_statuses = zeroBSCRM_getCompanyStatuses();
531                break;
532            case ZBS_TYPE_INVOICE:
533                $valid_statuses = zeroBSCRM_getInvoicesStatuses();
534                break;
535            case ZBS_TYPE_TRANSACTION:
536                $valid_statuses = zeroBSCRM_getTransactionsStatuses( true );
537                break;
538            default:
539                return false;
540        }
541
542        // if required, check if default status is a valid one
543        return in_array( $obj_status, $valid_statuses, true );
544    }
545
546    // takes in an obj type str (e.g. 'contact') and returns DEFINED KEY ID = 1
547    public function objTypeID( $objTypeStr = '' ) {
548
549        // catch some legacy translations
550        if ( $objTypeStr == 'customer' ) {
551            $objTypeStr = 'contact';
552        }
553
554        $byStr = $this->getObjectTypesByKey();
555        if ( isset( $byStr[ $objTypeStr ] ) ) {
556            return $byStr[ $objTypeStr ];
557        }
558
559        // if not, fall back to old obj cpt types
560        $byStr = $this->getCPTObjectTypesByKey();
561        if ( isset( $byStr[ $objTypeStr ] ) ) {
562            return $byStr[ $objTypeStr ];
563        }
564
565        return -1;
566    }
567
568    // takes in an obj type int (e.g. 1) and returns key (e.g. 'contact')
569    public function objTypeKey( $objTypeID = -1 ) {
570
571        if ( isset( $this->typesByID[ $objTypeID ] ) ) {
572            return $this->typesByID[ $objTypeID ];
573        }
574
575        return -1;
576    }
577
578    // takes in an obj type int (e.g. 1) and returns field global var name (e.g. 'zbsCustomerFields')
579    public function objFieldVarName( $objTypeID = -1 ) {
580
581        if ( isset( $this->fieldModelsByID[ $objTypeID ] ) ) {
582            return $this->fieldModelsByID[ $objTypeID ];
583        }
584
585        return -1;
586    }
587
588    // takes in an obj type (e.g. 1) and returns obj model for that type (as per sub layer)
589    // uses getObjectLayerByType to keep generic. Use only in fairly high level generic funcs.
590    public function objModel( $objTypeID = -1 ) {
591
592        if ( $objTypeID > 0 ) {
593
594            $objLayer = $this->getObjectLayerByType( $objTypeID );
595
596            // if set, $objLayer will effectively be $zbs->DAL->contacts obj
597            if ( method_exists( $objLayer, 'objModel' ) ) {
598
599                // all good
600                return $objLayer->objModel();
601            }
602        }
603
604        return false;
605    }
606
607    // takes in an old taxonomy str (e.g. zerobscrm_customertag) and returns new obj key (e.g. 1)
608    public function cptTaxonomyToObjID( $taxonomyStr = -1 ) {
609
610        if ( isset( $this->oldTaxonomies[ $taxonomyStr ] ) ) {
611            return $this->oldTaxonomies[ $taxonomyStr ];
612        }
613
614        return -1;
615    }
616
617    // takes in an obj ID and gives back the list view slug
618    public function listViewSlugFromObjID( $objTypeID = -1 ) {
619
620        global $zbs;
621
622        if ( isset( $this->listViewRefs[ $objTypeID ] ) && isset( $zbs->slugs[ $this->listViewRefs[ $objTypeID ] ] ) ) {
623            return $zbs->slugs[ $this->listViewRefs[ $objTypeID ] ];
624        }
625
626        return '';
627    }
628
629    // retrieves single/plural 'str' for obj type id (e.g. 1 = Contact/Contacts)
630    public function typeStr( $typeInt = -1, $plural = false ) {
631
632        $typeInt = (int) $typeInt;
633        if ( $typeInt > 0 ) {
634
635            if ( isset( $this->typeNames[ $typeInt ] ) ) {
636
637                // plural
638                if ( $plural ) {
639                    return __( $this->typeNames[ $typeInt ][1], 'zero-bs-crm' );
640                }
641                // single
642                return __( $this->typeNames[ $typeInt ][0], 'zero-bs-crm' );
643
644            }
645        }
646        return '';
647    }
648
649    // retrieves old CPT for that type
650    public function typeCPT( $typeInt = -1 ) {
651
652        $typeInt = (int) $typeInt;
653        if ( $typeInt > 0 ) {
654
655            if ( isset( $this->typeCPT[ $typeInt ] ) ) {
656                return $this->typeCPT[ $typeInt ];
657            }
658        }
659        return false;
660    }
661
662    /*
663     * This function provides a general list of field slugs which should
664     * be hidden on front end aspects (e.g. Portal, WooSync->WooCommerce My Account)
665     *
666     * @param int $obj_type_int - optionally specifies whether 'globally' non inclusive, or 'globally+objmodel specific'
667     */
668    public function fields_to_hide_on_frontend( $obj_type_int = false ) {
669
670        // Globals
671        $fields_to_hide_on_frontend = array( 'status', 'email', 'zbs_site', 'zbs_team', 'zbs_owner' );
672
673        // Object type specific
674        if ( $this->isValidObjTypeID( $obj_type_int ) ) {
675
676            $obj_model = $this->objModel( $obj_type_int );
677
678            foreach ( $obj_model as $field_key => $field_array ) {
679
680                if ( is_array( $field_array ) && isset( $field_array['do_not_show_on_portal'] ) && $field_array['do_not_show_on_portal'] ) {
681
682                    if ( ! in_array( $field_key, $fields_to_hide_on_frontend ) ) {
683
684                        $fields_to_hide_on_frontend[] = $field_key;
685
686                    }
687                }
688            }
689        }
690
691        return $fields_to_hide_on_frontend;
692    }
693
694    /**
695     * Returns a count of objects of a type which are associated with an assignee (eg. company or contact)
696     * Supports Quotes, Invoices, Transactions, Events currently
697     *
698     * @param int $assignee_id The assignee ID (for example company / contact ID).
699     * @param int $obj_type_id The type constant being checked (eg ZBS_TYPE_QUOTE).
700     * @param int $zbs_type The assigne type, for example ZBS_TYPE_COMPANY, ZBS_TYPE_CONTACT (default contact).
701     *
702     * @return int The count of relevant objects of the given type.
703     */
704    public function specific_obj_type_count_for_assignee( $assignee_id, $obj_type_id, $zbs_type = ZBS_TYPE_CONTACT ) {
705        // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
706        global $ZBSCRM_t;
707        global $wpdb;
708
709        switch ( $obj_type_id ) {
710            case ZBS_TYPE_QUOTE:
711                $table_name = $ZBSCRM_t['quotes'];
712                break;
713
714            case ZBS_TYPE_INVOICE:
715                $table_name = $ZBSCRM_t['invoices'];
716                break;
717
718            case ZBS_TYPE_TRANSACTION:
719                $table_name = $ZBSCRM_t['transactions'];
720                break;
721
722            case ZBS_TYPE_TASK:
723                $table_name = $ZBSCRM_t['events'];
724                break;
725
726            default:
727                // For any unsupported objtype.
728                return -1;
729        }
730
731        $obj_query = 'SELECT COUNT(obj_table.id) FROM ' . $table_name . ' obj_table'
732            . ' INNER JOIN ' . $ZBSCRM_t['objlinks'] . ' obj_links'
733            . ' ON obj_table.id = obj_links.zbsol_objid_from'
734            . ' WHERE obj_links.zbsol_objtype_from = ' . $obj_type_id
735            . ' AND obj_links.zbsol_objtype_to = ' . $zbs_type
736            . ' AND obj_links.zbsol_objid_to = %d';
737
738        // Counting objs with objlinks to this assignee, ignoring ownership.
739        $query = $this->prepare( $obj_query, $assignee_id );
740        $count = (int) $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching -- To be refactored.
741
742        return $count;
743        // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
744    }
745
746    // =========== / HELPER/GET FUNCS ================================================
747    // ===============================================================================
748
749    // ===============================================================================
750    // ===========  OWNERSHIP HELPERS  ===============================================
751
752    // This func is a side-switch alternative to now-removed zeroBS_checkOwner
753    public function checkObjectOwner( $args = array() ) {
754
755        #} =========== LOAD ARGS ==============
756        $defaultArgs = array(
757
758            'objID'              => -1,
759            'objTypeID'          => -1,
760
761            // id to compare to
762            'potentialOwnerID'   => -1,
763
764            // if not owned, return true?
765            'allowNoOwnerAccess' => -1,
766
767        );
768        foreach ( $defaultArgs as $argK => $argV ) {
769            $$argK = $argV;
770            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
771                if ( is_array( $args[ $argK ] ) ) {
772                    $newData = $$argK;
773                    if ( ! is_array( $newData ) ) {
774                        $newData = array();
775                    } foreach ( $args[ $argK ] as $subK => $subV ) {
776                        $newData[ $subK ] = $subV;
777                    }$$argK = $newData;
778                } else {
779                    $$argK = $args[ $argK ]; }
780            }
781        }
782        #} =========== / LOAD ARGS =============)
783
784        if ( $objID !== -1 && $objTypeID > 0 ) {
785
786            $ownerID = $this->getObjectOwner(
787                array(
788                    'objID'     => $objID,
789                    'objTypeID' => $objTypeID,
790                )
791            );
792
793            if ( isset( $ownerID ) && $ownerID == $potentialOwnerID ) {
794                return true;
795            }
796            // no owner owns this!
797            elseif ( $allowNoOwnerAccess && $potentialOwner === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable,WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
798                return true;
799            }
800        }
801
802        return false;
803    }
804
805    // This func is a side-switch alternative to zeroBS_getOwner
806    public function getObjectOwner( $args = array() ) {
807
808        #} =========== LOAD ARGS ==============
809        $defaultArgs = array(
810
811            'objID'     => -1,
812            'objTypeID' => -1,
813
814        );
815        foreach ( $defaultArgs as $argK => $argV ) {
816            $$argK = $argV;
817            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
818                if ( is_array( $args[ $argK ] ) ) {
819                    $newData = $$argK;
820                    if ( ! is_array( $newData ) ) {
821                        $newData = array();
822                    } foreach ( $args[ $argK ] as $subK => $subV ) {
823                        $newData[ $subK ] = $subV;
824                    }$$argK = $newData;
825                } else {
826                    $$argK = $args[ $argK ]; }
827            }
828        }
829        #} =========== / LOAD ARGS =============)
830
831        if ( $objID !== -1 && $objTypeID > 0 ) {
832
833            return $this->getFieldByID(
834                array(
835                    'id'          => $objID,
836                    'objtype'     => $objTypeID,
837                    'colname'     => 'zbs_owner',
838                    'ignoreowner' => true,
839                )
840            );
841
842        }
843
844        return false;
845    }
846
847    // This func is a side-switch alternative to zeroBS_setOwner
848    public function setObjectOwner( $args = array() ) {
849
850        #} =========== LOAD ARGS ==============
851        $defaultArgs = array(
852
853            'objID'     => -1,
854            'objTypeID' => -1,
855
856            'ownerID'   => -1,
857
858        );
859        foreach ( $defaultArgs as $argK => $argV ) {
860            $$argK = $argV;
861            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
862                if ( is_array( $args[ $argK ] ) ) {
863                    $newData = $$argK;
864                    if ( ! is_array( $newData ) ) {
865                        $newData = array();
866                    } foreach ( $args[ $argK ] as $subK => $subV ) {
867                        $newData[ $subK ] = $subV;
868                    }$$argK = $newData;
869                } else {
870                    $$argK = $args[ $argK ]; }
871            }
872        }
873        #} =========== / LOAD ARGS =============)
874
875        if ( $objID !== -1 && $objTypeID > 0 ) {
876
877            return $this->setFieldByID(
878                array(
879
880                    'objID'       => $objID,
881                    'objTypeID'   => $objTypeID,
882
883                    'colname'     => 'zbs_owner',
884                    'coldatatype' => '%d', // %d/s
885                    'newValue'    => $ownerID,
886
887                )
888            );
889
890        }
891
892        return false;
893    }
894
895    // this is used to get specific user's settings via userSetting
896    private $userSettingPrefix = 'usrset_'; // completes via usrset_*ID*_key
897
898    /*
899    old way of doing it: (we now use zbs_owner :))
900    private function getUserSettingPrefix($userID=-1){
901
902        // completes usrset_*ID*_key
903        return $this->userSettingPrefix.$userID.'_';
904    } */
905
906    // Note: Following MUST be used together.
907
908        // this makes query vars (as appropriate) team + site (and owner if  $ignoreOwner not false)
909    public function ownershipQueryVars( $ignoreOwner = false ) {
910
911        $queryVars = array();
912
913        // add site
914        // FOR V3.0 SITE + TEAM NOT YET USED, (BUT THIS'll WORK)
915        // $queryVars[] = zeroBSCRM_site();
916
917        // add team
918        // FOR V3.0 SITE + TEAM NOT YET USED, (BUT THIS'll WORK)
919        // $queryVars[] = zeroBSCRM_team();
920
921        // add owner
922        if ( ! $ignoreOwner ) {
923            $queryVars[] = zeroBSCRM_user();
924        }
925
926        return $queryVars;
927    }
928        // this makes query str (as appropriate) team + site (and owner if  $ignoreOwner not false)
929        // $table ONLY needed when is a LEFT JOIN or similar.
930    public function ownershipSQL( $ignoreOwner = false, $table = '' ) {
931
932        // build
933        $q        = '';
934        $tableStr = '';
935        if ( ! empty( $table ) ) {
936            $tableStr = $table . '.';
937        }
938
939        // add site
940        // FOR V3.0 SITE + TEAM NOT YET USED, (BUT THIS'll WORK)
941        // $q = $this->spaceAnd($q).$tableStr.'zbs_site = %d';
942
943        // add team
944        // FOR V3.0 SITE + TEAM NOT YET USED, (BUT THIS'll WORK)
945        // $q = $this->spaceAnd($q).$tableStr.'zbs_team = %d';
946
947        // add owner
948        if ( ! $ignoreOwner ) {
949            $q = $this->spaceAnd( $q ) . $tableStr . 'zbs_owner = %d';
950        }
951
952        return $q;
953    }
954
955    // ===========  / OWNERSHIP HELPERS  =============================================
956    // ===============================================================================
957
958    // ===============================================================================
959    // ===========  ERROR HELPER FUNCS ===============================================
960    /* These are shared between DAL2 + DAL3, though are only included from v3.0 +   */
961
962        // retrieve errors from dal error stack
963    public function getErrors( $objTypeID = -1 ) {
964
965        // all:
966        if ( $objTypeID < 0 ) {
967            return $this->errorStack;
968        }
969
970        // specific
971        if ( is_array( $this->errorStack ) && isset( $this->errorStack[ $objTypeID ] ) ) {
972            return $this->errorStack[ $objTypeID ];
973        }
974
975        // ??
976        return array();
977    }
978
979        // add error to dal error stack
980    public function addError( $errorCode = -1, $objTypeID = -1, $errStr = '', $extraParam = false ) {
981
982        if ( $objTypeID > 0 && ! empty( $errStr ) ) {
983
984            // init
985            if ( ! isset( $this->errorStack ) || ! is_array( $this->errorStack ) ) {
986                $this->errorStack = array();
987            }
988            if ( ! isset( $this->errorStack[ $objTypeID ] ) || ! is_array( $this->errorStack[ $objTypeID ] ) ) {
989                $this->errorStack[ $objTypeID ] = array();
990            }
991
992            // if $errorCode, add to string
993            if ( $errorCode > 0 ) {
994                $errStr .= ' (' . __( 'Error #', 'zero-bs-crm' ) . $errorCode . ')';
995            }
996
997            // add
998            $this->errorStack[ $objTypeID ][] = array(
999                'code'  => $errorCode,
1000                'str'   => $errStr,
1001                'param' => $extraParam,
1002            );
1003        }
1004    }
1005
1006    // =========== / ERROR HELPER FUNCS ==============================================
1007    // ===============================================================================
1008
1009    /*
1010    ======================================================
1011    DAL CRUD
1012    ======================================================
1013    */
1014
1015    // ===============================================================================
1016    // ===========   OBJ LINKS   =======================================================
1017
1018    /**
1019     * returns objects against an obj (e.g. company's against contact id 101)
1020     * This is like getObjsLinksLinkedToObj, only it returns actual objs :)
1021     *
1022     * @param array $args   Associative array of arguments
1023     *                      obj array
1024     *
1025     * @return array result
1026     */
1027    public function getObjsLinkedToObj( $args = array() ) {
1028
1029        #} =========== LOAD ARGS ==============
1030        $defaultArgs = array(
1031
1032            'objtypefrom'            => -1,
1033            'objtypeto'              => -1,
1034
1035            // either or here, to specify direction of relationship
1036            'objfromid'              => -1,
1037            'objtoid'                => -1,
1038
1039            // this will be passed to the getCompanies(array()) func, if given
1040            'objRetrievePassthrough' => array(),
1041
1042            'count'                  => false, // only return count
1043
1044            // permissions
1045            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
1046            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
1047            // settings don't need owners yet :)
1048
1049        );
1050        foreach ( $defaultArgs as $argK => $argV ) {
1051            $$argK = $argV;
1052            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1053                if ( is_array( $args[ $argK ] ) ) {
1054                    $newData = $$argK;
1055                    if ( ! is_array( $newData ) ) {
1056                        $newData = array();
1057                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1058                        $newData[ $subK ] = $subV;
1059                    }$$argK = $newData;
1060                } else {
1061                    $$argK = $args[ $argK ]; }
1062            }
1063        }
1064        #} =========== / LOAD ARGS =============
1065
1066        // hard ignored for now :)
1067        $ignoreowner = true;
1068
1069        if ( empty( $objtypefrom ) ) {
1070            return false;
1071        }
1072        if ( $this->objTypeKey( $objtypefrom ) === -1 ) {
1073            return false;
1074        }
1075        if ( empty( $objtypeto ) ) {
1076            return false;
1077        }
1078        if ( $this->objTypeKey( $objtypeto ) === -1 ) {
1079            return false;
1080        }
1081
1082        #} Check ID
1083        $direction = 'from';
1084        $objfromid = (int) $objfromid;
1085        $objtoid   = (int) $objtoid;
1086        if ( $objtoid > 0 ) {
1087            $direction = 'to';
1088        }
1089
1090        if (
1091                ( ! empty( $objfromid ) && $objfromid > 0 )
1092                ||
1093                ( ! empty( $objtoid ) && $objtoid > 0 )
1094
1095            ) {
1096
1097            $res = array();
1098
1099            // get links - this could all be one query... optimise once other db objects moved over
1100            $objLinks = $this->getObjsLinksLinkedToObj(
1101                array(
1102                    'objtypefrom' => $objtypefrom, // contact
1103                    'objtypeto'   => $objtypeto, // company
1104                    'objfromid'   => $objfromid, // -1 or id
1105                    'objtoid'     => $objtoid,
1106                )
1107            );
1108
1109            if ( $count ) {
1110                if ( is_array( $objLinks ) ) {
1111                    return count( $objLinks );
1112                } else {
1113                    return 0;
1114                }
1115            }
1116
1117            if ( is_array( $objLinks ) && count( $objLinks ) > 0 ) {
1118
1119                // make an id array (useful)
1120                $idArray = array(); foreach ( $objLinks as $l ) {
1121
1122                    // switched direction
1123                    $xid = $l['objidto'];
1124                    if ( $direction == 'to' ) {
1125                        $xid = $l['objidfrom'];
1126                    }
1127
1128                    if ( ! in_array( $xid, $idArray ) ) {
1129                        $idArray[] = $xid;
1130                    }
1131                }
1132
1133                // load them all (type dependent)
1134                switch ( $objtypeto ) {
1135
1136                    // not yet used, but will work :)
1137                    case ZBS_TYPE_CONTACT:
1138                        return $this->contacts->getContacts( array( 'inArr' => $idArray ) );
1139                        break;
1140
1141                    case ZBS_TYPE_COMPANY:
1142                        return $this->companies->getCompanies( array( 'inArr' => $idArray ) );
1143                        break;
1144
1145                    case ZBS_TYPE_QUOTE:
1146                        return $this->quotes->getQuotes( array( 'inArr' => $idArray ) );
1147                        break;
1148
1149                    case ZBS_TYPE_INVOICE:
1150                        return $this->invoices->getInvoices( array( 'inArr' => $idArray ) );
1151                        break;
1152
1153                    case ZBS_TYPE_TRANSACTION:
1154                        return $this->transactions->getTransactions( array( 'inArr' => $idArray ) );
1155                        break;
1156
1157                    case ZBS_TYPE_TASK:
1158                        return $this->events->getEvents( array( 'inArr' => $idArray ) );
1159                        break;
1160
1161                    case ZBS_TYPE_QUOTETEMPLATE:
1162                        return $this->quotetemplates->getQuotetemplate( array( 'inArr' => $idArray ) );
1163                        break;
1164
1165                    /*
1166                    not used
1167                    case ZBS_TYPE_LOG:
1168                        return $this->logs->getLogs(array('inArr'=>$idArray));
1169                        break;
1170
1171                    case ZBS_TYPE_LINEITEM:
1172                        return $this->events->getEvents(array('inArr'=>$idArray));
1173                        break;
1174
1175                    case ZBS_TYPE_TASK_REMINDER:
1176                        return $this->events->getEvents(array('inArr'=>$idArray));
1177                        break;
1178                    */
1179
1180                }
1181            }
1182
1183            return $res;
1184
1185        } // / if ID
1186
1187        return false;
1188    }
1189
1190    /**
1191     * returns object link lines against an obj (e.g. link id, company id's against contact id 101)
1192     *
1193     * @param array $args   Associative array of arguments
1194     *                      objtypeid, objid
1195     *
1196     * @return array result
1197     */
1198    public function getObjsLinksLinkedToObj( $args = array() ) {
1199
1200        #} =========== LOAD ARGS ==============
1201        $defaultArgs = array(
1202
1203            'objtypefrom' => -1,
1204            'objtypeto'   => -1,
1205
1206            // either or here, to specify direction of relationship
1207            // if 'direction' = 'both', it'll check both
1208            'objfromid'   => -1,
1209            'objtoid'     => -1,
1210
1211            'direction'   => 'from', // from, to, both (both checks for both id's and is used to validate if links exist)
1212
1213            'count'       => false, // only return count
1214
1215            // permissions
1216            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
1217            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
1218            // settings don't need owners yet :)
1219
1220        );
1221        foreach ( $defaultArgs as $argK => $argV ) {
1222            $$argK = $argV;
1223            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1224                if ( is_array( $args[ $argK ] ) ) {
1225                    $newData = $$argK;
1226                    if ( ! is_array( $newData ) ) {
1227                        $newData = array();
1228                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1229                        $newData[ $subK ] = $subV;
1230                    }$$argK = $newData;
1231                } else {
1232                    $$argK = $args[ $argK ]; }
1233            }
1234        }
1235        #} =========== / LOAD ARGS =============
1236
1237        // hard ignored for now
1238        $ignoreowner = true;
1239
1240        if ( empty( $objtypefrom ) ) {
1241            return false;
1242        }
1243        if ( $this->objTypeKey( $objtypefrom ) === -1 ) {
1244            return false;
1245        }
1246        if ( empty( $objtypeto ) ) {
1247            return false;
1248        }
1249        if ( $this->objTypeKey( $objtypeto ) === -1 ) {
1250            return false;
1251        }
1252
1253        #} Check ID
1254        $objfromid = (int) $objfromid;
1255        $objtoid   = (int) $objtoid;
1256        if ( $objtoid > 0 && $direction != 'both' ) {
1257            $direction = 'to';
1258        }
1259
1260        if (
1261                ( ! empty( $objfromid ) && $objfromid > 0 )
1262                ||
1263                ( ! empty( $objtoid ) && $objtoid > 0 )
1264
1265            ) {
1266
1267            global $ZBSCRM_t, $wpdb;
1268            $wheres          = array( 'direct' => array() );
1269            $whereStr        = '';
1270            $additionalWhere = '';
1271            $params          = array();
1272            $res             = array();
1273
1274            #} Build query
1275            $query = 'SELECT * FROM ' . $ZBSCRM_t['objlinks'];
1276
1277            #} ============= WHERE ================
1278
1279                #} Add
1280                $wheres['zbsol_objtype_from'] = array( 'zbsol_objtype_from', '=', '%s', $objtypefrom );
1281                $wheres['zbsol_objtype_to']   = array( 'zbsol_objtype_to', '=', '%s', $objtypeto );
1282
1283                // which direction?
1284            if ( $direction == 'from' || $direction == 'both' ) {
1285                $wheres['zbsol_objid_from'] = array( 'zbsol_objid_from', '=', '%s', $objfromid );
1286            }
1287            if ( $direction == 'to' || $direction == 'both' ) {
1288                $wheres['zbsol_objid_to'] = array( 'zbsol_objid_to', '=', '%s', $objtoid );
1289            }
1290
1291            #} ============ / WHERE ==============
1292
1293            #} Build out any WHERE clauses
1294            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
1295            $whereStr  = $wheresArr['where'];
1296            $params    = $params + $wheresArr['params'];
1297            #} / Build WHERE
1298
1299            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
1300            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
1301            $ownQ   = $this->ownershipSQL( $ignoreowner );
1302            if ( ! empty( $ownQ ) ) {
1303                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
1304            }
1305            #} / Ownership
1306
1307            #} Append to sql (this also automatically deals with sortby and paging)
1308            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 10000 );
1309
1310            try {
1311
1312                #} Prep & run query
1313                $queryObj     = $this->prepare( $query, $params );
1314                $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
1315
1316            } catch ( Exception $e ) {
1317
1318                #} General SQL Err
1319                $this->catchSQLError( $e );
1320
1321            }
1322
1323            #} Interpret results (Result Set - multi-row)
1324            if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
1325
1326                // if count?
1327                if ( $count ) {
1328                    return count( $potentialRes );
1329                }
1330
1331                #} Has results, tidy + return
1332                foreach ( $potentialRes as $resDataLine ) {
1333
1334                        // tidy
1335                        $resArr = $this->tidy_objlink( $resDataLine );
1336
1337                        $res[] = $resArr;
1338
1339                }
1340            }
1341
1342            // if count?
1343            if ( $count ) {
1344                return 0;
1345            }
1346
1347            return $res;
1348
1349        } // / if ID
1350
1351        return false;
1352    }
1353
1354    /**
1355     * returns ID of first obj of link type (to obj)
1356     * e.g. ID first invoice linked to transaction ID (X)
1357     * (useful where transactions:invoices are only ever linked 1:1)
1358     *
1359     * @param array $args   Associative array of arguments
1360     *                      objtypeid, objid
1361     *
1362     * @return array result
1363     */
1364    public function getFirstIDLinkedToObj( $args = array() ) {
1365
1366        #} =========== LOAD ARGS ==============
1367        $defaultArgs = array(
1368
1369            'objtypefrom' => -1,
1370            'objtypeto'   => -1,
1371            'objfromid'   => -1,
1372
1373            // permissions
1374            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
1375            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
1376            // settings don't need owners yet :)
1377
1378        );
1379        foreach ( $defaultArgs as $argK => $argV ) {
1380            $$argK = $argV;
1381            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1382                if ( is_array( $args[ $argK ] ) ) {
1383                    $newData = $$argK;
1384                    if ( ! is_array( $newData ) ) {
1385                        $newData = array();
1386                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1387                        $newData[ $subK ] = $subV;
1388                    }$$argK = $newData;
1389                } else {
1390                    $$argK = $args[ $argK ]; }
1391            }
1392        }
1393        #} =========== / LOAD ARGS =============
1394
1395        // hard ignored for now
1396        $ignoreowner = true;
1397
1398        if ( empty( $objtypefrom ) ) {
1399            return false;
1400        }
1401        if ( $this->objTypeKey( $objtypefrom ) === -1 ) {
1402            return false;
1403        }
1404        if ( empty( $objtypeto ) ) {
1405            return false;
1406        }
1407        if ( $this->objTypeKey( $objtypeto ) === -1 ) {
1408            return false;
1409        }
1410
1411        #} Check ID
1412        $objfromid = (int) $objfromid;
1413
1414        if ( ! empty( $objfromid ) && $objfromid > 0 ) {
1415
1416            global $ZBSCRM_t, $wpdb;
1417            $wheres          = array( 'direct' => array() );
1418            $whereStr        = '';
1419            $additionalWhere = '';
1420            $params          = array();
1421            $res             = array();
1422
1423            #} Build query
1424            $query = 'SELECT zbsol_objid_to FROM ' . $ZBSCRM_t['objlinks'];
1425
1426            #} ============= WHERE ================
1427
1428                #} Add
1429                $wheres['zbsol_objtype_from'] = array( 'zbsol_objtype_from', '=', '%s', $objtypefrom );
1430                $wheres['zbsol_objtype_to']   = array( 'zbsol_objtype_to', '=', '%s', $objtypeto );
1431                $wheres['zbsol_objid_from']   = array( 'zbsol_objid_from', '=', '%s', $objfromid );
1432
1433            #} ============ / WHERE ==============
1434
1435            #} Build out any WHERE clauses
1436            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
1437            $whereStr  = $wheresArr['where'];
1438            $params    = $params + $wheresArr['params'];
1439            #} / Build WHERE
1440
1441            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
1442            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
1443            $ownQ   = $this->ownershipSQL( $ignoreowner );
1444            if ( ! empty( $ownQ ) ) {
1445                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
1446            }
1447            #} / Ownership
1448
1449            #} Append to sql (this also automatically deals with sortby and paging)
1450            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
1451
1452            try {
1453
1454                #} Prep & run query
1455                $queryObj     = $this->prepare( $query, $params );
1456                $potentialRes = $wpdb->get_var( $queryObj );
1457                if ( $potentialRes > -1 ) {
1458                    return (int) $potentialRes;
1459                }
1460            } catch ( Exception $e ) {
1461
1462                #} General SQL Err
1463                $this->catchSQLError( $e );
1464
1465            }
1466        } // / if ID
1467
1468        return false;
1469    }
1470
1471    /**
1472     * adds or updates a link object
1473     * E.G. Contact -> Company
1474     * this says "match obj X with obj Y" (effectively 'tagging' it)
1475     * Using this generic format, but as of v2.5+ there's only contact->company links in here
1476     *
1477     * @param array $args Associative array of arguments
1478     *              id (if update - probably never used here), data(objtype,objid,tagid)
1479     *
1480     * @return int line ID
1481     */
1482    public function addUpdateObjLink( $args = array() ) {
1483
1484        global $ZBSCRM_t, $wpdb;
1485
1486        #} ============ LOAD ARGS =============
1487        $defaultArgs = array(
1488
1489            'id'   => -1,
1490
1491            // OWNERS will all be set to -1 for objlinks for now :)
1492            // 'owner'           => -1
1493
1494            // fields (directly)
1495            'data' => array(
1496
1497                'objtypefrom' => -1,
1498                'objtypeto'   => -1,
1499                'objfromid'   => -1,
1500                'objtoid'     => -1,
1501
1502            ),
1503
1504        );
1505        foreach ( $defaultArgs as $argK => $argV ) {
1506            $$argK = $argV;
1507            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1508                if ( is_array( $args[ $argK ] ) ) {
1509                    $newData = $$argK;
1510                    if ( ! is_array( $newData ) ) {
1511                        $newData = array();
1512                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1513                        $newData[ $subK ] = $subV;
1514                    }$$argK = $newData;
1515                } else {
1516                    $$argK = $args[ $argK ]; }
1517            }
1518        }
1519        #} =========== / LOAD ARGS ============
1520
1521        #} ========== CHECK FIELDS ============
1522
1523        // check obtype is completed + legit
1524        if ( empty( $data['objtypefrom'] ) ) {
1525            return false;
1526        }
1527        if ( $this->objTypeKey( $data['objtypefrom'] ) === -1 ) {
1528            return false;
1529        }
1530        if ( empty( $data['objtypeto'] ) ) {
1531            return false;
1532        }
1533        if ( $this->objTypeKey( $data['objtypeto'] ) === -1 ) {
1534            return false;
1535        }
1536
1537            // if owner = -1, add current
1538            // for now, all -1 - not needed yet (makes tags dupe e.g.) if (!isset($owner) || $owner === -1) $owner = zeroBSCRM_user();
1539            $owner = -1;
1540
1541            #} check obj ids
1542        if ( empty( $data['objfromid'] ) || $data['objfromid'] < 1 || empty( $data['objtoid'] ) || $data['objtoid'] < 1 ) {
1543            return false;
1544        }
1545
1546        #} ========= / CHECK FIELDS ===========
1547
1548        #} Check if ID present
1549        $id = (int) $id;
1550        if ( ! empty( $id ) && $id > 0 ) {
1551
1552                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
1553
1554                #} Attempt update
1555            if ( $wpdb->update(
1556                $ZBSCRM_t['objlinks'],
1557                array(
1558
1559                    // ownership
1560                    // no need to update these (as of yet) - can't move teams etc.
1561                    // 'zbs_site' => zeroBSCRM_installSite(),
1562                    // 'zbs_team' => zeroBSCRM_installTeam(),
1563                    'zbs_owner'          => $owner,
1564
1565                    // fields
1566                    'zbsol_objtype_from' => $data['objtypefrom'],
1567                    'zbsol_objtype_to'   => $data['objtypeto'],
1568                    'zbsol_objid_from'   => $data['objfromid'],
1569                    'zbsol_objid_to'     => $data['objtoid'],
1570                ),
1571                array( // where
1572                    'ID' => $id,
1573                ),
1574                array( // field data types
1575                    '%d',
1576                    '%d',
1577                    '%d',
1578                    '%d',
1579                    '%d',
1580                ),
1581                array( // where data types
1582                    '%d',
1583                )
1584            ) !== false ) {
1585
1586                        // Successfully updated - Return id
1587                        return $id;
1588
1589            } else {
1590
1591                // FAILED update
1592                return false;
1593
1594            }
1595        } else {
1596
1597            #} No ID - must be an INSERT
1598            if ( $wpdb->insert(
1599                $ZBSCRM_t['objlinks'],
1600                array(
1601
1602                    // ownership
1603                    'zbs_site'           => zeroBSCRM_site(),
1604                    'zbs_team'           => zeroBSCRM_team(),
1605                    'zbs_owner'          => $owner,
1606
1607                    // fields
1608                    'zbsol_objtype_from' => $data['objtypefrom'],
1609                    'zbsol_objtype_to'   => $data['objtypeto'],
1610                    'zbsol_objid_from'   => $data['objfromid'],
1611                    'zbsol_objid_to'     => $data['objtoid'],
1612                ),
1613                array( // field data types
1614                    '%d',  // site
1615                    '%d',  // team
1616                    '%d',  // owner
1617
1618                    '%d',
1619                    '%d',
1620                    '%d',
1621                    '%d',
1622                )
1623            ) > 0 ) {
1624
1625                    #} Successfully inserted, lets return new ID
1626                    $newID = $wpdb->insert_id;
1627                    return $newID;
1628
1629            } else {
1630
1631                #} Failed to Insert
1632                return false;
1633
1634            }
1635        }
1636
1637        return false;
1638    }
1639
1640    /**
1641     * adds or updates object - object link  against an obj
1642     * this says "match company X,Y,Z with contact Y"
1643     *
1644     * @param array $args Associative array of arguments
1645     *              objtype,objid,tags (array of tagids)
1646     *
1647     * @return array $tags
1648     */
1649    public function addUpdateObjLinks( $args = array() ) {
1650
1651        global $ZBSCRM_t, $wpdb;
1652
1653        #} ============ LOAD ARGS =============
1654        $defaultArgs = array(
1655
1656            'owner'       => -1,
1657
1658            'objtypefrom' => -1,
1659            'objtypeto'   => -1,
1660            'objfromid'   => -1,
1661            'objtoids'    => -1, // array of ID's
1662
1663            'mode'        => 'replace', // replace|append|remove
1664
1665        );
1666        foreach ( $defaultArgs as $argK => $argV ) {
1667            $$argK = $argV;
1668            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1669                if ( is_array( $args[ $argK ] ) ) {
1670                    $newData = $$argK;
1671                    if ( ! is_array( $newData ) ) {
1672                        $newData = array();
1673                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1674                        $newData[ $subK ] = $subV;
1675                    }$$argK = $newData;
1676                } else {
1677                    $$argK = $args[ $argK ]; }
1678            }
1679        }
1680        #} =========== / LOAD ARGS ============
1681
1682        #} ========== CHECK FIELDS ============
1683
1684        // check obtype is completed + legit
1685        if ( empty( $objtypefrom ) ) {
1686            return false;
1687        }
1688        if ( $this->objTypeKey( $objtypefrom ) === -1 ) {
1689            return false;
1690        }
1691        if ( empty( $objtypeto ) ) {
1692            return false;
1693        }
1694        if ( $this->objTypeKey( $objtypeto ) === -1 ) {
1695            return false;
1696        }
1697
1698        // if owner = -1, add current
1699        if ( $owner === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1700            $owner = zeroBSCRM_user();
1701        }
1702
1703            // tagging id
1704            $objfromid = (int) $objfromid;
1705        if ( empty( $objfromid ) || $objfromid < 1 ) {
1706            return false;
1707        }
1708
1709            // to obj list
1710        if ( ! is_array( $objtoids ) ) {
1711            return false;
1712        }
1713
1714            // mode
1715        if ( gettype( $mode ) != 'string' || ! in_array( $mode, array( 'replace', 'append', 'remove' ) ) ) {
1716            return false;
1717        }
1718
1719        #} ========= / CHECK FIELDS ===========
1720
1721        switch ( $mode ) {
1722
1723            case 'replace':
1724                // cull all previous
1725                $deleted = $this->deleteObjLinks(
1726                    array(
1727                        'objtypefrom' => $objtypefrom, // contact
1728                        'objtypeto'   => $objtypeto, // company
1729                        'objfromid'   => $objfromid,
1730                    )
1731                ); // where contact id =
1732
1733                // cycle through & add
1734                foreach ( $objtoids as $objtoid ) {
1735
1736                    $added = $this->addUpdateObjLink(
1737                        array(
1738                            'data' => array(
1739                                'objtypefrom' => $objtypefrom,
1740                                'objtypeto'   => $objtypeto,
1741                                'objfromid'   => $objfromid,
1742                                'objtoid'     => $objtoid,
1743                                'owner'       => $owner,
1744                            ),
1745                        )
1746                    );
1747
1748                }
1749
1750                break;
1751
1752            case 'append':
1753                // get existing
1754                $objLinks = $this->getObjsLinksLinkedToObj(
1755                    array(
1756                        'objtypefrom' => $objtypefrom, // contact
1757                        'objtypeto'   => $objtypeto, // company
1758                        'objfromid'   => $objfromid,
1759                    )
1760                );
1761
1762                // make just ids
1763                $existingLinkIDs = array();
1764                foreach ( $objLinks as $l ) {
1765                    $existingLinkIDs[] = $l['id'];
1766                }
1767
1768                // cycle through& add
1769                foreach ( $objtoids as $objtoid ) {
1770
1771                    if ( ! in_array( $objtoid, $existingLinkIDs ) ) {
1772
1773                        // add a link
1774                        $this->addUpdateObjLink(
1775                            array(
1776                                'data' => array(
1777                                    'objtypefrom' => $objtypefrom,
1778                                    'objtypeto'   => $objtypeto,
1779                                    'objfromid'   => $objfromid,
1780                                    'objtoid'     => $objtoid,
1781                                    'owner'       => $owner,
1782                                ),
1783                            )
1784                        );
1785
1786                    }
1787                }
1788
1789                break;
1790
1791            case 'remove':
1792                // cycle through & remove links
1793                foreach ( $objtoids as $objtoid ) {
1794
1795                    // add a link
1796                    $this->deleteObjLinks(
1797                        array(
1798                            'objtypefrom' => $objtypefrom, // contact
1799                            'objtypeto'   => $objtypeto, // company
1800                            'objfromid'   => $objfromid,
1801                            'objtoid'     => $objtoid,
1802                        )
1803                    ); // where contact id =
1804
1805                }
1806
1807                break;
1808
1809        }
1810
1811        return false;
1812    }
1813
1814    /**
1815     * deletes all object links for a specific obj
1816     *
1817     * @param array $args Associative array of arguments
1818     *              id
1819     *
1820     * @return int success;
1821     */
1822    public function deleteObjLinks( $args = array() ) {
1823
1824        global $ZBSCRM_t, $wpdb;
1825
1826        #} ============ LOAD ARGS =============
1827        $defaultArgs = array(
1828
1829            'objtypefrom' => -1,
1830            'objtypeto'   => -1,
1831            'objfromid'   => -1,
1832            'objtoid'     => -1, // only toid/fromid to be set if want to delete all contact->company links
1833
1834        );
1835        foreach ( $defaultArgs as $argK => $argV ) {
1836            $$argK = $argV;
1837            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1838                if ( is_array( $args[ $argK ] ) ) {
1839                    $newData = $$argK;
1840                    if ( ! is_array( $newData ) ) {
1841                        $newData = array();
1842                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1843                        $newData[ $subK ] = $subV;
1844                    }$$argK = $newData;
1845                } else {
1846                    $$argK = $args[ $argK ]; }
1847            }
1848        }
1849        #} =========== / LOAD ARGS ============
1850
1851        #} ========== CHECK FIELDS ============
1852
1853        // check obtype is completed + legit
1854        if ( empty( $objtypefrom ) ) {
1855            return false;
1856        }
1857        if ( $this->objTypeKey( $objtypefrom ) === -1 ) {
1858            return false;
1859        }
1860        if ( empty( $objtypeto ) ) {
1861            return false;
1862        }
1863        if ( $this->objTypeKey( $objtypeto ) === -1 ) {
1864            return false;
1865        }
1866
1867            // obj id
1868            $objfromid = (int) $objfromid;
1869        $objtoid       = (int) $objtoid;
1870        if (
1871                ( empty( $objfromid ) || $objfromid < 1 )
1872                &&
1873                ( empty( $objtoid ) || $objtoid < 1 )
1874            ) {
1875            return false;
1876        }
1877
1878            // CHECK PERMISSIONS?
1879
1880            #} ========= / CHECK FIELDS ===========
1881
1882            // basics
1883            $where = array( // where
1884                'zbsol_objtype_from' => $objtypefrom,
1885                'zbsol_objtype_to'   => $objtypeto,
1886            );
1887
1888            $whereFormat = array( // where
1889                '%d',
1890                '%d',
1891            );
1892
1893            // any to add?
1894            if ( ! empty( $objfromid ) && $objfromid > 0 ) {
1895                $where['zbsol_objid_from'] = $objfromid;
1896                $whereFormat[]             = '%d';
1897            }
1898            if ( ! empty( $objtoid ) && $objtoid > 0 ) {
1899                $where['zbsol_objid_to'] = $objtoid;
1900                $whereFormat[]           = '%d';
1901            }
1902
1903            // brutal
1904            return $wpdb->delete(
1905                $ZBSCRM_t['objlinks'],
1906                $where,
1907                $whereFormat
1908            );
1909    }
1910
1911    /**
1912     * tidy's the object from wp db into clean array
1913     *
1914     * @param array $obj (DB obj)
1915     *
1916     * @return array (clean obj)
1917     */
1918    private function tidy_objlink( $obj = false ) {
1919
1920            $res = false;
1921
1922        if ( isset( $obj->ID ) ) {
1923            $res       = array();
1924            $res['id'] = $obj->ID;
1925            /*
1926            `zbs_site` INT NULL DEFAULT NULL,
1927            `zbs_team` INT NULL DEFAULT NULL,
1928            `zbs_owner` INT NOT NULL,
1929            */
1930
1931            $res['objtypefrom'] = $obj->zbsol_objtype_from;
1932            $res['objtypeto']   = $obj->zbsol_objtype_to;
1933            $res['objidfrom']   = $obj->zbsol_objid_from;
1934            $res['objidto']     = $obj->zbsol_objid_to;
1935
1936        }
1937
1938        return $res;
1939    }
1940
1941    // ===========   OBJ LINKS   =====================================================
1942    // ===============================================================================
1943
1944    // ===============================================================================
1945    // ===========   SETTINGS   ======================================================
1946
1947    /**
1948     * Wrapper, use $this->setting($key) for easy retrieval of singular
1949     * Simplifies $this->getSetting
1950     *
1951     * @param string key
1952     *
1953     * @return bool result
1954     */
1955    public function setting( $key = '', $default = false, $accept_cached = false ) {
1956        if ( ! empty( $key ) ) {
1957            return $this->getSetting(
1958                array(
1959                    'key'           => $key,
1960                    'fullDetails'   => false,
1961                    'default'       => $default,
1962                    'accept_cached' => $accept_cached,
1963                )
1964            );
1965        }
1966
1967        return $default;
1968    }
1969
1970    /**
1971     * Wrapper, use $this->userSetting($key) for easy retrieval of singular setting FOR USER ID
1972     * Simplifies $this->getSetting
1973     * Specific for USER settings, this prefixes setting keys with usrset_ID_
1974     *
1975     * @param string key
1976     *
1977     * @return bool result
1978     */
1979    public function userSetting( $userID = -1, $key = '', $default = false ) {
1980
1981        if ( ! empty( $key ) && $userID > 0 ) {
1982
1983            return $this->getSetting(
1984                array(
1985
1986                    // old way of doing it'key' => $this->getUserSettingPrefix($userID).$key,
1987                    'key'         => $this->userSettingPrefix . $key,
1988                    'fullDetails' => false,
1989                    'default'     => $default,
1990
1991                    // this makes it 'per user'
1992                    'ownedBy'     => $userID,
1993
1994                )
1995            );
1996
1997        }
1998
1999        return $default;
2000    }
2001
2002    /**
2003     * returns full setting line +- details
2004     *
2005     * @param array $args   Associative array of arguments
2006     *                      key, fullDetails, default
2007     *
2008     * @return array result
2009     */
2010    public function getSetting( $args = array() ) {
2011
2012        $key         = isset( $args['key'] ) ? $args['key'] : false;
2013        $fullDetails = isset( $args['fullDetails'] ) ? $args['fullDetails'] : false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2014
2015        if ( ! $fullDetails && static::mitigation_cache_for_issue_3504_contains_key( $key ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2016            return static::mitigation_cache_for_issue_3504_single_setting( $key ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2017        }
2018
2019        #} =========== LOAD ARGS ==============
2020        $defaultArgs = array(
2021
2022            'key'           => false,
2023            'default'       => false,
2024            'fullDetails'   => false, // set this to 1 and get ID|key|val, rather than just the val
2025
2026            // permissions - these are currently only used by screenoptions
2027            'ignoreowner'   => true, // this'll let you not-check the owner of obj
2028            'ownedBy'       => -1,
2029
2030            // returns scalar ID of line
2031            'onlyID'        => false,
2032
2033            // whether or not to accept cached variant.
2034            // Added in gh-2019, we often recall this function on one load, this allows us to accept a once-loaded version
2035            'accept_cached' => false,
2036
2037        );
2038        foreach ( $defaultArgs as $argK => $argV ) {
2039            $$argK = $argV;
2040            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2041                if ( is_array( $args[ $argK ] ) ) {
2042                    $newData = $$argK;
2043                    if ( ! is_array( $newData ) ) {
2044                        $newData = array();
2045                    } foreach ( $args[ $argK ] as $subK => $subV ) {
2046                        $newData[ $subK ] = $subV;
2047                    }$$argK = $newData;
2048                } else {
2049                    $$argK = $args[ $argK ]; }
2050            }
2051        }
2052        #} =========== / LOAD ARGS =============
2053
2054        #} Check key
2055        if ( ! empty( $key ) ) {
2056
2057            // if accepting a cached obj and is present, pass that to save the query
2058            // (only do so on singular return step, not full details)
2059            if ( ! $fullDetails && $accept_cached ) {
2060
2061                $cached = $this->get_cache_var( 'setting_' . $key );
2062
2063                if ( $cached ) {
2064
2065                    return $cached;
2066
2067                }
2068            }
2069
2070            global $ZBSCRM_t, $wpdb;
2071            $wheres          = array( 'direct' => array() );
2072            $whereStr        = '';
2073            $additionalWhere = '';
2074            $params          = array();
2075            $res             = array();
2076
2077            #} Build query
2078            $query = 'SELECT * FROM ' . $ZBSCRM_t['settings'];
2079
2080            #} ============= WHERE ================
2081
2082                #} Add ID
2083                $wheres['zbsset_key'] = array( 'zbsset_key', '=', '%s', $key );
2084
2085                #} Owned by
2086            if ( ! empty( $ownedBy ) && $ownedBy > 0 ) {
2087
2088                // would never hard-type this in (would make generic as in buildWPMetaQueryWhere)
2089                // but this is only here until MIGRATED to db2 globally
2090                // $wheres['incompany'] = array('ID','IN','(SELECT DISTINCT post_id FROM '.$wpdb->prefix."postmeta WHERE meta_key = 'zbs_company' AND meta_value = %d)",$inCompany);
2091                // Use obj links now
2092                $wheres['ownedBy'] = array( 'zbs_owner', '=', '%s', $ownedBy );
2093
2094            }
2095
2096            #} ============ / WHERE ==============
2097
2098            #} Build out any WHERE clauses
2099            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
2100            $whereStr  = $wheresArr['where'];
2101            $params    = $params + $wheresArr['params'];
2102            #} / Build WHERE
2103
2104            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
2105            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
2106            $ownQ   = $this->ownershipSQL( $ignoreowner );
2107            if ( ! empty( $ownQ ) ) {
2108                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
2109            }
2110            #} / Ownership
2111
2112            #} Append to sql (this also automatically deals with sortby and paging)
2113            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
2114
2115            try {
2116
2117                #} Prep & run query
2118                $queryObj     = $this->prepare( $query, $params );
2119                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
2120
2121            } catch ( Exception $e ) {
2122
2123                #} General SQL Err
2124                $this->catchSQLError( $e );
2125
2126            }
2127
2128            #} Interpret Results (ROW)
2129            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
2130
2131                // Has results, tidy + return
2132
2133                    // Only ID? return it directly
2134                if ( $onlyID === true ) {
2135                    return $potentialRes->ID;
2136                }
2137
2138                    // full line or scalar setting val
2139                if ( $fullDetails ) {
2140
2141                    $setting = $this->tidy_setting( $potentialRes );
2142
2143                } else {
2144
2145                    $setting = $this->tidy_settingSingular( $potentialRes );
2146
2147                    // We only update the mitigation cache when $fullDetails is false.
2148                    static::mitigation_cache_for_issue_3504_set_single_setting( $key, $setting ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2149
2150                    // cache (commonly retrieved)
2151                    $this->update_cache_var( 'setting_' . $key, $setting );
2152
2153                }
2154
2155                    return $setting;
2156
2157            }
2158        } // / if ID
2159
2160        if ( ! $fullDetails ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2161            // We only update the mitigation cache when $fullDetails is false.
2162            static::mitigation_cache_for_issue_3504_set_single_setting( $key, $default ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2163        }
2164
2165        return $default;
2166    }
2167
2168    /**
2169     * returns all settings as settings arr (later add autoload)
2170     *
2171     * @param array $args Associative array of arguments
2172     *
2173     * @return array of settings lines
2174     */
2175    public function getSettings( $args = array() ) {
2176        // If we have already loaded all settings in our cache, return them.
2177        // In this function we won't care if the values have changed or not because when they become invalid due to other functions, these functions should reset the cache.
2178        if ( static::mitigation_cache_for_issue_3504_contains_all_settings() ) {
2179            return static::mitigation_cache_for_issue_3504_all_settings();
2180        }
2181        // Reseting any single settings we may have already loaded.
2182        static::reset_mitigation_cache_for_issue_3504();
2183
2184        #} ============ LOAD ARGS =============
2185        $defaultArgs = array(
2186
2187            'autoloadOnly' => true,
2188            'fullDetails'  => false, // if true returns inc id etc.
2189
2190            // permissions
2191            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
2192            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
2193            // settings don't need owners yet :)
2194
2195        );
2196        foreach ( $defaultArgs as $argK => $argV ) {
2197            $$argK = $argV;
2198            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2199                if ( is_array( $args[ $argK ] ) ) {
2200                    $newData = $$argK;
2201                    if ( ! is_array( $newData ) ) {
2202                        $newData = array();
2203                    } foreach ( $args[ $argK ] as $subK => $subV ) {
2204                        $newData[ $subK ] = $subV;
2205                    }$$argK = $newData;
2206                } else {
2207                    $$argK = $args[ $argK ]; }
2208            }
2209        }
2210        #} =========== / LOAD ARGS =============
2211
2212        #} ========== CHECK FIELDS ============
2213
2214            // check obtype is legit
2215            // autoload?
2216
2217            $fields = 'ID,zbsset_key,zbsset_val';
2218        if ( $fullDetails ) {
2219            $fields = '*';
2220        }
2221
2222            // always ignore owner for now (settings global)
2223            $ignoreowner = true;
2224
2225        #} ========= / CHECK FIELDS ===========
2226
2227        global $ZBSCRM_t, $wpdb;
2228        $wheres          = array( 'direct' => array() );
2229        $whereStr        = '';
2230        $additionalWhere = '';
2231        $params          = array();
2232        $res             = array();
2233
2234        #} Build query
2235        $query = "SELECT $fields FROM " . $ZBSCRM_t['settings'];
2236
2237        #} ============= WHERE ================
2238
2239            #} autoload?
2240
2241        #} ============ / WHERE ===============
2242
2243        #} Build out any WHERE clauses
2244        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
2245        $whereStr  = $wheresArr['where'];
2246        $params    = $params + $wheresArr['params'];
2247        #} / Build WHERE
2248
2249        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
2250        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
2251        $ownQ   = $this->ownershipSQL( $ignoreowner );
2252        if ( ! empty( $ownQ ) ) {
2253            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
2254        }
2255        #} / Ownership
2256
2257        #} Append to sql (this also automatically deals with sortby and paging)
2258        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'ASC' ) . $this->buildPaging( 0, 10000 );
2259
2260        try {
2261
2262            #} Prep & run query
2263            $queryObj     = $this->prepare( $query, $params );
2264            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
2265
2266        } catch ( Exception $e ) {
2267
2268            #} General SQL Err
2269            $this->catchSQLError( $e );
2270
2271        }
2272
2273        #} Interpret results (Result Set - multi-row)
2274        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
2275
2276            #} Has results, tidy + return
2277            foreach ( $potentialRes as $resDataLine ) {
2278                // We are only caching the singular setting (i.e. the value), because it is good enough.
2279                static::mitigation_cache_for_issue_3504_set_single_setting( $resDataLine->zbsset_key, $this->tidy_settingSingular( $resDataLine ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2280
2281                if ( $fullDetails ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2282                    $resArr                = $this->tidy_setting( $resDataLine ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2283                    $res[ $resArr['key'] ] = $resArr; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2284                } else {
2285                    $res[ $resDataLine->zbsset_key ] = $this->tidy_settingSingular( $resDataLine ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2286                }
2287            }
2288        }
2289
2290        static::mitigation_cache_for_issue_3504_set_contains_all( true );
2291
2292        return $res;
2293    }
2294
2295    /**
2296     * Wrapper, use $this->updateSetting($key,$val) for easy update of setting
2297     * Uses $this->addUpdateSetting
2298     *
2299     * @param string key
2300     * @param string value
2301     *
2302     * @return bool result
2303     */
2304    public function updateSetting( $key = '', $val = '' ) {
2305
2306        if ( ! empty( $key ) ) {
2307
2308            return $this->addUpdateSetting(
2309                array(
2310
2311                    'data' => array(
2312
2313                        'key' => $key,
2314                        'val' => $val,
2315                    ),
2316
2317                )
2318            );
2319
2320        }
2321
2322        return false;
2323    }
2324
2325    /**
2326     * Wrapper, use $this->updateSetting($key,$val) for easy update of setting
2327     * Uses $this->addUpdateSetting
2328     *
2329     * @param string key
2330     * @param string value
2331     *
2332     * @return bool result
2333     */
2334    public function updateUserSetting( $userID = -1, $key = '', $val = '' ) {
2335
2336        // if -1 passed use current user?
2337
2338        if ( ! empty( $key ) && $userID > 0 ) {
2339
2340            // because the following addUpdateSetting is dumb to owners (e.g. can't update 'per owner')
2341            // we must set perOwnerSetting to force 1 setting per key per user (owner)
2342
2343            return $this->addUpdateSetting(
2344                array(
2345
2346                    'owner'           => $userID,
2347                    'data'            => array(
2348
2349                        // old way of doing it'key' => $this->getUserSettingPrefix($userID).$key,
2350                        'key' => $this->userSettingPrefix . $key,
2351                        'val' => $val,
2352                    ),
2353
2354                    'perOwnerSetting' => true,
2355
2356                )
2357            );
2358
2359        }
2360
2361        return false;
2362    }
2363
2364    /**
2365     * adds or updates a setting object
2366     * ... for a quicker wrapper, use $this->updateSetting($key,$val)
2367     *
2368     * @param array $args Associative array of arguments
2369     *              id (not req.), owner (not req.) data -> key/val
2370     *
2371     * @return int line ID
2372     */
2373    public function addUpdateSetting( $args = array() ) {
2374
2375        global $ZBSCRM_t, $wpdb;
2376
2377        #} ============ LOAD ARGS =============
2378        $defaultArgs = array(
2379
2380            'id'              => -1,
2381            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
2382            // meta don't need owners yet :)
2383            // not anymore! use this for screenoptions, will be ignored unless specifically set
2384            'owner'           => -1,
2385
2386            // fields (directly)
2387            'data'            => array(
2388
2389                'key' => '',
2390                'val' => '',
2391
2392            ),
2393
2394            'perOwnerSetting' => false, // if set to true this'll make sure only 1 key per 'owner' (potentially multi-key if set incorrectly, so beware)
2395
2396        );
2397        foreach ( $defaultArgs as $argK => $argV ) {
2398            $$argK = $argV;
2399            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2400                if ( is_array( $args[ $argK ] ) ) {
2401                    $newData = $$argK;
2402                    if ( ! is_array( $newData ) ) {
2403                        $newData = array();
2404                    } foreach ( $args[ $argK ] as $subK => $subV ) {
2405                        $newData[ $subK ] = $subV;
2406                    }$$argK = $newData;
2407                } else {
2408                    $$argK = $args[ $argK ]; }
2409            }
2410        }
2411        #} =========== / LOAD ARGS ============
2412
2413        // Invalidates our whole cache.
2414        static::reset_mitigation_cache_for_issue_3504();
2415
2416        #} ========== CHECK FIELDS ============
2417
2418            $id = (int) $id;
2419
2420        // if owner = -1, add current
2421        // Hard -1 for now - settings don't need - if (!isset($owner) || $owner === -1) $owner = zeroBSCRM_user();
2422        // ... they do now, (screen options) $owner = -1;
2423        if ( $owner !== -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2424            $owner = (int) $owner;
2425        }
2426
2427        // check key present + legit
2428        if ( empty( $data['key'] ) ) {
2429            return false;
2430        }
2431
2432            // setting ID finder - if obj key provided, check setting not already present (if so overwrite)
2433            // keeps unique...
2434        if ( ( empty( $id ) || $id <= 0 )
2435                &&
2436                ( isset( $data['key'] ) && ! empty( $data['key'] ) ) ) {
2437
2438            // if perOwnerSetting it's 1 key-ed ret per owner, so query bit diff here:
2439            if ( ! $perOwnerSetting ) {
2440
2441                // check existence + return ID
2442                $potentialID = (int) $this->getSetting(
2443                    array(
2444                        'key'    => $data['key'],
2445                        'onlyID' => true,
2446                    )
2447                );
2448
2449            } else {
2450
2451                // perownedBy
2452
2453                // if no owner, return false, cannot be (shouldn't be cos of above)
2454                if ( $owner <= 0 ) {
2455                    return false;
2456                }
2457
2458                // check existence + return ID
2459                $potentialID = (int) $this->getSetting(
2460                    array(
2461                        'key'     => $data['key'],
2462                        'onlyID'  => true,
2463                        'ownedBy' => $owner,
2464                    )
2465                );
2466            }
2467
2468            // override empty ID
2469            if ( ! empty( $potentialID ) && $potentialID > 0 ) {
2470                $id = $potentialID;
2471            }
2472        }
2473
2474        #} ========= / CHECK FIELDS ===========
2475
2476        #} Var up any val (json_encode)
2477        if ( in_array( gettype( $data['val'] ), array( 'object', 'array' ) ) ) {
2478
2479            // WH note: it was necessary to add JSON_UNESCAPED_SLASHES to properly save down without issue
2480            // combined with a more complex zeroBSCRM_stripSlashes recurrsive
2481            // https://stackoverflow.com/questions/7282755/how-to-remove-backslash-on-json-encode-function
2482            $data['val'] = json_encode( $data['val'], JSON_UNESCAPED_SLASHES );
2483
2484        }
2485
2486        if ( isset( $id ) && ! empty( $id ) && $id > 0 ) {
2487
2488            // echo 'updating setting id '.$id.'!';
2489
2490                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
2491
2492                #} Attempt update
2493            if ( $wpdb->update(
2494                $ZBSCRM_t['settings'],
2495                array(
2496
2497                    // ownership
2498                    // no need to update these (as of yet) - can't move teams etc.
2499                    // 'zbs_site' => zeroBSCRM_installSite(),
2500                    // 'zbs_team' => zeroBSCRM_installTeam(),
2501                    'zbs_owner'          => $owner,
2502
2503                    // fields
2504                    'zbsset_key'         => $data['key'],
2505                    'zbsset_val'         => $data['val'],
2506                    'zbsset_lastupdated' => time(),
2507                ),
2508                array( // where
2509                    'ID' => $id,
2510                ),
2511                array( // field data types
2512                    '%d',
2513                    '%s',
2514                    '%s',
2515                    '%d',
2516                ),
2517                array( // where data types
2518                    '%d',
2519                )
2520            ) !== false ) {
2521
2522                        // Successfully updated - Return id
2523                        return $id;
2524
2525            } else {
2526
2527                // FAILED update
2528                return false;
2529
2530            }
2531        } else {
2532
2533            #} No ID - must be an INSERT
2534            if ( $wpdb->insert(
2535                $ZBSCRM_t['settings'],
2536                array(
2537
2538                    // ownership
2539                    'zbs_site'           => zeroBSCRM_site(),
2540                    'zbs_team'           => zeroBSCRM_team(),
2541                    'zbs_owner'          => $owner,
2542
2543                    // fields
2544                    'zbsset_key'         => $data['key'],
2545                    'zbsset_val'         => $data['val'],
2546                    'zbsset_created'     => time(),
2547                    'zbsset_lastupdated' => time(),
2548                ),
2549                array( // field data types
2550                    '%d',  // site
2551                    '%d',  // team
2552                    '%d',  // owner
2553
2554                    '%s',
2555                    '%s',
2556                    '%d',
2557                    '%d',
2558                )
2559            ) > 0 ) {
2560
2561                    #} Successfully inserted, lets return new ID
2562                    $newID = $wpdb->insert_id;
2563                    return $newID;
2564
2565            } else {
2566
2567                #} Failed to Insert
2568                return false;
2569
2570            }
2571        }
2572
2573        return false;
2574    }
2575
2576    /**
2577     * deletes a setting object
2578     *
2579     * @param array $args Associative array of arguments
2580     *              id
2581     *
2582     * @return int success;
2583     */
2584    public function deleteSetting( $args = array() ) {
2585
2586        global $ZBSCRM_t, $wpdb;
2587
2588        #} ============ LOAD ARGS =============
2589        $defaultArgs = array(
2590
2591            'id'  => -1,
2592            'key' => '',
2593
2594        );
2595        foreach ( $defaultArgs as $argK => $argV ) {
2596            $$argK = $argV;
2597            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2598                if ( is_array( $args[ $argK ] ) ) {
2599                    $newData = $$argK;
2600                    if ( ! is_array( $newData ) ) {
2601                        $newData = array();
2602                    } foreach ( $args[ $argK ] as $subK => $subV ) {
2603                        $newData[ $subK ] = $subV;
2604                    }$$argK = $newData;
2605                } else {
2606                    $$argK = $args[ $argK ]; }
2607            }
2608        }
2609        #} =========== / LOAD ARGS ============
2610
2611        // if ID passed, Check ID & Delete :)
2612        $id = (int) $id;
2613        if ( $id > 0 ) {
2614            return zeroBSCRM_db2_deleteGeneric( $id, 'settings' );
2615        }
2616
2617        // if key, find and delete
2618        if ( ! empty( $key ) ) {
2619
2620            $setting_id = $this->getSetting(
2621                array(
2622                    'key'    => $key,
2623                    'onlyID' => true,
2624                )
2625            );
2626
2627            return zeroBSCRM_db2_deleteGeneric( $setting_id, 'settings' );
2628
2629        }
2630
2631        return false;
2632    }
2633
2634    /**
2635     * tidy's the object from wp db into clean array
2636     *
2637     * @param array $obj (DB obj)
2638     *
2639     * @return array (clean obj)
2640     */
2641    private function tidy_setting( $obj = false ) {
2642
2643        $res = false;
2644
2645        if ( isset( $obj->ID ) ) {
2646
2647            $res            = array();
2648            $res['id']      = $obj->ID;
2649            $res['key']     = $obj->zbsset_key;
2650            $res['val']     = $this->unpack_setting( $obj->zbsset_val );
2651            $res['created'] = $obj->zbsset_created;
2652            $res['updated'] = $obj->zbsset_lastupdated;
2653
2654        }
2655
2656        return $res;
2657    }
2658
2659    /**
2660     * tidy's the object from wp db into clean array
2661     *
2662     * @param array $obj (DB obj)
2663     *
2664     * @return string
2665     */
2666    private function tidy_settingSingular( $obj = false ) {
2667
2668        if ( isset( $obj->ID ) ) {
2669
2670            return $this->unpack_setting( $obj->zbsset_val );
2671
2672        }
2673
2674        return false;
2675    }
2676
2677    /**
2678     * Takes a setting as db value and attempts to cast it correctly
2679     *
2680     * @param mixed $var (DB value)
2681     *
2682     * @return mixed
2683     */
2684    private function unpack_setting( $var = false ) {
2685
2686        if ( $var !== false ) {
2687
2688            $value = $this->stripSlashes( $this->decodeIfJSON( $var ) );
2689
2690            // catch this oddly non-decoded case
2691            if ( $value == '[]' ) {
2692                return array();
2693            }
2694
2695            // if we've a string, check it isn't viably an int
2696            // .. if so, cast as int
2697            if ( is_string( $value ) && jpcrm_is_int( $value ) ) {
2698                $value = (int) $value;
2699            }
2700
2701            return $value;
2702        }
2703
2704        // fallback to returning the value passed
2705        return $var;
2706    }
2707
2708    // =========== / SETTINGS  =======================================================
2709    // ===============================================================================
2710
2711    // ===============================================================================
2712    // ===========   META ============================================================
2713
2714    /**
2715     * Wrapper, use $this->meta($objtype,$objid,$key) for easy retrieval of singular
2716     * Simplifies $this->getMeta
2717     *
2718     * @param int objtype
2719     * @param int objid
2720     * @param string key
2721     *
2722     * @return bool result
2723     */
2724    public function meta( $objtype = -1, $objid = -1, $key = '', $default = false ) {
2725
2726        if ( ! empty( $key ) ) {
2727
2728            return $this->getMeta(
2729                array(
2730
2731                    'objtype'     => $objtype,
2732                    'objid'       => $objid,
2733                    'key'         => $key,
2734                    'fullDetails' => false,
2735                    'default'     => $default,
2736
2737                )
2738            );
2739
2740        }
2741
2742        return $default;
2743    }
2744
2745    /**
2746     * returns full meta line +- details
2747     *
2748     * @param array $args   Associative array of arguments
2749     *                      key, fullDetails, default
2750     *
2751     * @return array result
2752     */
2753    public function getMeta( $args = array() ) {
2754
2755        #} =========== LOAD ARGS ==============
2756        $defaultArgs = array(
2757
2758            'objid'            => -1, // Object ID
2759            'objtype'          => -1, // Object Type
2760            'key'              => false, // key *Required
2761
2762            'default'          => false,
2763
2764            'fullDetails'      => false, // set this to 1 and get ID|key|val, rather than just the val
2765
2766            // permissions
2767            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
2768            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
2769            // meta don't need owners yet :)
2770
2771            'onlyID'           => false, // returns scalar ID of line
2772            'return_all_lines' => false, // if just specifying a key and setting this to true, will return all meta lines with key
2773
2774        );
2775        foreach ( $defaultArgs as $argK => $argV ) {
2776            $$argK = $argV;
2777            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2778                if ( is_array( $args[ $argK ] ) ) {
2779                    $newData = $$argK;
2780                    if ( ! is_array( $newData ) ) {
2781                        $newData = array();
2782                    } foreach ( $args[ $argK ] as $subK => $subV ) {
2783                        $newData[ $subK ] = $subV;
2784                    }$$argK = $newData;
2785                } else {
2786                    $$argK = $args[ $argK ]; }
2787            }
2788        }
2789        #} =========== / LOAD ARGS =============
2790
2791        #} =========== CHECK FIELDS =============
2792
2793        // check obtype is legit
2794        if ( $objtype !== -1 && $this->objTypeKey( $objtype ) === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2795            return false;
2796        }
2797
2798            // obj id
2799            $objid = (int) $objid;
2800
2801            // for now, meta hard ignores owners
2802            $ignoreowner = true;
2803
2804        #} =========== / CHECK FIELDS =============
2805
2806        #} Check key
2807        if ( ! empty( $key ) ) {
2808
2809            global $ZBSCRM_t, $wpdb;
2810            $wheres          = array( 'direct' => array() );
2811            $whereStr        = '';
2812            $additionalWhere = '';
2813            $params          = array();
2814            $res             = array();
2815
2816            #} Build query
2817            $query = 'SELECT * FROM ' . $ZBSCRM_t['meta'];
2818
2819            #} ============= WHERE ================
2820
2821                // Add ID
2822            if ( $objid > 0 ) {
2823                $wheres['zbsm_objid'] = array( 'zbsm_objid', '=', '%d', $objid );
2824            }
2825                // Add OBJTYPE
2826            if ( $objtype > 0 ) {
2827                $wheres['zbsm_objtype'] = array( 'zbsm_objtype', '=', '%d', $objtype );
2828            }
2829                // Add KEY
2830                $wheres['zbsm_key'] = array( 'zbsm_key', '=', '%s', $key );
2831
2832            #} ============ / WHERE ==============
2833
2834            #} Build out any WHERE clauses
2835            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
2836            $whereStr  = $wheresArr['where'];
2837            $params    = $params + $wheresArr['params'];
2838            #} / Build WHERE
2839
2840            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
2841            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
2842            $ownQ   = $this->ownershipSQL( $ignoreowner );
2843            if ( ! empty( $ownQ ) ) {
2844                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
2845            }
2846            #} / Ownership
2847
2848            #} Append to sql (this also automatically deals with sortby and paging)
2849            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' );
2850
2851            if ( ! $return_all_lines ) {
2852                $query .= $this->buildPaging( 0, 1 );
2853            }
2854
2855            try {
2856
2857                #} Prep & run query
2858                $queryObj = $this->prepare( $query, $params );
2859
2860                if ( ! $return_all_lines ) {
2861
2862                    // singular
2863                    $result = $wpdb->get_row( $queryObj, OBJECT );
2864
2865                } else {
2866
2867                    // multi-line
2868                    $result = $wpdb->get_results( $queryObj, OBJECT );
2869
2870                }
2871            } catch ( Exception $e ) {
2872
2873                #} General SQL Err
2874                $this->catchSQLError( $e );
2875
2876            }
2877
2878            // results
2879            if ( isset( $result ) ) {
2880
2881                #} Has results, tidy + return
2882                if ( ! $return_all_lines ) {
2883
2884                    #} Only ID? return it directly
2885                    if ( $onlyID === true ) {
2886                        return $result->ID;
2887                    }
2888
2889                    #} full line or scalar setting val
2890                    if ( $fullDetails ) {
2891                        return $this->tidy_meta( $result );
2892                    } else {
2893                        return $this->tidy_metaSingular( $result );
2894                    }
2895                } else {
2896
2897                    // multi-line
2898                    $results_array = array();
2899                    foreach ( $result as $index => $meta_line ) {
2900
2901                        $results_array[] = $this->tidy_meta( $meta_line );
2902
2903                    }
2904
2905                    return $results_array;
2906                }
2907            }
2908        } // / if ID
2909
2910        return $default;
2911    }
2912
2913    /**
2914     * returns FIRST ID which has matching meta keval pair
2915     *
2916     * @param array $args   Associative array of arguments
2917     *                      key, fullDetails, default
2918     *
2919     * @return array result
2920     */
2921    public function getIDWithMeta( $args = array() ) {
2922
2923        #} =========== LOAD ARGS ==============
2924        $defaultArgs = array(
2925
2926            'objtype' => -1, // REQ
2927            'key'     => false, // REQ
2928            'val'     => false, // REQ
2929
2930            // permissions
2931            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
2932            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
2933            // meta don't need owners yet :)
2934
2935        );
2936        foreach ( $defaultArgs as $argK => $argV ) {
2937            $$argK = $argV;
2938            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2939                if ( is_array( $args[ $argK ] ) ) {
2940                    $newData = $$argK;
2941                    if ( ! is_array( $newData ) ) {
2942                        $newData = array();
2943                    } foreach ( $args[ $argK ] as $subK => $subV ) {
2944                        $newData[ $subK ] = $subV;
2945                    }$$argK = $newData;
2946                } else {
2947                    $$argK = $args[ $argK ]; }
2948            }
2949        }
2950        #} =========== / LOAD ARGS =============
2951
2952        #} =========== CHECK FIELDS =============
2953
2954        // check obtype is completed + legit
2955        if ( empty( $objtype ) ) {
2956            return false;
2957        }
2958        if ( $this->objTypeKey( $objtype ) === -1 ) {
2959            return false;
2960        }
2961
2962            // meta key
2963        if ( empty( $key ) || empty( $val ) || $key == false || $val == false ) {
2964            return false;
2965        }
2966
2967            // for now, meta hard ignores owners
2968            $ignoreowner = true;
2969
2970        #} =========== / CHECK FIELDS =============
2971
2972            global $ZBSCRM_t, $wpdb;
2973            $wheres      = array( 'direct' => array() );
2974        $whereStr        = '';
2975        $additionalWhere = '';
2976        $params          = array();
2977        $res             = array();
2978
2979            #} Build query
2980            $query = 'SELECT zbsm_objid FROM ' . $ZBSCRM_t['meta'];
2981
2982            #} ============= WHERE ================
2983
2984                #} Add OBJTYPE
2985                $wheres['zbsm_objtype'] = array( 'zbsm_objtype', '=', '%d', $objtype );
2986                #} Add KEY
2987                $wheres['zbsm_key'] = array( 'zbsm_key', '=', '%s', $key );
2988                #} Add VAL
2989                $wheres['zbsm_val'] = array( 'zbsm_val', '=', '%s', $val );
2990
2991            #} ============ / WHERE ==============
2992
2993            #} Build out any WHERE clauses
2994            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
2995            $whereStr  = $wheresArr['where'];
2996        $params        = $params + $wheresArr['params'];
2997            #} / Build WHERE
2998
2999            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
3000            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
3001            $ownQ   = $this->ownershipSQL( $ignoreowner );
3002        if ( ! empty( $ownQ ) ) {
3003            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
3004        }
3005            #} / Ownership
3006
3007            #} Append to sql (this also automatically deals with sortby and paging)
3008            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
3009
3010        try {
3011
3012            #} Prep & run query
3013            $queryObj = $this->prepare( $query, $params );
3014            $v        = (int) $wpdb->get_var( $queryObj );
3015            if ( $v > -1 ) {
3016                return $v;
3017            }
3018        } catch ( Exception $e ) {
3019
3020            #} General SQL Err
3021            $this->catchSQLError( $e );
3022
3023        }
3024
3025        return -1;
3026    }
3027
3028    /**
3029     * Wrapper, use $this->updateMeta($objtype,$objid,$key,$val) for easy update of setting
3030     * Uses $this->addUpdateMeta
3031     * ... USE sub-layer rather than this direct, gives a degree of abstraction
3032     *
3033     * @param string key
3034     * @param string value
3035     *
3036     * @return bool result
3037     */
3038    public function updateMeta( $objtype = -1, $objid = -1, $key = '', $val = '' ) {
3039
3040        if ( ! empty( $key ) ) { // && !empty($val)
3041
3042            return $this->addUpdateMeta(
3043                array(
3044
3045                    'data' => array(
3046
3047                        'objid'   => $objid,
3048                        'objtype' => $objtype,
3049                        'key'     => $key,
3050                        'val'     => $val,
3051                    ),
3052
3053                )
3054            );
3055
3056        }
3057
3058        return false;
3059    }
3060
3061    /**
3062     * adds or updates a setting object
3063     * ... for a quicker wrapper, use $this->updateMeta($key,$val)
3064     *
3065     * @param array $args Associative array of arguments
3066     *              id (not req.), owner (not req.) data -> key/val
3067     *
3068     * @return int line ID
3069     */
3070    public function addUpdateMeta( $args = array() ) {
3071
3072        global $ZBSCRM_t, $wpdb;
3073
3074        #} ============ LOAD ARGS =============
3075        $defaultArgs = array(
3076
3077            'id'   => -1,
3078            // owner HARD disabled for this for now - not req. for each meta
3079            // 'owner'           => -1,
3080
3081            // fields (directly)
3082            'data' => array(
3083
3084                'objid'   => -1,
3085                'objtype' => -1,
3086                'key'     => '',
3087                'val'     => '',
3088
3089            ),
3090
3091        );
3092        foreach ( $defaultArgs as $argK => $argV ) {
3093            $$argK = $argV;
3094            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3095                if ( is_array( $args[ $argK ] ) ) {
3096                    $newData = $$argK;
3097                    if ( ! is_array( $newData ) ) {
3098                        $newData = array();
3099                    } foreach ( $args[ $argK ] as $subK => $subV ) {
3100                        $newData[ $subK ] = $subV;
3101                    }$$argK = $newData;
3102                } else {
3103                    $$argK = $args[ $argK ]; }
3104            }
3105        }
3106        #} =========== / LOAD ARGS ============
3107
3108        #} ========== CHECK FIELDS ============
3109
3110            $id = (int) $id;
3111
3112            // if owner = -1, add current
3113            // if (!isset($owner) || $owner === -1) $owner = zeroBSCRM_user();
3114            // owner HARD disabled for this for now - not req. for each meta
3115            $owner = -1;
3116
3117        // check key present + legit
3118        if ( empty( $data['key'] ) ) {
3119            return false;
3120        }
3121
3122            // check obtype is completed + legit
3123        if ( ! isset( $data['objtype'] ) || empty( $data['objtype'] ) ) {
3124            return false;
3125        }
3126        if ( $this->objTypeKey( $data['objtype'] ) === -1 ) {
3127            return false;
3128        }
3129
3130            // obj id
3131            $objid = (int) $data['objid'];
3132        if ( empty( $objid ) || $objid < 1 ) {
3133            return false;
3134        }
3135
3136            // meta ID finder - if obj key provided, check meta not already present (if so overwrite)
3137            // keeps unique...
3138        if ( ( empty( $id ) || $id <= 0 )
3139                &&
3140                ( isset( $data['key'] ) && ! empty( $data['key'] ) )
3141                // no need to check obj id + type here, as will return false above if not legit :)
3142                ) {
3143
3144            // check existence + return ID
3145            $potentialID = (int) $this->getMeta(
3146                array(
3147                    'objid'   => $objid,
3148                    'objtype' => $data['objtype'],
3149                    'key'     => $data['key'],
3150                    'onlyID'  => true,
3151                )
3152            );
3153
3154            // override empty ID
3155            if ( ! empty( $potentialID ) && $potentialID > 0 ) {
3156                $id = $potentialID;
3157            }
3158        }
3159
3160        #} ========= / CHECK FIELDS ===========
3161
3162        #} Var up any val (json_encode)
3163        if ( in_array( gettype( $data['val'] ), array( 'object', 'array' ) ) ) {
3164
3165            $data['val'] = json_encode( $data['val'] );
3166
3167        }
3168
3169        if ( isset( $id ) && ! empty( $id ) && $id > 0 ) {
3170
3171                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
3172
3173                #} Attempt update
3174            if ( $wpdb->update(
3175                $ZBSCRM_t['meta'],
3176                array(
3177
3178                    // ownership
3179                    // no need to update these (as of yet) - can't move teams etc.
3180                    // 'zbs_site' => zeroBSCRM_installSite(),
3181                    // 'zbs_team' => zeroBSCRM_installTeam(),
3182                    'zbs_owner'        => $owner,
3183
3184                    // fields
3185                    'zbsm_objtype'     => $data['objtype'],
3186                    'zbsm_objid'       => $objid,
3187                    'zbsm_key'         => $data['key'],
3188                    'zbsm_val'         => $data['val'],
3189                    'zbsm_lastupdated' => time(),
3190                ),
3191                array( // where
3192                    'ID' => $id,
3193                ),
3194                array( // field data types
3195                    '%d',
3196                    '%d',
3197                    '%d',
3198                    '%s',
3199                    '%s',
3200                    '%d',
3201                ),
3202                array( // where data types
3203                    '%d',
3204                )
3205            ) !== false ) {
3206
3207                        // Successfully updated - Return id
3208                        return $id;
3209
3210            } else {
3211
3212                // FAILED update
3213                return false;
3214
3215            }
3216        } else {
3217
3218            #} No ID - must be an INSERT
3219            if ( $wpdb->insert(
3220                $ZBSCRM_t['meta'],
3221                array(
3222
3223                    // ownership
3224                    'zbs_site'         => zeroBSCRM_site(),
3225                    'zbs_team'         => zeroBSCRM_team(),
3226                    'zbs_owner'        => $owner,
3227
3228                    // fields
3229                    'zbsm_objtype'     => $data['objtype'],
3230                    'zbsm_objid'       => $objid,
3231                    'zbsm_key'         => $data['key'],
3232                    'zbsm_val'         => $data['val'],
3233                    'zbsm_created'     => time(),
3234                    'zbsm_lastupdated' => time(),
3235                ),
3236                array( // field data types
3237                    '%d',  // site
3238                    '%d',  // team
3239                    '%d',  // owner
3240
3241                    '%d',
3242                    '%d',
3243                    '%s',
3244                    '%s',
3245                    '%d',
3246                    '%d',
3247                )
3248            ) > 0 ) {
3249
3250                    #} Successfully inserted, lets return new ID
3251                    $newID = $wpdb->insert_id;
3252                    return $newID;
3253
3254            } else {
3255
3256                #} Failed to Insert
3257                return false;
3258
3259            }
3260        }
3261
3262        return false;
3263    }
3264
3265    /**
3266     * deletes a meta object based on objid + key
3267     *
3268     * @param array $args Associative array of arguments
3269     *              id
3270     *
3271     * @return int success;
3272     */
3273    public function deleteMeta( $args = array() ) {
3274
3275        global $ZBSCRM_t, $wpdb;
3276
3277        #} ============ LOAD ARGS =============
3278        $defaultArgs = array(
3279
3280            'objtype' => -1,
3281            'objid'   => -1,
3282            'key'     => '',
3283
3284        );
3285        foreach ( $defaultArgs as $argK => $argV ) {
3286            $$argK = $argV;
3287            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3288                if ( is_array( $args[ $argK ] ) ) {
3289                    $newData = $$argK;
3290                    if ( ! is_array( $newData ) ) {
3291                        $newData = array();
3292                    } foreach ( $args[ $argK ] as $subK => $subV ) {
3293                        $newData[ $subK ] = $subV;
3294                    }$$argK = $newData;
3295                } else {
3296                    $$argK = $args[ $argK ]; }
3297            }
3298        }
3299        #} =========== / LOAD ARGS ============
3300
3301        #} Check ID, find, & Delete :)
3302        $objtype = (int) $objtype;
3303        if ( isset( $objtype ) && $objtype !== -1 && $this->objTypeKey( $objtype ) === -1 ) {
3304            return false;
3305        }
3306        $objid = (int) $objid;
3307        if ( empty( $objid ) || $objid < 1 ) {
3308            return false;
3309        }
3310        if ( empty( $key ) ) {
3311            return false;
3312        }
3313
3314        #} FIND?
3315        $potentialID = (int) $this->getMeta(
3316            array(
3317                'objid'   => $objid,
3318                'objtype' => $objtype,
3319                'key'     => $key,
3320                'onlyID'  => true,
3321            )
3322        );
3323
3324        // override empty ID
3325        if ( ! empty( $potentialID ) && $potentialID > 0 ) {
3326
3327            return $this->deleteMetaByMetaID( array( 'id' => $potentialID ) );
3328
3329        }
3330
3331        return false;
3332    }
3333
3334    /**
3335     * deletes a meta object from a meta id
3336     *
3337     * @param array $args Associative array of arguments
3338     *              id
3339     *
3340     * @return int success;
3341     */
3342    public function deleteMetaByMetaID( $args = array() ) {
3343
3344        global $ZBSCRM_t, $wpdb;
3345
3346        #} ============ LOAD ARGS =============
3347        $defaultArgs = array(
3348
3349            'id' => -1,
3350
3351        );
3352        foreach ( $defaultArgs as $argK => $argV ) {
3353            $$argK = $argV;
3354            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3355                if ( is_array( $args[ $argK ] ) ) {
3356                    $newData = $$argK;
3357                    if ( ! is_array( $newData ) ) {
3358                        $newData = array();
3359                    } foreach ( $args[ $argK ] as $subK => $subV ) {
3360                        $newData[ $subK ] = $subV;
3361                    }$$argK = $newData;
3362                } else {
3363                    $$argK = $args[ $argK ]; }
3364            }
3365        }
3366        #} =========== / LOAD ARGS ============
3367
3368        #} Check ID & Delete :)
3369        $id = (int) $id;
3370        return zeroBSCRM_db2_deleteGeneric( $id, 'meta' );
3371    }
3372
3373    /**
3374     * tidy's the object from wp db into clean array
3375     *
3376     * @param array $obj (DB obj)
3377     *
3378     * @return array (clean obj)
3379     */
3380    private function tidy_meta( $obj = false ) {
3381
3382            $res = false;
3383
3384        if ( isset( $obj->ID ) ) {
3385            $res            = array();
3386            $res['id']      = $obj->ID;
3387            $res['objtype'] = $obj->zbsm_objtype;
3388            $res['objid']   = $obj->zbsm_objid;
3389            $res['key']     = $obj->zbsm_key;
3390            $res['val']     = $this->stripSlashes( $obj->zbsm_val );
3391            $res['created'] = $obj->zbsm_created;
3392            $res['updated'] = $obj->zbsm_lastupdated;
3393
3394        }
3395
3396        return $res;
3397    }
3398
3399    /**
3400     * tidy's the object from wp db into clean array
3401     *
3402     * @param array $obj (DB obj)
3403     *
3404     * @return string
3405     */
3406    private function tidy_metaSingular( $obj = false ) {
3407
3408        $res = false;
3409
3410        if ( isset( $obj->ID ) ) {
3411            return $this->stripSlashes( $this->decodeIfJSON( $obj->zbsm_val ) );
3412        }
3413
3414        return $res;
3415    }
3416
3417    // =========== / META  ===========================================================
3418    // ===============================================================================
3419
3420    // ===============================================================================
3421    // ===========   TAGS  ===========================================================
3422    /**
3423     * returns full tag line +- details
3424     *
3425     * @param int id        tag id
3426     * @param array                $args   Associative array of arguments
3427     *                                     withStats
3428     *
3429     * @return array result
3430     */
3431    public function getTag( $id = -1, $args = array() ) {
3432
3433        #} =========== LOAD ARGS ==============
3434        $defaultArgs = array(
3435
3436            // Alternative search criteria to ID :)
3437            // .. LEAVE blank if using ID
3438            // objtype + name or slug
3439            'objtype'   => -1,
3440            'name'      => '',
3441            'slug'      => '',
3442
3443            'withStats' => false,
3444
3445            // permissions
3446            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
3447            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
3448            // Tags don't need owners yet :)
3449
3450            // returns scalar ID of line
3451            'onlyID'    => false,
3452            'onlySlug'  => false,
3453
3454        );
3455        foreach ( $defaultArgs as $argK => $argV ) {
3456            $$argK = $argV;
3457            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3458                if ( is_array( $args[ $argK ] ) ) {
3459                    $newData = $$argK;
3460                    if ( ! is_array( $newData ) ) {
3461                        $newData = array();
3462                    } foreach ( $args[ $argK ] as $subK => $subV ) {
3463                        $newData[ $subK ] = $subV;
3464                    }$$argK = $newData;
3465                } else {
3466                    $$argK = $args[ $argK ]; }
3467            }
3468        }
3469        #} =========== / LOAD ARGS =============
3470
3471        #} ========== CHECK FIELDS ============
3472
3473            $id = (int) $id;
3474
3475            // got objtype / name/slug?
3476
3477        // check obtype is legit (if completed)
3478        if ( $objtype !== -1 && $this->objTypeKey( $objtype ) === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
3479
3480                // if using obj type - check name/slug
3481            if ( empty( $name ) && empty( $slug ) ) {
3482                return false;
3483            }
3484
3485                // ... else should be good to search
3486
3487        }
3488
3489            // Tags don't need owners yet :)
3490            $ignoreowner = true;
3491
3492        #} ========= / CHECK FIELDS ===========
3493
3494        #} Check ID or name/type
3495        if ( $id > 0 || $objtype > 0 ) {
3496
3497            global $ZBSCRM_t, $wpdb;
3498            $wheres          = array( 'direct' => array() );
3499            $whereStr        = '';
3500            $additionalWhere = '';
3501            $params          = array();
3502            $res             = array();
3503
3504            #} Build query
3505            $query = 'SELECT * FROM ' . $ZBSCRM_t['tags'];
3506
3507            #} ============= WHERE ================
3508
3509            // ID
3510            if ( $id > 0 ) {
3511                $wheres['ID'] = array( 'ID', '=', '%d', $id );
3512            }
3513
3514            // Object Type
3515            if ( $objtype > 0 ) {
3516                $wheres['zbstag_objtype'] = array( 'zbstag_objtype', '=', '%d', $objtype );
3517            }
3518
3519            // Name
3520            if ( ! empty( $name ) ) {
3521                $wheres['zbstag_name'] = array( 'zbstag_name', '=', '%s', $name );
3522            }
3523
3524            // Slug
3525            if ( ! empty( $slug ) ) {
3526                $wheres['zbstag_slug'] = array( 'zbstag_slug', '=', '%s', $slug );
3527            }
3528
3529            #} ============ / WHERE ==============
3530
3531            #} Build out any WHERE clauses
3532            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
3533            $whereStr  = $wheresArr['where'];
3534            $params    = $params + $wheresArr['params'];
3535            #} / Build WHERE
3536
3537            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
3538            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
3539            $ownQ   = $this->ownershipSQL( $ignoreowner );
3540            if ( ! empty( $ownQ ) ) {
3541                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
3542            }
3543            #} / Ownership
3544
3545            #} Append to sql (this also automatically deals with sortby and paging)
3546            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
3547
3548            try {
3549
3550                #} Prep & run query
3551                $queryObj     = $this->prepare( $query, $params );
3552                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
3553
3554            } catch ( Exception $e ) {
3555
3556                #} General SQL Err
3557                $this->catchSQLError( $e );
3558
3559            }
3560
3561            #} Interpret Results (ROW)
3562            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
3563
3564                #} Has results, tidy + return
3565
3566                    #} Only ID? return it directly
3567                if ( $onlyID === true ) {
3568                    return $potentialRes->ID;
3569                }
3570
3571                    #} Only slug? return it directly
3572                if ( $onlySlug === true ) {
3573                    return $potentialRes->zbstag_slug;
3574                }
3575
3576                    // tidy
3577                    $res = $this->tidy_tag( $potentialRes );
3578
3579                // with stats?
3580                if ( $withStats ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable,WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
3581
3582                        // add all stats lines
3583                        $res['stats'] = $this->getTagStats( array( 'tagid' => $potentialRes->ID ) );
3584
3585                }
3586
3587                    return $res;
3588
3589            }
3590        } // / if ID
3591
3592        return false;
3593    }
3594
3595    /**
3596     * returns tag detail lines
3597     *
3598     * @param array $args Associative array of arguments
3599     *              withStats, searchPhrase, sortByField, sortOrder, page, perPage
3600     *
3601     * @return array of tag lines
3602     */
3603    public function getAllTags( $args = array() ) {
3604
3605        #} ============ LOAD ARGS =============
3606        $defaultArgs = array(
3607
3608            'searchPhrase' => '',
3609            'withStats'    => false,
3610
3611            'sortByField'  => 'ID',
3612            'sortOrder'    => 'ASC',
3613            'page'         => 0,
3614            'perPage'      => 100,
3615
3616            // permissions
3617            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
3618            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
3619            // Tags don't need owners yet :)
3620
3621        );
3622        foreach ( $defaultArgs as $argK => $argV ) {
3623            $$argK = $argV;
3624            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3625                if ( is_array( $args[ $argK ] ) ) {
3626                    $newData = $$argK;
3627                    if ( ! is_array( $newData ) ) {
3628                        $newData = array();
3629                    } foreach ( $args[ $argK ] as $subK => $subV ) {
3630                        $newData[ $subK ] = $subV;
3631                    }$$argK = $newData;
3632                } else {
3633                    $$argK = $args[ $argK ]; }
3634            }
3635        }
3636        #} =========== / LOAD ARGS =============
3637
3638        // Tags don't need owners yet :)
3639        $ignoreowner = true;
3640
3641        global $ZBSCRM_t, $wpdb;
3642        $wheres          = array( 'direct' => array() );
3643        $whereStr        = '';
3644        $additionalWhere = '';
3645        $params          = array();
3646        $res             = array();
3647
3648        #} Build query
3649        $query = 'SELECT * FROM ' . $ZBSCRM_t['tags'];
3650
3651        #} ============= WHERE ================
3652
3653            #} Add Search phrase
3654        if ( ! empty( $searchPhrase ) ) {
3655
3656            $wheres['zbstag_name'] = array( 'zbstag_name', 'LIKE', '%s', '%' . $searchPhrase . '%' );
3657
3658        }
3659
3660        #} ============ / WHERE ===============
3661
3662        #} Build out any WHERE clauses
3663        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
3664        $whereStr  = $wheresArr['where'];
3665        $params    = $params + $wheresArr['params'];
3666        #} / Build WHERE
3667
3668        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
3669        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
3670        $ownQ   = $this->ownershipSQL( $ignoreowner );
3671        if ( ! empty( $ownQ ) ) {
3672            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
3673        }
3674        #} / Ownership
3675
3676        #} Append to sql (this also automatically deals with sortby and paging)
3677        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
3678
3679        try {
3680
3681            #} Prep & run query
3682            $queryObj     = $this->prepare( $query, $params );
3683            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
3684
3685        } catch ( Exception $e ) {
3686
3687            #} General SQL Err
3688            $this->catchSQLError( $e );
3689
3690        }
3691
3692        #} Interpret results (Result Set - multi-row)
3693        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
3694
3695            #} Has results, tidy + return
3696            foreach ( $potentialRes as $resDataLine ) {
3697
3698                    // tidy
3699                    $resArr = $this->tidy_tag( $resDataLine );
3700
3701                    // with stats?
3702                if ( isset( $withStats ) && $withStats ) {
3703
3704                    // add all stats lines
3705                    $res['stats'] = $this->getTagStats( array( 'tagid' => $resDataLine->ID ) );
3706
3707                }
3708
3709                    $res[] = $resArr;
3710
3711            }
3712        }
3713
3714        return $res;
3715    }
3716
3717    /**
3718     * adds or updates a tag object
3719     *
3720     * @param array $args Associative array of arguments
3721     *              id (if update), ???
3722     *
3723     * @return int line ID
3724     */
3725    public function addUpdateTag( $args = array() ) {
3726
3727        global $ZBSCRM_t, $wpdb;
3728
3729        #} ============ LOAD ARGS =============
3730        $defaultArgs = array(
3731
3732            'id'   => -1,
3733
3734            // fields (directly)
3735            'data' => array(
3736
3737                'objtype' => -1,
3738                'name'    => '',
3739                'slug'    => '',
3740                // OWNERS will all be set to -1 for tags for now :)
3741                // 'owner'           => -1
3742
3743            ),
3744
3745        );
3746        foreach ( $defaultArgs as $argK => $argV ) {
3747            $$argK = $argV;
3748            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3749                if ( is_array( $args[ $argK ] ) ) {
3750                    $newData = $$argK;
3751                    if ( ! is_array( $newData ) ) {
3752                        $newData = array();
3753                    } foreach ( $args[ $argK ] as $subK => $subV ) {
3754                        $newData[ $subK ] = $subV;
3755                    }$$argK = $newData;
3756                } else {
3757                    $$argK = $args[ $argK ]; }
3758            }
3759        }
3760        #} =========== / LOAD ARGS ============
3761
3762        #} ========== CHECK FIELDS ============
3763
3764            $id = (int) $id;
3765
3766        // check obtype is completed + legit
3767        if ( empty( $data['objtype'] ) ) {
3768            return false;
3769        }
3770        if ( $this->objTypeKey( $data['objtype'] ) === -1 ) {
3771            return false;
3772        }
3773
3774            // if owner = -1, add current
3775            // tags don't really need this level of ownership
3776            // so leaving as -1 for now :)
3777            // if (!isset($data['owner']) || $data['owner'] === -1) $data['owner'] = zeroBSCRM_user();
3778            $data['owner'] = -1;
3779
3780            // check name present + legit
3781        if ( ! isset( $data['name'] ) || empty( $data['name'] ) ) {
3782            return false;
3783        }
3784        if ( empty( $data['slug'] ) ) {
3785
3786            $potential_slug = sanitize_key( $data['name'] ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
3787
3788            // catch empty slugs as per gh-462, chinese characters, for example
3789            if ( empty( $potential_slug ) ) {
3790                $this->get_new_tag_slug( $data['objtype'], 'tag', true ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
3791            } else {
3792                $data['slug'] = $this->get_new_tag_slug( $data['objtype'], $potential_slug ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
3793            }
3794
3795            // if slug STILL empty (e.g. database error?), return false for now...
3796            if ( empty( $data['slug'] ) ) {
3797                return false;
3798            }
3799        }
3800
3801            // tag ID finder - if obj name provided, check tag not already present (if so overwrite)
3802            // keeps unique...
3803        if ( ( empty( $id ) || $id <= 0 )
3804                &&
3805                (
3806                    ( isset( $data['name'] ) && ! empty( $data['name'] ) ) ||
3807                    ( isset( $data['slug'] ) && ! empty( $data['slug'] ) )
3808                ) ) {
3809
3810            // check by slug
3811            // check existence + return ID
3812            $potentialID = (int) $this->getTag(
3813                -1,
3814                array(
3815                    'objtype' => $data['objtype'],
3816                    'slug'    => $data['slug'],
3817                    'onlyID'  => true,
3818                )
3819            );
3820
3821            // override empty ID
3822            if ( ! empty( $potentialID ) && $potentialID > 0 ) {
3823                $id = $potentialID;
3824            }
3825        }
3826
3827        #} ========= / CHECK FIELDS ===========
3828
3829        #} Check if ID present
3830        $id = (int) $id;
3831        if ( ! empty( $id ) && $id > 0 ) {
3832
3833                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
3834
3835                #} Attempt update
3836            if ( $wpdb->update(
3837                $ZBSCRM_t['tags'],
3838                array(
3839
3840                    // ownership
3841                    // no need to update these (as of yet) - can't move teams etc.
3842                    // 'zbs_site' => zeroBSCRM_installSite(),
3843                    // 'zbs_team' => zeroBSCRM_installTeam(),
3844                    'zbs_owner'          => $data['owner'],
3845
3846                    // fields
3847                    'zbstag_objtype'     => $data['objtype'],
3848                    'zbstag_name'        => $data['name'],
3849                    'zbstag_slug'        => $data['slug'],
3850                    'zbstag_lastupdated' => time(),
3851                ),
3852                array( // where
3853                    'ID' => $id,
3854                ),
3855                array( // field data types
3856                    '%d',
3857                    '%d',
3858                    '%s',
3859                    '%s',
3860                    '%d',
3861                ),
3862                array( // where data types
3863                    '%d',
3864                )
3865            ) !== false ) {
3866
3867                        // Successfully updated - Return id
3868                        return $id;
3869
3870            } else {
3871
3872                // FAILED update
3873                return false;
3874
3875            }
3876        } else {
3877
3878            #} No ID - must be an INSERT
3879            if ( $wpdb->insert(
3880                $ZBSCRM_t['tags'],
3881                array(
3882
3883                    // ownership
3884                    'zbs_site'           => zeroBSCRM_site(),
3885                    'zbs_team'           => zeroBSCRM_team(),
3886                    'zbs_owner'          => $data['owner'],
3887
3888                    // fields
3889                    'zbstag_objtype'     => $data['objtype'],
3890                    'zbstag_name'        => $data['name'],
3891                    'zbstag_slug'        => $data['slug'],
3892                    'zbstag_created'     => time(),
3893                    'zbstag_lastupdated' => time(),
3894                ),
3895                array( // field data types
3896                    '%d',  // site
3897                    '%d',  // team
3898                    '%d',  // owner
3899
3900                    '%d',
3901                    '%s',
3902                    '%s',
3903                    '%d',
3904                    '%d',
3905                )
3906            ) > 0 ) {
3907
3908                    #} Successfully inserted, lets return new ID
3909                    $newID = $wpdb->insert_id;
3910                    return $newID;
3911
3912            } else {
3913
3914                #} Failed to Insert
3915                return false;
3916
3917            }
3918        }
3919
3920        return false;
3921    }
3922
3923    /**
3924     * adds or updates any object's tags
3925     * ... this is really just a wrapper for addUpdateTagObjLinks
3926     *
3927     * @param array $args Associative array of arguments
3928     *              id (if update), owner, data (array of field data)
3929     *
3930     * @return int line ID
3931     */
3932    public function addUpdateObjectTags( $args = array() ) {
3933
3934        global $ZBSCRM_t, $wpdb;
3935
3936        #} ============ LOAD ARGS =============
3937        $defaultArgs = array(
3938
3939            'objid'     => -1, // REQ
3940            'objtype'   => -1, // REQ
3941
3942            // generic pass-through (array of tag strings or tag IDs):
3943            'tag_input' => -1,
3944
3945            // or either specific:
3946            'tagIDs'    => -1,
3947            'tags'      => -1,
3948
3949            'mode'      => 'replace',
3950
3951        );
3952        foreach ( $defaultArgs as $argK => $argV ) {
3953            $$argK = $argV;
3954            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3955                if ( is_array( $args[ $argK ] ) ) {
3956                    $newData = $$argK;
3957                    if ( ! is_array( $newData ) ) {
3958                        $newData = array();
3959                    } foreach ( $args[ $argK ] as $subK => $subV ) {
3960                        $newData[ $subK ] = $subV;
3961                    }$$argK = $newData;
3962                } else {
3963                    $$argK = $args[ $argK ]; }
3964            }
3965        }
3966        #} =========== / LOAD ARGS ============
3967
3968        #} ========== CHECK FIELDS ============
3969
3970            // check id
3971            $objid = (int) $objid;
3972        if ( empty( $objid ) || $objid <= 0 ) {
3973            return false;
3974        }
3975
3976        // check obtype is legit (if completed)
3977        if ( $objtype === -1 || $this->objTypeKey( $objtype ) === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
3978            return false;
3979        }
3980
3981        #} ========= / CHECK FIELDS ===========
3982
3983            // If passed tag_input, infer if using ID's or tags
3984        if ( is_array( $tag_input ) ) {
3985
3986            // assume ID's
3987            $tagIDs = $tag_input;
3988
3989            // got strings?
3990            foreach ( $tag_input as $tag ) {
3991
3992                // if it's not an int, we can assume it's a string
3993                if ( ! jpcrm_is_int( $tag ) ) {
3994
3995                    // process as strings (tags)
3996                    $tagIDs = -1;
3997                    $tags   = $tag_input;
3998                    break;
3999
4000                }
4001            }
4002        }
4003
4004        // If using tags, convert these to ids :)
4005        // @phan-suppress-next-line PhanImpossibleCondition -- Phan is confused; this var is initialized at the beginning of the function.
4006        if ( is_array( $tags ) ) {
4007
4008                // overwrite
4009                $tagIDs = array();
4010
4011                // cycle through + find
4012            foreach ( $tags as $tag ) {
4013
4014                $tagID = $this->getTag(
4015                    -1,
4016                    array(
4017                        'objtype' => $objtype,
4018                        'name'    => $tag,
4019                        'onlyID'  => true,
4020                    )
4021                );
4022
4023                // echo 'looking for tag "'.$tag.'" got id '.$tagID.'!<br >';
4024
4025                if ( ! empty( $tagID ) ) {
4026                    $tagIDs[] = $tagID;
4027                } else {
4028
4029                    // create
4030                    $tagID = $this->addUpdateTag(
4031                        array(
4032                            'data' => array(
4033                                'objtype' => $objtype,
4034                                'name'    => $tag,
4035                            ),
4036                        )
4037                    );
4038                    // add
4039                    if ( ! empty( $tagID ) ) {
4040                        $tagIDs[] = $tagID;
4041                    }
4042                }
4043            }
4044        }
4045
4046        return $this->addUpdateTagObjLinks(
4047            array(
4048                'objtype' => $objtype,
4049                'objid'   => $objid,
4050                'tagIDs'  => $tagIDs,
4051                'mode'    => $mode,
4052            )
4053        );
4054    }
4055
4056    /**
4057     * deletes a tag object
4058     *
4059     * @param array $args Associative array of arguments
4060     *              id
4061     *
4062     * @return int success;
4063     */
4064    public function deleteTag( $args = array() ) {
4065
4066        global $ZBSCRM_t, $wpdb;
4067
4068        #} ============ LOAD ARGS =============
4069        $defaultArgs = array(
4070
4071            'id'          => -1,
4072            'deleteLinks' => true,
4073
4074        );
4075        foreach ( $defaultArgs as $argK => $argV ) {
4076            $$argK = $argV;
4077            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4078                if ( is_array( $args[ $argK ] ) ) {
4079                    $newData = $$argK;
4080                    if ( ! is_array( $newData ) ) {
4081                        $newData = array();
4082                    } foreach ( $args[ $argK ] as $subK => $subV ) {
4083                        $newData[ $subK ] = $subV;
4084                    }$$argK = $newData;
4085                } else {
4086                    $$argK = $args[ $argK ]; }
4087            }
4088        }
4089        #} =========== / LOAD ARGS ============
4090
4091        #} Check ID & Delete :)
4092        $id = (int) $id;
4093        if ( ! empty( $id ) && $id > 0 ) {
4094
4095            $deleted = zeroBSCRM_db2_deleteGeneric( $id, 'tags' );
4096
4097            // if links, also delete them!
4098            if ( $deleteLinks ) {
4099
4100                $deletedLinks = $wpdb->delete(
4101                    $ZBSCRM_t['taglinks'],
4102                    array( // where
4103                        'zbstl_tagid' => $id,
4104                    ),
4105                    array(
4106                        '%d',
4107                    )
4108                );
4109            }
4110
4111            return $deleted;
4112
4113        }
4114
4115        return false;
4116    }
4117
4118    /**
4119     * retrieves stats for tag (how many contacts/obj's use this tag) (effectively counts tag links split per obj)
4120     *
4121     * @param array $args Associative array of arguments
4122     *              id
4123     *
4124     * @return array
4125     */
4126    public function getAllTagStats( $args = array() ) {
4127
4128        #} ============ LOAD ARGS =============
4129        $defaultArgs = array(
4130
4131            'id'    => -1,
4132            'owner' => -1,
4133
4134            // permissions
4135            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
4136            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
4137            // Tags don't need owners yet :)
4138
4139        );
4140        foreach ( $defaultArgs as $argK => $argV ) {
4141            $$argK = $argV;
4142            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4143                if ( is_array( $args[ $argK ] ) ) {
4144                    $newData = $$argK;
4145                    if ( ! is_array( $newData ) ) {
4146                        $newData = array();
4147                    } foreach ( $args[ $argK ] as $subK => $subV ) {
4148                        $newData[ $subK ] = $subV;
4149                    }$$argK = $newData;
4150                } else {
4151                    $$argK = $args[ $argK ]; }
4152            }
4153        }
4154        #} =========== / LOAD ARGS ============
4155
4156        $ignoreowner = true;
4157
4158        #} Check ID
4159        $id = (int) $id;
4160        if ( ! empty( $id ) && $id > 0 ) {
4161
4162            global $ZBSCRM_t, $wpdb;
4163            $wheres          = array( 'direct' => array() );
4164            $whereStr        = '';
4165            $additionalWhere = '';
4166            $params          = array();
4167            $res             = array();
4168
4169            #} Build query
4170            $query = 'SELECT COUNT(zbstl_objid) c, zbstl_objtype FROM ' . $ZBSCRM_t['taglinks'];
4171
4172            #} ============= WHERE ================
4173
4174                #} Add ID
4175                $wheres['zbstl_tagid'] = array( 'zbstl_tagid', '=', '%d', $id );
4176
4177            // If 'owner' is set then have to ignore owner, because can't do both
4178            if ( $owner > 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
4179
4180                    // stops ownership check
4181                    $ignoreowner = true;
4182
4183                    // adds owner to query
4184                    $wheres['zbs_owner'] = array( 'zbs_owner', '=', '%d', $owner );
4185
4186            }
4187
4188            #} ============ / WHERE ==============
4189
4190            #} Build out any WHERE clauses
4191            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
4192            $whereStr  = $wheresArr['where'];
4193            $params    = $params + $wheresArr['params'];
4194            #} / Build WHERE
4195
4196            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
4197            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
4198            $ownQ   = $this->ownershipSQL( $ignoreowner );
4199            if ( ! empty( $ownQ ) ) {
4200                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
4201            }
4202            #} / Ownership
4203
4204            #} ============ CUSTOM GROUP/ORDERBY ==============
4205
4206                // this allows grouping :)
4207                $orderByCustom = ' GROUP BY zbstl_objtype ORDER BY c ASC';
4208
4209            #} ============ / CUSTOM GROUP/ORDERBY ============
4210
4211            #} Append to sql (and use our custom order by etc.)
4212            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $orderByCustom;
4213
4214            try {
4215
4216                #} Prep & run query
4217                $queryObj     = $this->prepare( $query, $params );
4218                $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
4219
4220            } catch ( Exception $e ) {
4221
4222                #} General SQL Err
4223                $this->catchSQLError( $e );
4224
4225            }
4226
4227            #} Interpret results (Result Set - multi-row)
4228            if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
4229
4230                #} Has results, tidy + return
4231                foreach ( $potentialRes as $resDataLine ) {
4232
4233                        // tidy
4234                        $res[] = $this->tidy_tagstat( $resDataLine );
4235
4236                }
4237            }
4238
4239            return $res;
4240
4241        } // / if ID
4242
4243        return false;
4244    }
4245
4246    /**
4247     * retrieves stats for tag (how many contacts/obj's use this tag)
4248     * this version returns specific count of uses for an objtypeid
4249     *
4250     * @param array $args Associative array of arguments
4251     *              id
4252     *
4253     * @return array
4254     */
4255    public function getTagObjStats( $args = array() ) {
4256
4257        #} ============ LOAD ARGS =============
4258        $defaultArgs = array(
4259
4260            'id'        => -1,
4261            'objtypeid' => -1,
4262            'owner'     => -1,
4263
4264            // permissions
4265            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
4266            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
4267            // Tags don't need owners yet :)
4268
4269            // returns scalar ID of line
4270            'onlyID'    => false,
4271
4272        );
4273        foreach ( $defaultArgs as $argK => $argV ) {
4274            $$argK = $argV;
4275            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4276                if ( is_array( $args[ $argK ] ) ) {
4277                    $newData = $$argK;
4278                    if ( ! is_array( $newData ) ) {
4279                        $newData = array();
4280                    } foreach ( $args[ $argK ] as $subK => $subV ) {
4281                        $newData[ $subK ] = $subV;
4282                    }$$argK = $newData;
4283                } else {
4284                    $$argK = $args[ $argK ]; }
4285            }
4286        }
4287        #} =========== / LOAD ARGS ============
4288
4289        $ignoreowner = true;
4290
4291        #} Check ID
4292        $id = (int) $id;
4293        if ( ! empty( $id ) && $id > 0 ) {
4294
4295            global $ZBSCRM_t, $wpdb;
4296            $wheres          = array( 'direct' => array() );
4297            $whereStr        = '';
4298            $additionalWhere = '';
4299            $params          = array();
4300            $res             = array();
4301
4302            #} Build query
4303            $query = 'SELECT COUNT(zbstl_objid) c, zbstl_objtype FROM ' . $ZBSCRM_t['taglinks'];
4304
4305            #} ============= WHERE ================
4306
4307                #} Add ID
4308                $wheres['zbstl_tagid'] = array( 'zbstl_tagid', '=', '%d', $id );
4309
4310                #} Adds a specific type id
4311            if ( ! empty( $objtypeid ) ) {
4312
4313                $wheres['zbstl_objtype'] = array( 'zbstl_objtype', '=', '%d', $objtypeid );
4314
4315            }
4316
4317            // If 'owner' is set then have to ignore owner, because can't do both
4318            if ( $owner > 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
4319
4320                    // stops ownership check
4321                    $ignoreowner = true;
4322
4323                    // adds owner to query
4324                    $wheres['zbs_owner'] = array( 'zbs_owner', '=', '%d', $owner );
4325
4326            }
4327
4328            #} ============ / WHERE ==============
4329
4330            #} Build out any WHERE clauses
4331            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
4332            $whereStr  = $wheresArr['where'];
4333            $params    = $params + $wheresArr['params'];
4334            #} / Build WHERE
4335
4336            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
4337            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
4338            $ownQ   = $this->ownershipSQL( $ignoreowner );
4339            if ( ! empty( $ownQ ) ) {
4340                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
4341            }
4342            #} / Ownership
4343
4344            #} ============ CUSTOM GROUP/ORDERBY ==============
4345
4346                // this allows grouping :)
4347                $orderByCustom = ' GROUP BY zbstl_objtype ORDER BY c ASC LIMIT 0,1';
4348
4349            #} ============ / CUSTOM GROUP/ORDERBY ============
4350
4351            #} Append to sql (and use our custom order by etc.)
4352            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $orderByCustom;
4353
4354            try {
4355
4356                #} Prep & run query
4357                $queryObj     = $this->prepare( $query, $params );
4358                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
4359
4360            } catch ( Exception $e ) {
4361
4362                #} General SQL Err
4363                $this->catchSQLError( $e );
4364
4365            }
4366
4367            #} Interpret Results (ROW)
4368            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
4369
4370                #} Only ID? return it directly
4371                if ( $onlyID === true ) {
4372                    return $potentialRes->ID;
4373                }
4374
4375                #} Has results, tidy + return
4376                return $this->tidy_tagstat( $potentialRes );
4377
4378            }
4379        } // / if ID
4380
4381        return false;
4382    }
4383
4384    /**
4385     * Checks if a tag slug exists
4386     *
4387     * @param int    $obj_type_id Object type id.
4388     * @param string $slug Tag slug to check.
4389     *
4390     * @return string tag slug
4391     */
4392    public function tag_slug_exists( int $obj_type_id, string $slug ) {
4393        $slug_exists = $this->getTag(
4394            -1,
4395            array(
4396                'objtype' => $obj_type_id,
4397                'slug'    => $slug,
4398                'onlyID'  => true,
4399            )
4400        );
4401        return $slug_exists !== false;
4402    }
4403
4404    /**
4405     * Get a unique tag slug
4406     *
4407     * @param int    $obj_type_id Object type id.
4408     * @param string $slug Tag slug to check.
4409     * @param bool   $force_iteration Force iteration to occur (e.g. use `slug-N` instead of `slug`).
4410     *
4411     * @return string unique tag slug
4412     */
4413    public function get_new_tag_slug( int $obj_type_id, string $slug, bool $force_iteration = false ) {
4414        global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
4415        $slug_exists = $this->tag_slug_exists( $obj_type_id, $slug );
4416
4417        // slug as provided doesn't exist, so use that
4418        if ( ! $slug_exists && ! $force_iteration ) {
4419            return $slug;
4420        }
4421
4422        $slug_base = $slug . '-';
4423
4424        // get last iteration of tag slug
4425        $sql_query = 'SELECT CAST(TRIM(LEADING %s FROM zbstag_slug) AS SIGNED) AS slug_iteration FROM ' . $ZBSCRM_t['tags'] . ' WHERE zbstag_slug LIKE CONCAT(%s,"%") AND zbstag_objtype = %d ORDER BY slug_iteration DESC LIMIT 1'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
4426
4427        $cur_slug_iteration = $wpdb->get_var( $wpdb->prepare( $sql_query, $slug_base, $slug_base, $obj_type_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
4428
4429        // slug hasn't yet iterated, so use first iteration
4430        if ( $cur_slug_iteration === null ) {
4431            return $slug_base . '1';
4432        }
4433
4434        // otherwise use next iteration
4435        $next_slug_iteration = (int) $cur_slug_iteration + 1;
4436        return $slug_base . $next_slug_iteration;
4437    }
4438
4439    /**
4440     * tidy's the object from wp db into clean array
4441     *
4442     * @param array $obj (DB obj)
4443     *
4444     * @return array (clean obj)
4445     */
4446    private function tidy_tag( $obj = false ) {
4447
4448        $res = false;
4449
4450        if ( isset( $obj->ID ) ) {
4451            $res       = array();
4452            $res['id'] = $obj->ID;
4453            /*
4454                `zbs_site` INT NULL DEFAULT NULL,
4455                `zbs_team` INT NULL DEFAULT NULL,
4456                `zbs_owner` INT NOT NULL,
4457            */
4458
4459            $res['objtype'] = $obj->zbstag_objtype;
4460            $res['name']    = $this->stripSlashes( $obj->zbstag_name );
4461            $res['slug']    = $obj->zbstag_slug;
4462
4463            $res['created']     = $obj->zbstag_created;
4464            $res['lastupdated'] = $obj->zbstag_lastupdated;
4465
4466        }
4467
4468        return $res;
4469    }
4470
4471    /**
4472     * tidy's the object from wp db into clean array
4473     *
4474     * @param array $obj (DB obj)
4475     *
4476     * @return array (clean obj)
4477     */
4478    private function tidy_tagstat( $obj = false ) {
4479
4480            $res = false;
4481
4482        if ( isset( $obj->ID ) ) {
4483            $res              = array();
4484            $res['count']     = $obj->c;
4485            $res['objtypeid'] = $obj->zbstl_objtype;
4486            $res['objtype']   = $this->objTypeKey( $obj->zbstl_objtype );
4487
4488        }
4489
4490        return $res;
4491    }
4492
4493    // =========== / TAGS      =======================================================
4494    // ===============================================================================
4495
4496    // ===============================================================================
4497    // ===========   TAG LINKS  =======================================================
4498    /**
4499     * returns tags against an obj type (e.g. contact tags)
4500     *
4501     * @param array $args   Associative array of arguments
4502     *                      objtypeid
4503     *
4504     * @return array result
4505     */
4506    public function getTagsForObjType( $args = array() ) {
4507
4508        #} =========== LOAD ARGS ==============
4509        $defaultArgs = array(
4510
4511            'objtypeid'    => -1,
4512
4513            // select
4514            'excludeEmpty' => -1,
4515            'excludeIDs'   => -1, // if is an array of tag id's will exclude these :)
4516
4517            // with
4518            'withCount'    => -1,
4519
4520            // sort
4521            'sortByField'  => 'zbstag_name',
4522            'sortOrder'    => 'ASC',
4523
4524            'page'         => 0, // this is what page it is (gets * by for limit)
4525            'perPage'      => 10000,
4526
4527            // permissions
4528            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
4529            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
4530            // Tags don't need owners yet :)
4531
4532        );
4533        foreach ( $defaultArgs as $argK => $argV ) {
4534            $$argK = $argV;
4535            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4536                if ( is_array( $args[ $argK ] ) ) {
4537                    $newData = $$argK;
4538                    if ( ! is_array( $newData ) ) {
4539                        $newData = array();
4540                    } foreach ( $args[ $argK ] as $subK => $subV ) {
4541                        $newData[ $subK ] = $subV;
4542                    }$$argK = $newData;
4543                } else {
4544                    $$argK = $args[ $argK ]; }
4545            }
4546        }
4547        #} =========== / LOAD ARGS =============
4548
4549        $ignoreowner = true;
4550
4551        #} Check ID
4552        $objtypeid = (int) $objtypeid;
4553        if ( ! empty( $objtypeid ) && $objtypeid > 0 ) {
4554
4555            global $ZBSCRM_t, $wpdb;
4556            $wheres          = array( 'direct' => array() );
4557            $whereStr        = '';
4558            $additionalWhere = '';
4559            $params          = array();
4560            $res             = array();
4561
4562            #} ============ EXTRA SELECT ==============
4563
4564                $extraSelect = '';
4565
4566            if ( $withCount !== -1 || $excludeEmpty !== -1 ) {
4567
4568                // could make this distinct zbstl_objid if need more precision
4569                // NOTE! Ownership leak here - this'll count GLOBALLY! todo: add ownership into this subquery
4570                $extraSelect = ',(SELECT COUNT(taglink.ID) FROM ' . $ZBSCRM_t['taglinks'] . ' taglink WHERE zbstl_tagid = tags.ID AND zbstl_objtype = %d) tagcount';
4571                $params[]    = $objtypeid;
4572
4573            }
4574
4575            #} ============ / EXTRA SELECT ==============
4576
4577            #} Build query
4578            $query = 'SELECT tags.*' . $extraSelect . ' FROM ' . $ZBSCRM_t['tags'] . ' tags';
4579
4580            #} ============= WHERE ================
4581
4582                // type id
4583                $wheres['zbstag_objtype'] = array( 'zbstag_objtype', '=', '%d', $objtypeid );
4584
4585                // if exclude empty
4586            if ( $excludeEmpty ) {
4587                $wheres['direct'][] = array( '(SELECT COUNT(taglink.ID) FROM ' . $ZBSCRM_t['taglinks'] . ' taglink WHERE zbstl_tagid = tags.ID AND zbstl_objtype = %d) > 0', array( $objtypeid ) );
4588
4589            }
4590
4591            if ( is_array( $excludeIDs ) ) {
4592
4593                $checkedExcludedIDs = array();
4594                foreach ( $excludeIDs as $potentialID ) {
4595                    $pID = (int) $potentialID;
4596                    if ( $pID > 0 && ! in_array( $pID, $checkedExcludedIDs ) ) {
4597                        $checkedExcludedIDs[] = $pID;
4598                    }
4599                }
4600
4601                if ( count( $checkedExcludedIDs ) > 0 ) {
4602
4603                    // add exclude ids query part (okay to directly inject here, as validated ints above.)
4604                    $wheres['excludedids'] = array( 'ID', 'NOT IN', '(' . implode( ',', $checkedExcludedIDs ) . ')' );
4605
4606                }
4607            }
4608
4609            #} ============ / WHERE ==============
4610
4611            #} Build out any WHERE clauses
4612            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
4613            $whereStr  = $wheresArr['where'];
4614            $params    = $params + $wheresArr['params'];
4615            #} / Build WHERE
4616
4617            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
4618            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
4619            $ownQ   = $this->ownershipSQL( $ignoreowner );
4620            if ( ! empty( $ownQ ) ) {
4621                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
4622            }
4623            #} / Ownership
4624
4625            #} Append to sql (this also automatically deals with sortby and paging)
4626            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
4627
4628            try {
4629
4630                #} Prep & run query
4631                $queryObj     = $this->prepare( $query, $params );
4632                $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
4633
4634            } catch ( Exception $e ) {
4635
4636                #} General SQL Err
4637                $this->catchSQLError( $e );
4638
4639            }
4640
4641            #} Interpret results (Result Set - multi-row)
4642            if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
4643
4644                #} Has results, tidy + return
4645                foreach ( $potentialRes as $resDataLine ) {
4646
4647                        // tidy
4648                        $resArr = $this->tidy_tag( $resDataLine );
4649
4650                    if ( $withCount !== -1 ) {
4651
4652                        if ( isset( $resDataLine->tagcount ) ) {
4653                            $resArr['count'] = $resDataLine->tagcount;
4654                        } else {
4655                            $resArr['count'] = -1;
4656                        }
4657                    }
4658
4659                        $res[] = $resArr;
4660
4661                }
4662            }
4663
4664            return $res;
4665
4666        } // / if ID
4667
4668        return false;
4669    }
4670    /**
4671     * returns tags against an obj (e.g. contact id 101)
4672     *
4673     * @param array $args   Associative array of arguments
4674     *                      objtypeid, objid
4675     *
4676     * @return array result
4677     */
4678    public function getTagsForObjID( $args = array() ) {
4679
4680        #} =========== LOAD ARGS ==============
4681        $defaultArgs = array(
4682
4683            'objtypeid' => -1,
4684            'objid'     => -1,
4685
4686            // with
4687            'withCount' => -1,
4688            'onlyID'    => -1,
4689
4690            // permissions
4691            // 'ignoreowner'     => false // this'll let you not-check the owner of obj
4692            // NOTE 'owner' will ALWAYS be ignored by this, but allows for team/site
4693            // Tags don't need owners yet :)
4694
4695        );
4696        foreach ( $defaultArgs as $argK => $argV ) {
4697            $$argK = $argV;
4698            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4699                if ( is_array( $args[ $argK ] ) ) {
4700                    $newData = $$argK;
4701                    if ( ! is_array( $newData ) ) {
4702                        $newData = array();
4703                    } foreach ( $args[ $argK ] as $subK => $subV ) {
4704                        $newData[ $subK ] = $subV;
4705                    }$$argK = $newData;
4706                } else {
4707                    $$argK = $args[ $argK ]; }
4708            }
4709        }
4710        #} =========== / LOAD ARGS =============
4711
4712        $ignoreowner = true;
4713
4714        #} Check ID
4715        $objtypeid = (int) $objtypeid;
4716        $objid     = (int) $objid;
4717        if ( ! empty( $objtypeid ) && $objtypeid > 0 && ! empty( $objid ) && $objid > 0 ) {
4718
4719            global $ZBSCRM_t, $wpdb;
4720            $wheres          = array( 'direct' => array() );
4721            $whereStr        = '';
4722            $additionalWhere = '';
4723            $params          = array();
4724            $res             = array();
4725
4726            #} Build query
4727            $query = 'SELECT * FROM ' . $ZBSCRM_t['tags'];
4728
4729            #} ============= WHERE ================
4730
4731                #} Add ID
4732                // rather than using the $wheres, here we have to manually add, because sub queries don't work otherwise.
4733                $whereStr = ' WHERE ID in (SELECT zbstl_tagid FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = %d)';
4734                $params[] = $objtypeid;
4735            $params[]     = $objid;
4736
4737            #} ============ / WHERE ==============
4738
4739            #} Build out any WHERE clauses
4740            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
4741            $whereStr  = $wheresArr['where'];
4742            $params    = $params + $wheresArr['params'];
4743            #} / Build WHERE
4744
4745            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
4746            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
4747            $ownQ   = $this->ownershipSQL( $ignoreowner );
4748            if ( ! empty( $ownQ ) ) {
4749                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
4750            }
4751            #} / Ownership
4752
4753            #} Append to sql (this also automatically deals with sortby and paging)
4754            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 10000 );
4755            // echo $query; print_r($params);
4756
4757            try {
4758
4759                #} Prep & run query
4760                $queryObj     = $this->prepare( $query, $params );
4761                $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
4762
4763            } catch ( Exception $e ) {
4764
4765                #} General SQL Err
4766                $this->catchSQLError( $e );
4767
4768            }
4769
4770            #} Interpret results (Result Set - multi-row)
4771            if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
4772
4773                #} Has results, tidy + return
4774                foreach ( $potentialRes as $resDataLine ) {
4775
4776                        #} Only ID? return it directly
4777                    if ( $onlyID === true ) {
4778                        $resObj = $resDataLine->ID;
4779                    } else { // tidy
4780                            $resObj = $this->tidy_tag( $resDataLine );
4781                    }
4782
4783                    if ( $withCount ) {
4784
4785                    }
4786
4787                        $res[] = $resObj;
4788
4789                }
4790            }
4791
4792            return $res;
4793
4794        } // / if ID
4795
4796        return false;
4797    }
4798
4799    /**
4800     * adds or updates a tag link object
4801     * this says "match tag X with obj Y" (effectively 'tagging' it)
4802     * NOTE: DO NOT CALL DIRECTLY, ALWAYS use addUpdateTagObjLinks (or it's wrappers) - because those fire actions :)
4803     *
4804     * @param array $args Associative array of arguments
4805     *              id (if update - probably never used here), data(objtype,objid,tagid)
4806     *
4807     * @return int line ID
4808     */
4809    public function addUpdateTagObjLink( $args = array() ) {
4810
4811        global $ZBSCRM_t, $wpdb;
4812
4813        #} ============ LOAD ARGS =============
4814        $defaultArgs = array(
4815
4816            'id'    => -1,
4817            'owner' => -1,
4818
4819            // fields (directly)
4820            'data'  => array(
4821
4822                'objtype' => -1,
4823                'objid'   => -1,
4824                'tagid'   => -1,
4825
4826            ),
4827
4828        );
4829        foreach ( $defaultArgs as $argK => $argV ) {
4830            $$argK = $argV;
4831            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4832                if ( is_array( $args[ $argK ] ) ) {
4833                    $newData = $$argK;
4834                    if ( ! is_array( $newData ) ) {
4835                        $newData = array();
4836                    } foreach ( $args[ $argK ] as $subK => $subV ) {
4837                        $newData[ $subK ] = $subV;
4838                    }$$argK = $newData;
4839                } else {
4840                    $$argK = $args[ $argK ]; }
4841            }
4842        }
4843        #} =========== / LOAD ARGS ============
4844
4845        #} ========== CHECK FIELDS ============
4846
4847        // check obtype is completed + legit
4848        if ( empty( $data['objtype'] ) ) {
4849            return false;
4850        }
4851        if ( $this->objTypeKey( $data['objtype'] ) === -1 ) {
4852            return false;
4853        }
4854
4855        // if owner = -1, add current
4856        if ( $owner === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
4857            $owner = zeroBSCRM_user();
4858        }
4859
4860            $objid = (int) $data['objid'];
4861        $tagid     = (int) $data['tagid'];
4862        if ( empty( $data['objid'] ) || $data['objid'] < 1 || empty( $data['tagid'] ) || $data['tagid'] < 1 ) {
4863            return false;
4864        }
4865
4866        #} ========= / CHECK FIELDS ===========
4867
4868        #} Check if ID present
4869        $id = (int) $id;
4870        if ( ! empty( $id ) && $id > 0 ) {
4871
4872                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
4873
4874                #} Attempt update
4875            if ( $wpdb->update(
4876                $ZBSCRM_t['taglinks'],
4877                array(
4878
4879                    // ownership
4880                    // no need to update these (as of yet) - can't move teams etc.
4881                    // 'zbs_site' => zeroBSCRM_installSite(),
4882                    // 'zbs_team' => zeroBSCRM_installTeam(),
4883                    'zbs_owner'     => $owner,
4884
4885                    // fields
4886                    'zbstl_objtype' => $data['objtype'],
4887                    'zbstl_objid'   => $data['objid'],
4888                    'zbstl_tagid'   => $data['tagid'],
4889                ),
4890                array( // where
4891                    'ID' => $id,
4892                ),
4893                array( // field data types
4894                    '%d',
4895                    '%d',
4896                    '%d',
4897                    '%d',
4898                ),
4899                array( // where data types
4900                    '%d',
4901                )
4902            ) !== false ) {
4903
4904                        // Successfully updated - Return id
4905                        return $id;
4906
4907            } else {
4908
4909                // FAILED update
4910                return false;
4911
4912            }
4913        } else {
4914
4915            #} No ID - must be an INSERT
4916            if ( $wpdb->insert(
4917                $ZBSCRM_t['taglinks'],
4918                array(
4919
4920                    // ownership
4921                    'zbs_site'      => zeroBSCRM_site(),
4922                    'zbs_team'      => zeroBSCRM_team(),
4923                    'zbs_owner'     => $owner,
4924
4925                    // fields
4926                    'zbstl_objtype' => $data['objtype'],
4927                    'zbstl_objid'   => $data['objid'],
4928                    'zbstl_tagid'   => $data['tagid'],
4929                ),
4930                array( // field data types
4931                    '%d',  // site
4932                    '%d',  // team
4933                    '%d',  // owner
4934
4935                    '%d',
4936                    '%d',
4937                    '%d',
4938                )
4939            ) > 0 ) {
4940
4941                    #} Successfully inserted, lets return new ID
4942                    $newID = $wpdb->insert_id;
4943                    return $newID;
4944
4945            } else {
4946
4947                #} Failed to Insert
4948                return false;
4949
4950            }
4951        }
4952
4953        return false;
4954    }
4955
4956    /**
4957     * adds or updates tag link objects against an obj
4958     * this says "match tag X,Y,Z with obj Y" (effectively 'tagging' it)
4959     *
4960     * @param array $args Associative array of arguments
4961     *              objtype,objid,tags (array of tagids)
4962     *
4963     * @return array $tags
4964     */
4965    public function addUpdateTagObjLinks( $args = array() ) {
4966
4967        global $ZBSCRM_t, $wpdb;
4968
4969        #} ============ LOAD ARGS =============
4970        $defaultArgs = array(
4971
4972            'owner'   => -1,
4973
4974            'objtype' => -1,
4975            'objid'   => -1,
4976            'tagIDs'  => -1, // array of tag ID's
4977
4978            'mode'    => 'replace', // replace|append|remove
4979
4980        );
4981        foreach ( $defaultArgs as $argK => $argV ) {
4982            $$argK = $argV;
4983            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4984                if ( is_array( $args[ $argK ] ) ) {
4985                    $newData = $$argK;
4986                    if ( ! is_array( $newData ) ) {
4987                        $newData = array();
4988                    } foreach ( $args[ $argK ] as $subK => $subV ) {
4989                        $newData[ $subK ] = $subV;
4990                    }$$argK = $newData;
4991                } else {
4992                    $$argK = $args[ $argK ]; }
4993            }
4994        }
4995        #} =========== / LOAD ARGS ============
4996
4997        #} ========== CHECK FIELDS ============
4998
4999        // check obtype is completed + legit
5000        if ( empty( $objtype ) ) {
5001            return false;
5002        }
5003        if ( $this->objTypeKey( $objtype ) === -1 ) {
5004            return false;
5005        }
5006
5007        // if owner = -1, add current
5008        if ( $owner === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
5009            $owner = zeroBSCRM_user();
5010        }
5011
5012            // tagging id
5013            $objid = (int) $objid;
5014        if ( empty( $objid ) || $objid < 1 ) {
5015            return false;
5016        }
5017
5018            // tag list
5019        if ( ! is_array( $tagIDs ) ) {
5020            return false;
5021        }
5022
5023            // mode
5024        if ( gettype( $mode ) != 'string' || ! in_array( $mode, array( 'replace', 'append', 'remove' ) ) ) {
5025            return false;
5026        }
5027
5028        #} ========= / CHECK FIELDS ===========
5029
5030        switch ( $mode ) {
5031
5032            case 'replace':
5033                // (for actions) log starting objs
5034                $existingTagIDs = $this->getTagsForObjID(
5035                    array(
5036                        'objtypeid' => $objtype,
5037                        'objid'     => $objid,
5038                        'onlyID'    => true,
5039                    )
5040                );
5041                if ( ! is_array( $existingTagIDs ) ) {
5042                    $existingTagIDs = array();
5043                }
5044                $removedTagsByID = array();
5045                $addedTagsByID   = array();
5046
5047                // cull all previous
5048                $deleted = $this->deleteTagObjLinks(
5049                    array(
5050                        'objid'   => $objid,
5051                        'objtype' => $objtype,
5052                    )
5053                );
5054
5055                // cycle through & add
5056                foreach ( $tagIDs as $tid ) {
5057
5058                    $added = $this->addUpdateTagObjLink(
5059                        array(
5060                            'data' => array(
5061                                'objid'   => $objid,
5062                                'objtype' => $objtype,
5063                                'tagid'   => $tid,
5064                            ),
5065                        )
5066                    );
5067
5068                    if ( $added !== false ) {
5069
5070                        if ( ! in_array( $tid, $existingTagIDs ) ) {
5071                            $addedTagsByID[] = $tid; // tag was added
5072                        }
5073                        // else
5074                            // tag was already in there, just re-added
5075                    }
5076                }
5077
5078                // actions
5079
5080                    // check removed
5081                foreach ( $existingTagIDs as $tid ) {
5082
5083                    if ( ! in_array( $tid, $tagIDs ) ) {
5084                        $removedTagsByID[] = $tid;
5085                    }
5086                }
5087
5088                    // fire actions for each tag
5089
5090                        // added to
5091                if ( count( $addedTagsByID ) > 0 ) {
5092                    foreach ( $addedTagsByID as $tagID ) {
5093                        do_action( 'zbs_tag_added_to_objid', $tagID, $objtype, $objid );
5094                    }
5095                }
5096
5097                        // removed from
5098                if ( count( $removedTagsByID ) > 0 ) {
5099                    foreach ( $removedTagsByID as $tagID ) {
5100                        do_action( 'zbs_tag_removed_from_objid', $tagID, $objtype, $objid );
5101                    }
5102                }
5103
5104                        // return
5105                return true;
5106
5107                break;
5108
5109            case 'append':
5110                // get existing
5111                $existingTagIDs = $this->getTagsForObjID(
5112                    array(
5113                        'objtypeid' => $objtype,
5114                        'objid'     => $objid,
5115                        'onlyID'    => true,
5116                    )
5117                );
5118
5119                // make just ids
5120                // no need, added ,'onlyID'=>true above
5121                // $existingTagIDs = array(); foreach ($tags as $t) $existingTagIDs[] = $t['id'];
5122
5123                // cycle through& add
5124                foreach ( $tagIDs as $tid ) {
5125
5126                    if ( ! in_array( $tid, $existingTagIDs ) ) {
5127
5128                        // add a link
5129                        $this->addUpdateTagObjLink(
5130                            array(
5131                                'data' => array(
5132                                    'objid'   => $objid,
5133                                    'objtype' => $objtype,
5134                                    'tagid'   => $tid,
5135                                ),
5136                            )
5137                        );
5138
5139                        // fire action
5140                        do_action( 'zbs_tag_added_to_objid', $tid, $objtype, $objid );
5141
5142                    }
5143                }
5144
5145                $this->compile_segments_from_tagIDs( $tagIDs, $owner ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
5146
5147                return true;
5148
5149                break;
5150
5151            case 'remove':
5152                // get existing
5153                $existingTagIDs = $this->getTagsForObjID(
5154                    array(
5155                        'objtypeid' => $objtype,
5156                        'objid'     => $objid,
5157                        'onlyID'    => true,
5158                    )
5159                );
5160
5161                // cycle through & remove links
5162                foreach ( $tagIDs as $tid ) {
5163
5164                    if ( in_array( $tid, $existingTagIDs ) ) {
5165
5166                        // delete link
5167                        $this->deleteTagObjLink(
5168                            array(
5169                                'objid'   => $objid,
5170                                'objtype' => $objtype,
5171                                'tagid'   => $tid,
5172                            )
5173                        );
5174
5175                        // action
5176                        do_action( 'zbs_tag_removed_from_objid', $tid, $objtype, $objid );
5177
5178                    }
5179                }
5180
5181                $this->compile_segments_from_tagIDs( $tagIDs, $owner ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
5182
5183                return true;
5184
5185                break;
5186
5187        }
5188
5189        return false;
5190    }
5191
5192    /**
5193     * Compiles segments based on an array of given tag IDs
5194     *
5195     * @param array $tagIDs An array of tag IDs.
5196     * @param ID    $owner An ID representing the owner of the current tagID.
5197     */
5198    public function compile_segments_from_tagIDs( $tagIDs, $owner ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
5199        global $zbs;
5200        $segments = $zbs->DAL->segments->getSegments( $owner, 1000, 0, true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
5201        foreach ( $segments as $segment ) {
5202            foreach ( $segment['conditions'] as $condition ) {
5203                if ( $condition['type'] === 'tagged' && in_array( $condition['value'], $tagIDs ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,WordPress.PHP.StrictInArray.MissingTrueStrict
5204                    $zbs->DAL->segments->compileSegment( $segment['id'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
5205                }
5206            }
5207        }
5208    }
5209
5210    /**
5211     * deletes a tag object link
5212     *
5213     * @param array $args Associative array of arguments
5214     *              id
5215     *
5216     * @return int success;
5217     */
5218    public function deleteTagObjLink( $args = array() ) {
5219
5220        global $ZBSCRM_t, $wpdb;
5221
5222        #} ============ LOAD ARGS =============
5223        $defaultArgs = array(
5224
5225            'id'      => -1,
5226
5227            // or...
5228
5229            'objtype' => -1,
5230            'objid'   => -1,
5231            'tagid'   => -1,
5232
5233        );
5234        foreach ( $defaultArgs as $argK => $argV ) {
5235            $$argK = $argV;
5236            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5237                if ( is_array( $args[ $argK ] ) ) {
5238                    $newData = $$argK;
5239                    if ( ! is_array( $newData ) ) {
5240                        $newData = array();
5241                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5242                        $newData[ $subK ] = $subV;
5243                    }$$argK = $newData;
5244                } else {
5245                    $$argK = $args[ $argK ]; }
5246            }
5247        }
5248        #} =========== / LOAD ARGS ============
5249
5250        #} Check ID & Delete :) (IF ID PRESENT)
5251        $id = (int) $id;
5252        if ( $id > 0 ) {
5253            return zeroBSCRM_db2_deleteGeneric( $id, 'taglinks' );
5254        }
5255
5256        #} ... else delete by objtype etc.
5257
5258        #} ========== CHECK FIELDS ============
5259
5260        // check obtype is completed + legit
5261        if ( empty( $objtype ) ) {
5262            return false;
5263        }
5264        if ( $this->objTypeKey( $objtype ) === -1 ) {
5265            return false;
5266        }
5267
5268            // obj id
5269            $objid = (int) $objid;
5270        if ( empty( $objid ) || $objid < 1 ) {
5271            return false;
5272        }
5273
5274            // tag id
5275            $tagid = (int) $tagid;
5276        if ( empty( $tagid ) || $tagid < 1 ) {
5277            return false;
5278        }
5279
5280            // CHECK PERMISSIONS?
5281
5282        #} ========= / CHECK FIELDS ===========
5283
5284            #} ... if here then is trying to delete specific tag linkid
5285            return $wpdb->delete(
5286                $ZBSCRM_t['taglinks'],
5287                array( // where
5288                    'zbstl_objtype' => $objtype,
5289                    'zbstl_objid'   => $objid,
5290                    'zbstl_tagid'   => $tagid,
5291                ),
5292                array(
5293                    '%d',
5294                    '%d',
5295                    '%d',
5296                )
5297            );
5298    }
5299
5300    /**
5301     * deletes all tag object links for a specific obj
5302     *
5303     * @param array $args Associative array of arguments
5304     *              id
5305     *
5306     * @return int success;
5307     */
5308    public function deleteTagObjLinks( $args = array() ) {
5309
5310        global $ZBSCRM_t, $wpdb;
5311
5312        #} ============ LOAD ARGS =============
5313        $defaultArgs = array(
5314
5315            'objtype' => -1,
5316            'objid'   => -1,
5317
5318        );
5319        foreach ( $defaultArgs as $argK => $argV ) {
5320            $$argK = $argV;
5321            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5322                if ( is_array( $args[ $argK ] ) ) {
5323                    $newData = $$argK;
5324                    if ( ! is_array( $newData ) ) {
5325                        $newData = array();
5326                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5327                        $newData[ $subK ] = $subV;
5328                    }$$argK = $newData;
5329                } else {
5330                    $$argK = $args[ $argK ]; }
5331            }
5332        }
5333        #} =========== / LOAD ARGS ============
5334
5335        #} ========== CHECK FIELDS ============
5336
5337        // check obtype is completed + legit
5338        if ( empty( $objtype ) ) {
5339            return false;
5340        }
5341        if ( $this->objTypeKey( $objtype ) === -1 ) {
5342            return false;
5343        }
5344
5345            // obj id
5346            $objid = (int) $objid;
5347        if ( empty( $objid ) || $objid < 1 ) {
5348            return false;
5349        }
5350
5351            // CHECK PERMISSIONS?
5352
5353        #} ========= / CHECK FIELDS ===========
5354
5355        // brutal
5356        return $wpdb->delete(
5357            $ZBSCRM_t['taglinks'],
5358            array( // where
5359                'zbstl_objtype' => $objtype,
5360                'zbstl_objid'   => $objid,
5361            ),
5362            array(
5363                '%d',
5364                '%d',
5365            )
5366        );
5367    }
5368
5369    /**
5370     * tidy's the object from wp db into clean array
5371     *
5372     * @param array $obj (DB obj)
5373     *
5374     * @return array (clean obj)
5375     */
5376    private function tidy_taglink( $obj = false ) {
5377
5378            $res = false;
5379
5380        if ( isset( $obj->ID ) ) {
5381            $res       = array();
5382            $res['id'] = $obj->ID;
5383            /*
5384            `zbs_site` INT NULL DEFAULT NULL,
5385            `zbs_team` INT NULL DEFAULT NULL,
5386            `zbs_owner` INT NOT NULL,
5387            */
5388
5389            $res['objtype'] = $obj->zbstag_objtype;
5390            $res['name']    = $this->stripSlashes( $obj->zbstag_name );
5391            $res['slug']    = $obj->zbstag_slug;
5392
5393            $res['created']     = $obj->zbstag_created;
5394            $res['lastupdated'] = $obj->zbstag_lastupdated;
5395
5396        }
5397
5398        return $res;
5399    }
5400
5401    // =========== / TAG LINKS      ==================================================
5402    // ===============================================================================
5403
5404    // ===============================================================================
5405    // ===========   CUSTOM FIELDS   =================================================
5406
5407    /**
5408     * returns true if field key exists as custom field for CONTACT
5409     *
5410     * @param array $args Associative array of arguments
5411     *              objtypeid
5412     *
5413     * @return array of customfield field keys
5414     */
5415    public function isActiveCustomField_Contact( $customFieldKey = '' ) {
5416
5417        #} These are simply stored in settings with a key of customfields_objtype e.g. customfields_contact
5418        if ( ! empty( $objtypeid ) && $objtypeid > 0 && ! empty( $customFieldKey ) ) {
5419
5420            // get custom fields
5421            $customFields = $this->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_CONTACT ) );
5422
5423            // validate there
5424            if ( is_array( $customFields ) ) {
5425                foreach ( $customFields as $cfK => $cfV ) {
5426
5427                    if ( $cfK == $customFieldKey ) {
5428                        return true;
5429                    }
5430                }
5431            }
5432        }
5433
5434        return false;
5435    }
5436
5437    /**
5438     * returns true if field key exists as custom field for obj
5439     *
5440     * @param array $args Associative array of arguments
5441     *              objtypeid
5442     *
5443     * @return array of customfield field keys
5444     */
5445    public function isActiveCustomField( $args = array() ) {
5446
5447        #} ============ LOAD ARGS =============
5448        $defaultArgs = array(
5449
5450            'objtypeid'      => -1,
5451            'customFieldKey' => '',
5452
5453        );
5454        foreach ( $defaultArgs as $argK => $argV ) {
5455            $$argK = $argV;
5456            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5457                if ( is_array( $args[ $argK ] ) ) {
5458                    $newData = $$argK;
5459                    if ( ! is_array( $newData ) ) {
5460                        $newData = array();
5461                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5462                        $newData[ $subK ] = $subV;
5463                    }$$argK = $newData;
5464                } else {
5465                    $$argK = $args[ $argK ]; }
5466            }
5467        }
5468        #} =========== / LOAD ARGS =============
5469
5470        #} These are simply stored in settings with a key of customfields_objtype e.g. customfields_contact
5471        if ( ! empty( $objtypeid ) && $objtypeid > 0 && ! empty( $customFieldKey ) ) {
5472
5473            // get custom fields
5474            $customFields = $this->getActiveCustomFields( array( 'objtypeid' => $objtypeid ) );
5475
5476            // validate there
5477            if ( is_array( $customFields ) ) {
5478                foreach ( $customFields as $cfK => $cfV ) {
5479
5480                    if ( $cfK == $customFieldKey ) {
5481                        return true;
5482                    }
5483                }
5484            }
5485        }
5486
5487        return false;
5488    }
5489
5490    /**
5491     * returns active custom field keys for an obj type
5492     *
5493     * @param array $args Associative array of arguments
5494     *              objtypeid
5495     *
5496     * @return array of customfield field keys
5497     */
5498    public function getActiveCustomFields( $args = array() ) {
5499
5500        #} ============ LOAD ARGS =============
5501        $defaultArgs = array(
5502
5503            // object type id
5504            'objtypeid'     => -1,
5505
5506            // whether or not to accept cached variant.
5507            // Added in gh-2019, we often recall this function on one load, this allows us to accept a once-loaded version
5508            'accept_cached' => true,
5509
5510        );
5511        foreach ( $defaultArgs as $argK => $argV ) {
5512            $$argK = $argV;
5513            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5514                if ( is_array( $args[ $argK ] ) ) {
5515                    $newData = $$argK;
5516                    if ( ! is_array( $newData ) ) {
5517                        $newData = array();
5518                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5519                        $newData[ $subK ] = $subV;
5520                    }$$argK = $newData;
5521                } else {
5522                    $$argK = $args[ $argK ]; }
5523            }
5524        }
5525        #} =========== / LOAD ARGS =============
5526
5527        #} These are simply stored in settings with a key of customfields_objtype e.g. customfields_contact
5528        if ( ! empty( $objtypeid ) && $objtypeid > 0 ) {
5529
5530            // retrieve
5531            return $this->setting( 'customfields_' . $this->objTypeKey( $objtypeid ), array(), $accept_cached );
5532
5533        }
5534
5535        return array();
5536    }
5537
5538    /**
5539     * updates active custom field keys for an obj type
5540     * No checking whatsoever
5541     *
5542     * @param array $args Associative array of arguments
5543     *              objtypeid
5544     *
5545     * @return array of customfield field keys
5546     */
5547    public function updateActiveCustomFields( $args = array() ) {
5548
5549        #} ============ LOAD ARGS =============
5550        $defaultArgs = array(
5551
5552            'objtypeid' => -1,
5553            'fields'    => array(),
5554
5555        );
5556        foreach ( $defaultArgs as $argK => $argV ) {
5557            $$argK = $argV;
5558            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5559                if ( is_array( $args[ $argK ] ) ) {
5560                    $newData = $$argK;
5561                    if ( ! is_array( $newData ) ) {
5562                        $newData = array();
5563                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5564                        $newData[ $subK ] = $subV;
5565                    }$$argK = $newData;
5566                } else {
5567                    $$argK = $args[ $argK ]; }
5568            }
5569        }
5570        #} =========== / LOAD ARGS =============
5571
5572        #} These are simply stored in settings with a key of customfields_objtype e.g. customfields_contact
5573        if ( ! empty( $objtypeid ) && $objtypeid > 0 ) {
5574
5575            return $this->updateSetting( 'customfields_' . $this->objTypeKey( $objtypeid ), $fields );
5576
5577        }
5578
5579        return array();
5580    }
5581
5582    /**
5583     * returns scalar value of 1 custom field line (or it's ID)
5584     * ... real custom fields will be got as part of getCustomers more commonly (this is for 1 alone)
5585     *
5586     * @param array $args   Associative array of arguments
5587     *                      objtypeid,objid,objkey
5588     *
5589     * @return array result
5590     */
5591    public function getCustomFieldVal( $args = array() ) {
5592
5593        #} =========== LOAD ARGS ==============
5594        $defaultArgs = array(
5595
5596            'objtypeid'   => -1, // e.g. 1 = contact
5597            'objid'       => -1, // e.g. contact #101
5598            'objkey'      => '', // e.g. notes
5599
5600            // permissions
5601            'ignoreowner' => false, // this'll let you not-check the owner of obj
5602
5603            // returns scalar ID of line
5604            'onlyID'      => false,
5605
5606        );
5607        foreach ( $defaultArgs as $argK => $argV ) {
5608            $$argK = $argV;
5609            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5610                if ( is_array( $args[ $argK ] ) ) {
5611                    $newData = $$argK;
5612                    if ( ! is_array( $newData ) ) {
5613                        $newData = array();
5614                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5615                        $newData[ $subK ] = $subV;
5616                    }$$argK = $newData;
5617                } else {
5618                    $$argK = $args[ $argK ]; }
5619            }
5620        }
5621        #} =========== / LOAD ARGS =============
5622
5623        #} Check IDs
5624        $objtypeid = (int) $objtypeid;
5625        $objid     = (int) $objid;
5626        if ( ! empty( $objtypeid ) && $objtypeid > 0 && ! empty( $objid ) && $objid > 0 && ! empty( $objkey ) ) {
5627
5628            global $ZBSCRM_t, $wpdb;
5629            $wheres          = array( 'direct' => array() );
5630            $whereStr        = '';
5631            $additionalWhere = '';
5632            $params          = array();
5633            $res             = array();
5634
5635            #} Build query
5636            $query = 'SELECT ID,zbscf_objval FROM ' . $ZBSCRM_t['customfields'];
5637
5638            #} ============= WHERE ================
5639
5640                #} Add obj type
5641                $wheres['zbscf_objtype'] = array( 'zbscf_objtype', '=', '%d', $objtypeid );
5642
5643                #} Add obj ID
5644                $wheres['zbscf_objid'] = array( 'zbscf_objid', '=', '%d', $objid );
5645
5646                #} Add obj key
5647                $wheres['zbscf_objkey'] = array( 'zbscf_objkey', '=', '%s', $objkey );
5648
5649            #} ============ / WHERE ==============
5650
5651            #} Build out any WHERE clauses
5652            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
5653            $whereStr  = $wheresArr['where'];
5654            $params    = $params + $wheresArr['params'];
5655            #} / Build WHERE
5656
5657            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
5658            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
5659            $ownQ   = $this->ownershipSQL( $ignoreowner );
5660            if ( ! empty( $ownQ ) ) {
5661                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
5662            }
5663            #} / Ownership
5664
5665            #} Append to sql (this also automatically deals with sortby and paging)
5666            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
5667
5668            try {
5669
5670                #} Prep & run query
5671                $queryObj     = $this->prepare( $query, $params );
5672                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
5673
5674            } catch ( Exception $e ) {
5675
5676                #} General SQL Err
5677                $this->catchSQLError( $e );
5678
5679            }
5680
5681            #} Interpret Results (ROW)
5682            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
5683
5684                #} Has results, tidy + return
5685
5686                    #} Only ID? return it directly
5687                if ( $onlyID === true ) {
5688                    return $potentialRes->ID;
5689                }
5690
5691                    // tidy
5692                    $res = $this->tidy_customfieldvalSingular( $potentialRes );
5693
5694                    return $res;
5695
5696            }
5697        } // / if ID
5698
5699        return false;
5700    }
5701
5702    /**
5703     * adds or updates a customfield object
5704     * NOTE: because these are specific to unique ID of obj, there's no need for site/team etc. here
5705     *
5706     * @param array $args Associative array of arguments
5707     *              id (if update), owner, data (array of field data)
5708     *
5709     * @return int line ID
5710     */
5711    public function addUpdateCustomField( $args = array() ) {
5712
5713        global $ZBSCRM_t, $wpdb;
5714
5715        #} ============ LOAD ARGS =============
5716        $defaultArgs = array(
5717
5718            'id'    => -1, // Custom field line ID (not obj id!)
5719            'owner' => -1,
5720
5721            // fields (directly)
5722            'data'  => array(
5723                'objtype' => -1,
5724                'objid'   => -1,
5725                'objkey'  => '',
5726                'objval'  => 'NULL',
5727            ),
5728
5729        );
5730        foreach ( $defaultArgs as $argK => $argV ) {
5731            $$argK = $argV;
5732            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5733                if ( is_array( $args[ $argK ] ) ) {
5734                    $newData = $$argK;
5735                    if ( ! is_array( $newData ) ) {
5736                        $newData = array();
5737                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5738                        $newData[ $subK ] = $subV;
5739                    }$$argK = $newData;
5740                } else {
5741                    $$argK = $args[ $argK ]; }
5742            }
5743        }
5744        #} =========== / LOAD ARGS ============
5745
5746        #} ========== CHECK FIELDS ============
5747
5748            $id = (int) $id;
5749
5750        if ( ! empty( $data['objid'] ) ) { // phpcs:ignore -- PHPCS chokes on this line, but the var does exist.
5751            $data['objid'] = (int) $data['objid']; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
5752        }
5753        if ( isset( $data['objtype'] ) ) {
5754            $data['objtype'] = (int) $data['objtype'];
5755        }
5756
5757            // check obtype is completed + legit
5758        if ( ! isset( $data['objtype'] ) || empty( $data['objtype'] ) ) {
5759            return false;
5760        }
5761        if ( $this->objTypeKey( $data['objtype'] ) === -1 ) {
5762            return false;
5763        }
5764
5765            // check key + ID present
5766        if ( ! isset( $data['objkey'] ) || empty( $data['objkey'] ) ) {
5767            return false;
5768        }
5769        if ( $data['objid'] <= 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
5770            return false;
5771        }
5772
5773        // if owner = -1, add current
5774        if ( $owner === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
5775            $owner = zeroBSCRM_user();
5776        }
5777
5778            // ID finder - if obj id +  key + val + typeid provided, check CF not already present (if so overwrite)
5779        if ( ( empty( $id ) || $id <= 0 )
5780                &&
5781                ( isset( $data['objtype'] ) && ! empty( $data['objtype'] ) )
5782                &&
5783                ( isset( $data['objid'] ) && ! empty( $data['objid'] ) )
5784                &&
5785                ( isset( $data['objkey'] ) && ! empty( $data['objkey'] ) ) ) {
5786
5787            // check existence + return ID
5788            $potentialID = (int) $this->getCustomFieldVal(
5789                array(
5790                    'objtypeid'   => $data['objtype'],
5791                    'objid'       => $data['objid'],
5792                    'objkey'      => $data['objkey'],
5793                    'onlyID'      => true,
5794                    'ignoreowner' => true,
5795                )
5796            );
5797            // override empty ID
5798            if ( ! empty( $potentialID ) && $potentialID > 0 ) {
5799                $id = $potentialID;
5800            }
5801        }
5802
5803            // handle radio, select, and checkbox fields
5804        if ( is_array( $data['objval'] ) ) {
5805            $data['objval'] = implode( ',', $data['objval'] );
5806        }
5807
5808        #} ========= / CHECK FIELDS ===========
5809
5810        #} Check if ID present
5811        if ( ! empty( $id ) && $id > 0 ) {
5812
5813                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
5814
5815                #} Attempt update
5816            if ( $wpdb->update(
5817                $ZBSCRM_t['customfields'],
5818                array(
5819
5820                    // ownership
5821                    // no need to update these (as of yet) - can't move teams etc.
5822                    // 'zbs_site' => zeroBSCRM_installSite(),
5823                    // 'zbs_team' => zeroBSCRM_installTeam(),
5824                    'zbs_owner'         => $owner,
5825
5826                    // fields
5827                    'zbscf_objtype'     => $data['objtype'],
5828                    'zbscf_objid'       => $data['objid'],
5829                    'zbscf_objkey'      => $data['objkey'],
5830                    'zbscf_objval'      => $data['objval'],
5831
5832                    // 'zbscf_created' => time(),
5833                    'zbscf_lastupdated' => time(),
5834                ),
5835                array( // where
5836                    'ID' => $id,
5837                ),
5838                array( // field data types
5839                            // '%d',  // site
5840                            // '%d',  // team
5841                    '%d',  // owner
5842
5843                    '%d',
5844                    '%d',
5845                    '%s',
5846                    '%s',
5847
5848                    '%d', // last updated
5849                ),
5850                array( // where data types
5851                    '%d',
5852                )
5853            ) !== false ) {
5854
5855                        // Successfully updated - Return id
5856                        return $id;
5857
5858            } else {
5859
5860                // FAILED update
5861                return false;
5862
5863            }
5864        } else {
5865
5866            #} No ID - must be an INSERT
5867            if ( $wpdb->insert(
5868                $ZBSCRM_t['customfields'],
5869                array(
5870
5871                    // ownership
5872                    'zbs_site'          => zeroBSCRM_site(),
5873                    'zbs_team'          => zeroBSCRM_team(),
5874                    'zbs_owner'         => $owner,
5875
5876                    // fields
5877                    'zbscf_objtype'     => $data['objtype'],
5878                    'zbscf_objid'       => $data['objid'],
5879                    'zbscf_objkey'      => $data['objkey'],
5880                    'zbscf_objval'      => $data['objval'],
5881
5882                    'zbscf_created'     => time(),
5883                    'zbscf_lastupdated' => time(),
5884                ),
5885                array( // field data types
5886                    '%d',  // site
5887                    '%d',  // team
5888                    '%d',  // owner
5889
5890                    '%d',
5891                    '%d',
5892                    '%s',
5893                    '%s',
5894
5895                    '%d',
5896                    '%d',
5897                )
5898            ) > 0 ) {
5899
5900                    #} Successfully inserted, lets return new ID
5901                    $newID = $wpdb->insert_id;
5902                    return $newID;
5903
5904            } else {
5905
5906                #} Failed to Insert
5907                return false;
5908
5909            }
5910        }
5911
5912        return false;
5913    }
5914
5915    /**
5916     * deletes a customfield object
5917     *
5918     * @param array $args Associative array of arguments
5919     *              id
5920     *
5921     * @return int success;
5922     */
5923    public function deleteCustomField( $args = array() ) {
5924
5925        global $ZBSCRM_t, $wpdb;
5926
5927        #} ============ LOAD ARGS =============
5928        $defaultArgs = array(
5929
5930            'id' => -1,
5931
5932        );
5933        foreach ( $defaultArgs as $argK => $argV ) {
5934            $$argK = $argV;
5935            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5936                if ( is_array( $args[ $argK ] ) ) {
5937                    $newData = $$argK;
5938                    if ( ! is_array( $newData ) ) {
5939                        $newData = array();
5940                    } foreach ( $args[ $argK ] as $subK => $subV ) {
5941                        $newData[ $subK ] = $subV;
5942                    }$$argK = $newData;
5943                } else {
5944                    $$argK = $args[ $argK ]; }
5945            }
5946        }
5947        #} =========== / LOAD ARGS ============
5948
5949        #} Check ID & Delete :)
5950        $id = (int) $id;
5951        return zeroBSCRM_db2_deleteGeneric( $id, 'customfields' );
5952    }
5953
5954    /**
5955     * tidy's the object from wp db into clean array
5956     *
5957     * @param array $obj (DB obj)
5958     *
5959     * @return array (clean obj)
5960     */
5961    private function tidy_customfieldvalSingular( $obj = false ) {
5962
5963        $res = false;
5964
5965        if ( isset( $obj->ID ) ) {
5966
5967            // just return the value here!
5968            $res = $this->stripSlashes( $obj->zbscf_objval );
5969
5970        }
5971
5972        return $res;
5973    }
5974
5975    // =========== / CUSTOM FIELDS     ===============================================
5976    // ===============================================================================
5977
5978    // ===============================================================================
5979    // ===========   EXTERNAL SOURCES  ===============================================
5980    /**
5981     * returns first external source line +- details
5982     *
5983     * @param int id        tag id
5984     * @param array                $args   Associative array of arguments
5985     *                                     withStats
5986     *
5987     * @return array result
5988     */
5989    public function getExternalSource( $id = -1, $args = array() ) {
5990
5991        #} =========== LOAD ARGS ==============
5992        $defaultArgs = array(
5993
5994            // Alternative search criteria to ID :)
5995            // .. LEAVE blank if using ID
5996            // 'contactID'         => -1, // NOTE: This only returns the FIRST source, if using multiple sources, use getExternalSourcesForContact
5997            'objectID'    => -1,
5998            'objectType'  => -1,
5999            'source'      => -1, // Optional, if used with contact ID will return 1 line :D
6000            'origin'      => '', // Optional
6001
6002            // permissions
6003            'ignoreowner' => true, // this'll let you not-check the owner of obj
6004
6005            // returns scalar ID of line
6006            'onlyID'      => false,
6007
6008        );
6009        foreach ( $defaultArgs as $argK => $argV ) {
6010            $$argK = $argV;
6011            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6012                if ( is_array( $args[ $argK ] ) ) {
6013                    $newData = $$argK;
6014                    if ( ! is_array( $newData ) ) {
6015                        $newData = array();
6016                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6017                        $newData[ $subK ] = $subV;
6018                    }$$argK = $newData;
6019                } else {
6020                    $$argK = $args[ $argK ]; }
6021            }
6022        }
6023        #} =========== / LOAD ARGS =============
6024
6025        #} ========== CHECK FIELDS ============
6026
6027            $id         = (int) $id;
6028            $objectID   = (int) $objectID;
6029            $objectType = (int) $objectType;
6030
6031        #} ========= / CHECK FIELDS ===========
6032
6033        #} Check ID or name/type
6034        if (
6035            $objectType > 0 &&
6036                (
6037                    ( ! empty( $id ) && $id > 0 )
6038                    ||
6039                    ( ! empty( $objectID ) && $objectID > 0 )
6040                )
6041            ) {
6042
6043            global $ZBSCRM_t, $wpdb;
6044            $wheres          = array( 'direct' => array() );
6045            $whereStr        = '';
6046            $additionalWhere = '';
6047            $params          = array();
6048            $res             = array();
6049
6050            #} Build query
6051            $query = 'SELECT * FROM ' . $ZBSCRM_t['externalsources'];
6052
6053            #} ============= WHERE ================
6054
6055                // Line ID
6056            if ( ! empty( $id ) && $id > 0 ) {
6057                $wheres['ID'] = array( 'ID', '=', '%d', $id );
6058            }
6059
6060                // Object ID
6061            if ( ! empty( $objectID ) && $objectID > 0 ) {
6062                $wheres['zbss_objid'] = array( 'zbss_objid', '=', '%d', $objectID );
6063            }
6064
6065                    $wheres['zbss_objtype'] = array( 'zbss_objtype', '=', '%d', $objectType );
6066
6067                // Source
6068            if ( ! empty( $source ) && $source !== -1 ) {
6069                $wheres['zbss_source'] = array( 'zbss_source', '=', '%s', $source );
6070            }
6071
6072                // Origin
6073            if ( ! empty( $origin ) ) {
6074                $wheres['zbss_origin'] = array( 'zbss_origin', '=', '%s', $origin );
6075            }
6076
6077            #} ============ / WHERE ==============
6078
6079            #} Build out any WHERE clauses
6080            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
6081            $whereStr  = $wheresArr['where'];
6082            $params    = $params + $wheresArr['params'];
6083            #} / Build WHERE
6084
6085            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
6086            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
6087            $ownQ   = $this->ownershipSQL( $ignoreowner );
6088            if ( ! empty( $ownQ ) ) {
6089                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
6090            }
6091            #} / Ownership
6092
6093            #} Append to sql (this also automatically deals with sortby and paging)
6094            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
6095
6096            try {
6097
6098                #} Prep & run query
6099                $queryObj     = $this->prepare( $query, $params );
6100                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
6101
6102            } catch ( Exception $e ) {
6103
6104                #} General SQL Err
6105                $this->catchSQLError( $e );
6106
6107            }
6108
6109            #} Interpret Results (ROW)
6110            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
6111
6112                #} Has results, tidy + return
6113
6114                    #} Only ID? return it directly
6115                if ( $onlyID === true ) {
6116                    return $potentialRes->ID;
6117                }
6118
6119                    // tidy
6120                    $res = $this->tidy_externalsource( $potentialRes );
6121
6122                    return $res;
6123
6124            }
6125        } // / if ID
6126
6127        return false;
6128    }
6129
6130    /**
6131     * returns multiple external source line +- details
6132     *
6133     * @param int id        tag id
6134     * @param array                $args   Associative array of arguments
6135     *                                     withStats
6136     *
6137     * @return array results
6138     */
6139    public function getExternalSources( $id = -1, $args = array() ) {
6140
6141        #} =========== LOAD ARGS ==============
6142        $defaultArgs = array(
6143
6144            'objectID'          => -1,
6145            'objectType'        => -1,
6146
6147            'grouped_by_source' => false, // if true, will return array organised by source (e.g. array('woo'=>array({woosources})))
6148
6149            // permissions
6150            'ignoreowner'       => true, // this'll let you not-check the owner of obj
6151
6152        );
6153        foreach ( $defaultArgs as $argK => $argV ) {
6154            $$argK = $argV;
6155            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6156                if ( is_array( $args[ $argK ] ) ) {
6157                    $newData = $$argK;
6158                    if ( ! is_array( $newData ) ) {
6159                        $newData = array();
6160                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6161                        $newData[ $subK ] = $subV;
6162                    }$$argK = $newData;
6163                } else {
6164                    $$argK = $args[ $argK ]; }
6165            }
6166        }
6167        #} =========== / LOAD ARGS =============
6168
6169        #} ========== CHECK FIELDS ============
6170
6171            $id         = (int) $id;
6172            $objectID   = (int) $objectID;
6173            $objectType = (int) $objectType;
6174
6175        #} ========= / CHECK FIELDS ===========
6176
6177        #} Check ID or name/type
6178        if (
6179            $objectType > 0 &&
6180                (
6181                    ( ! empty( $id ) && $id > 0 )
6182                    ||
6183                    ( ! empty( $objectID ) && $objectID > 0 )
6184                )
6185            ) {
6186
6187            global $ZBSCRM_t, $wpdb;
6188            $wheres          = array( 'direct' => array() );
6189            $whereStr        = '';
6190            $additionalWhere = '';
6191            $params          = array();
6192            $res             = array();
6193
6194            #} Build query
6195            $query = 'SELECT * FROM ' . $ZBSCRM_t['externalsources'];
6196
6197            #} ============= WHERE ================
6198
6199                #} Add ID
6200            if ( ! empty( $id ) && $id > 0 ) {
6201                $wheres['ID'] = array( 'ID', '=', '%d', $id );
6202            }
6203
6204                #} zbss_objid
6205            if ( ! empty( $objectID ) && $objectID > 0 ) {
6206                $wheres['zbss_objid'] = array( 'zbss_objid', '=', '%d', $objectID );
6207            }
6208
6209                // zbss_objid
6210                $wheres['zbss_objtype'] = array( 'zbss_objtype', '=', '%d', $objectType ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
6211
6212            #} ============ / WHERE ==============
6213
6214            #} Build out any WHERE clauses
6215            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
6216            $whereStr  = $wheresArr['where'];
6217            $params    = $params + $wheresArr['params'];
6218            #} / Build WHERE
6219
6220            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
6221            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
6222            $ownQ   = $this->ownershipSQL( $ignoreowner );
6223            if ( ! empty( $ownQ ) ) {
6224                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
6225            }
6226            #} / Ownership
6227
6228            #} Append to sql (this also automatically deals with sortby and paging)
6229            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1000 );
6230
6231            try {
6232
6233                #} Prep & run query
6234                $queryObj     = $this->prepare( $query, $params );
6235                $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
6236
6237            } catch ( Exception $e ) {
6238
6239                #} General SQL Err
6240                $this->catchSQLError( $e );
6241
6242            }
6243
6244            $res = array();
6245
6246            #} Interpret results (Result Set - multi-row)
6247            if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
6248
6249                #} Has results, tidy + return
6250                foreach ( $potentialRes as $data_line ) {
6251
6252                    // default simple array
6253                    if ( ! $grouped_by_source ) {
6254
6255                        // tidy
6256                        $res[] = $this->tidy_externalsource( $data_line );
6257
6258                    } else {
6259
6260                        $tidied_line = $this->tidy_externalsource( $data_line );
6261
6262                        // grouped by source adds another dimension to array, keyed by source (e.g. 'woo')
6263                        if ( ! isset( $res[ $tidied_line['source'] ] ) ) {
6264
6265                            $res[ $tidied_line['source'] ] = array();
6266
6267                        }
6268
6269                        $res[ $tidied_line['source'] ][] = $tidied_line;
6270
6271                    }
6272                }
6273            }
6274
6275            return $res;
6276
6277        } // / if ID
6278
6279        return false;
6280    }
6281
6282    /**
6283     * adds or updates an external source object
6284     *
6285     * @param array $args Associative array of arguments
6286     *
6287     * @return int line ID
6288     */
6289    public function addUpdateExternalSource( $args = array() ) {
6290
6291        global $ZBSCRM_t, $wpdb;
6292
6293        #} ============ LOAD ARGS =============
6294        $defaultArgs = array(
6295
6296            'id'   => -1,
6297
6298            // fields (directly)
6299            'data' => array(
6300
6301                'objectType' => -1,
6302                'objectID'   => -1,
6303                'source'     => '',
6304                'uid'        => '',
6305                'origin'     => '',
6306                'owner'      => 0, // -1 for current user, for now, disregard owenship for ext sources
6307
6308            ),
6309
6310        );
6311        foreach ( $defaultArgs as $argK => $argV ) {
6312            $$argK = $argV;
6313            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6314                if ( is_array( $args[ $argK ] ) ) {
6315                    $newData = $$argK;
6316                    if ( ! is_array( $newData ) ) {
6317                        $newData = array();
6318                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6319                        $newData[ $subK ] = $subV;
6320                    }$$argK = $newData;
6321                } else {
6322                    $$argK = $args[ $argK ]; }
6323            }
6324        }
6325        #} =========== / LOAD ARGS ============
6326
6327        #} ========== CHECK FIELDS ============
6328
6329            $id = (int) $id;
6330
6331        // objectType
6332        if ( empty( $data['objectType'] ) || $data['objectType'] <= 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
6333            return false;
6334        }
6335
6336            // objectID
6337        if ( ! isset( $data['objectID'] ) || $data['objectID'] <= 0 ) {
6338            return false;
6339        }
6340
6341            // if owner = -1, add current
6342        if ( ! isset( $data['owner'] ) || $data['owner'] === -1 ) {
6343            $data['owner'] = zeroBSCRM_user();
6344        }
6345
6346            // check name present + legit
6347        if ( ! isset( $data['source'] ) || empty( $data['source'] ) ) {
6348            return false;
6349        }
6350
6351            // extsource ID finder - if obj source + cid provided, check not already present (if so overwrite)
6352            // keeps unique...
6353        if ( ( empty( $id ) || $id <= 0 )
6354                &&
6355                (
6356                    ( isset( $data['objectType'] ) && ! empty( $data['objectType'] ) ) ||
6357                    ( isset( $data['objectID'] ) && ! empty( $data['objectID'] ) ) ||
6358                    ( isset( $data['source'] ) && ! empty( $data['source'] ) )
6359                ) ) {
6360
6361            $args = array(
6362                'objectType' => $data['objectType'],
6363                'objectID'   => $data['objectID'],
6364                'source'     => $data['source'],
6365                'onlyID'     => true,
6366            );
6367
6368            // check by source + cid
6369            // check existence + return ID
6370            $potentialID = (int) $this->getExternalSource( -1, $args );
6371
6372            // override empty ID
6373            if ( ! empty( $potentialID ) && $potentialID > 0 ) {
6374                $id = $potentialID;
6375            }
6376        }
6377
6378        #} ========= / CHECK FIELDS ===========
6379
6380        #} Check if ID present
6381        $id = (int) $id;
6382        if ( ! empty( $id ) && $id > 0 ) {
6383
6384                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
6385
6386                #} Attempt update
6387            if ( $wpdb->update(
6388                $ZBSCRM_t['externalsources'],
6389                array(
6390
6391                    // ownership
6392                    // no need to update these (as of yet) - can't move teams etc.
6393                    // 'zbs_site' => zeroBSCRM_installSite(),
6394                    // 'zbs_team' => zeroBSCRM_installTeam(),
6395                    'zbs_owner'        => $data['owner'],
6396
6397                    // fields
6398                    'zbss_objid'       => $data['objectID'],
6399                    'zbss_objtype'     => $data['objectType'],
6400                    'zbss_source'      => $data['source'],
6401                    'zbss_uid'         => $data['uid'],
6402                    'zbss_origin'      => $data['origin'],
6403                    'zbss_lastupdated' => time(),
6404
6405                ),
6406                array( // where
6407                    'ID' => $id,
6408                ),
6409                array( // field data types
6410                    '%d',
6411                    '%d',
6412                    '%d',
6413                    '%s',
6414                    '%s',
6415                    '%s',
6416                    '%d',
6417                ),
6418                array( // where data types
6419                    '%d',
6420                )
6421            ) !== false ) {
6422
6423                        // Successfully updated - Return id
6424                        return $id;
6425
6426            } else {
6427
6428                // FAILED update
6429                return false;
6430
6431            }
6432        } else {
6433
6434            #} No ID - must be an INSERT
6435            if ( $wpdb->insert(
6436                $ZBSCRM_t['externalsources'],
6437                array(
6438
6439                    // ownership
6440                    'zbs_site'         => zeroBSCRM_site(),
6441                    'zbs_team'         => zeroBSCRM_team(),
6442                    'zbs_owner'        => $data['owner'],
6443
6444                    // fields
6445                    'zbss_objid'       => $data['objectID'],
6446                    'zbss_objtype'     => $data['objectType'],
6447                    'zbss_source'      => $data['source'],
6448                    'zbss_uid'         => $data['uid'],
6449                    'zbss_origin'      => $data['origin'],
6450                    'zbss_created'     => time(),
6451                    'zbss_lastupdated' => time(),
6452                ),
6453                array( // field data types
6454                    '%d',  // site
6455                    '%d',  // team
6456                    '%d',  // owner
6457
6458                    '%d',
6459                    '%d',
6460                    '%s',
6461                    '%s',
6462                    '%s',
6463                    '%d',
6464                    '%d',
6465                )
6466            ) > 0 ) {
6467
6468                    #} Successfully inserted, lets return new ID
6469                    $newID = $wpdb->insert_id;
6470
6471                    return $newID;
6472
6473            } else {
6474
6475                #} Failed to Insert
6476                return false;
6477
6478            }
6479        }
6480
6481        return false;
6482    }
6483
6484    /**
6485     * adds or updates an object's external sources
6486     *
6487     * @param array $args Associative array of arguments
6488     *
6489     * @return bool success
6490     */
6491    public function addUpdateExternalSources( $args = array() ) {
6492
6493        // ============ LOAD ARGS =============
6494        $defaultArgs = array(
6495
6496            'obj_id'           => -1,
6497            'obj_type_id'      => -1,
6498            'external_sources' => array(),
6499
6500        );
6501        foreach ( $defaultArgs as $argK => $argV ) {
6502            $$argK = $argV;
6503            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6504                if ( is_array( $args[ $argK ] ) ) {
6505                    $newData = $$argK;
6506                    if ( ! is_array( $newData ) ) {
6507                        $newData = array();
6508                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6509                        $newData[ $subK ] = $subV;
6510                    }$$argK = $newData;
6511                } else {
6512                    $$argK = $args[ $argK ]; }
6513            }
6514        }
6515        // =========== / LOAD ARGS ============
6516
6517        $obj_id      = (int) $obj_id;
6518        $obj_type_id = (int) $obj_type_id;
6519        if ( ! is_array( $external_sources ) ) {
6520            return '';
6521        }
6522
6523        $approvedExternalSource = ''; // for IA
6524
6525        foreach ( $external_sources as $es ) {
6526
6527            $external_source_id = isset( $es['id'] ) ? $es['id'] : -1;
6528            $origin             = isset( $es['origin'] ) ? $es['origin'] : null;
6529
6530            $external_source_id = $this->addUpdateExternalSource(
6531                array(
6532
6533                    'id'   => $external_source_id,
6534
6535                    // fields (directly)
6536                    'data' => array(
6537                        'objectID'   => $obj_id,
6538                        'objectType' => $obj_type_id,
6539                        'source'     => $es['source'],
6540                        'origin'     => $origin,
6541                        'uid'        => $es['uid'],
6542                    ),
6543                )
6544            );
6545
6546            $approvedExternalSource = array(
6547                'id'      => $external_source_id,
6548                'objID'   => $obj_id,
6549                'objType' => $obj_type_id,
6550                'source'  => $es['source'],
6551                'origin'  => $origin,
6552                'uid'     => $es['uid'],
6553            );
6554
6555        } // / each ext source
6556
6557        // this is a bit hackish, but allows DRY code without a refactor
6558        return $approvedExternalSource;
6559    }
6560
6561    /**
6562     * deletes an external source object
6563     *
6564     * @param array $args Associative array of arguments
6565     *              id
6566     *
6567     * @return int success;
6568     */
6569    public function deleteExternalSource( $args = array() ) {
6570
6571        global $ZBSCRM_t, $wpdb;
6572
6573        #} ============ LOAD ARGS =============
6574        $defaultArgs = array(
6575
6576            'id' => -1,
6577
6578        );
6579        foreach ( $defaultArgs as $argK => $argV ) {
6580            $$argK = $argV;
6581            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6582                if ( is_array( $args[ $argK ] ) ) {
6583                    $newData = $$argK;
6584                    if ( ! is_array( $newData ) ) {
6585                        $newData = array();
6586                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6587                        $newData[ $subK ] = $subV;
6588                    }$$argK = $newData;
6589                } else {
6590                    $$argK = $args[ $argK ]; }
6591            }
6592        }
6593        #} =========== / LOAD ARGS ============
6594
6595        #} Check ID & Delete :)
6596        $id = (int) $id;
6597        return zeroBSCRM_db2_deleteGeneric( $id, 'externalsources' );
6598    }
6599
6600    /**
6601     * deletes all external source lines for a specific obj
6602     *
6603     * @param array $args Associative array of arguments
6604     *              id
6605     *
6606     * @return int success;
6607     */
6608    public function delete_external_sources( $args = array() ) {
6609
6610        global $ZBSCRM_t, $wpdb;
6611
6612        #} ============ LOAD ARGS =============
6613        $defaultArgs = array(
6614
6615            'obj_type'   => -1,
6616            'obj_id'     => -1,
6617            'obj_source' => 'all', // 'all' = every source
6618
6619        );
6620        foreach ( $defaultArgs as $argK => $argV ) {
6621            $$argK = $argV;
6622            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6623                if ( is_array( $args[ $argK ] ) ) {
6624                    $newData = $$argK;
6625                    if ( ! is_array( $newData ) ) {
6626                        $newData = array();
6627                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6628                        $newData[ $subK ] = $subV;
6629                    }$$argK = $newData;
6630                } else {
6631                    $$argK = $args[ $argK ]; }
6632            }
6633        }
6634        #} =========== / LOAD ARGS ============
6635
6636        #} ========== CHECK FIELDS ============
6637
6638            // checks
6639        if ( empty( $obj_type ) || $this->objTypeKey( $obj_type ) === -1 ) {
6640            return false;
6641        }
6642        if ( empty( $obj_id ) ) {
6643            return false;
6644        }
6645
6646        #} ========= / CHECK FIELDS ===========
6647
6648            // basics
6649            $where = array( // where
6650                'zbss_objtype' => $obj_type,
6651                'zbss_objid'   => $obj_id,
6652            );
6653
6654            $whereFormat = array( // where
6655                '%d',
6656                '%d',
6657            );
6658
6659            // add source if passed add where clause for it (can delete just for a particular source)
6660            if ( $obj_source !== 'all' ) {
6661                $where['zbss_source'] = $obj_source;
6662                $whereFormat[]        = '%s';
6663            }
6664
6665            // brutal
6666            return $wpdb->delete(
6667                $ZBSCRM_t['externalsources'],
6668                $where,
6669                $whereFormat
6670            );
6671    }
6672
6673    /**
6674     * tidy's the object from wp db into clean array
6675     *
6676     * @param array $obj (DB obj)
6677     *
6678     * @return array (clean obj)
6679     */
6680    public function tidy_externalsource( $obj = false ) {
6681
6682            $res = false;
6683
6684        if ( isset( $obj->ID ) ) {
6685            $res       = array();
6686            $res['id'] = $obj->ID;
6687            /*
6688            `zbs_site` INT NULL DEFAULT NULL,
6689            `zbs_team` INT NULL DEFAULT NULL,
6690            `zbs_owner` INT NOT NULL,
6691            */
6692
6693            $res['objid']   = $obj->zbss_objid;
6694            $res['objtype'] = $obj->zbss_objtype;
6695            $res['source']  = $obj->zbss_source;
6696            $res['uid']     = $this->stripSlashes( $obj->zbss_uid );
6697            $res['origin']  = $this->stripSlashes( $obj->zbss_origin );
6698
6699            $res['created']     = $obj->zbss_created;
6700            $res['lastupdated'] = $obj->zbss_lastupdated;
6701
6702        }
6703
6704        return $res;
6705    }
6706
6707    /**
6708     * Returns a clean domain origin string for use with external sources, where possible
6709     *
6710     * @return string|bool(false) - tidied up domain origin, or false
6711     */
6712    public function clean_external_source_domain_string( $domain ) {
6713
6714        // clean it up a bit
6715        $origin = str_replace( array( 'https://', 'http://' ), '', $domain );
6716        $origin = rtrim( $origin, '/' );
6717
6718        // prefix it for later querying
6719        if ( ! empty( $origin ) ) {
6720
6721            return $origin;
6722
6723        }
6724
6725        return false;
6726    }
6727
6728    // =========== / External Sources      ===========================================
6729    // ===============================================================================
6730
6731    // ===============================================================================
6732    // ===========  Origin Helpers      ==============================================
6733    // To start with these will help store origin strings for external sources in a
6734    // machine-readable format
6735
6736    /**
6737     * Returns a prefixed origin string
6738     * (Currently only origins stored are domains)
6739     *
6740     * @param string $string - string to prefix
6741     * @param string $origin_type - a type of origin record
6742     *
6743     * @return string|bool(false) - prefixed origin string, or false
6744     */
6745    public function add_origin_prefix( $string, $origin_type ) {
6746
6747        switch ( $origin_type ) {
6748
6749            case 'domain':
6750                return $this->prefix_domain . $string;
6751                break;
6752
6753        }
6754
6755        return false;
6756    }
6757
6758    /**
6759     * Returns a de-prefixed origin string
6760     *
6761     * @param string $string - string to prefix
6762     *
6763     * @return string|bool(false) - deprefixed origin string, or false
6764     */
6765    public function remove_origin_prefix( $string ) {
6766
6767        // split at first :
6768        $split_point = strpos( $string, ':' );
6769
6770        if ( $split_point ) {
6771
6772            return substr( $string, $split_point + 1 );
6773
6774        }
6775
6776        return false;
6777    }
6778
6779    /**
6780     * Returns an origin string and type from a prefixed origin string
6781     *
6782     * @param string $string Prefixed origin string.
6783     *
6784     * @return array|bool(false) - origin string and type, or false
6785     */
6786    public function hydrate_origin( $string ) {
6787
6788        // domain
6789        if ( str_starts_with( $string, 'd:' ) ) {
6790
6791            return array(
6792                'origin'      => $this->remove_origin_prefix( $string ),
6793                'origin_type' => 'domain',
6794            );
6795
6796        }
6797
6798        return false;
6799    }
6800
6801    // =========== / Origin Helpers      =============================================
6802    // ===============================================================================
6803
6804    // ===============================================================================
6805    // ===========   Web Tracking (UTM etc.)  ========================================
6806
6807    /**
6808     * returns full tracking line +- details
6809     *
6810     * @param int id        tag id
6811     * @param array                $args   Associative array of arguments
6812     *                                     withStats
6813     *
6814     * @return array result
6815     */
6816    public function getTracking( $id = -1, $args = array() ) {
6817
6818        #} =========== LOAD ARGS ==============
6819        $defaultArgs = array(
6820
6821            // permissions
6822            'ignoreowner' => false, // this'll let you not-check the owner of obj
6823
6824            // returns scalar ID of line
6825            'onlyID'      => false,
6826
6827        );
6828        foreach ( $defaultArgs as $argK => $argV ) {
6829            $$argK = $argV;
6830            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6831                if ( is_array( $args[ $argK ] ) ) {
6832                    $newData = $$argK;
6833                    if ( ! is_array( $newData ) ) {
6834                        $newData = array();
6835                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6836                        $newData[ $subK ] = $subV;
6837                    }$$argK = $newData;
6838                } else {
6839                    $$argK = $args[ $argK ]; }
6840            }
6841        }
6842        #} =========== / LOAD ARGS =============
6843
6844        #} ========== CHECK FIELDS ============
6845
6846            $id = (int) $id;
6847        #} ========= / CHECK FIELDS ===========
6848
6849        #} Check ID or name/type
6850        if ( ! empty( $id ) && $id > 0 ) {
6851
6852            global $ZBSCRM_t, $wpdb;
6853            $wheres          = array( 'direct' => array() );
6854            $whereStr        = '';
6855            $additionalWhere = '';
6856            $params          = array();
6857            $res             = array();
6858
6859            #} Build query
6860            $query = 'SELECT * FROM ' . $ZBSCRM_t['tracking'];
6861
6862            #} ============= WHERE ================
6863
6864                // Add ID
6865                $wheres['ID'] = array( 'ID', '=', '%d', $id );
6866
6867            #} ============ / WHERE ==============
6868
6869            #} Build out any WHERE clauses
6870            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
6871            $whereStr  = $wheresArr['where'];
6872            $params    = $params + $wheresArr['params'];
6873            #} / Build WHERE
6874
6875            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
6876            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
6877            $ownQ   = $this->ownershipSQL( $ignoreowner );
6878            if ( ! empty( $ownQ ) ) {
6879                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
6880            }
6881            #} / Ownership
6882
6883            #} Append to sql (this also automatically deals with sortby and paging)
6884            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
6885
6886            try {
6887
6888                #} Prep & run query
6889                $queryObj     = $this->prepare( $query, $params );
6890                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
6891
6892            } catch ( Exception $e ) {
6893
6894                #} General SQL Err
6895                $this->catchSQLError( $e );
6896
6897            }
6898
6899            #} Interpret Results (ROW)
6900            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
6901
6902                #} Has results, tidy + return
6903
6904                    #} Only ID? return it directly
6905                if ( $onlyID === true ) {
6906                    return $potentialRes->ID;
6907                }
6908
6909                    // tidy
6910                    $res = $this->tidy_tracking( $potentialRes );
6911
6912                    return $res;
6913
6914            }
6915        } // / if ID
6916
6917        return false;
6918    }
6919
6920    /**
6921     * adds or updates a tracking object
6922     *
6923     * @param array $args Associative array of arguments
6924     *
6925     * @return int line ID
6926     */
6927    public function addUpdateTracking( $args = array() ) {
6928
6929        global $ZBSCRM_t, $wpdb;
6930
6931        #} ============ LOAD ARGS =============
6932        $defaultArgs = array(
6933
6934            'id'   => -1,
6935
6936            // fields (directly)
6937            'data' => array(
6938
6939                'contactID'     => -1,
6940                'action'        => '',
6941                'action_detail' => '',
6942                'referrer'      => '',
6943                'utm_source'    => '',
6944                'utm_medium'    => '',
6945                'utm_name'      => '',
6946                'utm_term'      => '',
6947                'utm_content'   => '',
6948
6949                'owner'         => -1,
6950
6951            ),
6952
6953        );
6954        foreach ( $defaultArgs as $argK => $argV ) {
6955            $$argK = $argV;
6956            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
6957                if ( is_array( $args[ $argK ] ) ) {
6958                    $newData = $$argK;
6959                    if ( ! is_array( $newData ) ) {
6960                        $newData = array();
6961                    } foreach ( $args[ $argK ] as $subK => $subV ) {
6962                        $newData[ $subK ] = $subV;
6963                    }$$argK = $newData;
6964                } else {
6965                    $$argK = $args[ $argK ]; }
6966            }
6967        }
6968        #} =========== / LOAD ARGS ============
6969
6970        #} ========== CHECK FIELDS ============
6971
6972            $id = (int) $id;
6973
6974        // contactID
6975        if ( empty( $data['contactID'] ) || $data['contactID'] <= 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
6976            return false;
6977        }
6978
6979            // if owner = -1, add current
6980        if ( ! isset( $data['owner'] ) || $data['owner'] === -1 ) {
6981            $data['owner'] = zeroBSCRM_user();
6982        }
6983
6984            // check action present + legit
6985        if ( ! isset( $data['action'] ) || empty( $data['action'] ) ) {
6986            return false;
6987        }
6988
6989        #} ========= / CHECK FIELDS ===========
6990
6991        #} Check if ID present
6992        $id = (int) $id;
6993        if ( ! empty( $id ) && $id > 0 ) {
6994
6995                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
6996
6997                #} Attempt update
6998            if ( $wpdb->update(
6999                $ZBSCRM_t['tracking'],
7000                array(
7001
7002                    // ownership
7003                    // no need to update these (as of yet) - can't move teams etc.
7004                    // 'zbs_site' => zeroBSCRM_installSite(),
7005                    // 'zbs_team' => zeroBSCRM_installTeam(),
7006                    'zbs_owner'          => $data['owner'],
7007
7008                    // fields
7009                    'zbst_contactid'     => $data['contactID'],
7010                    'zbst_action'        => $data['action'],
7011                    'zbst_action_detail' => $data['action_detail'],
7012                    'zbst_referrer'      => $data['referrer'],
7013                    'zbst_utm_source'    => $data['utm_source'],
7014                    'zbst_utm_medium'    => $data['utm_medium'],
7015                    'zbst_utm_name'      => $data['utm_name'],
7016                    'zbst_utm_term'      => $data['utm_term'],
7017                    'zbst_utm_content'   => $data['utm_content'],
7018
7019                    'zbst_lastupdated'   => time(),
7020                ),
7021                array( // where
7022                    'ID' => $id,
7023                ),
7024                array( // field data types
7025                    '%d',
7026
7027                    '%d',
7028                    '%s',
7029                    '%s',
7030                    '%s',
7031                    '%s',
7032                    '%s',
7033                    '%s',
7034                    '%s',
7035                    '%s',
7036
7037                    '%d',
7038                ),
7039                array( // where data types
7040                    '%d',
7041                )
7042            ) !== false ) {
7043
7044                        // Successfully updated - Return id
7045                        return $id;
7046
7047            } else {
7048
7049                // FAILED update
7050                return false;
7051
7052            }
7053        } else {
7054
7055            #} No ID - must be an INSERT
7056            if ( $wpdb->insert(
7057                $ZBSCRM_t['tracking'],
7058                array(
7059
7060                    // ownership
7061                    'zbs_site'           => zeroBSCRM_site(),
7062                    'zbs_team'           => zeroBSCRM_team(),
7063                    'zbs_owner'          => $data['owner'],
7064
7065                    // fields
7066                    'zbst_contactid'     => $data['contactID'],
7067                    'zbst_action'        => $data['action'],
7068                    'zbst_action_detail' => $data['action_detail'],
7069                    'zbst_referrer'      => $data['referrer'],
7070                    'zbst_utm_source'    => $data['utm_source'],
7071                    'zbst_utm_medium'    => $data['utm_medium'],
7072                    'zbst_utm_name'      => $data['utm_name'],
7073                    'zbst_utm_term'      => $data['utm_term'],
7074                    'zbst_utm_content'   => $data['utm_content'],
7075
7076                    'zbst_created'       => time(),
7077                    'zbst_lastupdated'   => time(),
7078                ),
7079                array( // field data types
7080                    '%d',  // site
7081                    '%d',  // team
7082                    '%d',  // owner
7083
7084                    '%d',
7085                    '%s',
7086                    '%s',
7087                    '%s',
7088                    '%s',
7089                    '%s',
7090                    '%s',
7091                    '%s',
7092                    '%s',
7093
7094                    '%d',
7095                    '%d',
7096                )
7097            ) > 0 ) {
7098
7099                    #} Successfully inserted, lets return new ID
7100                    $newID = $wpdb->insert_id;
7101                    return $newID;
7102
7103            } else {
7104
7105                #} Failed to Insert
7106                return false;
7107
7108            }
7109        }
7110
7111        return false;
7112    }
7113
7114    /**
7115     * deletes a tracking object
7116     *
7117     * @param array $args Associative array of arguments
7118     *              id
7119     *
7120     * @return int success;
7121     */
7122    public function deleteTracking( $args = array() ) {
7123
7124        global $ZBSCRM_t, $wpdb;
7125
7126        #} ============ LOAD ARGS =============
7127        $defaultArgs = array(
7128
7129            'id' => -1,
7130
7131        );
7132        foreach ( $defaultArgs as $argK => $argV ) {
7133            $$argK = $argV;
7134            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7135                if ( is_array( $args[ $argK ] ) ) {
7136                    $newData = $$argK;
7137                    if ( ! is_array( $newData ) ) {
7138                        $newData = array();
7139                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7140                        $newData[ $subK ] = $subV;
7141                    }$$argK = $newData;
7142                } else {
7143                    $$argK = $args[ $argK ]; }
7144            }
7145        }
7146        #} =========== / LOAD ARGS ============
7147
7148        #} Check ID & Delete :)
7149        $id = (int) $id;
7150        return zeroBSCRM_db2_deleteGeneric( $id, 'tracking' );
7151    }
7152
7153    /**
7154     * tidy's the object from wp db into clean array
7155     *
7156     * @param array $obj (DB obj)
7157     *
7158     * @return array (clean obj)
7159     */
7160    private function tidy_tracking( $obj = false ) {
7161
7162            $res = false;
7163
7164        if ( isset( $obj->ID ) ) {
7165            $res       = array();
7166            $res['id'] = $obj->ID;
7167            /*
7168            `zbs_site` INT NULL DEFAULT NULL,
7169            `zbs_team` INT NULL DEFAULT NULL,
7170            `zbs_owner` INT NOT NULL,
7171            */
7172
7173            $res['contactid']     = $obj->zbss_contactid;
7174            $res['action']        = $obj->zbst_action;
7175            $res['action_detail'] = $obj->zbst_action_detail;
7176            $res['referrer']      = $obj->zbst_referrer;
7177            $res['utm_source']    = $obj->zbst_utm_source;
7178            $res['utm_medium']    = $obj->zbst_utm_medium;
7179            $res['utm_name']      = $obj->zbst_utm_name;
7180            $res['utm_term']      = $obj->zbst_utm_term;
7181            $res['utm_content']   = $obj->zbst_utm_content;
7182
7183            $res['created']     = $obj->zbst_created;
7184            $res['lastupdated'] = $obj->zbst_lastupdated;
7185
7186        }
7187
7188        return $res;
7189    }
7190
7191    // =========== / Web Tracking (UTM etc.)      ====================================
7192    // ===============================================================================
7193
7194    // ===============================================================================
7195    // ===========   LOGS   ==========================================================
7196
7197    /**
7198     * returns cron log lines
7199     *
7200     * @param array $args Associative array of arguments
7201     *              searchPhrase, sortByField, sortOrder, page, perPage
7202     *
7203     * @return array of tag lines
7204     */
7205    public function getCronLogs( $args = array() ) {
7206
7207        // ============ LOAD ARGS =============
7208        $with_notes = true;
7209
7210        // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
7211        $defaultArgs = array(
7212            'job'         => '',
7213            'sortByField' => 'ID', // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
7214            'sortOrder'   => 'DESC', // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
7215            'page'        => 0,
7216            'perPage'     => 100, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
7217            'with_notes'  => true,
7218            'ignoreowner' => false, // this'll let you not-check the owner of obj
7219
7220        );
7221        foreach ( $defaultArgs as $argK => $argV ) {
7222            $$argK = $argV;
7223            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7224                if ( is_array( $args[ $argK ] ) ) {
7225                    $newData = $$argK;
7226                    if ( ! is_array( $newData ) ) {
7227                        $newData = array();
7228                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7229                        $newData[ $subK ] = $subV;
7230                    }$$argK = $newData;
7231                } else {
7232                    $$argK = $args[ $argK ]; }
7233            }
7234        }
7235        #} =========== / LOAD ARGS =============
7236
7237        #} ========== CHECK FIELDS ============
7238
7239        #} ========= / CHECK FIELDS ===========
7240
7241        global $ZBSCRM_t, $wpdb;
7242        $wheres          = array( 'direct' => array() );
7243        $whereStr        = '';
7244        $additionalWhere = '';
7245        $params          = array();
7246        $res             = array();
7247
7248        #} Build query
7249        $query = 'SELECT * FROM ' . $ZBSCRM_t['cronmanagerlogs'];
7250
7251        #} ============= WHERE ================
7252
7253            #} job
7254        if ( ! empty( $job ) && $job > 0 ) {
7255            $wheres['job'] = array( 'job', '=', '%s', $job );
7256        }
7257
7258        if ( $with_notes ) {
7259            $wheres['notes'] = array( 'jobnotes', '<>', '%s', '' );
7260        }
7261
7262        #} ============ / WHERE ===============
7263
7264        #} Build out any WHERE clauses
7265        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
7266        $whereStr  = $wheresArr['where'];
7267        $params    = $params + $wheresArr['params'];
7268        #} / Build WHERE
7269
7270        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
7271        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
7272        $ownQ   = $this->ownershipSQL( $ignoreowner );
7273        if ( ! empty( $ownQ ) ) {
7274            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
7275        }
7276        #} / Ownership
7277
7278        #} Append to sql (this also automatically deals with sortby and paging)
7279        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
7280
7281        try {
7282
7283            #} Prep & run query
7284            $queryObj     = $this->prepare( $query, $params );
7285            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
7286
7287        } catch ( Exception $e ) {
7288
7289            #} General SQL Err
7290            $this->catchSQLError( $e );
7291
7292        }
7293
7294        #} Interpret results (Result Set - multi-row)
7295        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
7296
7297            #} Has results, tidy + return
7298            foreach ( $potentialRes as $resDataLine ) {
7299
7300                    // tidy
7301                    $resArr = $this->tidy_cronlog( $resDataLine );
7302
7303                    $res[] = $resArr;
7304
7305            }
7306        }
7307
7308        return $res;
7309    }
7310
7311    /**
7312     * adds or updates a cron log object
7313     *
7314     * @param array $args Associative array of arguments
7315     *              id (not req.), owner (not req.) data -> key/val
7316     *
7317     * @return int line ID
7318     */
7319    public function addUpdateCronLog( $args = array() ) {
7320
7321        global $ZBSCRM_t, $wpdb;
7322
7323        #} ============ LOAD ARGS =============
7324        $defaultArgs = array(
7325
7326            'id'    => -1,
7327            'owner' => -1,
7328
7329            // fields (directly)
7330            'data'  => array(
7331
7332                'job'         => '',
7333                'jobstatus'   => -1,
7334                'jobstarted'  => -1,
7335                'jobfinished' => -1,
7336                'jobnotes'    => '',
7337
7338            ),
7339
7340        );
7341        foreach ( $defaultArgs as $argK => $argV ) {
7342            $$argK = $argV;
7343            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7344                if ( is_array( $args[ $argK ] ) ) {
7345                    $newData = $$argK;
7346                    if ( ! is_array( $newData ) ) {
7347                        $newData = array();
7348                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7349                        $newData[ $subK ] = $subV;
7350                    }$$argK = $newData;
7351                } else {
7352                    $$argK = $args[ $argK ]; }
7353            }
7354        }
7355        #} =========== / LOAD ARGS ============
7356
7357        #} ========== CHECK FIELDS ============
7358
7359            $id = (int) $id;
7360
7361        // if owner = -1, add current
7362        if ( $owner === -1 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
7363            $owner = zeroBSCRM_user();
7364        }
7365
7366        #} ========= / CHECK FIELDS ===========
7367
7368        $dataArr = array(
7369
7370            // ownership
7371            // no need to update these (as of yet) - can't move teams etc.
7372            // 'zbs_site' => zeroBSCRM_installSite(),
7373            // 'zbs_team' => zeroBSCRM_installTeam(),
7374            'zbs_owner'   => $owner,
7375
7376            // fields
7377            'job'         => $data['job'],
7378            'jobstatus'   => $data['jobstatus'],
7379            'jobstarted'  => $data['jobstarted'],
7380            'jobfinished' => $data['jobfinished'],
7381            'jobnotes'    => $data['jobnotes'],
7382        );
7383
7384        $dataTypes = array( // field data types
7385            '%d',
7386
7387            '%s',
7388            '%d',
7389            '%d',
7390            '%d',
7391            '%s',
7392        );
7393
7394        if ( isset( $id ) && ! empty( $id ) && $id > 0 ) {
7395
7396                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
7397
7398                #} Attempt update
7399            if ( $wpdb->update(
7400                $ZBSCRM_t['cronmanagerlogs'],
7401                $dataArr,
7402                array( // where
7403                    'ID' => $id,
7404                ),
7405                $dataTypes,
7406                array( // where data types
7407                    '%d',
7408                )
7409            ) !== false ) {
7410
7411                        // Successfully updated - Return id
7412                        return $id;
7413
7414            } else {
7415
7416                // FAILED update
7417                return false;
7418
7419            }
7420        } else {
7421
7422            // add team etc
7423            $dataArr['zbs_site'] = zeroBSCRM_site();
7424            $dataTypes[]         = '%d';
7425            $dataArr['zbs_team'] = zeroBSCRM_team();
7426            $dataTypes[]         = '%d';
7427
7428            #} No ID - must be an INSERT
7429            if ( $wpdb->insert(
7430                $ZBSCRM_t['cronmanagerlogs'],
7431                $dataArr,
7432                $dataTypes
7433            ) > 0 ) {
7434
7435                    #} Successfully inserted, lets return new ID
7436                    $newID = $wpdb->insert_id;
7437
7438                    return $newID;
7439
7440            } else {
7441
7442                #} Failed to Insert
7443                return false;
7444
7445            }
7446        }
7447
7448        return false;
7449    }
7450
7451    /**
7452     * deletes a CRON Log object
7453     * NOTE! this doesn't yet delete any META!
7454     *
7455     * @param array $args Associative array of arguments
7456     *              id
7457     *
7458     * @return int success;
7459     */
7460    public function deleteCronLog( $args = array() ) {
7461
7462        global $ZBSCRM_t, $wpdb;
7463
7464        #} ============ LOAD ARGS =============
7465        $defaultArgs = array(
7466
7467            'id' => -1,
7468
7469        );
7470        foreach ( $defaultArgs as $argK => $argV ) {
7471            $$argK = $argV;
7472            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7473                if ( is_array( $args[ $argK ] ) ) {
7474                    $newData = $$argK;
7475                    if ( ! is_array( $newData ) ) {
7476                        $newData = array();
7477                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7478                        $newData[ $subK ] = $subV;
7479                    }$$argK = $newData;
7480                } else {
7481                    $$argK = $args[ $argK ]; }
7482            }
7483        }
7484        #} =========== / LOAD ARGS ============
7485
7486        #} Check ID & Delete :)
7487        $id = (int) $id;
7488        return zeroBSCRM_db2_deleteGeneric( $id, 'cronmanagerlogs' );
7489    }
7490
7491    /**
7492     * tidy's the object from wp db into clean array
7493     *
7494     * @param array $obj (DB obj)
7495     *
7496     * @return array (clean obj)
7497     */
7498    private function tidy_cronlog( $obj = false ) {
7499
7500            $res = false;
7501
7502        if ( isset( $obj->ID ) ) {
7503            $res          = array();
7504            $res['id']    = $obj->ID;
7505            $res['owner'] = $obj->zbs_owner;
7506
7507            $res['job']         = $obj->job;
7508            $res['jobstatus']   = $obj->jobstatus;
7509            $res['jobstarted']  = $obj->jobstarted;
7510            $res['jobfinished'] = $obj->jobfinished;
7511            $res['jobnotes']    = $obj->jobnotes;
7512
7513        }
7514
7515        return $res;
7516    }
7517
7518    // =========== / CRONLOGS  =======================================================
7519    // ===============================================================================
7520
7521    // ===============================================================================
7522    // ============= GENERIC  ========================================================
7523
7524    /**
7525     * Wrapper function for emptying tables (use with care)
7526     * ... can only truncate tables in our ZBSCRM_t
7527     *
7528     * @param string $tableKey (refers to ZBSCRM_t global)
7529     *
7530     * @return result
7531     */
7532    public function truncate( $tableKey = '' ) {
7533
7534        global $ZBSCRM_t;
7535
7536        if ( is_string( $tableKey ) && ! empty( $tableKey ) && isset( $ZBSCRM_t[ $tableKey ] ) ) {
7537
7538            global $wpdb;
7539            return $wpdb->query( 'TRUNCATE TABLE `' . $ZBSCRM_t[ $tableKey ] . '`' );
7540
7541        }
7542
7543        return false;
7544    }
7545
7546    // =========== / GENERIC  ========================================================
7547    // ===============================================================================
7548
7549    // ===============================================================================
7550    // ===========   FIELD HELPERS     ===============================================
7551
7552    /**
7553     * Returns a field from an object model if it exists
7554     *
7555     * @param CRM_TYPE int object_type_id - object type ID
7556     * @param string field_key - key of field (e.g. `status`)
7557     *
7558     * @return bool|array - field type info (as per direct from the object_model)
7559     */
7560    public function get_model_field_info( $object_type_id = -1, $field_key = '' ) {
7561
7562        // valid obj type id and key?
7563        if ( $this->isValidObjTypeID( $object_type_id ) && ! empty( $field_key ) ) {
7564
7565            // get object layer and load object model
7566            $object_layer = $this->getObjectLayerByType( $object_type_id );
7567            if ( $object_layer ) {
7568
7569                $object_model = $object_layer->objModel( true );
7570
7571                // if set, return
7572                if ( is_array( $object_model ) && isset( $object_model[ $field_key ] ) ) {
7573
7574                    return $object_model[ $field_key ];
7575
7576                }
7577            }
7578
7579            // as a temporary workaround until we have addresses loaded from DAL3
7580            // check address field presence via global
7581            if ( $object_type_id == ZBS_TYPE_ADDRESS ) {
7582
7583                // effectively returns $zbsAddressFields:
7584                $obj_model_global = $this->get_object_field_global( ZBS_TYPE_ADDRESS );
7585
7586                if ( isset( $obj_model_global[ $field_key ] ) ) {
7587
7588                    // As an aside, the $potential_field format will be different here
7589                    // as this takes from the globals model, not the DAL model
7590                    // ... please bear this in mind if interacting with address fields this way
7591                    return $obj_model_global[ $field_key ];
7592
7593                }
7594            }
7595        }
7596
7597        return false;
7598    }
7599
7600    /**
7601     * Returns true if a field from an object model exists
7602     *
7603     * @param CRM_TYPE int object_type_id - object type ID
7604     * @param string field_key - key of field (e.g. `status`)
7605     *
7606     * @return bool - does field exist
7607     */
7608    public function does_model_field_exist( $object_type_id = -1, $field_key = '' ) {
7609
7610        // valid obj type id and key?
7611        if ( $this->isValidObjTypeID( $object_type_id ) && ! empty( $field_key ) ) {
7612
7613            // Check for field existence
7614            $potential_field = $this->get_model_field_info( $object_type_id, $field_key );
7615
7616            if ( $potential_field !== false ) {
7617
7618                // found it
7619                return true;
7620
7621            }
7622        }
7623
7624        return false;
7625    }
7626
7627    // =========== / FIELD HELPERS     ===============================================
7628    // ===============================================================================
7629
7630    /*
7631    ======================================================
7632    / DAL CRUD
7633    ======================================================
7634    */
7635
7636    /*
7637    ======================================================
7638    Formatters (Generic)
7639    ======================================================
7640    */
7641
7642    // legacy signpost, this is now overwritten by DAL->contacts->fullname
7643    public function format_fullname( $contactArr = array() ) {
7644
7645        return $this->contacts->format_fullname( $contactArr );
7646    }
7647
7648    // legacy signpost, this is now overwritten by DAL->[contacts|companies]->format_name_etc
7649    public function format_name_etc( $objectArr = array(), $args = array() ) {
7650
7651        #} =========== LOAD ARGS ==============
7652        $defaultArgs = array(
7653
7654            'incFirstLineAddr' => false,
7655            'incID'            => false,
7656            'company'          => false, // if true, looks for 'name' not 'fname+lname'
7657
7658        );
7659        foreach ( $defaultArgs as $argK => $argV ) {
7660            $$argK = $argV;
7661            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7662                if ( is_array( $args[ $argK ] ) ) {
7663                    $newData = $$argK;
7664                    if ( ! is_array( $newData ) ) {
7665                        $newData = array();
7666                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7667                        $newData[ $subK ] = $subV;
7668                    }$$argK = $newData;
7669                } else {
7670                    $$argK = $args[ $argK ]; }
7671            }
7672        }
7673        #} =========== / LOAD ARGS =============
7674
7675        if ( ! $company ) {
7676            return $this->contacts->format_name_etc( $objectArr, $args );
7677        } else {
7678            return $this->companies->format_name_etc( $objectArr, $args );
7679        }
7680    }
7681
7682    /**
7683     * Returns a formatted address
7684     * via getContactFullName this replaces zeroBS_customerAddr in dal1
7685     * NOTE, post v3.0 applies to ANY form of addr.
7686     *
7687     * @param array $obj (tidied db obj)
7688     *
7689     * @return string address
7690     */
7691    public function format_address( $contactArr = array(), $args = array() ) {
7692
7693        #} =========== LOAD ARGS ==============
7694        $defaultArgs = array(
7695
7696            'addrFormat' => 'short',
7697            'delimiter'  => ', ', // could use <br>
7698            'secondaddr' => false, // if true, use second address (if present in contact_arr)
7699
7700        );
7701        foreach ( $defaultArgs as $argK => $argV ) {
7702            $$argK = $argV;
7703            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7704                if ( is_array( $args[ $argK ] ) ) {
7705                    $newData = $$argK;
7706                    if ( ! is_array( $newData ) ) {
7707                        $newData = array();
7708                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7709                        $newData[ $subK ] = $subV;
7710                    }$$argK = $newData;
7711                } else {
7712                    $$argK = $args[ $argK ]; }
7713            }
7714        }
7715        #} =========== / LOAD ARGS =============
7716
7717        $ret         = '';
7718        $fieldPrefix = '';
7719        if ( $secondaddr ) {
7720            $fieldPrefix = 'sec';
7721        }
7722        // v3.0 exception, contacts need this prefix for second :/
7723        // attempt to account for that:
7724        if ( $secondaddr && ! isset( $contactArr[ $fieldPrefix . 'addr1' ] ) && isset( $contactArr[ 'secaddr_' . 'addr1' ] ) ) {
7725            $fieldPrefix = 'secaddr_';
7726        }
7727
7728        #} Legacy from DAL1:
7729        $addrCustomFields = $this->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_ADDRESS ) );
7730
7731        if ( $addrFormat == 'short' ) {
7732
7733            if ( isset( $contactArr[ $fieldPrefix . 'addr1' ] ) && ! empty( $contactArr[ $fieldPrefix . 'addr1' ] ) ) {
7734                $ret = $contactArr[ $fieldPrefix . 'addr1' ];
7735            }
7736            if ( isset( $contactArr[ $fieldPrefix . 'city' ] ) && ! empty( $contactArr[ $fieldPrefix . 'city' ] ) ) {
7737                $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $fieldPrefix . 'city' ];
7738            }
7739        } elseif ( $addrFormat == 'full' ) {
7740
7741            if ( isset( $contactArr[ $fieldPrefix . 'addr1' ] ) && ! empty( $contactArr[ $fieldPrefix . 'addr1' ] ) ) {
7742                $ret = $contactArr[ $fieldPrefix . 'addr1' ];
7743            }
7744            if ( isset( $contactArr[ $fieldPrefix . 'addr2' ] ) && ! empty( $contactArr[ $fieldPrefix . 'addr2' ] ) ) {
7745                $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $fieldPrefix . 'addr2' ];
7746            }
7747            if ( isset( $contactArr[ $fieldPrefix . 'city' ] ) && ! empty( $contactArr[ $fieldPrefix . 'city' ] ) ) {
7748                $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $fieldPrefix . 'city' ];
7749            }
7750            if ( isset( $contactArr[ $fieldPrefix . 'county' ] ) && ! empty( $contactArr[ $fieldPrefix . 'county' ] ) ) {
7751                $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $fieldPrefix . 'county' ];
7752            }
7753            if ( isset( $contactArr[ $fieldPrefix . 'postcode' ] ) && ! empty( $contactArr[ $fieldPrefix . 'postcode' ] ) ) {
7754                $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $fieldPrefix . 'postcode' ];
7755            }
7756            if ( isset( $contactArr[ $fieldPrefix . 'country' ] ) && ! empty( $contactArr[ $fieldPrefix . 'country' ] ) && zeroBSCRM_getSetting( 'countries' ) == 1 ) {
7757                $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $fieldPrefix . 'country' ];
7758            }
7759
7760            // any custom fields here
7761            if ( is_array( $addrCustomFields ) && count( $addrCustomFields ) > 0 ) {
7762
7763                foreach ( $addrCustomFields as $cK => $cF ) {
7764
7765                    // v2:
7766                    // $cKN = (int)$cK+1;
7767                    // $cKey = $fieldPrefix.'addr_cf'.$cKN;
7768                    // v3:
7769                    $cKey = ( $secondaddr ) ? 'secaddr_' . $cK : 'addr_' . $cK;
7770
7771                    if ( isset( $contactArr[ $cKey ] ) && ! empty( $contactArr[ $cKey ] ) ) {
7772
7773                        // if someone is using date custom fields here, output date, not uts - super edge case gh-349
7774                        if ( isset( $contactArr[ $cKey . '_cfdate' ] ) && ! empty( $contactArr[ $cKey . '_cfdate' ] ) ) {
7775                            $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $cKey . '_cfdate' ];
7776                        } else {
7777                            $ret .= $this->delimiterIf( $delimiter, $ret ) . $contactArr[ $cKey ];
7778                        }
7779                    }
7780                }
7781            }
7782        }
7783
7784        $trimRet = trim( $ret );
7785        return $trimRet;
7786    }
7787
7788    public function makeSlug( $string, $replace = array(), $delimiter = '-' ) {
7789
7790        // NOTE: the following can likely be replaced with sanitize_title
7791        // https://wordpress.stackexchange.com/questions/74415/how-does-wordpress-generate-url-slugs
7792        // return sanitize_title(sanitize_title($string, '', 'save'), '', 'query');
7793
7794        // https://github.com/phalcon/incubator/blob/master/Library/Phalcon/Utils/Slug.php
7795        // and
7796        // https://stackoverflow.com/questions/4910627/php-iconv-translit-for-removing-accents-not-working-as-excepted
7797        // if (!extension_loaded('iconv')) {
7798        // throw new Exception('iconv module not loaded');
7799        // }
7800        // Save the old locale and set the new locale to UTF-8
7801        $oldLocale = setlocale( LC_ALL, '0' );
7802        setlocale( LC_ALL, 'en_US.UTF-8' );
7803
7804        // replace non letter or digits by -
7805        $clean = preg_replace( '#[^\\pL\d]+#u', '-', $string );
7806
7807        // transliterate
7808        if ( function_exists( 'iconv' ) ) {
7809            $clean = @iconv( 'UTF-8', 'ASCII//TRANSLIT', $clean );
7810        }
7811        // else? smt else?
7812
7813        // replace
7814        if ( ! empty( $replace ) ) {
7815            $clean = str_replace( (array) $replace, ' ', $clean );
7816        }
7817
7818        // clean
7819        $clean = $this->makeSlugCleanStr( $clean, $delimiter );
7820
7821        // Revert back to the old locale
7822        setlocale( LC_ALL, $oldLocale );
7823        return $clean;
7824    }
7825
7826    private function makeSlugCleanStr( $string = '', $delimiter = '-' ) {
7827
7828        // fix for ascii passing (I think) of ' resulting in -039- in place of '
7829        $string = str_replace( '-039-', '', $string );
7830
7831        // replace non letter or non digits by -
7832        $string = preg_replace( '#[^\pL\d]+#u', '-', $string );
7833        // Trim trailing -
7834        $string = trim( $string, '-' );
7835        $clean  = preg_replace( '~[^-\w]+~', '', $string );
7836        $clean  = strtolower( $clean );
7837        $clean  = preg_replace( '#[\/_|+ -]+#', $delimiter, $clean );
7838        $clean  = trim( $clean, $delimiter );
7839
7840        return $clean;
7841    }
7842
7843    /*
7844    ======================================================
7845    / Formatters
7846    ======================================================
7847    */
7848
7849    /*
7850    ======================================================
7851    To be sorted helpers
7852    ======================================================
7853    */
7854
7855        /**
7856         * helper - returns single field against db table WHERE X
7857         * Will only work for native fields (not Cutom fields)
7858         *
7859         * @param array WHERE clauses (not Req.)
7860         * @param string tablename
7861         * @param string colname
7862         *
7863         * @return string
7864         */
7865    public function getFieldByWHERE( $args = array() ) {
7866
7867        #} =========== LOAD ARGS ==============
7868        $defaultArgs = array(
7869
7870            'where'       => -1,
7871            'objtype'     => -1,
7872            'colname'     => '',
7873
7874            // permissions
7875            'ignoreowner' => false, // this'll let you not-check the owner of obj
7876
7877        );
7878        foreach ( $defaultArgs as $argK => $argV ) {
7879            $$argK = $argV;
7880            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7881                if ( is_array( $args[ $argK ] ) ) {
7882                    $newData = $$argK;
7883                    if ( ! is_array( $newData ) ) {
7884                        $newData = array();
7885                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7886                        $newData[ $subK ] = $subV;
7887                    }$$argK = $newData;
7888                } else {
7889                    $$argK = $args[ $argK ]; }
7890            }
7891        }
7892        #} =========== / LOAD ARGS =============
7893
7894        #} ========== CHECK FIELDS ============
7895
7896            // check obtype is legit
7897            $objtype = (int) $objtype;
7898        if ( ! isset( $objtype ) || $objtype == -1 || $this->objTypeKey( $objtype ) === -1 ) {
7899            return false;
7900        }
7901
7902            // check field (or 'COUNT(x)')
7903        if ( empty( $colname ) ) {
7904            return false;
7905        }
7906
7907        #} ========= / CHECK FIELDS ===========
7908
7909        global $ZBSCRM_t, $wpdb;
7910        $wheres          = array( 'direct' => array() );
7911        $whereStr        = '';
7912        $additionalWhere = '';
7913        $params          = array();
7914        $res             = array();
7915
7916        #} Build query - NOTE this is vulnerable to injection.
7917        $query = "SELECT $colname FROM " . $this->lazyTable( $objtype );
7918        // $params[] = $colname;
7919
7920        #} ============= WHERE ================
7921
7922            #} Add any where's
7923            $wheres = $where;
7924
7925        #} ============ / WHERE ==============
7926
7927        #} Build out any WHERE clauses
7928        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
7929        $whereStr  = $wheresArr['where'];
7930        $params    = $params + $wheresArr['params'];
7931        #} / Build WHERE
7932
7933        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
7934        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
7935        $ownQ   = $this->ownershipSQL( $ignoreowner );
7936        if ( ! empty( $ownQ ) ) {
7937            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
7938        }
7939        #} / Ownership
7940
7941        #} Append to sql (this also automatically deals with sortby and paging)
7942        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
7943
7944        try {
7945
7946            #} Prep & run query
7947            $queryObj = $this->prepare( $query, $params );
7948            return $wpdb->get_var( $queryObj );
7949
7950        } catch ( Exception $e ) {
7951
7952            #} General SQL Err
7953            $this->catchSQLError( $e );
7954
7955        }
7956
7957        return false;
7958    }
7959
7960        /**
7961         * helper - returns single field against db table (where ID =)
7962         * Will only work for native fields (not Cutom fields)
7963         *
7964         * @param int objID     object id
7965         * @param int objTypeID objectType id
7966         * @param string colname
7967         *
7968         * @return string
7969         */
7970    public function getFieldByID( $args = array() ) {
7971
7972        #} =========== LOAD ARGS ==============
7973        $defaultArgs = array(
7974
7975            'id'          => -1,
7976            'objtype'     => -1,
7977            'colname'     => '',
7978
7979            // permissions
7980            'ignoreowner' => false, // this'll let you not-check the owner of obj
7981
7982        );
7983        foreach ( $defaultArgs as $argK => $argV ) {
7984            $$argK = $argV;
7985            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
7986                if ( is_array( $args[ $argK ] ) ) {
7987                    $newData = $$argK;
7988                    if ( ! is_array( $newData ) ) {
7989                        $newData = array();
7990                    } foreach ( $args[ $argK ] as $subK => $subV ) {
7991                        $newData[ $subK ] = $subV;
7992                    }$$argK = $newData;
7993                } else {
7994                    $$argK = $args[ $argK ]; }
7995            }
7996        }
7997        #} =========== / LOAD ARGS =============
7998
7999        #} ========== CHECK FIELDS ============
8000
8001            // check id
8002            $id = (int) $id;
8003        if ( ! isset( $id ) || $id < 1 ) {
8004            return false;
8005        }
8006
8007            // check obtype is legit
8008            $objtype = (int) $objtype;
8009        if ( ! isset( $objtype ) || $objtype == -1 || $this->objTypeKey( $objtype ) === -1 ) {
8010            return false;
8011        }
8012
8013            // check field
8014        if ( empty( $colname ) ) {
8015            return false;
8016        }
8017
8018        #} ========= / CHECK FIELDS ===========
8019
8020        global $ZBSCRM_t, $wpdb;
8021        $wheres          = array( 'direct' => array() );
8022        $whereStr        = '';
8023        $additionalWhere = '';
8024        $params          = array();
8025        $res             = array();
8026
8027        #} Build query - NOTE this is vulnerable to injection.
8028        $query = "SELECT $colname FROM " . $this->lazyTable( $objtype );
8029        // $params[] = $colname;
8030
8031        #} ============= WHERE ================
8032
8033        // Add ID
8034        $wheres['ID'] = array( 'ID', '=', '%d', $id );
8035
8036        #} ============ / WHERE ==============
8037
8038        #} Build out any WHERE clauses
8039        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
8040        $whereStr  = $wheresArr['where'];
8041        $params    = $params + $wheresArr['params'];
8042        #} / Build WHERE
8043
8044        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
8045        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
8046        $ownQ   = $this->ownershipSQL( $ignoreowner );
8047        if ( ! empty( $ownQ ) ) {
8048            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
8049        }
8050        #} / Ownership
8051
8052        #} Append to sql (this also automatically deals with sortby and paging)
8053        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
8054
8055        try {
8056
8057            #} Prep & run query
8058            $queryObj = $this->prepare( $query, $params );
8059            return $wpdb->get_var( $queryObj );
8060
8061        } catch ( Exception $e ) {
8062
8063            #} General SQL Err
8064            $this->catchSQLError( $e );
8065
8066        }
8067
8068        return false;
8069    }
8070
8071        /**
8072         * helper - forces update of field for obj id + type,
8073         * THIS IS HARD usage, not for beginners/non-directors.
8074         * ... can break things if using this, so only use strictly for globally generic columns
8075         * ... e.g. zbs_owner, which appears in all obj.
8076         * ... ALWAYS use the DAL->contacts->whatever before this, where possible
8077         * // NOTE NOTE - THIS does not update "lastupdated" for each obj... AVOID USE!
8078         *
8079         * @param int objID     object id
8080         * @param int objTypeID objectType id
8081         * @param string colname
8082         *
8083         * @return string
8084         */
8085    public function setFieldByID( $args = array() ) {
8086
8087        #} =========== LOAD ARGS ==============
8088        $defaultArgs = array(
8089
8090            'objID'       => -1,
8091            'objTypeID'   => -1,
8092
8093            'colname'     => '',
8094            'coldatatype' => '%d', // %d/s
8095            'newValue'    => -99,
8096
8097        );
8098        foreach ( $defaultArgs as $argK => $argV ) {
8099            $$argK = $argV;
8100            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
8101                if ( is_array( $args[ $argK ] ) ) {
8102                    $newData = $$argK;
8103                    if ( ! is_array( $newData ) ) {
8104                        $newData = array();
8105                    } foreach ( $args[ $argK ] as $subK => $subV ) {
8106                        $newData[ $subK ] = $subV;
8107                    }$$argK = $newData;
8108                } else {
8109                    $$argK = $args[ $argK ]; }
8110            }
8111        }
8112        #} =========== / LOAD ARGS =============
8113
8114        // this'll only update columnames present in here:
8115        $restrictedToColumns = array( 'zbs_owner' );
8116
8117        if ( in_array( $colname, $restrictedToColumns ) && $objID > 0 && $objTypeID > 0 && ! empty( $colname ) && ! empty( $coldatatype ) && $newValue !== -99 ) {
8118
8119            global $wpdb;
8120
8121            // got table?
8122            $tableName = $this->lazyTable( $objTypeID );
8123
8124            if ( empty( $tableName ) ) {
8125                return false;
8126            }
8127
8128            #} Attempt update
8129            if ( $wpdb->update(
8130                $tableName,
8131                array(
8132
8133                    $colname => $newValue,
8134
8135                ),
8136                array( // where
8137                    'ID' => $objID,
8138                ),
8139                array( // field data types
8140                    $coldatatype,
8141                ),
8142                array( // where data types
8143                    '%d',
8144                )
8145            ) !== false ) {
8146
8147                        // Successfully updated - Return id
8148                        return $objID;
8149
8150            } else {
8151
8152                // FAILED update
8153                return false;
8154
8155            }
8156        }
8157
8158        return false;
8159    }
8160
8161        // brutal switch for lazy tablenames
8162    public function lazyTable( $objType = -1 ) {
8163
8164        global $ZBSCRM_t;
8165
8166        switch ( $objType ) {
8167
8168            case ZBS_TYPE_CONTACT:
8169                return $ZBSCRM_t['contacts'];
8170                break;
8171            // dal3:
8172            case ZBS_TYPE_COMPANY:
8173                return $ZBSCRM_t['companies'];
8174                break;
8175            case ZBS_TYPE_QUOTE:
8176                return $ZBSCRM_t['quotes'];
8177                break;
8178            case ZBS_TYPE_INVOICE:
8179                return $ZBSCRM_t['invoices'];
8180                break;
8181            case ZBS_TYPE_TRANSACTION:
8182                return $ZBSCRM_t['transactions'];
8183                break;
8184            case ZBS_TYPE_TASK:
8185                return $ZBSCRM_t['events'];
8186                break;
8187            case ZBS_TYPE_FORM:
8188                return $ZBSCRM_t['forms'];
8189                break;
8190            case ZBS_TYPE_LOG:
8191                return $ZBSCRM_t['logs'];
8192                break;
8193            case ZBS_TYPE_SEGMENT:
8194                return $ZBSCRM_t['segments'];
8195                break;
8196            case ZBS_TYPE_LINEITEM:
8197                return $ZBSCRM_t['lineitems'];
8198                break;
8199            case ZBS_TYPE_TASK_REMINDER:
8200                return $ZBSCRM_t['eventreminders'];
8201                break;
8202            case ZBS_TYPE_QUOTETEMPLATE:
8203                return $ZBSCRM_t['quotetemplates'];
8204                break;
8205
8206        }
8207
8208        return false;
8209    }
8210
8211        // brutal switch for lazy tidy func
8212    public function lazyTidy( $objType = -1, $obj = false ) {
8213
8214        switch ( $objType ) {
8215
8216            case ZBS_TYPE_CONTACT:
8217                return $this->contacts->tidy_contact( $obj );
8218                break;
8219            // dal3:
8220            case ZBS_TYPE_COMPANY:
8221                return $this->companies->tidy_company( $obj );
8222                break;
8223            case ZBS_TYPE_QUOTE:
8224                return $this->quotes->tidy_quote( $obj );
8225                break;
8226            case ZBS_TYPE_INVOICE:
8227                return $this->invoices->tidy_invoice( $obj );
8228                break;
8229            case ZBS_TYPE_TRANSACTION:
8230                return $this->transactions->tidy_transaction( $obj );
8231                break;
8232            case ZBS_TYPE_TASK:
8233                return $this->events->tidy_event( $obj );
8234                break;
8235            case ZBS_TYPE_FORM:
8236                return $this->forms->tidy_form( $obj );
8237                break;
8238            case ZBS_TYPE_LOG:
8239                return $this->logs->tidy_log( $obj );
8240                break;
8241            case ZBS_TYPE_SEGMENT:
8242                return $this->segments->tidy_segment( $obj );
8243                break;
8244            case ZBS_TYPE_LINEITEM:
8245                return $this->lineitems->tidy_lineitem( $obj );
8246                break;
8247            case ZBS_TYPE_TASK_REMINDER:
8248                return $this->eventreminders->tidy_eventreminder( $obj );
8249                break;
8250            case ZBS_TYPE_QUOTETEMPLATE:
8251                return $this->quotetemplates->tidy_quotetemplate( $obj );
8252                break;
8253
8254        }
8255
8256        return false;
8257    }
8258
8259        // guesses at a tidy... lazy, remove these if hit walls
8260    public function lazyTidyGeneric( $obj = false ) {
8261
8262        $res = false;
8263
8264        foreach ( $obj as $propKey => $prop ) {
8265
8266            if ( ! is_array( $res ) ) {
8267                $res = array();
8268            }
8269
8270            if ( $propKey != 'ID' && strpos( $propKey, '_' ) > 0 ) {
8271
8272                // zbs_owner -> owner
8273                $newKey = substr( $propKey, strpos( $propKey, '_' ) + 1 );
8274
8275                $res[ $newKey ] = $this->stripSlashes( $prop );
8276
8277            } else {
8278                $res['id'] = $prop;
8279            }
8280        }
8281
8282        return $res;
8283    }
8284
8285        // appends a space, if req. (lazy helper for amongst queries)
8286    public function space( $str = '', $pre = false ) {
8287
8288        if ( ! empty( $str ) ) {
8289            if ( $pre ) {
8290                return ' ' . $str;
8291            } else {
8292                return $str . ' ';
8293            }
8294        }
8295
8296        return $str;
8297    }
8298
8299        // appends a space and 'AND', if req. (lazy helper for amongst queries)
8300    public function spaceAnd( $str = '' ) {
8301
8302        if ( ! empty( $str ) ) {
8303            return $str . ' AND ';
8304        }
8305
8306        return $str;
8307    }
8308
8309        // appends a space and 'Where', if req. (lazy helper for amongst queries)
8310    public function spaceWhere( $str = '' ) {
8311
8312        $trimmedStr = trim( $str );
8313        if ( ! empty( $trimmedStr ) ) {
8314            return ' WHERE ' . $trimmedStr;
8315        }
8316
8317        return $str;
8318    }
8319
8320        // returns delimiter, if str != epty
8321        // used to be zeroBS_delimiterIf pre dal1
8322    public function delimiterIf( $delimiter, $ifStr = '' ) {
8323
8324        if ( ! empty( $ifStr ) ) {
8325            return $delimiter;
8326        }
8327
8328        return '';
8329    }
8330
8331        // internal middle man for zeroBSCRM_stripSlashes where ALWAYS returns
8332    public function stripSlashes( $obj = false ) {
8333
8334        return zeroBSCRM_stripSlashes( $obj, true );
8335    }
8336
8337        // if it thinks str is json, it'll decode + return obj, otherwise returns str
8338        // this only works with arr/obj
8339        // Note that `[]` doesn't hydrate into array with this
8340    public function decodeIfJSON( $str = '' ) {
8341
8342        if ( zeroBSCRM_isJson( $str ) ) {
8343            return json_decode( $str, true ); // true req. https://stackoverflow.com/questions/22878219/json-encode-turns-array-into-an-object
8344        }
8345
8346        return $str;
8347    }
8348
8349        /*
8350         * Builds Custom Fields Order by Str
8351         * .. ultimately returns $sortByField unless numeric, if so casts the custom field (varchar)
8352         * .. into an INT/DECIMAL in MySQL for the search
8353         */
8354    public function build_custom_field_order_by_str( $sortByField = '', $customField = array() ) {
8355
8356        // check if this custom field requires any special casting in it's sort string
8357
8358        // where the CF is a numeric field, we'll need to also use CAST(* AS SIGNED)
8359        if ( $customField[0] == 'numberint' ) {
8360
8361            $sortByField = 'CAST(' . $sortByField . ' AS SIGNED)';
8362
8363        }
8364
8365        // where the CF is a decimal field, we'll need to use CAST(* AS DECIMAL)
8366        if ( $customField[0] == 'numberfloat' ) {
8367
8368            $sortByField = 'CAST(' . $sortByField . ' AS DECIMAL(18,2))';
8369        }
8370
8371        return $sortByField;
8372    }
8373
8374        /*
8375         * Build's an escaped imploded, DB safe CSV
8376         * e.g. "'a','b','c'" as used in SELECT * FROM x WHERE y in ('a','b','c')
8377         */
8378    public function build_csv( $array = array() ) {
8379
8380        // only arrays
8381        if ( ! is_array( $array ) ) {
8382            return '';
8383        }
8384
8385        // Generate escaped csv, e.g. 'Call','Email'
8386        $array = array_map(
8387            function ( $v ) {
8388                return "'" . esc_sql( $v ) . "'";
8389            },
8390            $array
8391        );
8392
8393        // return
8394        return implode( ',', $array );
8395    }
8396
8397        // takes wherestr + additionalwhere and outputs legit SQL
8398        // GENERIC helper for all queries :)
8399    public function buildWhereStr( $whereStr = '', $additionalWhere = '' ) {
8400
8401        // echo 'W:'.$whereStr.'<br >AW:'.$additionalWhere.'!!<br ><Br >';
8402
8403        #} Build
8404        $where = trim( $whereStr );
8405
8406        #} Any additional
8407        if ( ! empty( $additionalWhere ) ) {
8408            if ( ! empty( $where ) ) {
8409                $where = $this->spaceAnd( $where );
8410            } else {
8411                $where = 'WHERE ';
8412            }
8413            $where .= $additionalWhere;
8414        }
8415
8416        return $this->space( $where, true );
8417    }
8418
8419        // add where's to SQL
8420        // +
8421        // feed in params
8422        // GENERIC helper for all queries :)
8423    public function buildWheres( $wheres = array(), $whereStr = '', $params = array(), $andOr = 'AND', $includeInitialWHERE = true ) {
8424
8425        $ret = array(
8426            'where'  => $whereStr,
8427            'params' => $params,
8428        );
8429        if ( $andOr != 'AND' && $andOr != 'OR' ) {
8430            $andOr = 'AND';
8431        }
8432
8433            // clear empty direct
8434        if ( isset( $wheres['direct'] ) && is_array( $wheres['direct'] ) && count( $wheres['direct'] ) == 0 ) {
8435            unset( $wheres['direct'] );
8436        }
8437
8438        if ( is_array( $wheres ) && count( $wheres ) > 0 ) {
8439            foreach ( $wheres as $key => $whereArr ) {
8440
8441                if ( empty( $ret['where'] ) && $includeInitialWHERE ) {
8442                    $ret['where'] .= ' WHERE ';
8443                }
8444
8445                // Where's are passed 2 ways, "direct":
8446                // array(SQL,array(params))
8447                if ( $key == 'direct' ) {
8448
8449                    // several under 1 direct
8450                    foreach ( $whereArr as $directWhere ) {
8451
8452                        if ( isset( $directWhere[0] ) && isset( $directWhere[1] ) ) {
8453
8454                            // multi-direct ANDor
8455                            if ( ! empty( $ret['where'] ) && $ret['where'] != ' WHERE ' ) {
8456                                $ret['where'] .= ' ' . $andOr . ' ';
8457                            }
8458
8459                            // ++ query
8460                            $ret['where'] .= $directWhere[0];
8461
8462                            // ++ params (any number, if set)
8463                            if ( is_array( $directWhere[1] ) ) {
8464                                foreach ( $directWhere[1] as $x ) {
8465                                    $ret['params'][] = $x;
8466                                }
8467                            } else {
8468                                $ret['params'][] = $directWhere[1];
8469                            }
8470                        }
8471                    }
8472                } else {
8473
8474                    if ( ! empty( $ret['where'] ) && $ret['where'] != ' WHERE ' ) {
8475                        $ret['where'] .= ' ' . $andOr . ' ';
8476                    }
8477
8478                    // Other way:
8479                    // irrelevantKEY => array(fieldname,operator,comparisonval,array(params))
8480                    // e.g. array('ID','=','%d',array(123))
8481                    // e.g. array('ID','IN','(SUBSELECT)',array(123))
8482
8483                    // build where (e.g. "X = Y" or "Z IN (1,2,3)")
8484                    $ret['where'] .= $whereArr[0] . ' ' . $whereArr[1] . ' ' . $whereArr[2];
8485
8486                    // ++ params (any number, if set)
8487                    if ( isset( $whereArr[3] ) ) {
8488                        if ( is_array( $whereArr[3] ) ) {
8489                            foreach ( $whereArr[3] as $x ) {
8490                                $ret['params'][] = $x;
8491                            }
8492                        } else {
8493                            $ret['params'][] = $whereArr[3];
8494                        }
8495                    }
8496
8497                    /*
8498                    legacy
8499
8500                    // add in - NOTE: this is TRUSTING key + whereArr[0]
8501                    $ret['where'] .= $key.' '.$whereArr[0].' '.$whereArr[2];
8502
8503                    // feed in params
8504                    $ret['params'][] = $whereArr[1];
8505                    */
8506
8507                }
8508            }
8509        }
8510
8511        return $ret;
8512    }
8513
8514    /**
8515     * Builds JOIN SQL for DAL queries.
8516     *
8517     * This was built for use by segments. Note that it's currently hard-coded for contacts, but could easily be extended.
8518     *
8519     * Note that $joins is an array of arrays, each of which contain three keys:
8520     *    `table_query`: a subquery used to derive a table
8521     *    `table_alias`: an alias we can use for this table
8522     *    `vars`: the values that will be used by $wpdb->prepare()
8523     *
8524     * @param array $joins Array of arrays of join params.
8525     * @param bool  $match_all Whether to match all (restrictive) or match any (UNION).
8526     *
8527     * @return array Looks like array( $join_sql, $join_params )
8528     */
8529    public function build_joins( $joins = array(), $match_all = true ) {
8530        if ( ! is_array( $joins ) || empty( $joins ) ) {
8531            return '';
8532        }
8533        $join_sql    = ' JOIN ( ';
8534        $join_params = array_merge( ...array_column( $joins, 'vars' ) );
8535
8536        if ( ! $match_all ) {
8537            // Simple; just join the tables!
8538            $join_sql .= implode( ' UNION ', array_column( $joins, 'table_query' ) );
8539        } else {
8540            $join_count        = 0;
8541            $first_table_alias = '';
8542
8543            foreach ( $joins as $join ) {
8544                $table_alias = $join['table_alias'] . '_' . $join_count;
8545                if ( $join_count === 0 ) {
8546                    $first_table_alias = $table_alias;
8547
8548                    $join_sql .= "SELECT {$table_alias}.contact_id FROM ({$join['table_query']}) AS {$table_alias}";
8549                } else {
8550                    $join_sql .= " JOIN ({$join['table_query']}) AS {$table_alias} ON {$table_alias}.contact_id = {$first_table_alias}.contact_id";
8551                }
8552                ++$join_count;
8553            }
8554        }
8555
8556        $join_sql .= ') AS join_table ON join_table.contact_id = ID';
8557
8558        return array( $join_sql, $join_params );
8559    }
8560
8561        // takes sortby field + order and returns str if not empty :)
8562        // Note: Is trusting legitimacy of $sortByField as parametised in wp db doesn't seem to work
8563        // can also now pass array (multi-sort)
8564        // e.g. $sortByField = 'zbsc_fname' OR $sortByField = array('zbsc_fname'=>'ASC','zbsc_lname' => 'DESC');
8565    public function buildSort( $sortByField = '', $sortOrder = 'ASC' ) {
8566
8567        #} Sort by
8568        if ( ! is_array( $sortByField ) && ! empty( $sortByField ) ) {
8569
8570            $sortOrder = strtoupper( $sortOrder );
8571
8572            if ( ! in_array( $sortOrder, array( 'DESC', 'ASC' ) ) ) {
8573                $sortOrder = 'DESC';
8574            }
8575            return ' ORDER BY ' . $sortByField . ' ' . $sortOrder;
8576
8577        } elseif ( is_array( $sortByField ) ) {
8578
8579            $orderByStr = '';
8580            foreach ( $sortByField as $field => $order ) {
8581
8582                if ( ! empty( $orderByStr ) ) {
8583                    $orderByStr .= ', ';
8584                }
8585                $orderByStr .= $field . ' ' . strtoupper( $order );
8586
8587            }
8588
8589            if ( ! empty( $orderByStr ) ) {
8590                return ' ORDER BY ' . $orderByStr;
8591            }
8592        }
8593
8594        return '';
8595    }
8596
8597        // takes $page and $perPage and adds limit str if req.
8598    public function buildPaging( $page = -1, $perPage = -1 ) {
8599
8600        #} Pagination
8601        if ( $page == -1 && $perPage == -1 ) {
8602
8603            // NO LIMITS :o
8604
8605        } else {
8606
8607            $perPage = (int) $perPage;
8608
8609            // Because SQL USING zero indexed page numbers, we remove -1 here
8610            // ... DO NOT change this without seeing usage of the function (e.g. list view) - which'll break
8611            $page = (int) $page - 1;
8612            if ( $page < 0 ) {
8613                $page = 0;
8614            }
8615
8616            // page needs multiplying :)
8617            if ( $page > 0 ) {
8618                $page = $page * $perPage;
8619            }
8620
8621            // check params realistic
8622            // todo, for now, brute pass
8623            return ' LIMIT ' . (int) $page . ',' . (int) $perPage;
8624
8625        }
8626
8627        return '';
8628    }
8629
8630        // builds WHERE query for meta key / val pairs.
8631        // e.g. Get customers in Company id 9:
8632        // ... contacts where their ID is in post_id WHERE meta_key = zbs_company and meta_value = 9
8633        // infill for half-migrated stuff
8634    public function buildWPMetaQueryWhere( $metaKey = -1, $metaVal = -1 ) {
8635
8636        if ( ! empty( $metaKey ) && ! empty( $metaVal ) ) {
8637
8638            global $wpdb;
8639            return array(
8640
8641                'sql'    => 'ID IN (SELECT DISTINCT post_id FROM ' . $wpdb->prefix . 'postmeta WHERE meta_key = %s AND meta_value = %d)',
8642                'params' => array( $metaKey, $metaVal ),
8643            );
8644
8645        }
8646
8647        return false;
8648    }
8649
8650    /**
8651     * Generates GROUP_CONCAT SQL compatible with both SQLite and MySQL
8652     *
8653     * @param string $field Field that will be concatenated.
8654     * @param string $separator Separator added between concatenated fields.
8655     *
8656     * @return string
8657     */
8658    public function build_group_concat( $field, $separator ) {
8659        $db_engine = jpcrm_database_engine();
8660        if ( $db_engine === 'sqlite' ) {
8661            return sprintf( 'GROUP_CONCAT(%s, "%s")', $field, $separator );
8662        } else {
8663            return sprintf( 'GROUP_CONCAT(%s SEPARATOR "%s")', $field, $separator );
8664        }
8665    }
8666
8667        // this returns %s etc. for common field names, will default to %s unless somt obv a date
8668    public function getTypeStr( $fieldKey = '' ) {
8669
8670        if ( $fieldKey == 'zbs_site' || $fieldKey == 'zbs_team' || $fieldKey == 'zbs_owner' ) {
8671            return '%d';
8672        }
8673
8674        if ( strpos( $fieldKey, '_created' ) > 0 ) {
8675            return '%d';
8676        }
8677        if ( strpos( $fieldKey, '_lastupdated' ) > 0 ) {
8678            return '%d';
8679        }
8680        if ( strpos( $fieldKey, '_lastcontacted' ) > 0 ) {
8681            return '%d';
8682        }
8683        if ( strpos( $fieldKey, '_id' ) > 0 ) {
8684            return '%d';
8685        }
8686        if ( strpos( $fieldKey, '_ID' ) > 0 ) {
8687            return '%d';
8688        }
8689        if ( $fieldKey == 'id' || $fieldKey == 'ID' ) {
8690            return '%d';
8691        }
8692
8693        return '%s';
8694    }
8695
8696        /*
8697         * Converts verbs such as 'equal' to '='
8698         * Note: these are relatively idiosyncratic as were part of segmentation layer but got generalised here
8699         */
8700    public function comparison_to_sql_symbol( $comparison_verb ) {
8701
8702        switch ( $comparison_verb ) {
8703
8704            case 'equal':
8705            case 'equals':
8706                return '=';
8707                break;
8708            case 'notequal':
8709                return '<>';
8710                break;
8711            case 'larger':
8712                return '>';
8713                break;
8714            case 'largerequal':
8715                return '>=';
8716                break;
8717            case 'less':
8718                return '<';
8719                break;
8720            case 'lessequal':
8721                return '<=';
8722                break;
8723            case 'floatrange':
8724                // this is somewhat like hotglue, e.g. `WHERE column_a [BETWEEN %s AND ] %s`
8725                return 'BETWEEN %s AND ';
8726                break;
8727            case 'intrange':
8728                // this is somewhat like hotglue, e.g. `WHERE column_a [BETWEEN %d AND ] %d`
8729                return 'BETWEEN %d AND ';
8730                break;
8731
8732        }
8733
8734        return false;
8735    }
8736
8737    public function prepare( $sql = '', $params = array() ) {
8738
8739        global $wpdb;
8740
8741        // empty arrays causes issues in wpdb prepare
8742        if ( is_array( $params ) && count( $params ) <= 0 ) {
8743            return $sql;
8744        }
8745
8746        // normal return
8747        return $wpdb->prepare( $sql, $params );
8748    }
8749
8750        // not yet used
8751    public function catchSQLError( $errObj = -1 ) {
8752
8753        // log?
8754
8755        return false;
8756    }
8757
8758        /*
8759        * Retrieves a key-value pair from centralised global
8760        *
8761        * @param string $key
8762        *
8763        */
8764    public function get_cache_var( $key ) {
8765
8766        if ( isset( $this->cache[ $key ] ) ) {
8767
8768            return $this->cache[ $key ];
8769
8770        }
8771
8772        return false;
8773    }
8774
8775        /*
8776        * Stores a key-value pair in centralised global
8777        *  This is designed to allow basic caching at a DAL level without spawning multiple globals
8778        *
8779        * @param string $key
8780        * @param mixed $value
8781        *
8782        */
8783    public function update_cache_var( $key, $value ) {
8784
8785        // simplistic
8786        $this->cache[ $key ] = $value;
8787    }
8788
8789    /*
8790    ======================================================
8791    / To be sorted helpers
8792    ======================================================
8793    */
8794
8795    /*
8796    ======================================================
8797    Middle Man funcs (until DAL3.1)
8798    ======================================================
8799    */
8800
8801    // TEMP LOGGING to BACKTRACE LEGACY CALLS:
8802    /*
8803    CREATE TABLE `templogs` (
8804    `ID` int(32) NOT NULL AUTO_INCREMENT PRIMARY KEY,
8805    `funcname` varchar(500) NOT NULL,
8806    `filename` varchar(500) NOT NULL,
8807    `notes` longtext NOT NULL,
8808    `time` int(14) NOT NULL
8809    ) ENGINE='InnoDB' COLLATE 'utf8_general_ci'; */
8810
8811    // temporary function used in v3.0 prep to weed out bad/old reference use
8812    // logs to table 'templogs' if table exists
8813    // left in until 3.1 - see #gh-146
8814    private function v3templogBacktrace( $funcName = '', $caller = false, $backtrace = false ) {
8815
8816        global $ZBSCRM_t, $wpdb;
8817
8818        $tableExist = $wpdb->get_results( "SHOW TABLES LIKE 'templogs'" );
8819        if ( count( $tableExist ) >= 1 ) {
8820
8821            if ( $wpdb->insert(
8822                'templogs',
8823                array(
8824                    // fields
8825                    'funcname' => $funcName,
8826                    'filename' => $caller['file'] . ':' . $caller['line'],
8827                    'notes'    => print_r( $backtrace, 1 ), // ,
8828                    'time'     => time(),
8829                ),
8830                array(
8831                    '%s',
8832                    '%s',
8833                    '%s',
8834                    '%d',
8835                )
8836            ) <= 0 ) {
8837                exit( 'ERROR: Failed to log backtrace error (' . $funcName . ')!<pre>' . print_r( array( $backtrace, $caller, $funcName ), 1 ) . '</pre>' );
8838            }
8839        }
8840
8841        return true;
8842    }
8843
8844    /**
8845     * Counts below are as of 19 November 2024.
8846     */
8847
8848    /* Stripe Sync: 1 */
8849    public function getContact( ...$args ) {
8850
8851        // hard-typed
8852        $funcName = 'getContact';
8853
8854        // retrieve backtrace
8855        $backtrace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 );
8856        $caller    = array_shift( $backtrace );
8857
8858        // log to db, if logging
8859        $this->v3templogBacktrace( $funcName, $caller, $backtrace );
8860
8861        // return, if available
8862        if ( method_exists( $this->contacts, $funcName ) ) {
8863            return call_user_func_array( array( $this->contacts, $funcName ), func_get_args() );
8864        }
8865
8866        // ultimate fallback
8867        return false;
8868    }
8869
8870    /* Automations: 1 */
8871    public function getContacts( ...$args ) {
8872
8873        // hard-typed
8874        $funcName = 'getContacts';
8875
8876        // retrieve backtrace
8877        $backtrace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 );
8878        $caller    = array_shift( $backtrace );
8879
8880        // log to db, if logging
8881        $this->v3templogBacktrace( $funcName, $caller, $backtrace );
8882
8883        // return, if available
8884        if ( method_exists( $this->contacts, $funcName ) ) {
8885            return call_user_func_array( array( $this->contacts, $funcName ), func_get_args() );
8886        }
8887
8888        // ultimate fallback
8889        return false;
8890    }
8891
8892    /* Stripe Sync: 1, Awesome Support: 1 */
8893    public function addUpdateContact( ...$args ) {
8894
8895        // hard-typed
8896        $funcName = 'addUpdateContact';
8897
8898        // retrieve backtrace
8899        $backtrace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 );
8900        $caller    = array_shift( $backtrace );
8901
8902        // log to db, if logging
8903        $this->v3templogBacktrace( $funcName, $caller, $backtrace );
8904
8905        // return, if available
8906        if ( method_exists( $this->contacts, $funcName ) ) {
8907            return call_user_func_array( array( $this->contacts, $funcName ), func_get_args() );
8908        }
8909
8910        // ultimate fallback
8911        return false;
8912    }
8913
8914    /* Automations: 1, Livestorm: 1 */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
8915    public function addUpdateContactTags( ...$args ) {
8916
8917        // hard-typed
8918        $funcName = 'addUpdateContactTags';
8919
8920        // retrieve backtrace
8921        $backtrace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 );
8922        $caller    = array_shift( $backtrace );
8923
8924        // log to db, if logging
8925        $this->v3templogBacktrace( $funcName, $caller, $backtrace );
8926
8927        // return, if available
8928        if ( method_exists( $this->contacts, $funcName ) ) {
8929            return call_user_func_array( array( $this->contacts, $funcName ), func_get_args() );
8930        }
8931
8932        // ultimate fallback
8933        return false;
8934    }
8935
8936    /* Automations: 1 */
8937    public function deleteContact( ...$args ) {
8938
8939        // hard-typed
8940        $funcName = 'deleteContact';
8941
8942        // retrieve backtrace
8943        $backtrace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 );
8944        $caller    = array_shift( $backtrace );
8945
8946        // log to db, if logging
8947        $this->v3templogBacktrace( $funcName, $caller, $backtrace );
8948
8949        // return, if available
8950        if ( method_exists( $this->contacts, $funcName ) ) {
8951            return call_user_func_array( array( $this->contacts, $funcName ), func_get_args() );
8952        }
8953
8954        // ultimate fallback
8955        return false;
8956    }
8957
8958    /* Advanced Segments: 4 */
8959    public function segmentBuildDirectOrClause( ...$args ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid,VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable,Squiz.Commenting.FunctionComment.WrongStyle
8960        // hard-typed
8961        $func_name = 'segmentBuildDirectOrClause';
8962
8963        // retrieve backtrace
8964        $backtrace = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
8965        $caller    = array_shift( $backtrace );
8966
8967        // log to db, if logging
8968        $this->v3templogBacktrace( $func_name, $caller, $backtrace );
8969
8970        if ( method_exists( $this->segments, $func_name ) ) {
8971            return call_user_func_array( array( $this->segments, $func_name ), func_get_args() );
8972        }
8973
8974        // ultimate fallback
8975        return false;
8976    }
8977
8978    /*
8979    ======================================================
8980    / Middle Man funcs (until DAL3.0)
8981    ======================================================
8982    */
8983} // / DAL class
8984
8985// Initialize the static mitigation cache.
8986zbsDAL::reset_mitigation_cache_for_issue_3504();