Changeset 3760857
- Timestamp:
- 10/23/2026 03:36:23 AM (3 weeks ago)
- Location:
- clicksyncr-for-kit
- Files:
-
- 176 added
- 48 edited
-
tags/26.10.22.23 (added)
-
tags/26.10.22.23/assets (added)
-
tags/26.10.22.23/assets/css (added)
-
tags/26.10.22.23/assets/css/admin.css (added)
-
tags/26.10.22.23/assets/img (added)
-
tags/26.10.22.23/assets/img/icon.svg (added)
-
tags/26.10.22.23/assets/js (added)
-
tags/26.10.22.23/assets/js/builder-panel.js (added)
-
tags/26.10.22.23/assets/js/form-picker.js (added)
-
tags/26.10.22.23/assets/js/ninja-builder.js (added)
-
tags/26.10.22.23/assets/js/notices.js (added)
-
tags/26.10.22.23/assets/js/panel.js (added)
-
tags/26.10.22.23/clicksyncr-for-kit.php (added)
-
tags/26.10.22.23/includes (added)
-
tags/26.10.22.23/includes/bootstrap.php (added)
-
tags/26.10.22.23/includes/signls (added)
-
tags/26.10.22.23/includes/signls/LICENSE (added)
-
tags/26.10.22.23/includes/signls/SOURCE.md (added)
-
tags/26.10.22.23/includes/signls/bridge-1-1-5.php (added)
-
tags/26.10.22.23/includes/signls/bridge-1-1-6.php (added)
-
tags/26.10.22.23/includes/signls/bridge-1-1-7.php (added)
-
tags/26.10.22.23/includes/signls/loader.php (added)
-
tags/26.10.22.23/includes/signls/manifest.sha256 (added)
-
tags/26.10.22.23/includes/signls/sdk.php (added)
-
tags/26.10.22.23/includes/signls/src (added)
-
tags/26.10.22.23/includes/signls/src/Collector.php (added)
-
tags/26.10.22.23/includes/signls/src/Consent.php (added)
-
tags/26.10.22.23/includes/signls/src/CounterStore.php (added)
-
tags/26.10.22.23/includes/signls/src/Identity.php (added)
-
tags/26.10.22.23/includes/signls/src/ObservationSanitizer.php (added)
-
tags/26.10.22.23/includes/signls/src/ProductAdapterInterface.php (added)
-
tags/26.10.22.23/includes/signls/src/Runtime.php (added)
-
tags/26.10.22.23/includes/signls/src/Sanitizer.php (added)
-
tags/26.10.22.23/includes/signls/src/Scheduler.php (added)
-
tags/26.10.22.23/includes/signls/src/SiteIdentity.php (added)
-
tags/26.10.22.23/includes/signls/src/StateStore.php (added)
-
tags/26.10.22.23/includes/signls/src/Transport.php (added)
-
tags/26.10.22.23/includes/src (added)
-
tags/26.10.22.23/includes/src/API (added)
-
tags/26.10.22.23/includes/src/API/KitClient.php (added)
-
tags/26.10.22.23/includes/src/Admin (added)
-
tags/26.10.22.23/includes/src/Admin/AdminBar.php (added)
-
tags/26.10.22.23/includes/src/Admin/BuilderNotice.php (added)
-
tags/26.10.22.23/includes/src/Admin/Notices.php (added)
-
tags/26.10.22.23/includes/src/Admin/PanelHeader.php (added)
-
tags/26.10.22.23/includes/src/Admin/PanelRows.php (added)
-
tags/26.10.22.23/includes/src/Admin/PluginList.php (added)
-
tags/26.10.22.23/includes/src/Admin/SetupLink.php (added)
-
tags/26.10.22.23/includes/src/Builders (added)
-
tags/26.10.22.23/includes/src/Builders/Adapter.php (added)
-
tags/26.10.22.23/includes/src/Builders/CF7.php (added)
-
tags/26.10.22.23/includes/src/Builders/FluentForms.php (added)
-
tags/26.10.22.23/includes/src/Builders/FluentIntegration.php (added)
-
tags/26.10.22.23/includes/src/Builders/Formidable.php (added)
-
tags/26.10.22.23/includes/src/Builders/FormidableAction.php (added)
-
tags/26.10.22.23/includes/src/Builders/Forminator.php (added)
-
tags/26.10.22.23/includes/src/Builders/ForminatorFormSettings.php (added)
-
tags/26.10.22.23/includes/src/Builders/Gravity.php (added)
-
tags/26.10.22.23/includes/src/Builders/GravityFeed.php (added)
-
tags/26.10.22.23/includes/src/Builders/NinjaAction.php (added)
-
tags/26.10.22.23/includes/src/Builders/NinjaForms.php (added)
-
tags/26.10.22.23/includes/src/Builders/WPForms.php (added)
-
tags/26.10.22.23/includes/src/CF7 (added)
-
tags/26.10.22.23/includes/src/CF7/FieldMapper.php (added)
-
tags/26.10.22.23/includes/src/CF7/FieldService.php (added)
-
tags/26.10.22.23/includes/src/CF7/SubmissionHandler.php (added)
-
tags/26.10.22.23/includes/src/Core (added)
-
tags/26.10.22.23/includes/src/Core/Access.php (added)
-
tags/26.10.22.23/includes/src/Core/Advanced.php (added)
-
tags/26.10.22.23/includes/src/Core/ApiHost.php (added)
-
tags/26.10.22.23/includes/src/Core/Assets.php (added)
-
tags/26.10.22.23/includes/src/Core/Brand.php (added)
-
tags/26.10.22.23/includes/src/Core/Builder.php (added)
-
tags/26.10.22.23/includes/src/Core/BuilderPanel.php (added)
-
tags/26.10.22.23/includes/src/Core/Catalog.php (added)
-
tags/26.10.22.23/includes/src/Core/Context.php (added)
-
tags/26.10.22.23/includes/src/Core/Credentials.php (added)
-
tags/26.10.22.23/includes/src/Core/Current.php (added)
-
tags/26.10.22.23/includes/src/Core/DeliveryPolicy.php (added)
-
tags/26.10.22.23/includes/src/Core/Dispatch.php (added)
-
tags/26.10.22.23/includes/src/Core/Edition.php (added)
-
tags/26.10.22.23/includes/src/Core/KeyHints.php (added)
-
tags/26.10.22.23/includes/src/Core/Panel.php (added)
-
tags/26.10.22.23/includes/src/Core/Provider.php (added)
-
tags/26.10.22.23/includes/src/Core/Settings.php (added)
-
tags/26.10.22.23/includes/src/Core/StatusPanel.php (added)
-
tags/26.10.22.23/includes/src/Core/StatusPreference.php (added)
-
tags/26.10.22.23/includes/src/Core/StatusResolver.php (added)
-
tags/26.10.22.23/includes/src/Core/SubscriberStatus.php (added)
-
tags/26.10.22.23/includes/src/Core/SubscriberSync.php (added)
-
tags/26.10.22.23/includes/src/Core/SyncStats.php (added)
-
tags/26.10.22.23/includes/src/Core/Val.php (added)
-
tags/26.10.22.23/includes/src/Formidable (added)
-
tags/26.10.22.23/includes/src/Formidable/Handler.php (added)
-
tags/26.10.22.23/includes/src/Forminator (added)
-
tags/26.10.22.23/includes/src/Forminator/FormHooks.php (added)
-
tags/26.10.22.23/includes/src/Forminator/Integration.php (added)
-
tags/26.10.22.23/includes/src/Forminator/Submission.php (added)
-
tags/26.10.22.23/includes/src/REST (added)
-
tags/26.10.22.23/includes/src/REST/Connection.php (added)
-
tags/26.10.22.23/includes/src/REST/Controller.php (added)
-
tags/26.10.22.23/includes/src/REST/Permissions.php (added)
-
tags/26.10.22.23/includes/src/Signals (added)
-
tags/26.10.22.23/includes/src/Signals/ProductAdapter.php (added)
-
tags/26.10.22.23/includes/src/Signals/Sharing.php (added)
-
tags/26.10.22.23/includes/src/WPForms (added)
-
tags/26.10.22.23/includes/src/WPForms/SubmissionHandler.php (added)
-
tags/26.10.22.23/languages (added)
-
tags/26.10.22.23/languages/README.txt (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-ca.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-ca.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-cs_CZ.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-cs_CZ.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-da_DK.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-da_DK.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-de_CH.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-de_CH.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-de_DE.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-de_DE.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-el.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-el.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_AU.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_AU.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_CA.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_CA.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_GB.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_GB.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_US.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_US.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_ZA.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-en_ZA.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-es_ES.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-es_ES.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-es_MX.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-es_MX.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-fi.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-fi.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-fr_CA.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-fr_CA.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-fr_FR.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-fr_FR.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-he_IL.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-he_IL.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-hu_HU.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-hu_HU.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-it_IT.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-it_IT.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-nb_NO.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-nb_NO.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-nl_NL.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-nl_NL.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-pl_PL.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-pl_PL.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-pt_BR.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-pt_BR.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-pt_PT.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-pt_PT.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-ro_RO.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-ro_RO.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-ru_RU.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-ru_RU.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-sk_SK.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-sk_SK.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-sl_SI.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-sl_SI.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-sv_SE.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-sv_SE.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-tr_TR.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-tr_TR.po (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-zh_TW.mo (added)
-
tags/26.10.22.23/languages/clicksyncr-for-kit-zh_TW.po (added)
-
tags/26.10.22.23/license.txt (added)
-
tags/26.10.22.23/readme.txt (added)
-
tags/26.10.22.23/uninstall.php (added)
-
trunk/assets/css/admin.css (modified) (16 diffs)
-
trunk/assets/js/builder-panel.js (modified) (1 diff)
-
trunk/assets/js/form-picker.js (modified) (1 diff)
-
trunk/assets/js/ninja-builder.js (modified) (2 diffs)
-
trunk/assets/js/notices.js (modified) (1 diff)
-
trunk/assets/js/panel.js (modified) (1 diff)
-
trunk/clicksyncr-for-kit.php (modified) (1 diff)
-
trunk/includes/bootstrap.php (modified) (1 diff)
-
trunk/includes/signls/SOURCE.md (modified) (1 diff)
-
trunk/includes/signls/loader.php (modified) (1 diff)
-
trunk/includes/signls/manifest.sha256 (modified) (2 diffs)
-
trunk/includes/signls/src/Collector.php (modified) (1 diff)
-
trunk/includes/signls/src/Identity.php (modified) (6 diffs)
-
trunk/includes/signls/src/Sanitizer.php (modified) (4 diffs)
-
trunk/includes/signls/src/SiteIdentity.php (modified) (4 diffs)
-
trunk/includes/signls/src/Transport.php (modified) (6 diffs)
-
trunk/includes/src/Admin/AdminBar.php (modified) (1 diff)
-
trunk/includes/src/Admin/PanelHeader.php (modified) (1 diff)
-
trunk/includes/src/Admin/PanelRows.php (modified) (4 diffs)
-
trunk/includes/src/Builders/Adapter.php (modified) (1 diff)
-
trunk/includes/src/Builders/CF7.php (modified) (3 diffs)
-
trunk/includes/src/Builders/FluentForms.php (modified) (2 diffs)
-
trunk/includes/src/Builders/FluentIntegration.php (modified) (9 diffs)
-
trunk/includes/src/Builders/Formidable.php (modified) (3 diffs)
-
trunk/includes/src/Builders/FormidableAction.php (modified) (2 diffs)
-
trunk/includes/src/Builders/Forminator.php (modified) (3 diffs)
-
trunk/includes/src/Builders/ForminatorFormSettings.php (modified) (2 diffs)
-
trunk/includes/src/Builders/Gravity.php (modified) (2 diffs)
-
trunk/includes/src/Builders/GravityFeed.php (modified) (10 diffs)
-
trunk/includes/src/Builders/NinjaAction.php (modified) (3 diffs)
-
trunk/includes/src/Builders/NinjaForms.php (modified) (5 diffs)
-
trunk/includes/src/Builders/WPForms.php (modified) (3 diffs)
-
trunk/includes/src/CF7/SubmissionHandler.php (modified) (2 diffs)
-
trunk/includes/src/Core/Access.php (modified) (2 diffs)
-
trunk/includes/src/Core/Advanced.php (modified) (14 diffs)
-
trunk/includes/src/Core/ApiHost.php (added)
-
trunk/includes/src/Core/Context.php (modified) (2 diffs)
-
trunk/includes/src/Core/Credentials.php (modified) (3 diffs)
-
trunk/includes/src/Core/Dispatch.php (added)
-
trunk/includes/src/Core/Panel.php (modified) (1 diff)
-
trunk/includes/src/Core/Provider.php (modified) (1 diff)
-
trunk/includes/src/Core/StatusResolver.php (modified) (5 diffs)
-
trunk/includes/src/Core/SubscriberSync.php (modified) (2 diffs)
-
trunk/includes/src/Formidable/Handler.php (modified) (2 diffs)
-
trunk/includes/src/Forminator/FormHooks.php (modified) (4 diffs)
-
trunk/includes/src/Forminator/Integration.php (modified) (3 diffs)
-
trunk/includes/src/REST/Connection.php (modified) (3 diffs)
-
trunk/includes/src/Signals/Sharing.php (modified) (3 diffs)
-
trunk/includes/src/WPForms/SubmissionHandler.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
clicksyncr-for-kit/trunk/assets/css/admin.css
r3726852 r3760857 3 3 background: #fff; 4 4 border-bottom: 1px solid #c5d1d9; 5 margin: 0 0 20px 0; 6 } 5 margin: -16px -16px 20px; 6 } 7 7 8 #clicksyncr-for-kit .syncr-header__inner { 8 9 display: flex; … … 13 14 padding: 14px 16px; 14 15 } 15 #clicksyncr-for-kit .syncr-header { 16 margin: -16px -16px 20px; 17 } 16 18 17 #clicksyncr-for-kit .syncr-header__brand { 19 18 display: flex; … … 21 20 gap: 10px; 22 21 } 22 23 23 #clicksyncr-for-kit .syncr-header__title { 24 24 font-size: 18px; … … 27 27 letter-spacing: -0.3px; 28 28 } 29 29 30 #clicksyncr-for-kit .syncr-header__badge { 30 31 background: #4d5d6d; … … 37 38 letter-spacing: 0.5px; 38 39 } 40 39 41 #clicksyncr-for-kit .syncr-header__version { 40 42 font-size: 12px; … … 42 44 font-weight: 400; 43 45 } 46 44 47 #clicksyncr-for-kit .syncr-header__context { 45 48 display: flex; … … 48 51 margin-left: auto; 49 52 } 53 50 54 #clicksyncr-for-kit .syncr-header__status { 51 55 display: flex; … … 55 59 border-right: 1px solid #c5d1d9; 56 60 } 61 57 62 #clicksyncr-for-kit .syncr-header__status-dot { 58 63 width: 8px; … … 60 65 border-radius: 50%; 61 66 } 67 62 68 #clicksyncr-for-kit .syncr-header__status-dot--connected { 63 69 background: #00a32a; 64 70 } 71 65 72 #clicksyncr-for-kit .syncr-header__status-dot--pending { 66 73 background: #dba617; 67 74 } 75 68 76 #clicksyncr-for-kit .syncr-header__status-dot--neutral { 69 77 background: #c3c4c7; 70 78 } 79 71 80 #clicksyncr-for-kit .syncr-header__status-text { 72 81 font-size: 13px; 73 82 color: #50575e; 74 83 } 84 75 85 #clicksyncr-for-kit .syncr-header__actions { 76 86 display: flex; … … 78 88 gap: 12px; 79 89 } 90 80 91 #clicksyncr-for-kit .syncr-header__badge--pro, 81 92 #clicksyncr-for-kit .syncr-header__badge--lite { … … 83 94 color: #fff; 84 95 } 96 85 97 #clicksyncr-for-kit .syncr-header__support { 86 98 display: flex; … … 93 105 transition: color 0.2s; 94 106 } 107 95 108 #clicksyncr-for-kit .syncr-header__support:hover { 96 109 color: #1d2327; 97 110 } 111 98 112 #clicksyncr-for-kit .syncr-sparkles { 99 113 display: inline-block; … … 106 120 background-size: contain; 107 121 } 122 108 123 #clicksyncr-for-kit .syncr-header__review { 109 124 display: flex; … … 115 130 transition: color 0.2s; 116 131 } 132 117 133 #clicksyncr-for-kit .syncr-header__review:hover { 118 134 color: #1d2327; 119 135 } 120 #clicksyncr-for-kit .syncr-sparkles { 121 display: inline-block; 122 vertical-align: middle; 123 width: 80px; 124 height: 16px; 125 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 16'%3E%3Cpath fill='%23EFBF04' d='M8 0l2.5 5 5.5.8-4 3.9.9 5.3L8 12.5 3.1 15l.9-5.3-4-3.9L5.5 5zM24 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8zM40 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8zM56 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8zM72 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8z'/%3E%3C/svg%3E"); 126 background-repeat: no-repeat; 127 background-size: contain; 128 } 136 129 137 /* === end clicksyncr kit === */ 130 138 131 139 /* === clicksyncr kit: panel. generated, do not edit === */ 132 140 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) { position: relative; } 141 133 142 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) [hidden] { display: none !important; } 143 134 144 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rows { padding: 4px 0 0; container-type: inline-size; } 135 145 136 146 .sui-modal:has(.syncr-rows) .sui-modal-content.sui-modal-lg { max-width: 1100px !important; } 147 137 148 .ninja-forms-app #nf-builder.nf-drawer-closed::before { pointer-events: none; } 138 149 139 150 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-card { background: rgba(255, 255, 255, .95); border: 1px solid #d8e0e8; border-radius: 12px; margin: 0 0 20px; padding: 20px; } 151 140 152 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-card > h3 { margin: 0 0 4px; font-size: 18px; font-weight: 600; text-transform: none; letter-spacing: normal; } 153 141 154 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-card > .description { margin: 0 0 18px; color: #5d6878; } 155 142 156 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-block--rules h4 { margin: 18px 0 4px; font-size: 13px; } 157 143 158 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-block--rules > .description { margin: 0 0 12px; color: #5d6878; } 159 144 160 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } 161 145 162 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-actions--end { justify-content: flex-end; margin-top: 12px; } 163 146 164 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-notice { margin: 14px 0 4px; padding: 11px 13px; border-left: 3px solid #6c8ef3; border-radius: 2px; background: #f0f4ff; color: #1d2327; } 165 147 166 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-locked-box { display: flex; align-items: center; gap: 10px; margin: 0 0 12px; padding: 14px; border: 1px dashed #ccd5df; border-radius: 9px; background: #f8fafc; color: #5d6878; } 167 148 168 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-locked-box > span { display: grid; width: 28px; height: 28px; place-items: center; border-radius: 50%; background: #e8edf2; } 169 149 170 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-locked-box p { margin: 0; } 150 171 151 172 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-key-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } 173 152 174 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) input.syncr-key { width: 25em; max-width: 100%; } 175 153 176 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-key-toggle { display: inline-flex; align-items: center; justify-content: center; padding: 0 28px; } 177 154 178 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-key-toggle .dashicons { line-height: 1; } 155 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-btn-connected, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-btn-connected:hover, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-btn-connected:focus { background: #00a32a; border-color: #00a32a; color: #fff; } 179 180 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-btn-connected, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-btn-connected:hover, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-btn-connected:focus { background: #007a1f; border-color: #007a1f; color: #fff; } 181 156 182 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-feedback--error { color: #d63638; } 157 183 158 184 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 24px; } 185 159 186 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tiles--extra { margin-top: 16px; } 187 160 188 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tiles--single { grid-template-columns: minmax(0, 1fr); } 189 161 190 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tiles--single .syncr-tile label + label { display: none; } 191 162 192 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile { display: block; padding: 13px; border: 1px solid #e1e6ec; border-radius: 9px; background: #fbfcfe; } 193 163 194 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile .syncr-actions label { display: inline-flex; align-items: center; gap: 4px; width: auto; margin: 0 12px 0 0; } 195 164 196 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile > p.description { margin: 10px 0 4px; } 197 165 198 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile label, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile select { display: block; width: 100%; max-width: 100%; } 199 166 200 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile label { margin-bottom: 5px; } 201 167 202 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile label + label { margin-top: 6px; margin-bottom: 0; } 168 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile__meta { color: #999; margin-left: 4px; } 169 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile__required { color: #dd3d36; font-weight: 400; margin-left: 4px; } 203 204 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile__meta { color: #646970; margin-left: 4px; } 205 206 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile__required { color: #b32d2e; font-weight: 400; margin-left: 4px; } 207 170 208 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile--missing select { border-color: #d63638; } 209 171 210 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile .syncr-actions--end { margin-top: 10px; } 211 172 212 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile input.regular-text { width: 100%; max-width: none; } 213 173 214 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tile .description { margin: 6px 0 0; } 215 174 216 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-readback { margin: 10px 0 0; color: #5d6878; } 217 175 218 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-readback--warn { color: #d63638; } 219 176 220 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-block--destination .syncr-actions--end { margin-top: 12px; } 177 221 178 222 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__table:not(:empty) { overflow: hidden; border: 1px solid #e1e6ec; border-radius: 9px; background: #fbfcfe; } 223 179 224 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__head, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__row { display: grid; grid-template-columns: 42px minmax(145px, .85fr) minmax(145px, .75fr) minmax(220px, 1fr) 34px; align-items: center; } 225 180 226 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__head { background: #f6f7f7; border-bottom: 1px solid #e1e6ec; } 227 181 228 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__head span { padding: 9px 10px; color: #50575e; font-weight: 600; } 229 182 230 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__row { background: #fbfcfe; } 231 183 232 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__row + .syncr-rules__row { border-top: 1px solid #e1e6ec; } 233 184 234 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__num { padding: 10px; color: #1d2327; font-weight: 600; white-space: nowrap; } 235 185 236 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__cell { padding: 10px; } 237 186 238 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__cell select { display: block; width: 100%; min-width: 0; max-width: 100%; } 239 187 240 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__remove { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; padding: 0; color: #646970; text-decoration: none; } 241 188 242 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__remove:hover { color: #b32d2e; } 243 189 244 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__remove:focus { color: #3858e9; text-decoration: none; } 245 190 246 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__empty { margin: 0; color: #5d6878; } 191 247 192 248 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy { overflow: hidden; border: 1px solid #d8e0e8; border-radius: 10px; } 249 193 250 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row { display: grid; grid-template-columns: minmax(170px, 220px) minmax(220px, 310px) minmax(240px, 1fr); align-items: center; gap: 20px; min-height: 76px; padding: 13px 15px; background: #fff; } 251 194 252 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row + .syncr-policy__row { border-top: 1px solid #e7ebef; } 253 195 254 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row:nth-child(even) { background: #fbfcfd; } 255 196 256 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row--warning { background: #fffaf0; } 257 197 258 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row + .syncr-policy__row--joined { border-top: 0; min-height: 0; padding-top: 0; margin-top: -8px; } 259 198 260 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row:nth-child(even) + .syncr-policy__row--joined { background: #fbfcfd; } 261 199 262 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row:nth-child(odd) + .syncr-policy__row--joined { background: #fff; } 263 200 264 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-row--sub .syncr-policy__name { padding-left: 18px; } 265 201 266 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__name strong, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__name span { display: block; } 267 202 268 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__name strong { font-size: 13px; } 269 203 270 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__name span, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__note, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__checks p { color: #5d6878; font-size: 12px; } 271 204 272 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__note, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__checks p, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-managed p { margin: 0; } 273 205 274 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control select, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control input[type="text"] { box-sizing: border-box; width: 100%; max-width: none; } 275 206 276 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control .syncr-pair { display: flex; gap: 6px; } 277 207 278 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control .syncr-pair select { width: 45%; } 279 208 280 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__checks label { font-weight: 600; } 281 209 282 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__checks p { margin-top: 2px; padding-left: 24px; } 283 210 284 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control .syncr-checklist { max-height: 190px; overflow: auto; background: #fff; border: 1px solid #8c8f94; border-radius: 4px; padding: 6px 10px; } 285 211 286 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-managed { padding: 9px 10px; border: 1px solid #cbd4dc; border-radius: 6px; background: #f8fafc; } 287 212 288 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-managed p { margin-top: 3px; color: #5d6878; font-size: 12px; } 289 213 290 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-locked { margin: 0; color: #5d6878; } 291 214 292 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control .syncr-pro-conditions select.regular-text { width: auto; max-width: 100%; } 293 215 294 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control .syncr-pro-condition-row select, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__control .syncr-pro-condition-row input { width: auto; } 216 295 217 296 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools { overflow: hidden; border: 1px solid #e1e6ec; border-radius: 9px; background: #fbfcfe; } 297 218 298 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__row { display: grid; grid-template-columns: minmax(190px, 240px) minmax(280px, 1fr); align-items: center; gap: 18px; padding: 14px; } 299 219 300 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__row + .syncr-tools__row { border-top: 1px solid #e1e6ec; } 301 220 302 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__copy strong, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__copy p { display: block; } 303 221 304 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__copy strong { color: #1d2327; } 305 222 306 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__copy p { margin: 2px 0 0; color: #646970; } 307 223 308 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-field { display: grid; grid-template-columns: minmax(220px, 1fr) auto; align-items: end; gap: 8px; } 309 224 310 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-field label { display: block; } 311 225 312 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-field input, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-field select, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control textarea { box-sizing: border-box; width: 100%; max-width: none; } 313 226 314 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-field > div:not(.syncr-tool-results):not(.syncr-actions) { grid-column: 1; } 315 227 316 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-field .syncr-actions { grid-column: 2; } 317 228 318 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control p { margin: 6px 0 0; } 319 229 320 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-results { grid-column: 1 / -1; width: 100%; margin-top: 6px; } 321 230 322 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-results p { margin: 4px 0; } 323 231 324 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-results ul { margin: 2px 0 0 18px; list-style: disc; } 232 325 233 326 @container (max-width: 782px) { 327 234 328 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tiles { grid-template-columns: minmax(0, 1fr); } 329 235 330 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-policy__row { grid-template-columns: minmax(0, 1fr); gap: 7px; } 331 236 332 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__row { grid-template-columns: minmax(0, 1fr); gap: 10px; } 333 237 334 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-tools__control .syncr-tool-field { grid-template-columns: minmax(0, 1fr); } 238 335 } 336 239 337 @container (max-width: 600px) { 338 240 339 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__head { display: none; } 340 241 341 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__row { grid-template-columns: 42px minmax(0, 1fr) 34px; align-items: start; padding: 10px 0; } 342 242 343 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__row .syncr-rules__num { grid-column: 1; grid-row: 1; } 344 243 345 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__cell { grid-column: 2; padding: 4px 10px; } 346 244 347 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__cell::before { display: block; margin-bottom: 4px; color: #50575e; font-weight: 600; content: attr(data-syncr-column); } 348 245 349 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-rules__remove { grid-column: 3; grid-row: 1; } 246 350 } 247 351 248 352 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__toggle { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; } 353 249 354 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__viewer { margin-top: 10px; } 355 250 356 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__tabs { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } 357 251 358 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__tabs .button.is-active { background: #2271b1; border-color: #2271b1; color: #fff; } 359 252 360 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__auto { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 12px; color: #50575e; } 253 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__log { margin: 0; max-height: 320px; overflow: auto; padding: 10px 12px; background: #1d2327; color: #f0f0f1; font: 12px/1.6 Menlo, Consolas, monospace; white-space: pre-wrap; word-break: break-word; border-radius: 4px; } 361 362 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__log { margin: 0; max-height: 320px; overflow: auto; padding: 10px 12px; background: #1d2327; color: #f0f0f1; font: 12px/1.6 Menlo, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; border-radius: 4px; } 363 254 364 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__report { width: 100%; font: 12px/1.5 Menlo, Consolas, monospace; white-space: pre; overflow: auto; } 365 255 366 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__stats { display: grid; grid-template-columns: max-content 1fr; gap: 2px 16px; margin: 0 0 8px; } 367 256 368 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__stats dt { color: #50575e; } 369 257 370 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__stats dd { margin: 0; font-weight: 600; } 371 258 372 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__error { color: #b32d2e; margin: 6px 0; } 373 259 374 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__ok { color: #007017; margin-left: 8px; } 375 260 376 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__bad { color: #b32d2e; margin-left: 8px; } 261 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"])-section-advanced > .description { margin-bottom: 4px; } 377 378 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__note { color: #50617a; margin-left: 8px; } 379 380 #clicksyncr-for-kit-section-advanced > .description { margin-bottom: 4px; } 262 381 263 382 #submitdiv .syncr-kit-status { display: grid; grid-template-columns: max-content 1fr; gap: 2px 10px; align-items: baseline; } 383 264 384 #submitdiv .syncr-kit-status__name { font-weight: 600; text-decoration: none; } 385 265 386 #submitdiv .syncr-kit-status__facts { color: #646970; font-size: 12px; } 387 266 388 #submitdiv .syncr-kit-status__error { grid-column: 1 / -1; color: #b32d2e; font-size: 12px; } 389 267 390 #submitdiv .syncr-kit-status--failing .syncr-kit-status__facts { color: #b32d2e; } 268 391 269 392 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-section__toggle { all: unset; cursor: pointer; display: flex; align-items: center; gap: 8px; width: 100%; } 393 270 394 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-section__toggle:focus-visible { outline: 2px solid #2271b1; outline-offset: 2px; border-radius: 2px; } 395 271 396 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-section__chevron { width: 8px; height: 8px; border-right: 2px solid #50575e; border-bottom: 2px solid #50575e; transform: rotate(45deg); transition: transform .15s; margin-left: 2px; } 397 272 398 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-section--collapsible.is-collapsed .syncr-section__chevron { transform: rotate(-45deg); } 399 273 400 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-section--collapsible.is-collapsed > .description, :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-section--collapsible.is-collapsed > .syncr-tools { display: none; } 401 274 402 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-section--collapsible.is-collapsed > h3 { margin-bottom: 0; } 275 403 276 404 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__echo { margin-top: 8px; font-size: 12px; } 405 277 406 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__echo dl { display: grid; grid-template-columns: max-content 1fr; gap: 2px 12px; margin: 4px 0 10px; } 407 278 408 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__echo dt { color: #646970; } 409 279 410 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__echo dd { margin: 0; word-break: break-all; } 411 280 412 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .syncr-adv__sample { margin: 6px 0; padding: 8px; background: #f6f7f7; border: 1px solid #dcdcde; font-size: 11px; line-height: 1.5; white-space: pre-wrap; } 281 413 282 414 #wpbody-content > .notice.syncr-notice { display: block; clear: both; } 415 283 416 .notice.syncr-notice { --syncr-brand: #533afd; --syncr-brand-dark: #4032c8; --syncr-brand-pale: #d6d9fc; --syncr-tint: #f5f5ff; --syncr-ink: #1a2c44; --syncr-body: #50617a; --syncr-muted: #64748d; border-left-color: var(--syncr-brand); padding: 14px 16px; } 417 284 418 .notice.syncr-notice p { margin: 0; } 419 285 420 .notice.syncr-notice .syncr-notice__title { font-size: 15px; font-weight: 600; color: var(--syncr-ink); margin: 0 0 6px; } 421 286 422 .notice.syncr-notice .syncr-notice__body { color: var(--syncr-body); max-width: 78ch; line-height: 1.6; margin: 0; } 423 287 424 .notice.syncr-notice .syncr-notice__actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 14px 0 0; } 425 288 426 .notice.syncr-notice .syncr-notice__action { text-decoration: none; font-size: 13px; line-height: 1; border-radius: 4px; cursor: pointer; transition: background-color 0.15s, color 0.15s, border-color 0.15s; } 427 289 428 .notice.syncr-notice .syncr-notice__action--primary { background: var(--syncr-brand); border: 1px solid var(--syncr-brand); color: #fff; padding: 9px 16px; font-weight: 600; } 429 290 430 .notice.syncr-notice .syncr-notice__action--primary:hover, .notice.syncr-notice .syncr-notice__action--primary:focus { background: var(--syncr-brand-dark); border-color: var(--syncr-brand-dark); color: #fff; } 431 291 432 .notice.syncr-notice .syncr-notice__action--secondary { background: #fff; border: 1px solid var(--syncr-brand-pale); color: var(--syncr-brand-dark); padding: 9px 16px; font-weight: 600; } 433 292 434 .notice.syncr-notice .syncr-notice__action--secondary:hover, .notice.syncr-notice .syncr-notice__action--secondary:focus { background: var(--syncr-tint); border-color: var(--syncr-brand); color: var(--syncr-brand-dark); } 293 .notice.syncr-notice .syncr-notice__action--link { background: none; border: 0; color: var(--syncr-muted); padding: 9px 2px; } 435 436 .notice.syncr-notice .syncr-notice__action--link { background: none; border: 0; color: var(--syncr-body); padding: 9px 2px; } 437 294 438 .notice.syncr-notice .syncr-notice__action--link:hover, .notice.syncr-notice .syncr-notice__action--link:focus { color: var(--syncr-ink); text-decoration: underline; } 439 295 440 .notice.syncr-notice--onboarding { background: var(--syncr-tint); } 441 296 442 .notice.syncr-notice--failing { border-left-color: #d63638; } 443 297 444 .notice.syncr-notice.inline { margin: 16px 0 8px; } 298 445 299 446 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .button { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; width: auto; min-height: 40px; height: auto; margin: 0; padding: 0 16px; border: 1px solid; border-radius: 2px; font: 500 13px/38px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; text-transform: none; text-decoration: none; box-shadow: none; cursor: pointer; } 447 300 448 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .button:not(.syncr-btn-connected) { background: transparent; border-color: #3858e9; color: #3858e9; } 449 301 450 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .button.button-primary:not(.syncr-btn-connected) { background: #3858e9; border-color: transparent; color: #fff; } 451 302 452 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .button:not(.syncr-btn-connected):hover { background: #f0f3ff; color: #2145c7; } 453 303 454 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .button.button-primary:not(.syncr-btn-connected):hover { background: #2145c7; color: #fff; } 455 304 456 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .button:focus-visible { outline: 2px solid #3858e9; outline-offset: 2px; } 457 305 458 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]) .button:disabled { opacity: .55; cursor: default; } 459 306 460 [data-syncr-panel="clicksyncr-for-kit"] input[type="checkbox"] { width: 16px; height: 16px; min-width: 16px; margin: 0; } 307 461 308 462 [data-syncr-panel="clicksyncr-for-kit"].syncr-panel { color: #1d2327; font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } 463 309 464 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"]).syncr-panel label { text-transform: none; letter-spacing: normal; } 465 310 466 [data-syncr-panel="clicksyncr-for-kit"].syncr-panel .syncr-fragment-title { margin: 0 0 6px; font-size: 14px; font-weight: 600; } 467 311 468 [data-syncr-panel="clicksyncr-for-kit"].syncr-panel textarea::placeholder { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } 312 469 313 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-destination-extra[aria-disabled="true"] { opacity: .55; cursor: pointer; } 470 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-destination-extra[data-syncr-destination-tip] { opacity: .55; cursor: pointer; } 471 314 472 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-destination-extra:focus { outline: 2px solid #2271b1; outline-offset: 3px; } 473 315 474 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-main-caption { display: block; font-size: 12px; } 475 316 476 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-destination-tip { position: absolute; z-index: 100001; display: block; box-sizing: border-box; width: 350px; max-width: calc(100% - 16px); margin: 0; padding: 10px; background: #fff; border: 1px solid #8c8f94; border-radius: 4px; color: #1d2327; font-size: 13px; } 477 317 478 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-tile--primary { border-color: #2271b1; } 479 318 480 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-pro-chip { display: inline-block; margin-left: 4px; padding: 1px 5px; border-radius: 3px; background: #dce7f4; color: #22528a; font-size: 10px; font-weight: 700; } 481 319 482 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-destination-paused, 320 483 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-rule-paused { display: block; color: #76551e; font-size: 12px; } 484 321 485 :is(#clicksyncr-for-kit, [data-syncr-panel="clicksyncr-for-kit"] ) .syncr-license-row { padding: 12px; margin-bottom: 16px; border: 1px solid #ded7c4; border-radius: 4px; background: #fff8e7; } 486 322 487 /* === end clicksyncr kit: panel === */ 323 324 #syncr_kit_api_key {325 font-family: Menlo, Monaco, Consolas, "Courier New", monospace;326 }327 488 328 489 .syncr-ver { … … 405 566 406 567 @media (max-width: 782px) { 568 407 569 .fields-grid { 408 570 grid-template-columns: 1fr; … … 410 572 } 411 573 412 #syncr_kit_data {413 display: none;414 }415 416 574 .syncr-hidden { 417 575 display: none !important; 418 576 } 419 577 420 .syncr-api-key-input, 421 #syncr_kit_list_id { 578 .syncr-api-key-input { 422 579 vertical-align: middle; 423 580 } 424 581 425 .syncr-kit-toggle-key-visibility, 426 #syncr_kit_connect_btn, 427 #syncr_kit_refresh_lists { 582 .syncr-kit-toggle-key-visibility { 428 583 vertical-align: middle; 429 584 margin-left: 5px; -
clicksyncr-for-kit/trunk/assets/js/builder-panel.js
r3726852 r3760857 1 /** @package clicksyncr-for-kit */ 2 (function () { 3 'use strict'; 4 const panelId = 'clicksyncr-for-kit'; 5 const settingPrefix = 'cskit_'; 6 const fieldKey = (value) => String(value || '').replace(/^\{field:(.+)\}$/, '$1').replace(/^0$/, ''); 7 const inputKey = (value) => String(value || '').replace(/^\{inputs\.([^}]+)\}$/, '$1'); 8 const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); 1 /** @package */ 2 ( function () { 3 'use strict'; 4 const panelId = 'clicksyncr-for-kit'; 5 const settingPrefix = 'cskit_'; 6 const fieldKey = ( value ) => 7 String( value || '' ) 8 .replace( /^\{field:(.+)\}$/, '$1' ) 9 .replace( /^0$/, '' ); 10 const inputKey = ( value ) => 11 String( value || '' ).replace( /^\{inputs\.([^}]+)\}$/, '$1' ); 12 const same = ( a, b ) => JSON.stringify( a ) === JSON.stringify( b ); 9 13 10 function hydrate(root, values) { 11 const tiles = root.querySelector('[id="' + panelId + '-destination"]'); 12 if (tiles) { 13 tiles.dataset.syncrValue = values.group_id; 14 tiles.querySelectorAll('input[type=radio]').forEach((input) => { input.checked = input.value === values.group_id; }); 15 const primary = tiles.querySelector('[data-syncr-primary]'); 16 if (primary) primary.value = values.group_id; 17 } 18 const map = root.querySelector('[data-syncr-map]'); 19 if (map) map.dataset.syncrMap = JSON.stringify(values.field_map); 20 ['consent_field', 'status_preference'].forEach((key) => { 21 const control = root.querySelector('[name="' + root.dataset.syncrNamePrefix + '[' + key + ']"]'); 22 if (control) control.value = values[key]; 23 }); 24 } 14 function hydrate( root, values ) { 15 const tiles = root.querySelector( 16 '[id="' + panelId + '-destination"]' 17 ); 18 if ( tiles ) { 19 tiles.dataset.syncrValue = values.group_id; 20 tiles 21 .querySelectorAll( 'input[type=radio]' ) 22 .forEach( ( input ) => { 23 input.checked = input.value === values.group_id; 24 } ); 25 const primary = tiles.querySelector( '[data-syncr-primary]' ); 26 if ( primary ) { 27 primary.value = values.group_id; 28 } 29 } 30 const map = root.querySelector( '[data-syncr-map]' ); 31 if ( map ) { 32 map.dataset.syncrMap = JSON.stringify( values.field_map ); 33 } 34 [ 'consent_field', 'status_preference' ].forEach( ( key ) => { 35 const control = root.querySelector( 36 '[name="' + root.dataset.syncrNamePrefix + '[' + key + ']"]' 37 ); 38 if ( control ) { 39 control.value = values[ key ]; 40 } 41 } ); 42 } 25 43 26 function bind(root, store) { 27 if (root.syncrStore) return; 28 root.syncrStore = store; 29 const ninja = root.dataset.syncrBridge === 'ninjaforms'; 30 const keyOf = ninja ? fieldKey : inputKey; 31 const map = []; 32 if (ninja) { 33 Object.keys(store.all()).forEach((key) => { 34 if (key.indexOf(settingPrefix + 'map_') === 0 && keyOf(store.get(key))) { 35 map.push({provider: key.slice((settingPrefix + 'map_').length), form: keyOf(store.get(key))}); 36 } 37 }); 38 } else { 39 if (store.get('fieldEmailAddress')) map.push({provider: 'email', form: keyOf(store.get('fieldEmailAddress'))}); 40 Object.entries(store.get('merge_fields') || {}).forEach(([provider, value]) => { if (keyOf(value)) map.push({provider, form: keyOf(value)}); }); 41 } 42 hydrate(root, { 43 group_id: String(store.get(ninja ? settingPrefix + 'group_id' : 'list_id') || ''), 44 consent_field: ninja ? fieldKey(store.get(settingPrefix + 'consent_field')) : String(store.get('consent_field') || ''), 45 status_preference: String(store.get(ninja ? settingPrefix + 'status_preference' : 'status_preference') || 'active'), 46 field_map: map 47 }); 48 const set = (key, value, normalize) => { 49 const old = store.get(key); 50 if (normalize ? normalize(old) === normalize(value) : same(old, value)) return; 51 store.set(key, value); 52 }; 53 const controls = () => Array.from(root.querySelectorAll('input[name],select[name],textarea[name]')).filter((e) => !e.disabled && !e.closest('.syncr-locked-box')); 54 const proControls = JSON.parse(root.dataset.syncrProControls || '{}'); 55 const proValues = () => { 56 const values = {}; 57 const active = controls(); 58 Object.entries(proControls).forEach(([key, type]) => { 59 const name = root.dataset.syncrNamePrefix + '[' + key + ']'; 60 const inputs = active.filter((e) => e.name === name || e.name.indexOf(name + '[') === 0); 61 if (type === 'rules') { 62 const rows = {}; 63 inputs.forEach((control) => { 64 const parts = Array.from(control.name.slice(name.length).matchAll(/\[([^\]]*)\]/g), (match) => match[1]); 65 if (parts.length === 2) { rows[parts[0]] = rows[parts[0]] || {}; rows[parts[0]][parts[1]] = control.value; } 66 }); 67 values[key] = Object.values(rows); 68 } else if (type === 'checklist') values[key] = inputs.filter((e) => e.checked).map((e) => e.value); 69 else if (type === 'pair') { 70 values[key] = {}; 71 inputs.forEach((e) => { values[key][e.name.slice(name.length + 1, -1)] = e.value; }); 72 } else if (inputs.length) values[key] = type === 'checkbox' ? inputs[0].checked : inputs[0].value; 73 }); 74 return values; 75 }; 76 root.syncrCapturePro = () => { root.syncrProBaseline = proValues(); }; 77 const sync = () => { 78 const prefix = root.dataset.syncrNamePrefix; 79 const active = controls(); 80 const named = (key) => active.filter((e) => e.name === prefix + '[' + key + ']'); 81 ['group_id', 'consent_field', 'status_preference'].forEach((key) => { 82 const control = named(key).find((e) => e.type !== 'radio' || e.checked); 83 if (!control) return; 84 const nativeKey = ninja ? settingPrefix + key : key === 'group_id' ? 'list_id' : key; 85 const value = ninja && key === 'consent_field' && control.value ? '{field:' + control.value + '}' : control.value; 86 set(nativeKey, value, ninja && key === 'consent_field' ? fieldKey : (v) => String(v || '')); 87 }); 88 const mapping = {}; 89 active.filter((e) => e.name.indexOf(prefix + '[field_map][') === 0 && e.name.endsWith('[provider]')).forEach((provider) => { 90 const field = active.find((e) => e.name === provider.name.replace(/\[provider\]$/, '[form]')); 91 if (field) mapping[provider.value] = field.value; 92 }); 93 if (ninja) { 94 Object.entries(mapping).forEach(([provider, field]) => { 95 const key = settingPrefix + 'map_' + provider; 96 if (!field) { if (fieldKey(store.get(key))) store.remove(key); } 97 else set(key, provider === 'email' ? field : '{field:' + field + '}', fieldKey); 98 }); 99 } else { 100 const fields = Object.assign({}, store.get('merge_fields') || {}); 101 Object.entries(mapping).forEach(([provider, field]) => { 102 if (provider === 'email') set('fieldEmailAddress', field ? '{inputs.' + field + '}' : '', inputKey); 103 else if (!field) delete fields[provider]; 104 else if (inputKey(fields[provider]) !== field) fields[provider] = '{inputs.' + field + '}'; 105 }); 106 if (!same(fields, store.get('merge_fields') || {})) store.set('merge_fields', fields); 107 } 108 if (root.syncrProBaseline) { 109 const values = proValues(); 110 Object.keys(values).forEach((key) => { 111 if (same(values[key], root.syncrProBaseline[key])) return; 112 const nativeKey = key.replace(/:/g, '__'); 113 const type = proControls[key]; 114 const value = values[key]; 115 if (type === 'checklist') { 116 const controlsForKey = active.filter((e) => e.name === prefix + '[' + key + '][]'); 117 controlsForKey.forEach((control) => set(nativeKey + '__' + control.value, ninja ? (control.checked ? 1 : 0) : control.checked)); 118 } else if (type === 'pair') { 119 Object.entries(value).forEach(([part, item]) => set(nativeKey + '__' + part, item)); 120 } else if (type === 'field' && ninja) { 121 set(nativeKey, value ? '{field:' + value + '}' : '', fieldKey); 122 } else { 123 set(nativeKey, type === 'checkbox' && ninja ? (value ? 1 : 0) : value); 124 } 125 }); 126 root.syncrProBaseline = values; 127 } 128 }; 129 ['change', 'input'].forEach((event) => root.addEventListener(event, (e) => { sync(); if (ninja) e.stopPropagation(); })); 130 root.addEventListener('click', (e) => { 131 if (e.target.closest('[data-syncr-make-primary],.syncr-rules__add,.syncr-rules__remove')) queueMicrotask(sync); 132 if (ninja) e.stopPropagation(); 133 }); 134 } 44 function bind( root, store ) { 45 if ( root.syncrStore ) { 46 return; 47 } 48 root.syncrStore = store; 49 const ninja = root.dataset.syncrBridge === 'ninjaforms'; 50 const keyOf = ninja ? fieldKey : inputKey; 51 const map = []; 52 if ( ninja ) { 53 Object.keys( store.all() ).forEach( ( key ) => { 54 if ( 55 key.indexOf( settingPrefix + 'map_' ) === 0 && 56 keyOf( store.get( key ) ) 57 ) { 58 map.push( { 59 provider: key.slice( 60 ( settingPrefix + 'map_' ).length 61 ), 62 form: keyOf( store.get( key ) ), 63 } ); 64 } 65 } ); 66 } else { 67 if ( store.get( 'fieldEmailAddress' ) ) { 68 map.push( { 69 provider: 'email', 70 form: keyOf( store.get( 'fieldEmailAddress' ) ), 71 } ); 72 } 73 Object.entries( store.get( 'merge_fields' ) || {} ).forEach( 74 ( [ provider, value ] ) => { 75 if ( keyOf( value ) ) { 76 map.push( { provider, form: keyOf( value ) } ); 77 } 78 } 79 ); 80 } 81 hydrate( root, { 82 group_id: String( 83 store.get( ninja ? settingPrefix + 'group_id' : 'list_id' ) || 84 '' 85 ), 86 consent_field: ninja 87 ? fieldKey( store.get( settingPrefix + 'consent_field' ) ) 88 : String( store.get( 'consent_field' ) || '' ), 89 status_preference: String( 90 store.get( 91 ninja 92 ? settingPrefix + 'status_preference' 93 : 'status_preference' 94 ) || 'active' 95 ), 96 field_map: map, 97 } ); 98 const set = ( key, value, normalize ) => { 99 const old = store.get( key ); 100 if ( 101 normalize 102 ? normalize( old ) === normalize( value ) 103 : same( old, value ) 104 ) { 105 return; 106 } 107 store.set( key, value ); 108 }; 109 const controls = () => 110 Array.from( 111 root.querySelectorAll( 112 'input[name],select[name],textarea[name]' 113 ) 114 ).filter( 115 ( e ) => ! e.disabled && ! e.closest( '.syncr-locked-box' ) 116 ); 117 const proControls = JSON.parse( root.dataset.syncrProControls || '{}' ); 118 const proValues = () => { 119 const values = {}; 120 const active = controls(); 121 Object.entries( proControls ).forEach( ( [ key, type ] ) => { 122 const name = root.dataset.syncrNamePrefix + '[' + key + ']'; 123 const inputs = active.filter( 124 ( e ) => 125 e.name === name || e.name.indexOf( name + '[' ) === 0 126 ); 127 if ( type === 'rules' ) { 128 const rows = {}; 129 inputs.forEach( ( control ) => { 130 const parts = Array.from( 131 control.name 132 .slice( name.length ) 133 .matchAll( /\[([^\]]*)\]/g ), 134 ( match ) => match[ 1 ] 135 ); 136 if ( parts.length === 2 ) { 137 rows[ parts[ 0 ] ] = rows[ parts[ 0 ] ] || {}; 138 rows[ parts[ 0 ] ][ parts[ 1 ] ] = control.value; 139 } 140 } ); 141 values[ key ] = Object.values( rows ); 142 } else if ( type === 'checklist' ) { 143 values[ key ] = inputs 144 .filter( ( e ) => e.checked ) 145 .map( ( e ) => e.value ); 146 } else if ( type === 'pair' ) { 147 values[ key ] = {}; 148 inputs.forEach( ( e ) => { 149 values[ key ][ e.name.slice( name.length + 1, -1 ) ] = 150 e.value; 151 } ); 152 } else if ( inputs.length ) { 153 values[ key ] = 154 type === 'checkbox' 155 ? inputs[ 0 ].checked 156 : inputs[ 0 ].value; 157 } 158 } ); 159 return values; 160 }; 161 root.syncrCapturePro = () => { 162 root.syncrProBaseline = proValues(); 163 }; 164 const sync = () => { 165 const prefix = root.dataset.syncrNamePrefix; 166 const active = controls(); 167 const named = ( key ) => 168 active.filter( ( e ) => e.name === prefix + '[' + key + ']' ); 169 [ 'group_id', 'consent_field', 'status_preference' ].forEach( 170 ( key ) => { 171 const control = named( key ).find( 172 ( e ) => e.type !== 'radio' || e.checked 173 ); 174 if ( ! control ) { 175 return; 176 } 177 let nativeKey = key === 'group_id' ? 'list_id' : key; 178 if ( ninja ) { 179 nativeKey = settingPrefix + key; 180 } 181 const value = 182 ninja && key === 'consent_field' && control.value 183 ? '{field:' + control.value + '}' 184 : control.value; 185 set( 186 nativeKey, 187 value, 188 ninja && key === 'consent_field' 189 ? fieldKey 190 : ( v ) => String( v || '' ) 191 ); 192 } 193 ); 194 const mapping = {}; 195 active 196 .filter( 197 ( e ) => 198 e.name.indexOf( prefix + '[field_map][' ) === 0 && 199 e.name.endsWith( '[provider]' ) 200 ) 201 .forEach( ( provider ) => { 202 const field = active.find( 203 ( e ) => 204 e.name === 205 provider.name.replace( /\[provider\]$/, '[form]' ) 206 ); 207 if ( field ) { 208 mapping[ provider.value ] = field.value; 209 } 210 } ); 211 if ( ninja ) { 212 Object.entries( mapping ).forEach( ( [ provider, field ] ) => { 213 const key = settingPrefix + 'map_' + provider; 214 if ( ! field ) { 215 if ( fieldKey( store.get( key ) ) ) { 216 store.remove( key ); 217 } 218 } else { 219 set( 220 key, 221 provider === 'email' 222 ? field 223 : '{field:' + field + '}', 224 fieldKey 225 ); 226 } 227 } ); 228 } else { 229 const fields = Object.assign( 230 {}, 231 store.get( 'merge_fields' ) || {} 232 ); 233 Object.entries( mapping ).forEach( ( [ provider, field ] ) => { 234 if ( provider === 'email' ) { 235 set( 236 'fieldEmailAddress', 237 field ? '{inputs.' + field + '}' : '', 238 inputKey 239 ); 240 } else if ( ! field ) { 241 delete fields[ provider ]; 242 } else if ( inputKey( fields[ provider ] ) !== field ) { 243 fields[ provider ] = '{inputs.' + field + '}'; 244 } 245 } ); 246 if ( ! same( fields, store.get( 'merge_fields' ) || {} ) ) { 247 store.set( 'merge_fields', fields ); 248 } 249 } 250 if ( root.syncrProBaseline ) { 251 const values = proValues(); 252 Object.keys( values ).forEach( ( key ) => { 253 if ( same( values[ key ], root.syncrProBaseline[ key ] ) ) { 254 return; 255 } 256 const nativeKey = key.replace( /:/g, '__' ); 257 const type = proControls[ key ]; 258 const value = values[ key ]; 259 if ( type === 'checklist' ) { 260 const controlsForKey = active.filter( 261 ( e ) => e.name === prefix + '[' + key + '][]' 262 ); 263 controlsForKey.forEach( ( control ) => { 264 const checked = control.checked ? 1 : 0; 265 set( 266 nativeKey + '__' + control.value, 267 ninja ? checked : control.checked 268 ); 269 } ); 270 } else if ( type === 'pair' ) { 271 Object.entries( value ).forEach( ( [ part, item ] ) => 272 set( nativeKey + '__' + part, item ) 273 ); 274 } else if ( type === 'field' && ninja ) { 275 set( 276 nativeKey, 277 value ? '{field:' + value + '}' : '', 278 fieldKey 279 ); 280 } else { 281 const checked = value ? 1 : 0; 282 set( 283 nativeKey, 284 type === 'checkbox' && ninja ? checked : value 285 ); 286 } 287 } ); 288 root.syncrProBaseline = values; 289 } 290 }; 291 [ 'change', 'input' ].forEach( ( event ) => 292 root.addEventListener( event, ( e ) => { 293 sync(); 294 if ( ninja ) { 295 e.stopPropagation(); 296 } 297 } ) 298 ); 299 root.addEventListener( 'click', ( e ) => { 300 if ( 301 e.target.closest( 302 '[data-syncr-make-primary],.syncr-rules__add,.syncr-rules__remove' 303 ) 304 ) { 305 queueMicrotask( sync ); 306 } 307 if ( ninja ) { 308 e.stopPropagation(); 309 } 310 } ); 311 } 135 312 136 function prepare(root) { 137 if (root.dataset.syncrBridge === 'ninjaforms') return !!root.syncrStore; 138 if (root.dataset.syncrBridge !== 'fluentforms') return true; 139 if (root.syncrStore) return true; 140 let el = root; 141 while (el && !(el.__vue__ && el.__vue__.settings)) el = el.parentElement; 142 if (!el) return false; 143 const vm = el.__vue__; 144 bind(root, {all: () => vm.settings, get: (k) => vm.settings[k], set: (k, v) => vm.$set(vm.settings, k, v), remove: (k) => vm.$delete(vm.settings, k)}); 145 return true; 146 } 147 let hooked = false; 148 function ninjaHook() { 149 if (hooked || !window.Backbone || !window.Backbone.Radio) return; 150 hooked = true; 151 window.Backbone.Radio.channel('setting-type-clicksyncr_kit_panel').on('render:setting', function (setting, model, view) { 152 const root = view.el.querySelector('[data-syncr-bridge="ninjaforms"]'); 153 if (root) bind(root, {all: () => model.attributes, get: (k) => model.get(k), set: (k, v) => model.set(k, v), remove: (k) => model.unset(k)}); 154 }); 155 } 156 window.syncrBuilderPanel_kit = {prepare, ready: (root) => { if (root.syncrCapturePro) root.syncrCapturePro(); }}; 157 ninjaHook(); 158 document.addEventListener('DOMContentLoaded', ninjaHook); 159 }()); 313 function prepare( root ) { 314 if ( root.dataset.syncrBridge === 'ninjaforms' ) { 315 return !! root.syncrStore; 316 } 317 if ( root.dataset.syncrBridge !== 'fluentforms' ) { 318 return true; 319 } 320 if ( root.syncrStore ) { 321 return true; 322 } 323 let el = root; 324 while ( el && ! ( el.__vue__ && el.__vue__.settings ) ) { 325 el = el.parentElement; 326 } 327 if ( ! el ) { 328 return false; 329 } 330 const vm = el.__vue__; 331 bind( root, { 332 all: () => vm.settings, 333 get: ( k ) => vm.settings[ k ], 334 set: ( k, v ) => vm.$set( vm.settings, k, v ), 335 remove: ( k ) => vm.$delete( vm.settings, k ), 336 } ); 337 return true; 338 } 339 let hooked = false; 340 function ninjaHook() { 341 if ( hooked || ! window.Backbone || ! window.Backbone.Radio ) { 342 return; 343 } 344 hooked = true; 345 window.Backbone.Radio.channel( 'setting-type-clicksyncr_kit_panel' ).on( 346 'render:setting', 347 function ( setting, model, view ) { 348 const root = view.el.querySelector( 349 '[data-syncr-bridge="ninjaforms"]' 350 ); 351 if ( root ) { 352 bind( root, { 353 all: () => model.attributes, 354 get: ( k ) => model.get( k ), 355 set: ( k, v ) => model.set( k, v ), 356 remove: ( k ) => model.unset( k ), 357 } ); 358 } 359 } 360 ); 361 } 362 window.syncrBuilderPanel_kit = { 363 prepare, 364 ready: ( root ) => { 365 if ( root.syncrCapturePro ) { 366 root.syncrCapturePro(); 367 } 368 }, 369 }; 370 ninjaHook(); 371 document.addEventListener( 'DOMContentLoaded', ninjaHook ); 372 } )(); -
clicksyncr-for-kit/trunk/assets/js/form-picker.js
r3726852 r3760857 1 /** @package clicksyncr-for-kit */ 2 (function () { 3 'use strict'; 4 document.addEventListener('click', function (event) { 5 const go = event.target.closest('[data-syncr-form-go]'); 6 if (!go) return; 7 const picker = go.closest('[data-syncr-notice]').querySelector('[data-syncr-form-picker]'); 8 const url = new URL(picker.value, window.location.href); 9 if (url.origin === window.location.origin && url.pathname.includes('/wp-admin/')) window.location.assign(url.href); 10 }); 11 const params = new URLSearchParams(window.location.search); 12 if (params.get('clicksyncr_create') === 'kit' && params.get('page') === 'fluent_forms') { 13 const create = new MutationObserver(openCreate); 14 const timeout = setTimeout(function () { create.disconnect(); }, 20000); 15 function openCreate() { 16 const button = document.querySelector('.ff_all_forms_action_row button.el-button--primary'); 17 if (!button) return; 18 create.disconnect(); 19 clearTimeout(timeout); 20 button.click(); 21 } 22 create.observe(document.documentElement, {childList: true, subtree: true}); 23 openCreate(); 24 return; 25 } 26 if (params.get('clicksyncr_setup') !== 'kit') return; 27 const page = params.get('page'); 28 if (page === 'formidable' || page === 'forminator-cform-wizard') { 29 let added = false; 30 let clicked = false; 31 let lastClick = 0; 32 const visible = (node) => node && node.getClientRects().length > 0; 33 const timer = setInterval(openMapping, 200); 34 const timeout = setTimeout(function () { clearInterval(timer); }, 20000); 35 function finish() { clearInterval(timer); clearTimeout(timeout); } 36 function click(button) { 37 if (!visible(button) || Date.now() - lastClick < 500) return false; 38 lastClick = Date.now(); 39 button.click(); 40 return true; 41 } 42 function openMapping() { 43 if (page === 'formidable') { 44 const panel = document.querySelector('.frm_single_clicksyncr_kit_settings'); 45 const control = panel && panel.querySelector('#clicksyncr-for-kit-panel'); 46 const toggle = panel && panel.querySelector('.widget-action'); 47 const expanded = toggle && toggle.getAttribute('aria-expanded') === 'true'; 48 if (visible(control) && expanded) { panel.scrollIntoView({block: 'center'}); finish(); return; } 49 const add = [...document.querySelectorAll('[data-actiontype="clicksyncr_kit"].frm_active_action')].find(visible); 50 if (!visible(panel) && !add) { click(document.querySelector('a[href="#email_settings"]')); return; } 51 if (visible(panel)) { if (!expanded) click(toggle); return; } 52 if (!added && click(add)) added = true; 53 } else { 54 if (visible(document.querySelector('#clicksyncr-for-kit-destination'))) { finish(); return; } 55 const button = [...document.querySelectorAll('.connect-integration[data-slug="clicksyncrkit"]')].find(visible); 56 if (!button) { click(document.querySelector('#forminator-navigation-integrations')); return; } 57 if (!clicked && window.jQuery && window.jQuery._data(button, 'events')?.click && click(button)) clicked = true; 58 } 59 } 60 openMapping(); 61 return; 62 } 63 if (page !== 'ninja-forms') return; 64 let added = false; 65 let lastClick = 0; 66 const current = window.clicksyncr_kit_picker || {}; 67 const id = Number(current.actionId || params.get('clicksyncr_action')); 68 const visible = (node) => node && node.getClientRects().length > 0; 69 const timer = setInterval(openAction, 200); 70 const timeout = setTimeout(function () { clearInterval(timer); }, 20000); 71 function openAction() { 72 const settings = document.querySelector('[data-syncr-bridge="ninjaforms"]'); 73 if (visible(settings)) { 74 clearInterval(timer); 75 clearTimeout(timeout); 76 if (id > 0) { 77 const url = new URL(location.href); 78 url.searchParams.set('clicksyncr_action', String(id)); 79 history.replaceState(null, '', url.href); 80 } 81 return; 82 } 83 if (Date.now() - lastClick < 500) return; 84 const row = id > 0 ? document.querySelector('[data-testid="nf-action-row-' + id + '"] .nf-edit-settings') : null; 85 let button; 86 if (visible(row)) button = row; 87 else if (!visible(document.querySelector('[data-testid="nf-add-action-header"]'))) button = document.querySelector('[data-testid="nf-menu-actions"]'); 88 else if (!id && !added) { 89 const type = document.querySelector('[data-testid="nf-action-type-clicksyncr_kit"]'); 90 if (visible(type)) { added = true; button = type; } 91 else button = document.querySelector('[data-testid="nf-add-action-header"]'); 92 } 93 if (visible(button)) { lastClick = Date.now(); button.click(); } 94 } 95 openAction(); 96 })(); 1 /** @package */ 2 ( function () { 3 'use strict'; 4 document.addEventListener( 'click', function ( event ) { 5 const go = event.target.closest( '[data-syncr-form-go]' ); 6 if ( ! go ) { 7 return; 8 } 9 const picker = go 10 .closest( '[data-syncr-notice]' ) 11 .querySelector( '[data-syncr-form-picker]' ); 12 const url = new URL( picker.value, window.location.href ); 13 if ( 14 url.origin === window.location.origin && 15 url.pathname.includes( '/wp-admin/' ) 16 ) { 17 window.location.assign( url.href ); 18 } 19 } ); 20 const params = new URLSearchParams( window.location.search ); 21 if ( 22 params.get( 'clicksyncr_create' ) === 'kit' && 23 params.get( 'page' ) === 'fluent_forms' 24 ) { 25 const create = new MutationObserver( openCreate ); 26 const timeout = setTimeout( function () { 27 create.disconnect(); 28 }, 20000 ); 29 function openCreate() { 30 const button = document.querySelector( 31 '.ff_all_forms_action_row button.el-button--primary' 32 ); 33 if ( ! button ) { 34 return; 35 } 36 create.disconnect(); 37 clearTimeout( timeout ); 38 button.click(); 39 } 40 create.observe( document.documentElement, { 41 childList: true, 42 subtree: true, 43 } ); 44 openCreate(); 45 return; 46 } 47 if ( params.get( 'clicksyncr_setup' ) !== 'kit' ) { 48 return; 49 } 50 const page = params.get( 'page' ); 51 if ( page === 'formidable' || page === 'forminator-cform-wizard' ) { 52 let added = false; 53 let clicked = false; 54 let lastClick = 0; 55 const visible = ( node ) => node && node.getClientRects().length > 0; 56 const timer = setInterval( openMapping, 200 ); 57 const timeout = setTimeout( function () { 58 clearInterval( timer ); 59 }, 20000 ); 60 function finish() { 61 clearInterval( timer ); 62 clearTimeout( timeout ); 63 } 64 function click( button ) { 65 if ( ! visible( button ) || Date.now() - lastClick < 500 ) { 66 return false; 67 } 68 lastClick = Date.now(); 69 button.click(); 70 return true; 71 } 72 function openMapping() { 73 if ( page === 'formidable' ) { 74 const panel = document.querySelector( 75 '.frm_single_clicksyncr_kit_settings' 76 ); 77 const control = 78 panel && panel.querySelector( '#clicksyncr-for-kit-panel' ); 79 const toggle = panel && panel.querySelector( '.widget-action' ); 80 const expanded = 81 toggle && toggle.getAttribute( 'aria-expanded' ) === 'true'; 82 if ( visible( control ) && expanded ) { 83 panel.scrollIntoView( { block: 'center' } ); 84 finish(); 85 return; 86 } 87 const add = [ 88 ...document.querySelectorAll( 89 '[data-actiontype="clicksyncr_kit"].frm_active_action' 90 ), 91 ].find( visible ); 92 if ( ! visible( panel ) && ! add ) { 93 click( 94 document.querySelector( 'a[href="#email_settings"]' ) 95 ); 96 return; 97 } 98 if ( visible( panel ) ) { 99 if ( ! expanded ) { 100 click( toggle ); 101 } 102 return; 103 } 104 if ( ! added && click( add ) ) { 105 added = true; 106 } 107 } else { 108 if ( 109 visible( 110 document.querySelector( 111 '#clicksyncr-for-kit-destination' 112 ) 113 ) 114 ) { 115 finish(); 116 return; 117 } 118 const button = [ 119 ...document.querySelectorAll( 120 '.connect-integration[data-slug="clicksyncrkit"]' 121 ), 122 ].find( visible ); 123 if ( ! button ) { 124 click( 125 document.querySelector( 126 '#forminator-navigation-integrations' 127 ) 128 ); 129 return; 130 } 131 if ( 132 ! clicked && 133 window.jQuery && 134 window.jQuery._data( button, 'events' )?.click && 135 click( button ) 136 ) { 137 clicked = true; 138 } 139 } 140 } 141 openMapping(); 142 return; 143 } 144 if ( page !== 'ninja-forms' ) { 145 return; 146 } 147 let added = false; 148 let lastClick = 0; 149 const current = window.clicksyncr_kit_picker || {}; 150 const id = Number( current.actionId || params.get( 'clicksyncr_action' ) ); 151 const visible = ( node ) => node && node.getClientRects().length > 0; 152 const timer = setInterval( openAction, 200 ); 153 const timeout = setTimeout( function () { 154 clearInterval( timer ); 155 }, 20000 ); 156 function openAction() { 157 const settings = document.querySelector( 158 '[data-syncr-bridge="ninjaforms"]' 159 ); 160 if ( visible( settings ) ) { 161 clearInterval( timer ); 162 clearTimeout( timeout ); 163 if ( id > 0 ) { 164 const url = new URL( location.href ); 165 url.searchParams.set( 'clicksyncr_action', String( id ) ); 166 history.replaceState( null, '', url.href ); 167 } 168 return; 169 } 170 if ( Date.now() - lastClick < 500 ) { 171 return; 172 } 173 const row = 174 id > 0 175 ? document.querySelector( 176 '[data-testid="nf-action-row-' + 177 id + 178 '"] .nf-edit-settings' 179 ) 180 : null; 181 let button; 182 if ( visible( row ) ) { 183 button = row; 184 } else if ( 185 ! visible( 186 document.querySelector( '[data-testid="nf-add-action-header"]' ) 187 ) 188 ) { 189 button = document.querySelector( 190 '[data-testid="nf-menu-actions"]' 191 ); 192 } else if ( ! id && ! added ) { 193 const type = document.querySelector( 194 '[data-testid="nf-action-type-clicksyncr_kit"]' 195 ); 196 if ( visible( type ) ) { 197 added = true; 198 button = type; 199 } else { 200 button = document.querySelector( 201 '[data-testid="nf-add-action-header"]' 202 ); 203 } 204 } 205 if ( visible( button ) ) { 206 lastClick = Date.now(); 207 button.click(); 208 } 209 } 210 openAction(); 211 } )(); -
clicksyncr-for-kit/trunk/assets/js/ninja-builder.js
r3726852 r3760857 1 1 /** 2 * @package clicksyncr-for-kit2 * @package 3 3 * @author clicksyncr 4 4 * @copyright 2026 ClickSyncr … … 6 6 */ 7 7 ( function () { 8 'use strict';8 'use strict'; 9 9 10 var cfg = window.syncrNinja_kit || {}; 11 var PROMPT = cfg.placeholder || 'Select a form field'; 12 var EMPTY = cfg.notMapped || 'Not mapped'; 13 var NOT_USED = cfg.notUsed || 'Not used'; 14 var hooked = false; 15 function freshHtml() { 16 if (hooked || !window.Backbone || !window.Backbone.Radio) return; 17 hooked = true; 18 window.Backbone.Radio.channel('setting-type-html').on('before:renderSetting', function (setting, action) { 19 if (action.get('type') !== 'clicksyncr_kit') return; 20 action.set(setting.get('name'), setting.get('value'), {silent: true}); 21 }); 22 } 23 freshHtml(); 24 var BADGE = /·\s*\[/; 25 var REQUIRED = /\(required\)/i; 10 const cfg = window.syncrNinja_kit || {}; 11 const PROMPT = cfg.placeholder || 'Select a form field'; 12 const EMPTY = cfg.notMapped || 'Not mapped'; 13 const NOT_USED = cfg.notUsed || 'Not used'; 14 let hooked = false; 15 function freshHtml() { 16 if ( hooked || ! window.Backbone || ! window.Backbone.Radio ) { 17 return; 18 } 19 hooked = true; 20 window.Backbone.Radio.channel( 'setting-type-html' ).on( 21 'before:renderSetting', 22 function ( setting, action ) { 23 if ( action.get( 'type' ) !== 'clicksyncr_kit' ) { 24 return; 25 } 26 action.set( setting.get( 'name' ), setting.get( 'value' ), { 27 silent: true, 28 } ); 29 } 30 ); 31 } 32 freshHtml(); 33 const BADGE = /·\s*\[/; 34 const REQUIRED = /\(required\)/i; 26 35 27 function wantedFor(text) { 28 if (!BADGE.test(text)) return NOT_USED; 29 return REQUIRED.test(text) ? PROMPT : EMPTY; 30 } 36 function wantedFor( text ) { 37 if ( ! BADGE.test( text ) ) { 38 return NOT_USED; 39 } 40 return REQUIRED.test( text ) ? PROMPT : EMPTY; 41 } 31 42 32 function relabel() { 33 var labels = document.querySelectorAll('#nf-drawer label, .nf-drawer label'); 34 Array.prototype.forEach.call(labels, function (label) { 35 var select = label.querySelector('select'); 36 if (!select || !select.options.length) return; 37 var first = select.options[0]; 43 function relabel() { 44 const labels = document.querySelectorAll( 45 '#nf-drawer label, .nf-drawer label' 46 ); 47 Array.prototype.forEach.call( labels, function ( label ) { 48 const select = label.querySelector( 'select' ); 49 if ( ! select || ! select.options.length ) { 50 return; 51 } 52 const first = select.options[ 0 ]; 38 53 39 if (first.value !== '' && first.value !== '0') return; 40 if (first.textContent.trim() !== '--' && first.textContent.trim() !== PROMPT && first.textContent.trim() !== EMPTY && first.textContent.trim() !== NOT_USED) return; 41 var wanted = wantedFor(label.textContent.trim()); 42 if (first.textContent.trim() !== wanted) first.textContent = wanted; 43 }); 44 } 54 if ( first.value !== '' && first.value !== '0' ) { 55 return; 56 } 57 if ( 58 first.textContent.trim() !== '--' && 59 first.textContent.trim() !== PROMPT && 60 first.textContent.trim() !== EMPTY && 61 first.textContent.trim() !== NOT_USED 62 ) { 63 return; 64 } 65 const wanted = wantedFor( label.textContent.trim() ); 66 if ( first.textContent.trim() !== wanted ) { 67 first.textContent = wanted; 68 } 69 } ); 70 } 45 71 46 var queued = false; 47 function schedule() { 48 if (queued) return; 49 queued = true; 50 window.requestAnimationFrame(function () { queued = false; relabel(); }); 51 } 72 let queued = false; 73 function schedule() { 74 if ( queued ) { 75 return; 76 } 77 queued = true; 78 window.requestAnimationFrame( function () { 79 queued = false; 80 relabel(); 81 } ); 82 } 52 83 53 function start() { 54 freshHtml(); 55 relabel(); 56 if (window.MutationObserver) { 84 function start() { 85 freshHtml(); 86 relabel(); 87 if ( window.MutationObserver ) { 88 new MutationObserver( schedule ).observe( document.body, { 89 childList: true, 90 subtree: true, 91 } ); 92 } 93 } 57 94 58 new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true }); 59 } 60 } 61 62 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); 63 else start(); 64 }()); 95 if ( document.readyState === 'loading' ) { 96 document.addEventListener( 'DOMContentLoaded', start ); 97 } else { 98 start(); 99 } 100 } )(); -
clicksyncr-for-kit/trunk/assets/js/notices.js
r3726852 r3760857 1 1 /** 2 * @package clicksyncr-for-kit2 * @package 3 3 * @author clicksyncr 4 4 * @copyright 2026 ClickSyncr 5 5 * @license GPL-2.0-or-later 6 6 */ 7 ( function () {8 'use strict';7 ( function () { 8 'use strict'; 9 9 10 const cfg = window.clicksyncrNotices; 11 if (!cfg) return; 10 const cfg = window.clicksyncrNotices; 11 if ( ! cfg ) { 12 return; 13 } 12 14 13 document.addEventListener('click', function (event) { 14 const trigger = event.target.closest('[data-syncr-act]'); 15 if (!trigger) return; 16 const notice = trigger.closest('[data-syncr-notice]'); 17 if (!notice) return; 18 if (trigger.tagName !== 'A') event.preventDefault(); 19 const id = notice.getAttribute('data-syncr-notice'); 20 notice.remove(); 21 fetch(cfg.url, { 22 method: 'POST', 23 credentials: 'same-origin', 24 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce }, 25 body: JSON.stringify({ id: id, act: trigger.getAttribute('data-syncr-act') }) 26 }).catch(function () {}); 27 }); 28 })(); 15 document.addEventListener( 'click', function ( event ) { 16 const trigger = event.target.closest( '[data-syncr-act]' ); 17 if ( ! trigger ) { 18 return; 19 } 20 const notice = trigger.closest( '[data-syncr-notice]' ); 21 if ( ! notice ) { 22 return; 23 } 24 if ( trigger.tagName !== 'A' ) { 25 event.preventDefault(); 26 } 27 const id = notice.getAttribute( 'data-syncr-notice' ); 28 notice.remove(); 29 fetch( cfg.url, { 30 method: 'POST', 31 credentials: 'same-origin', 32 headers: { 33 'Content-Type': 'application/json', 34 'X-WP-Nonce': cfg.nonce, 35 }, 36 body: JSON.stringify( { 37 id, 38 act: trigger.getAttribute( 'data-syncr-act' ), 39 } ), 40 } ).catch( function () {} ); 41 } ); 42 } )(); -
clicksyncr-for-kit/trunk/assets/js/panel.js
r3726852 r3760857 1 1 /** 2 * @package clicksyncr-for-kit2 * @package 3 3 * @author clicksyncr 4 4 * @copyright 2026 ClickSyncr 5 5 * @license GPL-2.0-or-later 6 6 */ 7 (function () { 8 'use strict'; 9 10 const PANEL_ID = 'clicksyncr-for-kit'; 11 const cfg = window.syncrPanel_kit || { restUrl: '', nonce: '', i18n: {} }; 12 const t = (key, fallback) => (cfg.i18n && cfg.i18n[key]) || fallback; 13 14 function testMessage(result) { 15 if (result.skipped) return result.message || t('advTestSkipped', 'Skipped. Nothing was sent.'); 16 if (result.success) return t('advTestOk', 'Added. Check the provider for this address.'); 17 return t('advTestFailed', 'Not added.') + (result.message ? ' ' + result.message : ''); 18 } 19 20 function boot(root) { 21 22 const meta = root.querySelector('[id="' + PANEL_ID + '-panel"]') || root; 23 if (!root || !meta || root.dataset.syncrBooted === '1') return; 24 if (window.syncrBuilderPanel_kit && !window.syncrBuilderPanel_kit.prepare(root)) return; 25 root.dataset.syncrBooted = '1'; 26 27 const byId = (suffix) => root.id === PANEL_ID ? document.getElementById(PANEL_ID + '-' + suffix) : root.querySelector('[id="' + PANEL_ID + '-' + suffix + '"]'); 28 const el = (tag, attrs, text) => { 29 const node = document.createElement(tag); 30 Object.keys(attrs || {}).forEach((k) => node.setAttribute(k, attrs[k])); 31 if (text !== undefined) node.textContent = text; 32 return node; 33 }; 34 const say = (node, text, isError) => { 35 if (!node) return; 36 node.textContent = text || ''; 37 node.classList.toggle('syncr-feedback--error', !!isError); 38 }; 39 40 const settle = (control) => { 41 if (control.tagName === 'SELECT') Array.from(control.options).forEach((o) => { o.defaultSelected = o.selected; }); 42 else if (control.type === 'checkbox' || control.type === 'radio') control.defaultChecked = control.checked; 43 }; 44 const formId = parseInt(meta.dataset.syncrForm || '0', 10); 45 const builder = meta.dataset.syncrBuilder || ''; 46 let kinds = { email: ['email'], number: ['number', 'range'], date: ['date'], not_text: ['checkbox', 'acceptance'] }; 47 try { kinds = Object.assign(kinds, JSON.parse(meta.dataset.syncrKinds || '{}')); } catch (e) {} 48 49 async function api(path, body) { 50 const resp = await fetch(cfg.restUrl + path, { 51 method: 'POST', 52 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce }, 53 body: JSON.stringify(Object.assign({ form_id: formId, builder: builder }, body || {})) 54 }); 55 return resp.json(); 56 } 57 58 const formFields = Array.from((byId('field-options') || { options: [] }).options).map((o) => { 59 let options = null; 60 try { options = o.dataset.options ? JSON.parse(o.dataset.options) : null; } catch (e) { options = null; } 61 return { id: o.value, label: o.textContent, kind: o.dataset.kind || '', options: Array.isArray(options) ? options : null }; 62 }); 63 64 let tip = null; 65 let tipTrigger = null; 66 let consumeEscape = false; 67 const closeTip = () => { 68 if (tipTrigger) tipTrigger.removeAttribute('aria-describedby'); 69 if (tip) tip.remove(); 70 tip = null; tipTrigger = null; 71 }; 72 const openTip = (trigger) => { 73 closeTip(); 74 tipTrigger = trigger; 75 tip = el('span', { class: 'syncr-destination-tip', role: 'tooltip', id: PANEL_ID + '-destination-tip' }); 76 if (trigger.hasAttribute('data-syncr-key-tip')) { 77 tip.append(document.createTextNode(trigger.getAttribute('data-syncr-key-tip'))); 78 } else { 79 tip.append(document.createTextNode(t('destination_tip', '') + ' ')); 80 tip.appendChild(el('a', { href: t('pro_url', 'https://clicksyncr.com'), target: '_blank', rel: 'noopener noreferrer' }, t('see_pro', 'See Pro'))); 81 } 82 trigger.setAttribute('aria-describedby', tip.id); 83 root.appendChild(tip); 84 const anchor = trigger.getBoundingClientRect(); 85 const box = tip.getBoundingClientRect(); 86 const boundary = root.getBoundingClientRect(); 87 tip.style.left = Math.max(8, Math.min(anchor.left - boundary.left, boundary.width - box.width - 8)) + 'px'; 88 const top = anchor.bottom + box.height + 8 > window.innerHeight ? anchor.top - box.height - 8 : anchor.bottom + 8; 89 tip.style.top = Math.max(8, top - boundary.top) + 'px'; 90 }; 91 ['mouseover', 'focusin', 'click'].forEach((name) => root.addEventListener(name, (event) => { 92 const trigger = event.target.closest('[data-syncr-destination-tip],[data-syncr-key-tip]'); 93 if (trigger) { 94 if (name === 'click') event.preventDefault(); 95 if (trigger !== tipTrigger) openTip(trigger); 96 } else if (name !== 'mouseover' && tip && !tip.contains(event.target)) closeTip(); 97 })); 98 document.addEventListener('click', (event) => { 99 if (tip && !tip.contains(event.target) && !tipTrigger.contains(event.target)) closeTip(); 100 }); 101 window.addEventListener('scroll', closeTip, true); 102 window.addEventListener('resize', closeTip); 103 document.addEventListener('keydown', (event) => { 104 if (event.key !== 'Escape' || (!consumeEscape && (!tip || !tip.getClientRects().length))) return; 105 event.preventDefault(); 106 event.stopPropagation(); 107 consumeEscape = true; 108 closeTip(); 109 }, true); 110 document.addEventListener('keyup', (event) => { 111 if (event.key !== 'Escape' || !consumeEscape) return; 112 event.preventDefault(); 113 event.stopPropagation(); 114 consumeEscape = false; 115 }, true); 116 root.addEventListener('keydown', (event) => { 117 const trigger = event.target.closest('[data-syncr-destination-tip],[data-syncr-key-tip]'); 118 if (trigger && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openTip(trigger); } 119 }); 120 const keyInput = byId('connection'); 121 const baseInput = byId('connection-base'); 122 const baseRow = byId('connection-base-row'); 123 const updateBase = () => { 124 if (baseRow && keyInput) baseRow.hidden = !keyInput.value.toLowerCase().startsWith(baseRow.dataset.keyPrefix || ''); 125 }; 126 if (keyInput) keyInput.addEventListener('input', updateBase); 127 updateBase(); 128 const connectBtn = byId('connection-connect'); 129 const connectFeedback = byId('connection-feedback'); 130 const KEY_MASK = '••••••••••••••••'; 131 const isMask = (v) => v.indexOf('•') !== -1; 132 const masked = (key) => key.slice(0, 8) + KEY_MASK + key.slice(-4); 133 if (connectBtn && keyInput) { 134 135 const state = (connected) => { 136 connectBtn.classList.toggle('syncr-btn-connected', connected); 137 connectBtn.textContent = connected ? t('synced', 'Synced') : t('connect', 'Connect'); 138 }; 139 140 const revealBtn = byId('connection-reveal'); 141 let shown = false; 142 const shownState = (on) => { 143 shown = on; 144 if (!revealBtn) return; 145 revealBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); 146 revealBtn.setAttribute('aria-label', on ? t('hide_key', 'Hide the key') : t('show_key', 'Show the key')); 147 const icon = revealBtn.querySelector('.dashicons'); 148 if (icon) { icon.classList.toggle('dashicons-visibility', !on); icon.classList.toggle('dashicons-hidden', on); } 149 }; 150 if (revealBtn) { 151 revealBtn.addEventListener('click', async () => { 152 if (revealBtn.hasAttribute('data-syncr-key-tip')) return; 153 if (shown) { 154 keyInput.value = masked(keyInput.value); 155 shownState(false); 156 return; 157 } 158 if (!isMask(keyInput.value || '')) return; 159 revealBtn.disabled = true; 160 try { 161 const resp = await api('/reveal-key', {}); 162 if (!resp.success || !resp.data || !resp.data.key) throw new Error(resp.message || t('failed', 'Could not load.')); 163 keyInput.value = resp.data.key; 164 shownState(true); 165 } catch (err) { 166 say(connectFeedback, err.message, true); 167 } 168 revealBtn.disabled = false; 169 }); 170 } 171 const connect = async () => { 172 const key = (keyInput.value || '').trim(); 173 if (!key) { say(connectFeedback, t('enter_key', 'Enter an API key.'), true); keyInput.focus(); return; } 174 if (isMask(key)) return; 175 connectBtn.disabled = true; 176 say(connectFeedback, t('connecting', 'Connecting…')); 177 try { 178 const credential = { api_key: key }; 179 if (baseInput && baseRow && !baseRow.hidden) credential.api_base_url = baseInput.value.trim(); 180 const resp = await api('/save-api-key', credential); 181 if (!resp.success) throw new Error(resp.message || t('failed', 'That key was not accepted.')); 182 keyInput.value = masked(key); 183 keyInput.dataset.hasKey = '1'; 184 if (revealBtn) revealBtn.hidden = false; 185 shownState(false); 186 state(true); 187 say(connectFeedback, t('connected', 'Connected. Choose where people go.')); 188 await loadDestinations(resp.group_id || ''); 189 } catch (err) { 190 state(false); 191 say(connectFeedback, err.message, true); 192 } 193 connectBtn.disabled = false; 194 }; 195 connectBtn.addEventListener('click', connect); 196 const foundBtn = root.querySelector('[data-syncr-found-key]'); 197 if (foundBtn) foundBtn.addEventListener('click', async () => { 198 foundBtn.disabled = true; 199 const r = await api('/advanced/found-key', {}); 200 if (!r || !r.success || !r.key) { foundBtn.disabled = false; say(connectFeedback, (r && r.message) || t('failed', 'That key was not accepted.'), true); return; } 201 keyInput.value = r.key; 202 keyInput.dispatchEvent(new Event('input', { bubbles: true })); 203 const line = foundBtn.closest('.syncr-found-key'); 204 if (line) line.remove(); 205 await connect(); 206 }); 207 keyInput.addEventListener('input', () => { if (!isMask(keyInput.value || '')) { state(false); shownState(false); } }); 208 keyInput.addEventListener('paste', () => setTimeout(() => { updateBase(); if ((keyInput.value || '').trim().length >= 30 && (!baseRow || baseRow.hidden || baseInput.value.trim())) connect(); }, 50)); 209 } 210 211 const tiles = byId('destination'); 212 const refreshBtn = byId('destination-refresh'); 213 const destFeedback = byId('destination-feedback') || connectFeedback; 214 const tilesLocked = !!tiles && tiles.dataset.syncrLocked === '1'; 215 const destinationValue = () => { 216 if (!tiles) return ''; 217 const selected = tiles.querySelector('[data-syncr-primary],input[type=radio]:checked'); 218 return selected ? selected.value : (tiles.dataset.syncrValue || ''); 219 }; 220 function syncPrimary() { 221 if (!tiles) return; 222 const primary = destinationValue(); 223 const names = []; 224 let paused = false; 225 tiles.dataset.syncrValue = primary; 226 Array.from(tiles.querySelectorAll('.syncr-tile')).forEach((tile) => { 227 const isPrimary = tile.dataset.syncrDestination === primary; 228 const box = tile.querySelector('input[type=checkbox]'); 229 tile.classList.toggle('syncr-tile--primary', isPrimary); 230 const extra = tile.querySelector('.syncr-destination-extra'); 231 if (extra) extra.hidden = isPrimary; 232 if (box && isPrimary && !tilesLocked) box.checked = false; 233 const caption = tile.querySelector('.syncr-main-caption'); 234 if (caption) caption.textContent = isPrimary ? t('main_caption', 'Main list: every contact goes here') : t('send_here', 'Send each contact here'); 235 const saved = tilesLocked && box && box.checked && !isPrimary; 236 const note = tile.querySelector('.syncr-destination-paused'); 237 if (note) note.hidden = !saved; 238 paused = paused || saved; 239 if (isPrimary || (!tilesLocked && box && box.checked)) names.push(tile.dataset.syncrLabel); 240 }); 241 let status = byId('destination-status'); 242 if (!status) { 243 status = el('p', { id: PANEL_ID + '-destination-status', role: 'status', class: 'syncr-destination-status' }); 244 tiles.after(status); 245 } 246 status.textContent = names.length ? t('dest_status', 'Each contact goes to:') + ' ' + names.join(', ') + (paused ? ' ' + t('saved_paused', 'Saved extra lists and rules are paused until you renew.') : '') : ''; 247 } 248 function renderTiles(list, keep) { 249 const ticked = new Set(Array.from(tiles.querySelectorAll('input[type=checkbox]:checked')).map((b) => b.value)); 250 tiles.textContent = ''; 251 tiles.classList.toggle('syncr-tiles--single', list.length === 1); 252 if (!list.some((g) => String(g.id) === keep)) keep = list.length ? String(list[0].id) : ''; 253 if (!tilesLocked) tiles.appendChild(el('input', { type: 'hidden', name: tiles.dataset.syncrExtras.replace(/\[\]$/, ''), value: '' })); 254 list.forEach((g, index) => { 255 const id = String(g.id); 256 const tile = el('div', { class: 'syncr-tile', 'data-syncr-destination': id, 'data-syncr-label': g.label }); 257 const label = el('label'); 258 const choice = el('input', { type: 'radio', name: tiles.dataset.syncrName, value: id }); 259 choice.checked = id === keep; 260 label.append(choice, document.createTextNode(' ' + g.label)); 261 tile.appendChild(label); 262 tile.appendChild(el('span', { class: 'syncr-main-caption' })); 263 const attrs = {class: 'syncr-destination-extra'}; 264 if (tilesLocked) Object.assign(attrs, {role:'button',tabindex:'0','data-syncr-destination-tip':'','aria-disabled':'true'}); 265 const extra = el('label', attrs); 266 extra.hidden = id === keep; 267 const boxAttrs = {type:'checkbox',value:id}; 268 if (tilesLocked) Object.assign(boxAttrs, {tabindex:'-1','aria-disabled':'true'}); 269 else boxAttrs.name = builder === 'wpforms' ? tiles.dataset.syncrExtras.replace(/\[\]$/, '[' + index + ']') : tiles.dataset.syncrExtras; 270 const box = el('input', boxAttrs); 271 box.checked = ticked.has(id) && id !== keep; 272 extra.append(box, document.createTextNode(' ' + t('also_here', 'Also add here'))); 273 if (tilesLocked) extra.appendChild(el('span', {class:'syncr-pro-chip'}, 'PRO')); 274 tile.appendChild(extra); 275 const note = el('span', {class:'syncr-destination-paused'}, t('saved_extra', 'Saved. Renew your license to send here too.')); 276 note.hidden = !tilesLocked || !box.checked; 277 tile.appendChild(note); 278 tiles.appendChild(tile); 279 settle(choice);settle(box); 280 }); 281 tiles.dataset.syncrValue = keep; 282 syncPrimary(); 283 } 284 if (tiles) { 285 tiles.addEventListener('change', (event) => { 286 if (event.target.type === 'radio') { syncPrimary(); loadFields(); } 287 else if (!tilesLocked && event.target.type === 'checkbox') syncPrimary(); 288 }); 289 syncPrimary(); 290 } 291 if (refreshBtn) refreshBtn.addEventListener('click', () => loadDestinations(destinationValue())); 292 293 async function loadDestinations(keep) { 294 if (!tiles) return; 295 if (refreshBtn) refreshBtn.disabled = true; 296 try { 297 const resp = await api('/groups', {}); 298 if (!resp.success || !Array.isArray(resp.data)) throw new Error(resp.message || t('failed', 'Could not load.')); 299 renderTiles(resp.data.map((g) => ({ id: g.id, label: g.label })), keep || destinationValue()); 300 const empty = byId('destination-empty'); 301 if (empty) empty.hidden = resp.data.length > 0; 302 say(destFeedback, resp.data.length ? '' : t('destinations_0', '')); 303 if (destinationValue()) await loadFields(); 304 } catch (err) { 305 say(destFeedback, err.message, true); 306 } 307 if (refreshBtn) refreshBtn.disabled = false; 308 } 309 310 const mapping = byId('mapping'); 311 const savedMap = mapping ? JSON.parse(mapping.dataset.syncrMap || '[]') : []; 312 const mapName = mapping ? (mapping.dataset.syncrName || '') : ''; 313 const KIND_COMPAT = { number: kinds.number, integer: kinds.number, date: kinds.date, datetime: kinds.date }; 314 315 function compatible(providerType, providerId) { 316 if (providerId === 'email') return formFields.filter((f) => kinds.email.indexOf(f.kind) !== -1); 317 const allowed = KIND_COMPAT[(providerType || '').toLowerCase()]; 318 if (!allowed) return formFields.filter((f) => kinds.not_text.indexOf(f.kind) === -1); 319 return formFields.filter((f) => f.kind === '' || allowed.indexOf(f.kind) !== -1); 320 } 321 322 function currentMap() { 323 if (!mapping) return savedMap; 324 const rows = Array.from(mapping.querySelectorAll('.syncr-map-row')); 325 if (!rows.length) return savedMap; 326 return rows.map((row) => ({ 327 provider: row.querySelector('input[type=hidden]').value, 328 form: row.querySelector('select').value 329 })).filter((m) => m.form); 330 } 331 332 async function loadFields() { 333 if (!mapping || !destinationValue()) return; 334 const before = currentMap(); 335 try { 336 const resp = await api('/fields', { group_id: destinationValue() }); 337 if (!resp.success || !Array.isArray(resp.data)) throw new Error(resp.message || t('failed', 'Could not load.')); 338 renderMapping(resp.data, before); 339 } catch (err) { 340 say(destFeedback, err.message, true); 341 } 342 } 343 344 function renderMapping(providerFields, saved) { 345 const section = mapping.closest('.syncr-section') || root; 346 const contributed = Array.from(section.querySelectorAll('.syncr-tile--contributed')); 347 mapping.textContent = ''; 348 const grid = el('div', { class: 'syncr-tiles syncr-tiles--mapping' }); 349 mapping.appendChild(grid); 350 351 providerFields.forEach((field, index) => { 352 const required = field.id === 'email'; 353 const tile = el('div', { class: 'syncr-tile syncr-map-row' }); 354 const select = el('select', { name: mapName + '[' + index + '][form]', id: PANEL_ID + '-map-' + field.id }); 355 const label = el('label', { for: select.id }, field.label || field.id); 356 label.appendChild(el('span', { class: 'syncr-tile__meta' }, field.id + ' · ' + (field.type || 'text'))); 357 if (required) label.appendChild(el('span', { class: 'syncr-tile__required' }, t('required', 'Required'))); 358 359 select.appendChild(el('option', { value: '' }, required ? t('select_field', 'Select a form field') : t('choose', 'Choose…'))); 360 const options = compatible(field.type, field.id); 361 const own = options.filter((f) => f.kind !== 'context'); 362 const context = options.filter((f) => f.kind === 'context'); 363 own.forEach((f) => select.appendChild(el('option', { value: f.id }, f.label))); 364 if (context.length) { 365 366 const og = el('optgroup', { label: t('context_group', 'Signup context') }); 367 context.forEach((f) => og.appendChild(el('option', { value: f.id }, f.label))); 368 select.appendChild(og); 369 } 370 if (!own.length) { 371 const kind = (field.type || '').toLowerCase(); 372 const hint = required ? t('need_email', 'Add an email field to the form') : (kind === 'date' ? t('need_date', 'Add a date field to the form') : (KIND_COMPAT[kind] ? t('need_number', 'Add a number field to the form') : '')); 373 if (hint) { const none = el('option', { value: '', disabled: 'disabled' }, hint); select.appendChild(none); } 374 } 375 const savedRow = saved.find((m) => m.provider === field.id); 376 if (savedRow && savedRow.form) { 377 if (Array.from(select.options).some((o) => o.value === savedRow.form)) { 378 select.value = savedRow.form; 379 } else { 380 381 const gone = el('option', { value: '', 'data-syncr-missing': savedRow.form }, t('missing', '%s is not on this form').replace('%s', '[' + savedRow.form + ']')); 382 select.insertBefore(gone, select.firstChild); 383 select.value = ''; 384 tile.classList.add('syncr-tile--missing'); 385 } 386 } else if (required && own.length) { 387 select.value = own[0].id; 388 } 389 settle(select); 390 391 tile.appendChild(label); 392 tile.appendChild(select); 393 tile.appendChild(el('input', { type: 'hidden', name: mapName + '[' + index + '][provider]', value: field.id })); 394 grid.appendChild(tile); 395 }); 396 contributed.forEach((tile) => grid.appendChild(tile)); 397 section.querySelectorAll('.syncr-tiles--extra').forEach((wrap) => { if (!wrap.children.length) wrap.remove(); }); 398 readback(); 399 } 400 401 const rulesBox = byId('destination_rules'); 402 if (rulesBox) { 403 const rulesName = rulesBox.dataset.syncrName || ''; 404 let groupsMap = {}; 405 try { groupsMap = JSON.parse(rulesBox.dataset.syncrGroups || '{}'); } catch (e) { groupsMap = {}; } 406 let savedRules = []; 407 try { savedRules = JSON.parse(rulesBox.dataset.syncrRules || '[]'); } catch (e) { savedRules = []; } 408 const ruleTargets = (() => { try { return JSON.parse(rulesBox.dataset.syncrTargets || '[]'); } catch (e) { return []; } })(); 409 const choiceFields = () => formFields.filter((f) => f.options && f.options.length); 410 const table = el('div', { class: 'syncr-rules__table' }); 411 const empty = el('p', { class: 'syncr-rules__empty description' }, t('rule_none', 'No rules yet. Everyone joins the groups above.')); 412 const actions = el('div', { class: 'syncr-actions syncr-actions--end' }); 413 const add = el('button', { type: 'button', class: 'button syncr-rules__add' }, t('rule_add', 'Add rule')); 414 actions.appendChild(add); 415 rulesBox.appendChild(table); rulesBox.appendChild(empty); rulesBox.appendChild(actions); 416 const head = () => { 417 const h = el('div', { class: 'syncr-rules__head', 'aria-hidden': 'true' }); 418 ['#', t('rule_when', 'When this field'), t('rule_is', 'is this value'), t('rule_add_to', 'add subscriber to'), ''].forEach((text) => h.appendChild(el('span', {}, text))); 419 return h; 420 }; 421 const rows = () => Array.from(table.querySelectorAll('.syncr-rules__row')); 422 const renumber = () => { 423 rows().forEach((row, i) => { 424 row.querySelector('.syncr-rules__num').textContent = String(i + 1).padStart(2, '0'); 425 row.querySelectorAll('[data-part]').forEach((c) => { c.name = rulesName + '[' + i + '][' + c.dataset.part + ']'; }); 426 }); 427 const any = rows().length > 0; 428 empty.hidden = any; 429 const existing = table.querySelector('.syncr-rules__head'); 430 if (any && !existing) table.insertBefore(head(), table.firstChild); 431 if (!any && existing) existing.remove(); 432 }; 433 const valueOptions = (select, fieldId, chosen, initial) => { 434 select.textContent = ''; 435 select.appendChild(el('option', { value: '' }, t('rule_choose_value', 'Choose a value'))); 436 const field = choiceFields().find((f) => f.id === fieldId); 437 (field ? field.options : []).forEach((opt) => select.appendChild(el('option', { value: opt.value }, opt.label || opt.value))); 438 if (chosen && !Array.from(select.options).some((o) => o.value === chosen)) select.appendChild(el('option', { value: chosen }, chosen)); 439 select.value = chosen || ''; 440 if (initial) settle(select); 441 }; 442 const cell = (label, control) => { 443 const c = el('div', { class: 'syncr-rules__cell', 'data-syncr-column': label }); 444 c.appendChild(control); 445 return c; 446 }; 447 const row = (rule, initial) => { 448 const line = el('div', { class: 'syncr-rules__row' }); 449 const fieldSel = el('select', { 'data-part': 'field', 'aria-label': t('rule_when', 'When this field') }); 450 fieldSel.appendChild(el('option', { value: '' }, t('rule_choose_field', 'Choose a field'))); 451 choiceFields().forEach((f) => fieldSel.appendChild(el('option', { value: f.id }, '[' + f.id + ']'))); 452 if (rule.field && !Array.from(fieldSel.options).some((o) => o.value === rule.field)) fieldSel.appendChild(el('option', { value: rule.field }, '[' + rule.field + '] ' + t('rule_missing', '(not on this form)'))); 453 fieldSel.value = rule.field || ''; 454 const valueSel = el('select', { 'data-part': 'value', 'aria-label': t('rule_is', 'is this value') }); 455 valueOptions(valueSel, rule.field || '', rule.value || '', initial); 456 const groupSel = el('select', { 'data-part': 'group', 'aria-label': t('rule_add_to', 'add subscriber to') }); 457 groupSel.appendChild(el('option', { value: '' }, t('rule_choose_group', 'Choose a group'))); 458 Object.keys(groupsMap).forEach((id) => groupSel.appendChild(el('option', { value: id }, groupsMap[id]))); 459 460 const byGroup = {}; 461 ruleTargets.forEach((tg) => { (byGroup[tg.group || t('rule_targets', 'Other')] ||= []).push(tg); }); 462 Object.keys(byGroup).forEach((label) => { 463 const og = el('optgroup', { label: label }); 464 byGroup[label].forEach((tg) => og.appendChild(el('option', { value: tg.value }, tg.label || tg.value))); 465 groupSel.appendChild(og); 466 }); 467 if (rule.group && !Array.from(groupSel.options).some((o) => o.value === rule.group)) groupSel.appendChild(el('option', { value: rule.group }, rule.group)); 468 groupSel.value = rule.group || ''; 469 const remove = el('button', { type: 'button', class: 'button-link syncr-rules__remove', title: t('rule_remove', 'Remove rule') }); 470 remove.appendChild(el('span', { class: 'dashicons dashicons-no-alt', 'aria-hidden': 'true' })); 471 remove.appendChild(el('span', { class: 'screen-reader-text' }, t('rule_remove', 'Remove rule'))); 472 fieldSel.addEventListener('change', () => valueOptions(valueSel, fieldSel.value, '', false)); 473 remove.addEventListener('click', () => { line.remove(); renumber(); }); 474 line.appendChild(el('strong', { class: 'syncr-rules__num' }, '')); 475 line.appendChild(cell(t('rule_when', 'When this field'), fieldSel)); 476 line.appendChild(cell(t('rule_is', 'is this value'), valueSel)); 477 line.appendChild(cell(t('rule_add_to', 'add subscriber to'), groupSel)); 478 line.appendChild(remove); 479 if (initial) { settle(fieldSel); settle(groupSel); } 480 table.appendChild(line); 481 renumber(); 482 }; 483 savedRules.forEach((r) => row(r, true)); 484 if (!choiceFields().length) { 485 add.hidden = true; 486 empty.textContent = t('rule_no_fields', 'This form has no choice fields (a drop-down, checkboxes or radio buttons), so nothing here can decide where they go.'); 487 } 488 add.addEventListener('click', () => { row({ field: '', value: '', group: '' }, false); }); 489 renumber(); 490 } 491 492 const dependents = Array.from(root.querySelectorAll('[data-syncr-show]')); 493 function controlValue(name) { 494 const nodes = Array.from(root.querySelectorAll('[name="' + name + '"]')); 495 if (!nodes.length) return null; 496 const node = nodes[0]; 497 if (node.type === 'checkbox') return node.checked ? (node.value || '1') : ''; 498 if (node.type === 'radio') { const on = nodes.find((n) => n.checked); return on ? on.value : ''; } 499 return node.value || ''; 500 } 501 function applyDependents() { 502 dependents.forEach((row) => { 503 let conditions = []; 504 try { conditions = JSON.parse(row.dataset.syncrShow || '[]'); } catch (e) { return; } 505 let show = true; 506 conditions.forEach((c) => { 507 const v = controlValue(c.name); 508 if (v === null) return; 509 if ('is' in c && v !== c.is) show = false; 510 if ('not' in c && v === c.not) show = false; 511 }); 512 row.hidden = !show; 513 }); 514 } 515 if (dependents.length) { 516 applyDependents(); 517 root.addEventListener('change', applyDependents); 518 } 519 520 const readbackEl = root.querySelector('.syncr-readback'); 521 function readback() { 522 if (!readbackEl || !mapping) return; 523 const selects = Array.from(mapping.querySelectorAll('.syncr-map-row select')); 524 if (!selects.length) return; 525 const mapped = selects.filter((s) => s.value).length; 526 const missing = selects.filter((s) => !s.value && s.selectedOptions[0] && s.selectedOptions[0].dataset.syncrMissing).length; 527 let text = t('mapped', '%1$d of %2$d fields mapped.').replace('%1$d', String(mapped)).replace('%2$d', String(selects.length)); 528 if (missing) text += ' ' + t('missing_count', '%d mapped to fields that are no longer on this form; save to clear them.').replace('%d', String(missing)); 529 readbackEl.textContent = text; 530 readbackEl.classList.toggle('syncr-readback--warn', missing > 0); 531 } 532 root.addEventListener('change', (event) => { 533 const tile = event.target.closest && event.target.closest('.syncr-tile--missing'); 534 if (tile && event.target.value) tile.classList.remove('syncr-tile--missing'); 535 readback(); 536 }); 537 538 const adv = (part) => byId('adv-' + part); 539 const advSay = (el, text, ok) => { if (!el) return; el.textContent = text; el.className = ok === false ? 'syncr-adv__bad' : 'syncr-adv__ok'; }; 540 const debugToggle = adv('debug'); 541 const debugState = adv('debug-state'); 542 if (debugToggle) { 543 debugToggle.addEventListener('change', async () => { 544 debugToggle.disabled = true; 545 const r = await api('/advanced/settings', { debug_log: debugToggle.checked }); 546 debugToggle.disabled = false; 547 if (!r || !r.success) { debugToggle.checked = !debugToggle.checked; return; } 548 debugToggle.checked = !!r.logging; 549 if (debugState) debugState.textContent = r.logging ? (cfg.i18n.advLogOn || 'On for 14 days, then off by itself.') : (cfg.i18n.advLogOff || 'Off.'); 550 if (r.logging) loadLog(); 551 }); 552 } 553 const logBox = adv('log'); 554 const logMeta = adv('log-meta'); 555 const logAuto = adv('log-auto'); 556 let logSource = 'own'; 557 let logTimer = null; 558 async function loadLog() { 559 if (!logBox) return; 560 const r = await api('/advanced/log', { source: logSource }); 561 if (!r || !r.success) { logBox.textContent = cfg.i18n.advLogFailed || 'Could not read the log.'; return; } 562 if (!r.exists) { 563 logBox.textContent = logSource === 'site' 564 ? (cfg.i18n.advNoSiteLog || 'This site keeps no PHP error log. Set WP_DEBUG_LOG in wp-config.php while you test, then look here again.') 565 : (cfg.i18n.advNoLog || 'No log yet. Switch the debug log on and sync a form.'); 566 } else if (!r.lines.length) { 567 logBox.textContent = r.filtered ? t('no_matching_log', 'No matching ClickSyncr entries.') : (cfg.i18n.advLogEmpty || 'The log is empty.'); 568 } else { 569 logBox.textContent = r.lines.join('\n'); 570 logBox.scrollTop = logBox.scrollHeight; 571 } 572 if (logMeta) logMeta.textContent = r.exists ? (r.path + ' · ' + Math.round(r.size / 1024) + ' KB · ' + r.lines.length + ' ' + (cfg.i18n.advLines || 'lines shown')) : ''; 573 if (logMeta && r.filtered) logMeta.textContent += ' · ' + t('filtered_log', 'ClickSyncr entries only'); 574 } 575 Array.from(root.querySelectorAll('[data-syncr-log]')).forEach((btn) => { 576 btn.addEventListener('click', () => { 577 Array.from(root.querySelectorAll('[data-syncr-log]')).forEach((b) => b.classList.toggle('is-active', b === btn)); 578 logSource = btn.dataset.syncrLog; 579 loadLog(); 580 }); 581 }); 582 if (adv('log-open')) adv('log-open').addEventListener('click', loadLog); 583 if (adv('log-clear')) adv('log-clear').addEventListener('click', async () => { 584 if (!window.confirm(t('advClearConfirm', 'Clear the plugin log?'))) return; 585 const r = await api('/advanced/log-clear', {}); 586 if (!r || !r.success) { if (logBox) logBox.textContent = t('advClearFailed', 'Could not clear the log.'); return; } 587 loadLog(); 588 }); 589 if (adv('log-download')) adv('log-download').addEventListener('click', () => { 590 window.open(cfg.restUrl + '/advanced/log-download?_wpnonce=' + encodeURIComponent(cfg.nonce) + '&builder=' + encodeURIComponent(builder) + '&form_id=' + encodeURIComponent(formId), '_blank'); 591 }); 592 if (logAuto) logAuto.addEventListener('change', () => { 593 if (logTimer) { clearInterval(logTimer); logTimer = null; } 594 if (logAuto.checked) { loadLog(); logTimer = setInterval(() => { if (root.isConnected) loadLog(); else { clearInterval(logTimer); logTimer = null; } }, 5000); } 595 }); 596 597 const reportBox = adv('report'); 598 const reportCopy = adv('report-copy'); 599 if (adv('report-build')) adv('report-build').addEventListener('click', async () => { 600 advSay(adv('report-feedback'), cfg.i18n.advBuilding || 'Building…', true); 601 const r = await api('/advanced/report', {}); 602 if (!r || !r.success) { advSay(adv('report-feedback'), cfg.i18n.advReportFailed || 'Could not build the report.', false); return; } 603 reportBox.value = r.report; 604 if (reportCopy) reportCopy.disabled = false; 605 advSay(adv('report-feedback'), '', true); 606 }); 607 if (reportCopy) reportCopy.addEventListener('click', async () => { 608 try { await navigator.clipboard.writeText(reportBox.value); advSay(adv('report-feedback'), cfg.i18n.advCopied || 'Copied.', true); } 609 catch (e) { reportBox.select(); advSay(adv('report-feedback'), cfg.i18n.advSelectCopy || 'Selected. Press Ctrl+C or Cmd+C.', true); } 610 }); 611 612 if (adv('refresh')) adv('refresh').addEventListener('click', async () => { 613 const feedback = adv('refresh-feedback'); 614 advSay(feedback, '…', true); 615 const result = await api('/advanced/refresh', {}); 616 if (!result || !result.success) { advSay(feedback, t('advFailed', 'Failed.'), false); return; } 617 if (tiles) { 618 await loadDestinations(destinationValue()); 619 advSay(feedback, t('advRefreshed', 'Refreshed.'), true); 620 return; 621 } 622 const groups = await api('/groups', {}); 623 if (!groups || !groups.success) { advSay(feedback, groups && groups.message || t('advFailed', 'Failed.'), false); return; } 624 const group = meta.dataset.syncrGroup || (groups.data && groups.data[0] && groups.data[0].id); 625 if (group) { 626 const fields = await api('/fields', { group_id: group }); 627 if (!fields || !fields.success) { advSay(feedback, fields && fields.message || t('advFailed', 'Failed.'), false); return; } 628 } 629 advSay(feedback, t('advRefreshedNative', 'Refreshed. Reload this form to see updated lists and fields.'), true); 630 }); 631 632 const liveStats = root.querySelector('.syncr-adv__stats'); 633 if (builder === 'ninjaforms' && liveStats) { 634 api('/advanced/stats', {}).then((r) => { 635 if (r && r.success && root.isConnected) liveStats.outerHTML = r.html; 636 else advSay(adv('stats-feedback'), cfg.i18n.advFailed || 'Failed.', false); 637 }).catch(() => advSay(adv('stats-feedback'), cfg.i18n.advFailed || 'Failed.', false)); 638 } 639 if (adv('stats-reset')) adv('stats-reset').addEventListener('click', async () => { 640 const r = await api('/advanced/stats-reset', {}); 641 advSay(adv('stats-feedback'), r && r.success ? (cfg.i18n.advReset || 'Counters reset. Reload to see zeros.') : (cfg.i18n.advFailed || 'Failed.'), !!(r && r.success)); 642 }); 643 644 if (adv('current')) adv('current').addEventListener('change', async () => { 645 const box = adv('current'); 646 const before = !box.checked; 647 box.disabled = true; 648 try { 649 const result = await api('/advanced/current', { enabled: box.checked }); 650 box.checked = result && typeof result.enabled === 'boolean' ? result.enabled : before; 651 box.disabled = false; 652 advSay(adv('current-feedback'), result && result.success ? t('advSaved', 'Saved.') : t('advFailed', 'Failed.'), !!(result && result.success)); 653 } catch (error) { 654 box.checked = before; 655 box.disabled = false; 656 advSay(adv('current-feedback'), t('advFailed', 'Failed.'), false); 657 } 658 }); 659 660 if (adv('purge')) adv('purge').addEventListener('change', async () => { 661 const box = adv('purge'); 662 box.disabled = true; 663 const r = await api('/advanced/settings', { purge_on_uninstall: box.checked }); 664 box.disabled = false; 665 if (!r || !r.success) { box.checked = !box.checked; advSay(adv('purge-feedback'), cfg.i18n.advFailed || 'Failed.', false); return; } 666 advSay(adv('purge-feedback'), cfg.i18n.advSaved || 'Saved.', true); 667 }); 668 669 const testBtn = adv('test'); 670 if (testBtn) testBtn.addEventListener('click', async () => { 671 const email = (adv('test-email') || { value: '' }).value.trim(); 672 const out = adv('test-feedback'); 673 if (!email) { advSay(out, cfg.i18n.advTestEmail || 'Enter an email address first.', false); return; } 674 testBtn.disabled = true; 675 advSay(out, cfg.i18n.advSending || 'Sending…', true); 676 const r = await api('/advanced/test', { email: email }); 677 testBtn.disabled = false; 678 if (!r) { advSay(out, cfg.i18n.advFailed || 'Failed.', false); return; } 679 let text = testMessage(r); 680 if (r.dropped && r.dropped.length) text += ' ' + (cfg.i18n.advDropped || 'Dropped fields:') + ' ' + r.dropped.join(', '); 681 advSay(out, text, !!r.success); 682 683 const echo = out && out.parentNode ? out.parentNode.querySelector('.syncr-adv__echo') : null; 684 if (echo) echo.remove(); 685 if (r.success && !r.skipped && out && out.parentNode) { 686 const wrap = el('div', { class: 'syncr-adv__echo' }); 687 const list = (title, obj) => { 688 const keys = Object.keys(obj || {}); 689 if (!keys.length) return; 690 wrap.appendChild(el('strong', {}, title)); 691 const dl = el('dl', {}); 692 keys.forEach((k) => { dl.appendChild(el('dt', {}, k)); dl.appendChild(el('dd', {}, String(obj[k]))); }); 693 wrap.appendChild(dl); 694 }; 695 list(cfg.i18n.advSent || 'Sent', r.sent); 696 list(cfg.i18n.advStored || 'The provider answered', r.stored); 697 out.parentNode.appendChild(wrap); 698 } 699 }); 700 701 if (adv('context')) adv('context').addEventListener('change', async () => { 702 const box = adv('context'); 703 box.disabled = true; 704 const r = await api('/advanced/settings', { sender_context: box.checked }); 705 box.disabled = false; 706 if (!r || !r.success) { box.checked = !box.checked; advSay(adv('context-feedback'), cfg.i18n.advFailed || 'Failed.', false); return; } 707 advSay(adv('context-feedback'), cfg.i18n.advSaved || 'Saved.', true); 708 }); 709 710 if (adv('form-reset')) adv('form-reset').addEventListener('click', async () => { 711 if (!window.confirm(cfg.i18n.advResetConfirm || 'Forget everything saved here for this form?')) return; 712 const btn = adv('form-reset'); 713 btn.disabled = true; 714 const r = await api('/advanced/form-reset', {}); 715 btn.disabled = false; 716 if (!r || !r.success) { advSay(adv('form-reset-feedback'), (r && r.message) || cfg.i18n.advFailed || 'Failed.', false); return; } 717 advSay(adv('form-reset-feedback'), cfg.i18n.advResetDone || 'Reset. Reload to see the empty panel.', true); 718 }); 719 720 Array.from(document.querySelectorAll('#submitdiv a[data-syncr-tab="clicksyncr-for-kit-tab"]')).forEach((a) => { 721 a.addEventListener('click', (e) => { 722 const tab = byId('tab'); 723 if (!tab) return; 724 e.preventDefault(); 725 tab.click(); 726 const panel = document.getElementById('clicksyncr-for-kit'); 727 if (panel) panel.scrollIntoView({ block: 'start', behavior: 'smooth' }); 728 }); 729 }); 730 731 Array.from(root.querySelectorAll('.syncr-section--collapsible')).forEach((section) => { 732 const toggle = section.querySelector('.syncr-section__toggle'); 733 if (!toggle) return; 734 const key = 'syncr-kit-open-' + section.id; 735 const set = (open) => { 736 section.classList.toggle('is-collapsed', !open); 737 toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); 738 try { localStorage.setItem(key, open ? '1' : '0'); } catch (e) {} 739 }; 740 let remembered = null; 741 try { remembered = localStorage.getItem(key); } catch (e) { remembered = null; } 742 if (remembered === '1') set(true); 743 toggle.addEventListener('click', () => set(section.classList.contains('is-collapsed'))); 744 }); 745 746 if (window.syncrBuilderPanel_kit) window.syncrBuilderPanel_kit.ready(root); 747 if (destinationValue()) { 748 loadFields(); 749 } 750 } 751 752 const start = () => document.querySelectorAll('#' + PANEL_ID + ', [data-syncr-panel="' + PANEL_ID + '"]').forEach((root) => boot(root)); 753 start(); 754 if (window.MutationObserver) { 755 const watcher = new MutationObserver(start); 756 watcher.observe(document.body, { childList: true, subtree: true }); 757 } 758 })(); 7 ( function () { 8 'use strict'; 9 10 const PANEL_ID = 'clicksyncr-for-kit'; 11 const cfg = window.syncrPanel_kit || { restUrl: '', nonce: '', i18n: {} }; 12 const t = ( key, fallback ) => ( cfg.i18n && cfg.i18n[ key ] ) || fallback; 13 14 function testMessage( result ) { 15 if ( result.skipped ) { 16 return ( 17 result.message || 18 t( 'advTestSkipped', 'Skipped. Nothing was sent.' ) 19 ); 20 } 21 if ( result.success ) { 22 return t( 23 'advTestOk', 24 'Added. Check the provider for this address.' 25 ); 26 } 27 return ( 28 t( 'advTestFailed', 'Not added.' ) + 29 ( result.message ? ' ' + result.message : '' ) 30 ); 31 } 32 33 function boot( root ) { 34 const meta = 35 root.querySelector( '[id="' + PANEL_ID + '-panel"]' ) || root; 36 if ( ! root || ! meta || root.dataset.syncrBooted === '1' ) { 37 return; 38 } 39 if ( 40 window.syncrBuilderPanel_kit && 41 ! window.syncrBuilderPanel_kit.prepare( root ) 42 ) { 43 return; 44 } 45 root.dataset.syncrBooted = '1'; 46 47 const byId = ( suffix ) => 48 root.id === PANEL_ID 49 ? document.getElementById( PANEL_ID + '-' + suffix ) 50 : root.querySelector( 51 '[id="' + PANEL_ID + '-' + suffix + '"]' 52 ); 53 const el = ( tag, attrs, text ) => { 54 const node = document.createElement( tag ); 55 Object.keys( attrs || {} ).forEach( ( k ) => 56 node.setAttribute( k, attrs[ k ] ) 57 ); 58 if ( text !== undefined ) { 59 node.textContent = text; 60 } 61 return node; 62 }; 63 const say = ( node, text, isError ) => { 64 if ( ! node ) { 65 return; 66 } 67 node.textContent = text || ''; 68 node.classList.toggle( 'syncr-feedback--error', !! isError ); 69 }; 70 71 const settle = ( control ) => { 72 if ( control.tagName === 'SELECT' ) { 73 Array.from( control.options ).forEach( ( o ) => { 74 o.defaultSelected = o.selected; 75 } ); 76 } else if ( 77 control.type === 'checkbox' || 78 control.type === 'radio' 79 ) { 80 control.defaultChecked = control.checked; 81 } 82 }; 83 const formId = parseInt( meta.dataset.syncrForm || '0', 10 ); 84 const builder = meta.dataset.syncrBuilder || ''; 85 let kinds = { 86 email: [ 'email' ], 87 number: [ 'number', 'range' ], 88 date: [ 'date' ], 89 not_text: [ 'checkbox', 'acceptance' ], 90 }; 91 try { 92 kinds = Object.assign( 93 kinds, 94 JSON.parse( meta.dataset.syncrKinds || '{}' ) 95 ); 96 } catch {} 97 98 async function api( path, body ) { 99 const resp = await fetch( cfg.restUrl + path, { 100 method: 'POST', 101 headers: { 102 'Content-Type': 'application/json', 103 'X-WP-Nonce': cfg.nonce, 104 }, 105 body: JSON.stringify( 106 Object.assign( { form_id: formId, builder }, body || {} ) 107 ), 108 } ); 109 return resp.json(); 110 } 111 112 const formFields = Array.from( 113 ( byId( 'field-options' ) || { options: [] } ).options 114 ).map( ( o ) => { 115 let options = null; 116 try { 117 options = o.dataset.options 118 ? JSON.parse( o.dataset.options ) 119 : null; 120 } catch { 121 options = null; 122 } 123 return { 124 id: o.value, 125 label: o.textContent, 126 kind: o.dataset.kind || '', 127 options: Array.isArray( options ) ? options : null, 128 }; 129 } ); 130 131 let tip = null; 132 let tipTrigger = null; 133 let consumeEscape = false; 134 const closeTip = () => { 135 if ( tipTrigger ) { 136 tipTrigger.removeAttribute( 'aria-describedby' ); 137 } 138 if ( tip ) { 139 tip.remove(); 140 } 141 tip = null; 142 tipTrigger = null; 143 }; 144 const openTip = ( trigger ) => { 145 closeTip(); 146 tipTrigger = trigger; 147 tip = el( 'span', { 148 class: 'syncr-destination-tip', 149 role: 'tooltip', 150 id: PANEL_ID + '-destination-tip', 151 } ); 152 if ( trigger.hasAttribute( 'data-syncr-key-tip' ) ) { 153 tip.append( 154 document.createTextNode( 155 trigger.getAttribute( 'data-syncr-key-tip' ) 156 ) 157 ); 158 } else { 159 tip.append( 160 document.createTextNode( t( 'destination_tip', '' ) + ' ' ) 161 ); 162 tip.appendChild( 163 el( 164 'a', 165 { 166 href: t( 'pro_url', 'https://clicksyncr.com' ), 167 target: '_blank', 168 rel: 'noopener noreferrer', 169 }, 170 t( 'see_pro', 'See Pro' ) 171 ) 172 ); 173 } 174 trigger.setAttribute( 'aria-describedby', tip.id ); 175 root.appendChild( tip ); 176 const anchor = trigger.getBoundingClientRect(); 177 const box = tip.getBoundingClientRect(); 178 const boundary = root.getBoundingClientRect(); 179 tip.style.left = 180 Math.max( 181 8, 182 Math.min( 183 anchor.left - boundary.left, 184 boundary.width - box.width - 8 185 ) 186 ) + 'px'; 187 const top = 188 anchor.bottom + box.height + 8 > window.innerHeight 189 ? anchor.top - box.height - 8 190 : anchor.bottom + 8; 191 tip.style.top = Math.max( 8, top - boundary.top ) + 'px'; 192 }; 193 [ 'mouseover', 'focusin', 'click' ].forEach( ( name ) => 194 root.addEventListener( name, ( event ) => { 195 const trigger = event.target.closest( 196 '[data-syncr-destination-tip],[data-syncr-key-tip]' 197 ); 198 if ( trigger ) { 199 if ( name === 'click' ) { 200 event.preventDefault(); 201 } 202 if ( trigger !== tipTrigger ) { 203 openTip( trigger ); 204 } 205 } else if ( 206 name !== 'mouseover' && 207 tip && 208 ! tip.contains( event.target ) 209 ) { 210 closeTip(); 211 } 212 } ) 213 ); 214 document.addEventListener( 'click', ( event ) => { 215 if ( 216 tip && 217 ! tip.contains( event.target ) && 218 ! tipTrigger.contains( event.target ) 219 ) { 220 closeTip(); 221 } 222 } ); 223 const leaveTip = ( event ) => { 224 const next = event.relatedTarget; 225 if ( ! tip || ! tipTrigger ) { 226 return; 227 } 228 if ( 229 next && 230 ( tip.contains( next ) || tipTrigger.contains( next ) ) 231 ) { 232 return; 233 } 234 closeTip(); 235 }; 236 root.addEventListener( 'mouseout', ( event ) => { 237 if ( 238 tipTrigger && 239 ( tipTrigger.contains( event.target ) || 240 ( tip && tip.contains( event.target ) ) ) 241 ) { 242 leaveTip( event ); 243 } 244 } ); 245 root.addEventListener( 'focusout', ( event ) => { 246 if ( 247 tipTrigger && 248 ( tipTrigger.contains( event.target ) || 249 ( tip && tip.contains( event.target ) ) ) 250 ) { 251 leaveTip( event ); 252 } 253 } ); 254 window.addEventListener( 'scroll', closeTip, true ); 255 window.addEventListener( 'resize', closeTip ); 256 document.addEventListener( 257 'keydown', 258 ( event ) => { 259 if ( 260 event.key !== 'Escape' || 261 ( ! consumeEscape && 262 ( ! tip || ! tip.getClientRects().length ) ) 263 ) { 264 return; 265 } 266 event.preventDefault(); 267 event.stopPropagation(); 268 consumeEscape = true; 269 closeTip(); 270 }, 271 true 272 ); 273 document.addEventListener( 274 'keyup', 275 ( event ) => { 276 if ( event.key !== 'Escape' || ! consumeEscape ) { 277 return; 278 } 279 event.preventDefault(); 280 event.stopPropagation(); 281 consumeEscape = false; 282 }, 283 true 284 ); 285 root.addEventListener( 'keydown', ( event ) => { 286 const trigger = event.target.closest( 287 '[data-syncr-destination-tip],[data-syncr-key-tip]' 288 ); 289 if ( trigger && ( event.key === 'Enter' || event.key === ' ' ) ) { 290 event.preventDefault(); 291 openTip( trigger ); 292 } 293 } ); 294 const keyInput = byId( 'connection' ); 295 const baseInput = byId( 'connection-base' ); 296 const baseRow = byId( 'connection-base-row' ); 297 const updateBase = () => { 298 if ( baseRow && keyInput ) { 299 baseRow.hidden = ! keyInput.value 300 .toLowerCase() 301 .startsWith( baseRow.dataset.keyPrefix || '' ); 302 } 303 }; 304 if ( keyInput ) { 305 keyInput.addEventListener( 'input', updateBase ); 306 } 307 updateBase(); 308 const connectBtn = byId( 'connection-connect' ); 309 const connectFeedback = byId( 'connection-feedback' ); 310 const KEY_MASK = '••••••••••••••••'; 311 const isMask = ( v ) => v.indexOf( '•' ) !== -1; 312 const masked = ( key ) => 313 key.slice( 0, 8 ) + KEY_MASK + key.slice( -4 ); 314 if ( connectBtn && keyInput ) { 315 const state = ( connected ) => { 316 connectBtn.classList.toggle( 'syncr-btn-connected', connected ); 317 connectBtn.textContent = connected 318 ? t( 'synced', 'Synced' ) 319 : t( 'connect', 'Connect' ); 320 }; 321 322 const revealBtn = byId( 'connection-reveal' ); 323 let shown = false; 324 const shownState = ( on ) => { 325 shown = on; 326 if ( ! revealBtn ) { 327 return; 328 } 329 revealBtn.setAttribute( 'aria-pressed', on ? 'true' : 'false' ); 330 revealBtn.setAttribute( 331 'aria-label', 332 on 333 ? t( 'hide_key', 'Hide the key' ) 334 : t( 'show_key', 'Show the key' ) 335 ); 336 const icon = revealBtn.querySelector( '.dashicons' ); 337 if ( icon ) { 338 icon.classList.toggle( 'dashicons-visibility', ! on ); 339 icon.classList.toggle( 'dashicons-hidden', on ); 340 } 341 }; 342 if ( revealBtn ) { 343 revealBtn.addEventListener( 'click', async () => { 344 if ( revealBtn.hasAttribute( 'data-syncr-key-tip' ) ) { 345 return; 346 } 347 if ( shown ) { 348 keyInput.value = masked( keyInput.value ); 349 shownState( false ); 350 return; 351 } 352 if ( ! isMask( keyInput.value || '' ) ) { 353 return; 354 } 355 revealBtn.disabled = true; 356 try { 357 const resp = await api( '/reveal-key', {} ); 358 if ( 359 ! resp.success || 360 ! resp.data || 361 ! resp.data.key 362 ) { 363 throw new Error( 364 resp.message || t( 'failed', 'Could not load.' ) 365 ); 366 } 367 keyInput.value = resp.data.key; 368 shownState( true ); 369 } catch ( err ) { 370 say( connectFeedback, err.message, true ); 371 } 372 revealBtn.disabled = false; 373 } ); 374 } 375 const connect = async () => { 376 const key = ( keyInput.value || '' ).trim(); 377 if ( ! key ) { 378 say( 379 connectFeedback, 380 t( 'enter_key', 'Enter an API key.' ), 381 true 382 ); 383 keyInput.focus(); 384 return; 385 } 386 if ( isMask( key ) ) { 387 return; 388 } 389 connectBtn.disabled = true; 390 say( connectFeedback, t( 'connecting', 'Connecting…' ) ); 391 try { 392 const credential = { api_key: key }; 393 if ( baseInput && baseRow && ! baseRow.hidden ) { 394 credential.api_base_url = baseInput.value.trim(); 395 } 396 const resp = await api( '/save-api-key', credential ); 397 if ( ! resp.success ) { 398 throw new Error( 399 resp.message || 400 t( 'failed', 'That key was not accepted.' ) 401 ); 402 } 403 keyInput.value = masked( key ); 404 keyInput.dataset.hasKey = '1'; 405 if ( revealBtn ) { 406 revealBtn.hidden = false; 407 } 408 shownState( false ); 409 state( true ); 410 const pill = root.querySelector( '.syncr-header__status' ); 411 if ( pill ) { 412 const saved = !! ( resp.group_id || '' ); 413 const dot = pill.querySelector( 414 '.syncr-header__status-dot' 415 ); 416 const text = pill.querySelector( 417 '.syncr-header__status-text' 418 ); 419 if ( dot ) { 420 dot.className = 421 'syncr-header__status-dot syncr-header__status-dot--' + 422 ( saved ? 'connected' : 'pending' ); 423 } 424 if ( text ) { 425 text.textContent = saved 426 ? t( 'header_connected', '' ) 427 : t( 'header_pending', '' ); 428 } 429 } 430 say( 431 connectFeedback, 432 t( 'connected', 'Connected. Choose where people go.' ) 433 ); 434 await loadDestinations( resp.group_id || '' ); 435 } catch ( err ) { 436 state( false ); 437 say( connectFeedback, err.message, true ); 438 } 439 connectBtn.disabled = false; 440 }; 441 connectBtn.addEventListener( 'click', connect ); 442 const foundBtn = root.querySelector( '[data-syncr-found-key]' ); 443 if ( foundBtn ) { 444 foundBtn.addEventListener( 'click', async () => { 445 foundBtn.disabled = true; 446 const r = await api( '/advanced/found-key', {} ); 447 if ( ! r || ! r.success || ! r.key ) { 448 foundBtn.disabled = false; 449 say( 450 connectFeedback, 451 ( r && r.message ) || 452 t( 'failed', 'That key was not accepted.' ), 453 true 454 ); 455 return; 456 } 457 keyInput.value = r.key; 458 keyInput.dispatchEvent( 459 new Event( 'input', { bubbles: true } ) 460 ); 461 const line = foundBtn.closest( '.syncr-found-key' ); 462 if ( line ) { 463 line.remove(); 464 } 465 await connect(); 466 } ); 467 } 468 keyInput.addEventListener( 'input', () => { 469 if ( ! isMask( keyInput.value || '' ) ) { 470 state( false ); 471 shownState( false ); 472 } 473 } ); 474 keyInput.addEventListener( 'paste', () => 475 setTimeout( () => { 476 updateBase(); 477 if ( 478 ( keyInput.value || '' ).trim().length >= 30 && 479 ( ! baseRow || 480 baseRow.hidden || 481 baseInput.value.trim() ) 482 ) { 483 connect(); 484 } 485 }, 50 ) 486 ); 487 } 488 489 const tiles = byId( 'destination' ); 490 const refreshBtn = byId( 'destination-refresh' ); 491 const destFeedback = byId( 'destination-feedback' ) || connectFeedback; 492 const tilesLocked = !! tiles && tiles.dataset.syncrLocked === '1'; 493 const destinationValue = () => { 494 if ( ! tiles ) { 495 return ''; 496 } 497 const selected = tiles.querySelector( 498 '[data-syncr-primary],input[type=radio]:checked' 499 ); 500 return selected ? selected.value : tiles.dataset.syncrValue || ''; 501 }; 502 function syncPrimary() { 503 if ( ! tiles ) { 504 return; 505 } 506 const primary = destinationValue(); 507 const names = []; 508 let paused = false; 509 tiles.dataset.syncrValue = primary; 510 Array.from( tiles.querySelectorAll( '.syncr-tile' ) ).forEach( 511 ( tile ) => { 512 const isPrimary = tile.dataset.syncrDestination === primary; 513 const box = tile.querySelector( 'input[type=checkbox]' ); 514 tile.classList.toggle( 'syncr-tile--primary', isPrimary ); 515 const extra = tile.querySelector( 516 '.syncr-destination-extra' 517 ); 518 if ( extra ) { 519 extra.hidden = isPrimary; 520 } 521 if ( box && isPrimary && ! tilesLocked ) { 522 box.checked = false; 523 } 524 const caption = tile.querySelector( '.syncr-main-caption' ); 525 if ( caption ) { 526 caption.textContent = isPrimary 527 ? t( 528 'main_caption', 529 'Main list: every contact goes here' 530 ) 531 : t( 'send_here', 'Send each contact here' ); 532 } 533 const saved = 534 tilesLocked && box && box.checked && ! isPrimary; 535 const note = tile.querySelector( 536 '.syncr-destination-paused' 537 ); 538 if ( note ) { 539 note.hidden = ! saved; 540 } 541 paused = paused || saved; 542 if ( 543 isPrimary || 544 ( ! tilesLocked && box && box.checked ) 545 ) { 546 names.push( tile.dataset.syncrLabel ); 547 } 548 } 549 ); 550 let status = byId( 'destination-status' ); 551 if ( ! status ) { 552 status = el( 'p', { 553 id: PANEL_ID + '-destination-status', 554 role: 'status', 555 class: 'syncr-destination-status', 556 } ); 557 tiles.after( status ); 558 } 559 status.textContent = names.length 560 ? t( 'dest_status', 'Each contact goes to:' ) + 561 ' ' + 562 names.join( ', ' ) + 563 ( paused 564 ? ' ' + 565 t( 566 'saved_paused', 567 'Saved extra lists and rules are paused until you renew.' 568 ) 569 : '' ) 570 : ''; 571 } 572 function renderTiles( list, keep ) { 573 const ticked = new Set( 574 Array.from( 575 tiles.querySelectorAll( 'input[type=checkbox]:checked' ) 576 ).map( ( b ) => b.value ) 577 ); 578 tiles.textContent = ''; 579 tiles.classList.toggle( 'syncr-tiles--single', list.length === 1 ); 580 if ( ! list.some( ( g ) => String( g.id ) === keep ) ) { 581 keep = list.length ? String( list[ 0 ].id ) : ''; 582 } 583 if ( ! tilesLocked ) { 584 tiles.appendChild( 585 el( 'input', { 586 type: 'hidden', 587 name: tiles.dataset.syncrExtras.replace( /\[\]$/, '' ), 588 value: '', 589 } ) 590 ); 591 } 592 list.forEach( ( g, index ) => { 593 const id = String( g.id ); 594 const tile = el( 'div', { 595 class: 'syncr-tile', 596 'data-syncr-destination': id, 597 'data-syncr-label': g.label, 598 } ); 599 const label = el( 'label' ); 600 const choice = el( 'input', { 601 type: 'radio', 602 name: tiles.dataset.syncrName, 603 value: id, 604 } ); 605 choice.checked = id === keep; 606 label.append( 607 choice, 608 document.createTextNode( ' ' + g.label ) 609 ); 610 tile.appendChild( label ); 611 tile.appendChild( 612 el( 'span', { class: 'syncr-main-caption' } ) 613 ); 614 const attrs = { class: 'syncr-destination-extra' }; 615 if ( tilesLocked ) { 616 Object.assign( attrs, { 617 role: 'button', 618 tabindex: '0', 619 'data-syncr-destination-tip': '', 620 } ); 621 } 622 const extra = el( 'label', attrs ); 623 extra.hidden = id === keep; 624 const boxAttrs = { type: 'checkbox', value: id }; 625 if ( tilesLocked ) { 626 Object.assign( boxAttrs, { 627 tabindex: '-1', 628 'aria-disabled': 'true', 629 disabled: 'disabled', 630 } ); 631 } else { 632 boxAttrs.name = 633 builder === 'wpforms' 634 ? tiles.dataset.syncrExtras.replace( 635 /\[\]$/, 636 '[' + index + ']' 637 ) 638 : tiles.dataset.syncrExtras; 639 } 640 const box = el( 'input', boxAttrs ); 641 box.checked = ticked.has( id ) && id !== keep; 642 extra.append( 643 box, 644 document.createTextNode( 645 ' ' + t( 'also_here', 'Also add here' ) 646 ) 647 ); 648 if ( tilesLocked ) { 649 extra.appendChild( 650 el( 'span', { class: 'syncr-pro-chip' }, 'PRO' ) 651 ); 652 } 653 tile.appendChild( extra ); 654 const note = el( 655 'span', 656 { class: 'syncr-destination-paused' }, 657 t( 658 'saved_extra', 659 'Saved. Renew your license to send here too.' 660 ) 661 ); 662 note.hidden = ! tilesLocked || ! box.checked; 663 tile.appendChild( note ); 664 tiles.appendChild( tile ); 665 settle( choice ); 666 settle( box ); 667 } ); 668 tiles.dataset.syncrValue = keep; 669 syncPrimary(); 670 } 671 if ( tiles ) { 672 tiles.addEventListener( 'change', ( event ) => { 673 if ( event.target.type === 'radio' ) { 674 syncPrimary(); 675 loadFields(); 676 } else if ( 677 ! tilesLocked && 678 event.target.type === 'checkbox' 679 ) { 680 syncPrimary(); 681 } 682 } ); 683 syncPrimary(); 684 } 685 if ( refreshBtn ) { 686 refreshBtn.addEventListener( 'click', () => 687 loadDestinations( destinationValue() ) 688 ); 689 } 690 691 async function loadDestinations( keep ) { 692 if ( ! tiles ) { 693 return; 694 } 695 if ( refreshBtn ) { 696 refreshBtn.disabled = true; 697 } 698 try { 699 const resp = await api( '/groups', {} ); 700 if ( ! resp.success || ! Array.isArray( resp.data ) ) { 701 throw new Error( 702 resp.message || t( 'failed', 'Could not load.' ) 703 ); 704 } 705 renderTiles( 706 resp.data.map( ( g ) => ( { id: g.id, label: g.label } ) ), 707 keep || destinationValue() 708 ); 709 const empty = byId( 'destination-empty' ); 710 if ( empty ) { 711 empty.hidden = resp.data.length > 0; 712 } 713 say( 714 destFeedback, 715 resp.data.length ? '' : t( 'destinations_0', '' ) 716 ); 717 if ( destinationValue() ) { 718 await loadFields(); 719 } 720 } catch ( err ) { 721 say( destFeedback, err.message, true ); 722 } 723 if ( refreshBtn ) { 724 refreshBtn.disabled = false; 725 } 726 } 727 728 const mapping = byId( 'mapping' ); 729 const savedMap = mapping 730 ? JSON.parse( mapping.dataset.syncrMap || '[]' ) 731 : []; 732 const mapName = mapping ? mapping.dataset.syncrName || '' : ''; 733 const KIND_COMPAT = { 734 number: kinds.number, 735 integer: kinds.number, 736 date: kinds.date, 737 datetime: kinds.date, 738 }; 739 740 function compatible( providerType, providerId ) { 741 if ( providerId === 'email' ) { 742 return formFields.filter( 743 ( f ) => kinds.email.indexOf( f.kind ) !== -1 744 ); 745 } 746 const allowed = KIND_COMPAT[ ( providerType || '' ).toLowerCase() ]; 747 if ( ! allowed ) { 748 return formFields.filter( 749 ( f ) => kinds.not_text.indexOf( f.kind ) === -1 750 ); 751 } 752 return formFields.filter( 753 ( f ) => f.kind === '' || allowed.indexOf( f.kind ) !== -1 754 ); 755 } 756 757 function currentMap() { 758 if ( ! mapping ) { 759 return savedMap; 760 } 761 const rows = Array.from( 762 mapping.querySelectorAll( '.syncr-map-row' ) 763 ); 764 if ( ! rows.length ) { 765 return savedMap; 766 } 767 return rows 768 .map( ( row ) => ( { 769 provider: row.querySelector( 'input[type=hidden]' ).value, 770 form: row.querySelector( 'select' ).value, 771 } ) ) 772 .filter( ( m ) => m.form ); 773 } 774 775 async function loadFields() { 776 if ( 777 ! mapping || 778 ! destinationValue() || 779 ( keyInput && keyInput.dataset.hasKey !== '1' ) 780 ) { 781 return; 782 } 783 const before = currentMap(); 784 try { 785 const resp = await api( '/fields', { 786 group_id: destinationValue(), 787 } ); 788 if ( ! resp.success || ! Array.isArray( resp.data ) ) { 789 throw new Error( 790 resp.message || t( 'failed', 'Could not load.' ) 791 ); 792 } 793 renderMapping( resp.data, before ); 794 } catch ( err ) { 795 say( destFeedback, err.message, true ); 796 } 797 } 798 799 function renderMapping( providerFields, saved ) { 800 const section = mapping.closest( '.syncr-section' ) || root; 801 const contributed = Array.from( 802 section.querySelectorAll( '.syncr-tile--contributed' ) 803 ); 804 mapping.textContent = ''; 805 const grid = el( 'div', { 806 class: 'syncr-tiles syncr-tiles--mapping', 807 } ); 808 mapping.appendChild( grid ); 809 810 providerFields.forEach( ( field, index ) => { 811 const required = field.id === 'email'; 812 const tile = el( 'div', { class: 'syncr-tile syncr-map-row' } ); 813 const select = el( 'select', { 814 name: mapName + '[' + index + '][form]', 815 id: PANEL_ID + '-map-' + field.id, 816 } ); 817 const label = el( 818 'label', 819 { for: select.id }, 820 field.label || field.id 821 ); 822 label.appendChild( 823 el( 824 'span', 825 { class: 'syncr-tile__meta' }, 826 field.id + ' · ' + ( field.type || 'text' ) 827 ) 828 ); 829 if ( required ) { 830 label.appendChild( 831 el( 832 'span', 833 { class: 'syncr-tile__required' }, 834 t( 'required', 'Required' ) 835 ) 836 ); 837 } 838 839 select.appendChild( 840 el( 841 'option', 842 { value: '' }, 843 required 844 ? t( 'select_field', 'Select a form field' ) 845 : t( 'choose', 'Choose…' ) 846 ) 847 ); 848 const options = compatible( field.type, field.id ); 849 const own = options.filter( ( f ) => f.kind !== 'context' ); 850 const context = options.filter( ( f ) => f.kind === 'context' ); 851 own.forEach( ( f ) => 852 select.appendChild( 853 el( 'option', { value: f.id }, f.label ) 854 ) 855 ); 856 if ( context.length ) { 857 const og = el( 'optgroup', { 858 label: t( 'context_group', 'Signup context' ), 859 } ); 860 context.forEach( ( f ) => 861 og.appendChild( 862 el( 'option', { value: f.id }, f.label ) 863 ) 864 ); 865 select.appendChild( og ); 866 } 867 if ( ! own.length ) { 868 const kind = ( field.type || '' ).toLowerCase(); 869 let hint = ''; 870 if ( required ) { 871 hint = t( 872 'need_email', 873 'Add an email field to the form' 874 ); 875 } else if ( kind === 'date' ) { 876 hint = t( 'need_date', 'Add a date field to the form' ); 877 } else if ( KIND_COMPAT[ kind ] ) { 878 hint = t( 879 'need_number', 880 'Add a number field to the form' 881 ); 882 } 883 if ( hint ) { 884 const none = el( 885 'option', 886 { value: '', disabled: 'disabled' }, 887 hint 888 ); 889 select.appendChild( none ); 890 } 891 if ( 892 ! context.length && 893 ! ( 894 saved.find( ( m ) => m.provider === field.id ) || {} 895 ).form 896 ) { 897 if ( hint ) { 898 select.firstChild.textContent = hint; 899 } 900 if ( hint ) { 901 select.removeChild( select.lastChild ); 902 } 903 select.disabled = true; 904 } 905 } 906 const savedRow = saved.find( ( m ) => m.provider === field.id ); 907 if ( savedRow && savedRow.form ) { 908 if ( 909 Array.from( select.options ).some( 910 ( o ) => o.value === savedRow.form 911 ) 912 ) { 913 select.value = savedRow.form; 914 } else { 915 const gone = el( 916 'option', 917 { value: '', 'data-syncr-missing': savedRow.form }, 918 t( 'missing', '%s is not on this form' ).replace( 919 '%s', 920 '[' + savedRow.form + ']' 921 ) 922 ); 923 select.insertBefore( gone, select.firstChild ); 924 select.value = ''; 925 tile.classList.add( 'syncr-tile--missing' ); 926 } 927 } else if ( required && own.length ) { 928 select.value = own[ 0 ].id; 929 } 930 settle( select ); 931 932 tile.appendChild( label ); 933 tile.appendChild( select ); 934 tile.appendChild( 935 el( 'input', { 936 type: 'hidden', 937 name: mapName + '[' + index + '][provider]', 938 value: field.id, 939 } ) 940 ); 941 grid.appendChild( tile ); 942 } ); 943 contributed.forEach( ( tile ) => grid.appendChild( tile ) ); 944 section 945 .querySelectorAll( '.syncr-tiles--extra' ) 946 .forEach( ( wrap ) => { 947 if ( ! wrap.children.length ) { 948 wrap.remove(); 949 } 950 } ); 951 readback(); 952 } 953 954 const rulesBox = byId( 'destination_rules' ); 955 if ( rulesBox ) { 956 const rulesName = rulesBox.dataset.syncrName || ''; 957 let groupsMap = {}; 958 try { 959 groupsMap = JSON.parse( rulesBox.dataset.syncrGroups || '{}' ); 960 } catch { 961 groupsMap = {}; 962 } 963 let savedRules = []; 964 try { 965 savedRules = JSON.parse( rulesBox.dataset.syncrRules || '[]' ); 966 } catch { 967 savedRules = []; 968 } 969 const ruleTargets = ( () => { 970 try { 971 return JSON.parse( rulesBox.dataset.syncrTargets || '[]' ); 972 } catch { 973 return []; 974 } 975 } )(); 976 const choiceFields = () => 977 formFields.filter( ( f ) => f.options && f.options.length ); 978 const table = el( 'div', { class: 'syncr-rules__table' } ); 979 const empty = el( 980 'p', 981 { class: 'syncr-rules__empty description' }, 982 t( 983 'rule_none', 984 'No rules yet. Everyone joins the groups above.' 985 ) 986 ); 987 const actions = el( 'div', { 988 class: 'syncr-actions syncr-actions--end', 989 } ); 990 const add = el( 991 'button', 992 { type: 'button', class: 'button syncr-rules__add' }, 993 t( 'rule_add', 'Add rule' ) 994 ); 995 actions.appendChild( add ); 996 rulesBox.appendChild( table ); 997 rulesBox.appendChild( empty ); 998 rulesBox.appendChild( actions ); 999 const head = () => { 1000 const h = el( 'div', { 1001 class: 'syncr-rules__head', 1002 'aria-hidden': 'true', 1003 } ); 1004 [ 1005 '#', 1006 t( 'rule_when', 'When this field' ), 1007 t( 'rule_is', 'is this value' ), 1008 t( 'rule_add_to', 'add subscriber to' ), 1009 '', 1010 ].forEach( ( text ) => 1011 h.appendChild( el( 'span', {}, text ) ) 1012 ); 1013 return h; 1014 }; 1015 const rows = () => 1016 Array.from( table.querySelectorAll( '.syncr-rules__row' ) ); 1017 const renumber = () => { 1018 rows().forEach( ( row, i ) => { 1019 row.querySelector( '.syncr-rules__num' ).textContent = 1020 String( i + 1 ).padStart( 2, '0' ); 1021 row.querySelectorAll( '[data-part]' ).forEach( ( c ) => { 1022 c.name = 1023 rulesName + '[' + i + '][' + c.dataset.part + ']'; 1024 } ); 1025 } ); 1026 const any = rows().length > 0; 1027 empty.hidden = any; 1028 const existing = table.querySelector( '.syncr-rules__head' ); 1029 if ( any && ! existing ) { 1030 table.insertBefore( head(), table.firstChild ); 1031 } 1032 if ( ! any && existing ) { 1033 existing.remove(); 1034 } 1035 }; 1036 const valueOptions = ( select, fieldId, chosen, initial ) => { 1037 select.textContent = ''; 1038 select.appendChild( 1039 el( 1040 'option', 1041 { value: '' }, 1042 t( 'rule_choose_value', 'Choose a value' ) 1043 ) 1044 ); 1045 const field = choiceFields().find( ( f ) => f.id === fieldId ); 1046 ( field ? field.options : [] ).forEach( ( opt ) => 1047 select.appendChild( 1048 el( 1049 'option', 1050 { value: opt.value }, 1051 opt.label || opt.value 1052 ) 1053 ) 1054 ); 1055 if ( 1056 chosen && 1057 ! Array.from( select.options ).some( 1058 ( o ) => o.value === chosen 1059 ) 1060 ) { 1061 select.appendChild( 1062 el( 'option', { value: chosen }, chosen ) 1063 ); 1064 } 1065 select.value = chosen || ''; 1066 if ( initial ) { 1067 settle( select ); 1068 } 1069 }; 1070 const cell = ( label, control ) => { 1071 const c = el( 'div', { 1072 class: 'syncr-rules__cell', 1073 'data-syncr-column': label, 1074 } ); 1075 c.appendChild( control ); 1076 return c; 1077 }; 1078 const row = ( rule, initial ) => { 1079 const line = el( 'div', { class: 'syncr-rules__row' } ); 1080 const fieldSel = el( 'select', { 1081 'data-part': 'field', 1082 'aria-label': t( 'rule_when', 'When this field' ), 1083 } ); 1084 fieldSel.appendChild( 1085 el( 1086 'option', 1087 { value: '' }, 1088 t( 'rule_choose_field', 'Choose a field' ) 1089 ) 1090 ); 1091 choiceFields().forEach( ( f ) => 1092 fieldSel.appendChild( 1093 el( 'option', { value: f.id }, '[' + f.id + ']' ) 1094 ) 1095 ); 1096 if ( 1097 rule.field && 1098 ! Array.from( fieldSel.options ).some( 1099 ( o ) => o.value === rule.field 1100 ) 1101 ) { 1102 fieldSel.appendChild( 1103 el( 1104 'option', 1105 { value: rule.field }, 1106 '[' + 1107 rule.field + 1108 '] ' + 1109 t( 'rule_missing', '(not on this form)' ) 1110 ) 1111 ); 1112 } 1113 fieldSel.value = rule.field || ''; 1114 const valueSel = el( 'select', { 1115 'data-part': 'value', 1116 'aria-label': t( 'rule_is', 'is this value' ), 1117 } ); 1118 valueOptions( 1119 valueSel, 1120 rule.field || '', 1121 rule.value || '', 1122 initial 1123 ); 1124 const groupSel = el( 'select', { 1125 'data-part': 'group', 1126 'aria-label': t( 'rule_add_to', 'add subscriber to' ), 1127 } ); 1128 groupSel.appendChild( 1129 el( 1130 'option', 1131 { value: '' }, 1132 t( 'rule_choose_group', 'Choose a group' ) 1133 ) 1134 ); 1135 Object.keys( groupsMap ).forEach( ( id ) => 1136 groupSel.appendChild( 1137 el( 'option', { value: id }, groupsMap[ id ] ) 1138 ) 1139 ); 1140 1141 const byGroup = {}; 1142 ruleTargets.forEach( ( tg ) => { 1143 ( byGroup[ tg.group || t( 'rule_targets', 'Other' ) ] ||= 1144 [] ).push( tg ); 1145 } ); 1146 Object.keys( byGroup ).forEach( ( label ) => { 1147 const og = el( 'optgroup', { label } ); 1148 byGroup[ label ].forEach( ( tg ) => 1149 og.appendChild( 1150 el( 1151 'option', 1152 { value: tg.value }, 1153 tg.label || tg.value 1154 ) 1155 ) 1156 ); 1157 groupSel.appendChild( og ); 1158 } ); 1159 if ( 1160 rule.group && 1161 ! Array.from( groupSel.options ).some( 1162 ( o ) => o.value === rule.group 1163 ) 1164 ) { 1165 groupSel.appendChild( 1166 el( 'option', { value: rule.group }, rule.group ) 1167 ); 1168 } 1169 groupSel.value = rule.group || ''; 1170 const remove = el( 'button', { 1171 type: 'button', 1172 class: 'button-link syncr-rules__remove', 1173 title: t( 'rule_remove', 'Remove rule' ), 1174 } ); 1175 remove.appendChild( 1176 el( 'span', { 1177 class: 'dashicons dashicons-no-alt', 1178 'aria-hidden': 'true', 1179 } ) 1180 ); 1181 remove.appendChild( 1182 el( 1183 'span', 1184 { class: 'screen-reader-text' }, 1185 t( 'rule_remove', 'Remove rule' ) 1186 ) 1187 ); 1188 fieldSel.addEventListener( 'change', () => 1189 valueOptions( valueSel, fieldSel.value, '', false ) 1190 ); 1191 remove.addEventListener( 'click', () => { 1192 line.remove(); 1193 renumber(); 1194 } ); 1195 line.appendChild( 1196 el( 'strong', { class: 'syncr-rules__num' }, '' ) 1197 ); 1198 line.appendChild( 1199 cell( t( 'rule_when', 'When this field' ), fieldSel ) 1200 ); 1201 line.appendChild( 1202 cell( t( 'rule_is', 'is this value' ), valueSel ) 1203 ); 1204 line.appendChild( 1205 cell( t( 'rule_add_to', 'add subscriber to' ), groupSel ) 1206 ); 1207 line.appendChild( remove ); 1208 if ( initial ) { 1209 settle( fieldSel ); 1210 settle( groupSel ); 1211 } 1212 table.appendChild( line ); 1213 renumber(); 1214 }; 1215 savedRules.forEach( ( r ) => row( r, true ) ); 1216 if ( ! choiceFields().length ) { 1217 add.hidden = true; 1218 empty.textContent = t( 1219 'rule_no_fields', 1220 'This form has no choice fields (a drop-down, checkboxes or radio buttons), so nothing here can decide where they go.' 1221 ); 1222 } 1223 add.addEventListener( 'click', () => { 1224 row( { field: '', value: '', group: '' }, false ); 1225 } ); 1226 renumber(); 1227 } 1228 1229 const dependents = Array.from( 1230 root.querySelectorAll( '[data-syncr-show]' ) 1231 ); 1232 function controlValue( name ) { 1233 const nodes = Array.from( 1234 root.querySelectorAll( '[name="' + name + '"]' ) 1235 ); 1236 if ( ! nodes.length ) { 1237 return null; 1238 } 1239 const node = nodes[ 0 ]; 1240 if ( node.type === 'checkbox' ) { 1241 return node.checked ? node.value || '1' : ''; 1242 } 1243 if ( node.type === 'radio' ) { 1244 const on = nodes.find( ( n ) => n.checked ); 1245 return on ? on.value : ''; 1246 } 1247 return node.value || ''; 1248 } 1249 function applyDependents() { 1250 dependents.forEach( ( row ) => { 1251 let conditions = []; 1252 try { 1253 conditions = JSON.parse( row.dataset.syncrShow || '[]' ); 1254 } catch { 1255 return; 1256 } 1257 let show = true; 1258 conditions.forEach( ( c ) => { 1259 const v = controlValue( c.name ); 1260 if ( v === null ) { 1261 return; 1262 } 1263 if ( 'is' in c && v !== c.is ) { 1264 show = false; 1265 } 1266 if ( 'not' in c && v === c.not ) { 1267 show = false; 1268 } 1269 } ); 1270 row.hidden = ! show; 1271 } ); 1272 } 1273 if ( dependents.length ) { 1274 applyDependents(); 1275 root.addEventListener( 'change', applyDependents ); 1276 } 1277 1278 const readbackEl = root.querySelector( '.syncr-readback' ); 1279 function readback() { 1280 if ( ! readbackEl || ! mapping ) { 1281 return; 1282 } 1283 const selects = Array.from( 1284 mapping.querySelectorAll( '.syncr-map-row select' ) 1285 ); 1286 if ( ! selects.length ) { 1287 return; 1288 } 1289 const mapped = selects.filter( ( s ) => s.value ).length; 1290 const missing = selects.filter( 1291 ( s ) => 1292 ! s.value && 1293 s.selectedOptions[ 0 ] && 1294 s.selectedOptions[ 0 ].dataset.syncrMissing 1295 ).length; 1296 let text = t( 'mapped', '%1$d of %2$d fields mapped.' ) 1297 .replace( '%1$d', String( mapped ) ) 1298 .replace( '%2$d', String( selects.length ) ); 1299 if ( missing ) { 1300 text += 1301 ' ' + 1302 t( 1303 'missing_count', 1304 '%d mapped to fields that are no longer on this form; save to clear them.' 1305 ).replace( '%d', String( missing ) ); 1306 } 1307 readbackEl.textContent = text; 1308 readbackEl.classList.toggle( 'syncr-readback--warn', missing > 0 ); 1309 } 1310 root.addEventListener( 'change', ( event ) => { 1311 const tile = 1312 event.target.closest && 1313 event.target.closest( '.syncr-tile--missing' ); 1314 if ( tile && event.target.value ) { 1315 tile.classList.remove( 'syncr-tile--missing' ); 1316 } 1317 readback(); 1318 } ); 1319 1320 const adv = ( part ) => byId( 'adv-' + part ); 1321 const advSay = ( node, text, ok ) => { 1322 if ( ! node ) { 1323 return; 1324 } 1325 node.textContent = text; 1326 node.className = 'syncr-adv__ok'; 1327 if ( ok === false ) { 1328 node.className = 'syncr-adv__bad'; 1329 } 1330 if ( ok === null ) { 1331 node.className = 'syncr-adv__note'; 1332 } 1333 }; 1334 const busyStart = ( box ) => { 1335 box.syncrSeq = ( box.syncrSeq || 0 ) + 1; 1336 box.setAttribute( 'aria-busy', 'true' ); 1337 return box.syncrSeq; 1338 }; 1339 const busyEnd = ( box, seq ) => { 1340 if ( seq !== box.syncrSeq ) { 1341 return false; 1342 } 1343 box.removeAttribute( 'aria-busy' ); 1344 return true; 1345 }; 1346 const debugToggle = adv( 'debug' ); 1347 const debugState = adv( 'debug-state' ); 1348 if ( debugToggle ) { 1349 debugToggle.addEventListener( 'change', async () => { 1350 const seq = busyStart( debugToggle ); 1351 const r = await api( '/advanced/settings', { 1352 debug_log: debugToggle.checked, 1353 } ); 1354 if ( ! busyEnd( debugToggle, seq ) ) { 1355 return; 1356 } 1357 if ( ! r || ! r.success ) { 1358 debugToggle.checked = ! debugToggle.checked; 1359 return; 1360 } 1361 debugToggle.checked = !! r.logging; 1362 if ( debugState ) { 1363 debugState.textContent = r.logging 1364 ? cfg.i18n.advLogOn || 1365 'On for 14 days, then off by itself.' 1366 : cfg.i18n.advLogOff || 'Off.'; 1367 } 1368 if ( r.logging ) { 1369 loadLog(); 1370 } 1371 } ); 1372 } 1373 const logBox = adv( 'log' ); 1374 const logMeta = adv( 'log-meta' ); 1375 const logAuto = adv( 'log-auto' ); 1376 let logSource = 'own'; 1377 let logTimer = null; 1378 async function loadLog() { 1379 if ( ! logBox ) { 1380 return; 1381 } 1382 const r = await api( '/advanced/log', { source: logSource } ); 1383 if ( ! r || ! r.success ) { 1384 logBox.textContent = 1385 cfg.i18n.advLogFailed || 'Could not read the log.'; 1386 return; 1387 } 1388 if ( ! r.exists ) { 1389 logBox.textContent = 1390 logSource === 'site' 1391 ? cfg.i18n.advNoSiteLog || 1392 'This site keeps no PHP error log. Set WP_DEBUG_LOG in wp-config.php while you test, then look here again.' 1393 : cfg.i18n.advNoLog || 1394 'No log yet. Switch the debug log on and sync a form.'; 1395 } else if ( ! r.lines.length ) { 1396 logBox.textContent = r.filtered 1397 ? t( 'no_matching_log', 'No matching ClickSyncr entries.' ) 1398 : cfg.i18n.advLogEmpty || 'The log is empty.'; 1399 } else { 1400 logBox.textContent = r.lines.join( '\n' ); 1401 logBox.scrollTop = logBox.scrollHeight; 1402 } 1403 if ( logMeta ) { 1404 logMeta.textContent = r.exists 1405 ? r.path + 1406 ' · ' + 1407 Math.round( r.size / 1024 ) + 1408 ' KB · ' + 1409 r.lines.length + 1410 ' ' + 1411 ( cfg.i18n.advLines || 'lines shown' ) 1412 : ''; 1413 } 1414 if ( logMeta && r.filtered ) { 1415 logMeta.textContent += 1416 ' · ' + t( 'filtered_log', 'ClickSyncr entries only' ); 1417 } 1418 } 1419 Array.from( root.querySelectorAll( '[data-syncr-log]' ) ).forEach( 1420 ( btn ) => { 1421 btn.addEventListener( 'click', () => { 1422 Array.from( 1423 root.querySelectorAll( '[data-syncr-log]' ) 1424 ).forEach( ( b ) => { 1425 b.classList.toggle( 'is-active', b === btn ); 1426 b.setAttribute( 1427 'aria-pressed', 1428 b === btn ? 'true' : 'false' 1429 ); 1430 } ); 1431 logSource = btn.dataset.syncrLog; 1432 loadLog(); 1433 } ); 1434 } 1435 ); 1436 if ( adv( 'log-open' ) ) { 1437 adv( 'log-open' ).addEventListener( 'click', loadLog ); 1438 } 1439 if ( adv( 'log-clear' ) ) { 1440 adv( 'log-clear' ).addEventListener( 'click', async () => { 1441 if ( 1442 ! window.confirm( 1443 t( 'advClearConfirm', 'Clear the plugin log?' ) 1444 ) 1445 ) { 1446 return; 1447 } 1448 const r = await api( '/advanced/log-clear', {} ); 1449 if ( ! r || ! r.success ) { 1450 if ( logBox ) { 1451 logBox.textContent = t( 1452 'advClearFailed', 1453 'Could not clear the log.' 1454 ); 1455 } 1456 return; 1457 } 1458 loadLog(); 1459 } ); 1460 } 1461 if ( adv( 'log-download' ) ) { 1462 adv( 'log-download' ).addEventListener( 'click', () => { 1463 window.open( 1464 cfg.restUrl + 1465 '/advanced/log-download?_wpnonce=' + 1466 encodeURIComponent( cfg.nonce ) + 1467 '&builder=' + 1468 encodeURIComponent( builder ) + 1469 '&form_id=' + 1470 encodeURIComponent( formId ), 1471 '_blank' 1472 ); 1473 } ); 1474 } 1475 let logPolling = false; 1476 const pollLog = async () => { 1477 logTimer = null; 1478 if ( 1479 logPolling || 1480 ! logAuto.checked || 1481 ! root.isConnected || 1482 document.visibilityState === 'hidden' 1483 ) { 1484 return; 1485 } 1486 logPolling = true; 1487 try { 1488 await loadLog(); 1489 } finally { 1490 logPolling = false; 1491 } 1492 if ( logAuto.checked && root.isConnected && ! logTimer ) { 1493 logTimer = setTimeout( pollLog, 5000 ); 1494 } 1495 }; 1496 if ( logAuto ) { 1497 logAuto.addEventListener( 'change', () => { 1498 if ( logTimer ) { 1499 clearTimeout( logTimer ); 1500 logTimer = null; 1501 } 1502 if ( logAuto.checked ) { 1503 pollLog(); 1504 } 1505 } ); 1506 } 1507 if ( logAuto ) { 1508 window.addEventListener( 'pagehide', () => { 1509 if ( logTimer ) { 1510 clearTimeout( logTimer ); 1511 logTimer = null; 1512 } 1513 } ); 1514 } 1515 if ( logAuto ) { 1516 document.addEventListener( 'visibilitychange', () => { 1517 if ( 1518 document.visibilityState === 'visible' && 1519 logAuto.checked && 1520 ! logTimer 1521 ) { 1522 pollLog(); 1523 } 1524 } ); 1525 } 1526 1527 const reportBox = adv( 'report' ); 1528 const reportCopy = adv( 'report-copy' ); 1529 if ( adv( 'report-build' ) ) { 1530 adv( 'report-build' ).addEventListener( 'click', async () => { 1531 advSay( 1532 adv( 'report-feedback' ), 1533 cfg.i18n.advBuilding || 'Building…', 1534 true 1535 ); 1536 const r = await api( '/advanced/report', {} ); 1537 if ( ! r || ! r.success ) { 1538 advSay( 1539 adv( 'report-feedback' ), 1540 cfg.i18n.advReportFailed || 1541 'Could not build the report.', 1542 false 1543 ); 1544 return; 1545 } 1546 reportBox.value = r.report; 1547 if ( reportCopy ) { 1548 reportCopy.disabled = false; 1549 } 1550 advSay( adv( 'report-feedback' ), '', true ); 1551 } ); 1552 } 1553 if ( reportCopy ) { 1554 reportCopy.addEventListener( 'click', async () => { 1555 try { 1556 await navigator.clipboard.writeText( reportBox.value ); 1557 advSay( 1558 adv( 'report-feedback' ), 1559 cfg.i18n.advCopied || 'Copied.', 1560 true 1561 ); 1562 } catch { 1563 reportBox.select(); 1564 advSay( 1565 adv( 'report-feedback' ), 1566 cfg.i18n.advSelectCopy || 1567 'Selected. Press Ctrl+C or Cmd+C.', 1568 true 1569 ); 1570 } 1571 } ); 1572 } 1573 1574 if ( adv( 'refresh' ) ) { 1575 adv( 'refresh' ).addEventListener( 'click', async () => { 1576 const feedback = adv( 'refresh-feedback' ); 1577 advSay( feedback, '…', true ); 1578 const result = await api( '/advanced/refresh', {} ); 1579 if ( ! result || ! result.success ) { 1580 advSay( feedback, t( 'advFailed', 'Failed.' ), false ); 1581 return; 1582 } 1583 if ( tiles ) { 1584 await loadDestinations( destinationValue() ); 1585 advSay( feedback, t( 'advRefreshed', 'Refreshed.' ), true ); 1586 return; 1587 } 1588 const groups = await api( '/groups', {} ); 1589 if ( ! groups || ! groups.success ) { 1590 advSay( 1591 feedback, 1592 ( groups && groups.message ) || 1593 t( 'advFailed', 'Failed.' ), 1594 false 1595 ); 1596 return; 1597 } 1598 const group = 1599 meta.dataset.syncrGroup || 1600 ( groups.data && groups.data[ 0 ] && groups.data[ 0 ].id ); 1601 if ( group ) { 1602 const fields = await api( '/fields', { group_id: group } ); 1603 if ( ! fields || ! fields.success ) { 1604 advSay( 1605 feedback, 1606 ( fields && fields.message ) || 1607 t( 'advFailed', 'Failed.' ), 1608 false 1609 ); 1610 return; 1611 } 1612 } 1613 advSay( 1614 feedback, 1615 t( 1616 'advRefreshedNative', 1617 'Refreshed. Reload this form to see updated lists and fields.' 1618 ), 1619 true 1620 ); 1621 } ); 1622 } 1623 1624 const liveStats = root.querySelector( '.syncr-adv__stats' ); 1625 if ( builder === 'ninjaforms' && liveStats ) { 1626 api( '/advanced/stats', {} ) 1627 .then( ( r ) => { 1628 if ( r && r.success && root.isConnected ) { 1629 liveStats.outerHTML = r.html; 1630 } else { 1631 advSay( 1632 adv( 'stats-feedback' ), 1633 cfg.i18n.advFailed || 'Failed.', 1634 false 1635 ); 1636 } 1637 } ) 1638 .catch( () => 1639 advSay( 1640 adv( 'stats-feedback' ), 1641 cfg.i18n.advFailed || 'Failed.', 1642 false 1643 ) 1644 ); 1645 } 1646 if ( adv( 'stats-reset' ) ) { 1647 adv( 'stats-reset' ).addEventListener( 'click', async () => { 1648 const r = await api( '/advanced/stats-reset', {} ); 1649 advSay( 1650 adv( 'stats-feedback' ), 1651 r && r.success 1652 ? cfg.i18n.advReset || 1653 'Counters reset. Reload to see zeros.' 1654 : cfg.i18n.advFailed || 'Failed.', 1655 !! ( r && r.success ) 1656 ); 1657 } ); 1658 } 1659 1660 if ( adv( 'current' ) ) { 1661 adv( 'current' ).addEventListener( 'change', async () => { 1662 const box = adv( 'current' ); 1663 const before = ! box.checked; 1664 const seq = busyStart( box ); 1665 try { 1666 const result = await api( '/advanced/current', { 1667 enabled: box.checked, 1668 } ); 1669 if ( ! busyEnd( box, seq ) ) { 1670 return; 1671 } 1672 box.checked = 1673 result && typeof result.enabled === 'boolean' 1674 ? result.enabled 1675 : before; 1676 advSay( 1677 adv( 'current-feedback' ), 1678 result && result.success 1679 ? t( 'advSaved', 'Saved.' ) 1680 : t( 'advFailed', 'Failed.' ), 1681 !! ( result && result.success ) 1682 ); 1683 } catch { 1684 if ( ! busyEnd( box, seq ) ) { 1685 return; 1686 } 1687 box.checked = before; 1688 advSay( 1689 adv( 'current-feedback' ), 1690 t( 'advFailed', 'Failed.' ), 1691 false 1692 ); 1693 } 1694 } ); 1695 } 1696 1697 if ( adv( 'sharing' ) ) { 1698 adv( 'sharing' ).addEventListener( 'change', async () => { 1699 const box = adv( 'sharing' ); 1700 const before = ! box.checked; 1701 const seq = busyStart( box ); 1702 try { 1703 const result = await api( '/advanced/sharing', { 1704 enabled: box.checked, 1705 } ); 1706 if ( ! busyEnd( box, seq ) ) { 1707 return; 1708 } 1709 box.checked = 1710 result && typeof result.enabled === 'boolean' 1711 ? result.enabled 1712 : before; 1713 advSay( 1714 adv( 'sharing-feedback' ), 1715 result && result.success 1716 ? t( 'advSaved', 'Saved.' ) 1717 : t( 'advFailed', 'Failed.' ), 1718 !! ( result && result.success ) 1719 ); 1720 } catch { 1721 if ( ! busyEnd( box, seq ) ) { 1722 return; 1723 } 1724 box.checked = before; 1725 advSay( 1726 adv( 'sharing-feedback' ), 1727 t( 'advFailed', 'Failed.' ), 1728 false 1729 ); 1730 } 1731 } ); 1732 } 1733 1734 if ( adv( 'purge' ) ) { 1735 adv( 'purge' ).addEventListener( 'change', async () => { 1736 const box = adv( 'purge' ); 1737 const seq = busyStart( box ); 1738 const r = await api( '/advanced/settings', { 1739 purge_on_uninstall: box.checked, 1740 } ); 1741 if ( ! busyEnd( box, seq ) ) { 1742 return; 1743 } 1744 if ( ! r || ! r.success ) { 1745 box.checked = ! box.checked; 1746 advSay( 1747 adv( 'purge-feedback' ), 1748 cfg.i18n.advFailed || 'Failed.', 1749 false 1750 ); 1751 return; 1752 } 1753 advSay( 1754 adv( 'purge-feedback' ), 1755 cfg.i18n.advSaved || 'Saved.', 1756 true 1757 ); 1758 } ); 1759 } 1760 1761 const testBtn = adv( 'test' ); 1762 if ( 1763 testBtn && 1764 testBtn.disabled && 1765 typeof PerformanceObserver === 'function' 1766 ) { 1767 let pending = null; 1768 let last = 0; 1769 const watcher = new PerformanceObserver( ( list ) => { 1770 const saved = list 1771 .getEntries() 1772 .some( 1773 ( e ) => 1774 [ 'xmlhttprequest', 'fetch' ].indexOf( 1775 e.initiatorType 1776 ) !== -1 && e.name.indexOf( cfg.restUrl ) !== 0 1777 ); 1778 if ( ! saved || pending || ! destinationValue() ) { 1779 return; 1780 } 1781 pending = setTimeout( 1782 async () => { 1783 last = Date.now(); 1784 const r = await api( '/advanced/stats', {} ).catch( 1785 () => null 1786 ); 1787 pending = null; 1788 if ( ! r || ! r.ready || ! testBtn.disabled ) { 1789 return; 1790 } 1791 watcher.disconnect(); 1792 testBtn.disabled = false; 1793 const email = adv( 'test-email' ); 1794 if ( email ) { 1795 email.disabled = false; 1796 } 1797 const out = adv( 'test-feedback' ); 1798 if ( out ) { 1799 out.textContent = ''; 1800 } 1801 }, 1802 Math.max( 800, 5000 - ( Date.now() - last ) ) 1803 ); 1804 } ); 1805 watcher.observe( { type: 'resource' } ); 1806 } 1807 if ( testBtn ) { 1808 testBtn.addEventListener( 'click', async () => { 1809 const email = ( 1810 adv( 'test-email' ) || { value: '' } 1811 ).value.trim(); 1812 const out = adv( 'test-feedback' ); 1813 if ( ! email ) { 1814 advSay( 1815 out, 1816 cfg.i18n.advTestEmail || 1817 'Enter an email address first.', 1818 false 1819 ); 1820 return; 1821 } 1822 testBtn.disabled = true; 1823 advSay( out, cfg.i18n.advSending || 'Sending…', true ); 1824 const r = await api( '/advanced/test', { email } ); 1825 testBtn.disabled = false; 1826 if ( ! r ) { 1827 advSay( out, cfg.i18n.advFailed || 'Failed.', false ); 1828 return; 1829 } 1830 let text = testMessage( r ); 1831 if ( r.dropped && r.dropped.length ) { 1832 text += 1833 ' ' + 1834 ( cfg.i18n.advDropped || 'Dropped fields:' ) + 1835 ' ' + 1836 r.dropped.join( ', ' ); 1837 } 1838 advSay( out, text, r.skipped ? null : !! r.success ); 1839 1840 const echo = 1841 out && out.parentNode 1842 ? out.parentNode.querySelector( '.syncr-adv__echo' ) 1843 : null; 1844 if ( echo ) { 1845 echo.remove(); 1846 } 1847 if ( r.success && ! r.skipped && out && out.parentNode ) { 1848 const wrap = el( 'div', { class: 'syncr-adv__echo' } ); 1849 const list = ( title, obj ) => { 1850 const keys = Object.keys( obj || {} ); 1851 if ( ! keys.length ) { 1852 return; 1853 } 1854 wrap.appendChild( el( 'strong', {}, title ) ); 1855 const dl = el( 'dl', {} ); 1856 keys.forEach( ( k ) => { 1857 dl.appendChild( el( 'dt', {}, k ) ); 1858 dl.appendChild( 1859 el( 'dd', {}, String( obj[ k ] ) ) 1860 ); 1861 } ); 1862 wrap.appendChild( dl ); 1863 }; 1864 list( cfg.i18n.advSent || 'Sent', r.sent ); 1865 list( 1866 cfg.i18n.advStored || 'The provider answered', 1867 r.stored 1868 ); 1869 out.parentNode.appendChild( wrap ); 1870 } 1871 } ); 1872 } 1873 1874 if ( adv( 'context' ) ) { 1875 adv( 'context' ).addEventListener( 'change', async () => { 1876 const box = adv( 'context' ); 1877 const seq = busyStart( box ); 1878 const r = await api( '/advanced/settings', { 1879 sender_context: box.checked, 1880 } ); 1881 if ( ! busyEnd( box, seq ) ) { 1882 return; 1883 } 1884 if ( ! r || ! r.success ) { 1885 box.checked = ! box.checked; 1886 advSay( 1887 adv( 'context-feedback' ), 1888 cfg.i18n.advFailed || 'Failed.', 1889 false 1890 ); 1891 return; 1892 } 1893 advSay( 1894 adv( 'context-feedback' ), 1895 cfg.i18n.advSaved || 'Saved.', 1896 true 1897 ); 1898 } ); 1899 } 1900 1901 if ( adv( 'form-reset' ) ) { 1902 adv( 'form-reset' ).addEventListener( 'click', async () => { 1903 if ( 1904 ! window.confirm( 1905 cfg.i18n.advResetConfirm || 1906 'Forget everything saved here for this form?' 1907 ) 1908 ) { 1909 return; 1910 } 1911 const btn = adv( 'form-reset' ); 1912 btn.disabled = true; 1913 const r = await api( '/advanced/form-reset', {} ); 1914 btn.disabled = false; 1915 if ( ! r || ! r.success ) { 1916 advSay( 1917 adv( 'form-reset-feedback' ), 1918 ( r && r.message ) || cfg.i18n.advFailed || 'Failed.', 1919 false 1920 ); 1921 return; 1922 } 1923 advSay( 1924 adv( 'form-reset-feedback' ), 1925 cfg.i18n.advResetDone || 1926 'Reset. Reload to see the empty panel.', 1927 true 1928 ); 1929 } ); 1930 } 1931 1932 Array.from( 1933 document.querySelectorAll( 1934 '#submitdiv a[data-syncr-tab="clicksyncr-for-kit-tab"]' 1935 ) 1936 ).forEach( ( a ) => { 1937 a.addEventListener( 'click', ( e ) => { 1938 const tab = byId( 'tab' ); 1939 if ( ! tab ) { 1940 return; 1941 } 1942 e.preventDefault(); 1943 tab.click(); 1944 const panel = document.getElementById( 'clicksyncr-for-kit' ); 1945 if ( panel ) { 1946 panel.scrollIntoView( { 1947 block: 'start', 1948 behavior: 'smooth', 1949 } ); 1950 } 1951 } ); 1952 } ); 1953 1954 Array.from( 1955 root.querySelectorAll( '.syncr-section--collapsible' ) 1956 ).forEach( ( section ) => { 1957 const toggle = section.querySelector( '.syncr-section__toggle' ); 1958 if ( ! toggle ) { 1959 return; 1960 } 1961 const key = 'syncr-kit-open-' + section.id; 1962 const set = ( open ) => { 1963 section.classList.toggle( 'is-collapsed', ! open ); 1964 toggle.setAttribute( 'aria-expanded', open ? 'true' : 'false' ); 1965 try { 1966 localStorage.setItem( key, open ? '1' : '0' ); 1967 } catch {} 1968 }; 1969 let remembered = null; 1970 try { 1971 remembered = localStorage.getItem( key ); 1972 } catch { 1973 remembered = null; 1974 } 1975 if ( remembered === '1' ) { 1976 set( true ); 1977 } 1978 toggle.addEventListener( 'click', () => 1979 set( section.classList.contains( 'is-collapsed' ) ) 1980 ); 1981 } ); 1982 1983 if ( window.syncrBuilderPanel_kit ) { 1984 window.syncrBuilderPanel_kit.ready( root ); 1985 } 1986 if ( destinationValue() ) { 1987 loadFields(); 1988 } 1989 } 1990 1991 const start = () => 1992 document 1993 .querySelectorAll( 1994 '#' + PANEL_ID + ', [data-syncr-panel="' + PANEL_ID + '"]' 1995 ) 1996 .forEach( ( root ) => boot( root ) ); 1997 start(); 1998 if ( window.MutationObserver ) { 1999 const watcher = new MutationObserver( start ); 2000 watcher.observe( document.body, { childList: true, subtree: true } ); 2001 } 2002 } )(); -
clicksyncr-for-kit/trunk/clicksyncr-for-kit.php
r3726852 r3760857 4 4 * Plugin URI: https://clicksyncr.com/integrations/kit 5 5 * Description: Connect your forms to Kit. Sync every submission from Contact Form 7, WPForms, Gravity Forms, Ninja Forms, Fluent Forms, Formidable, or Forminator to a Kit form as a subscriber, with custom-field mapping and consent control. Works with accounts previously known as ConvertKit. 6 * Version: 26.10. 03.136 * Version: 26.10.22.23 7 7 * Author: ClickSyncr 8 8 * Author URI: https://profiles.wordpress.org/clicksyncr -
clicksyncr-for-kit/trunk/includes/bootstrap.php
r3726852 r3760857 105 105 106 106 \ClickSyncr\Kit\Core\Provider::register(); 107 \ClickSyncr\Kit\Core\Credentials::boot(); 107 108 \ClickSyncr\Kit\Core\Advanced::boot(); 108 109 \ClickSyncr\Kit\Core\Context::boot(); 110 109 111 \ClickSyncr\Kit\Admin\Notices::boot(); 110 112 foreach ( Builder::adapters() as $syncr_kit_adapter ) { -
clicksyncr-for-kit/trunk/includes/signls/SOURCE.md
r3726852 r3760857 1 1 # Source provenance 2 2 3 - SDK version: `1.1.1 2`3 - SDK version: `1.1.15` 4 4 - Package root: `sdk/wordpress/` 5 5 - License: `GPL-3.0-or-later` -
clicksyncr-for-kit/trunk/includes/signls/loader.php
r3726852 r3760857 147 147 require_once __DIR__ . '/bridge-1-1-7.php'; 148 148 149 Signls_Sdk_Loader::add_candidate( '1.1.1 2', __DIR__ . '/sdk.php', '7.4.0' );149 Signls_Sdk_Loader::add_candidate( '1.1.15', __DIR__ . '/sdk.php', '7.4.0' ); -
clicksyncr-for-kit/trunk/includes/signls/manifest.sha256
r3726852 r3760857 1 1 # signls-sdk-manifest-v1 2 sdk_version=1.1.1 23 canonical_commit= b9d164fade0be4be457894db62f68a1770865e294 canonical_tag=v1.1.1 22 sdk_version=1.1.15 3 canonical_commit=0e80d3165265fbd1f8994f4b42c4b4b6b94923eb 4 canonical_tag=v1.1.15 5 5 license=GPL-3.0-or-later 6 6 045ddb9a7b60743eac12126857d21e389a3bba94bd1b5f0b876c0b2d5d14a09f bridge-1-1-5.php … … 8 8 0a5e2014fbdced833ca4f8b09dff4b63464831b5a41b39b2f1f1d0296f28bf2e bridge-1-1-7.php 9 9 c75e5f21e26098fcf67f427623d3763345201378eca508b61956a14b3004c526 LICENSE 10 c0c339ba6525a80c6b9d0f92f0e4353bea0e4ca3d544300d85381f61c9e23048SOURCE.md11 be4f32ced82b8235d53a2ff73bf165f079927a51f64aa3996066477801339c89loader.php10 b900d9eb4d5c2d3cdbf94c85d04911e0160dd7e6e4ee8b9632d0a08e4534611c SOURCE.md 11 e5b4defd1fe083c2ab23e69d006eea6d43ca7e765731ed444a11886200e2b436 loader.php 12 12 24b25f051643be1a81953df6e6bd5c420ef335f229165c69cfc043c61933935c sdk.php 13 f3f260473dc8d9d07d764447a7604e1b2b59f83448ad23285b8365f259062251src/Collector.php13 da8bf9abd53ca89d0cbc9fee0d21eb23c38ed398df733a8c824f320dba13f38f src/Collector.php 14 14 d1ddaa1cdec0878a8d2969576ade2dd4b0561b561d231a62ea93205fd5ae429c src/Consent.php 15 15 b928bb884fa5b738f631b8139a9b1e3fca232feb1eea847bcdbb25ec6b94d2c8 src/CounterStore.php 16 21ebaa64439fe02ce0b2f30400030e566c9fe97d631fa58f25fb2ac947390a6asrc/Identity.php16 7d0cd0524abcacbe27dd203dc1b3b1b76ae62182efa05033cb0db1a5194f0029 src/Identity.php 17 17 8c7c552964adcb702a0d0932993ce2c6f7bc6164266ab6a07af1351913a1e30e src/ObservationSanitizer.php 18 18 3b383acc9a80060b19cba1dcd004a8ccfaf290d3e2cdbff62005eabe2f7de2a6 src/ProductAdapterInterface.php 19 19 a036ba2eaf7226933becb7d46e2071e4bb480f1cb2bbf999cc5212fefdd38485 src/Runtime.php 20 d 6a5be13697522768fa6d8d9194953aab6b5bf7970822314e6d982fe750dc472src/Sanitizer.php20 d56d47159caa59436ab629cb1fecdec72e9e0875b69bf4c38181d65b731d38bc src/Sanitizer.php 21 21 7a4dc9dc000fcbc873b2476677b1b4dd70214d7ace8023f03455c456412a73e4 src/Scheduler.php 22 38c91f7efc978934b51ce606edb0dcabbe7022dd3005913eb444a48091dd829esrc/SiteIdentity.php22 7e38c926de036e80c15501f0e4bd9b1b85db7bc597225af5738de3028591c605 src/SiteIdentity.php 23 23 f1aeb80697d3f17194374a5ad80ca1ffeb2c9c0f36023be37e20609455bced5b src/StateStore.php 24 9f8769213a7d313d319f6b5bc2f45bb53115964fd752ff523253c6faf73181ccsrc/Transport.php24 b58d61359946611de3df3920eb64d7b01e20e6d6b89a1dad45e9ad548379d135 src/Transport.php -
clicksyncr-for-kit/trunk/includes/signls/src/Collector.php
r3726852 r3760857 49 49 $payload['observations'] = ObservationSanitizer::sanitize( $observation_source, $observation_schema ); 50 50 $legacy = isset( $payload['observations']['legacy_lifecycle'] ) && is_array( $payload['observations']['legacy_lifecycle'] ) ? $payload['observations']['legacy_lifecycle'] : array(); 51 if ( array_key_exists( 'site_url', $legacy ) && '' === $legacy['site_url'] ) { 52 $payload['observations']['legacy_lifecycle'] = array(); 53 $missing = isset( $payload['observations']['missing_sections'] ) && is_array( $payload['observations']['missing_sections'] ) ? $payload['observations']['missing_sections'] : array(); 54 $missing[] = 'legacy_lifecycle'; 55 $payload['observations']['missing_sections'] = array_values( array_unique( array_filter( $missing, 'is_string' ) ) ); 56 if ( 'complete' === ( $payload['observations']['collection_status'] ?? '' ) ) { 57 $payload['observations']['collection_status'] = 'partial'; 58 } 51 if ( array_key_exists( 'legacy_lifecycle', $payload['observations'] ) && empty( $legacy['site_url'] ) ) { 52 $payload['observations']['legacy_lifecycle'] = (object) array(); 53 $missing = isset( $payload['observations']['missing_sections'] ) && is_array( $payload['observations']['missing_sections'] ) ? $payload['observations']['missing_sections'] : array(); 54 $missing[] = 'legacy_lifecycle'; 55 $payload['observations']['missing_sections'] = array_values( array_unique( array_filter( $missing, 'is_string' ) ) ); 56 $payload['observations']['collection_status'] = 'partial'; 59 57 } 60 58 } -
clicksyncr-for-kit/trunk/includes/signls/src/Identity.php
r3726852 r3760857 2 2 /** 3 3 * Stable installation and rotating device identity. 4 * 5 * The device rotates only when the stored-origin fingerprint changes (a real 6 * migration or clone), at most once per 24 hours, or when the server proves a 7 * credential is shared. Upgrades re-key the stored fingerprint without rotating. 4 8 * 5 9 * @package signls-sdk … … 13 17 final class Identity { 14 18 19 public const ORIGIN_VERSION = SiteIdentity::ORIGIN_VERSION; 20 21 private const ROTATION_INTERVAL = DAY_IN_SECONDS; 22 15 23 private $state; 16 24 … … 22 30 $current_origin = $this->origin_fingerprint(); 23 31 $stored_origin = (string) $this->state->get( 'origin_fingerprint', '' ); 32 $stored_version = (int) $this->state->get( 'origin_fingerprint_version', 0 ); 24 33 $device_id = (string) $this->state->get( 'device_id', '' ); 25 34 26 if ( ! self::valid_uuid( $device_id ) || ( '' !== $stored_origin && ! hash_equals( $stored_origin, $current_origin ) )) {35 if ( ! self::valid_uuid( $device_id ) ) { 27 36 $device_id = self::uuid4(); 28 37 $this->state->set_many( 29 38 array( 30 'device_id' => $device_id, 31 'origin_fingerprint' => $current_origin, 39 'device_id' => $device_id, 40 'origin_fingerprint' => $current_origin, 41 'origin_fingerprint_version' => self::ORIGIN_VERSION, 32 42 ) 33 43 ); 34 44 $this->clear_credential(); 35 } elseif ( '' === $stored_origin ) { 36 $this->state->set( 'origin_fingerprint', $current_origin ); 45 return $device_id; 46 } 47 48 if ( '' === $stored_origin ) { 49 $this->state->set_many( 50 array( 51 'origin_fingerprint' => $current_origin, 52 'origin_fingerprint_version' => self::ORIGIN_VERSION, 53 ) 54 ); 55 return $device_id; 56 } 57 58 if ( $stored_version < self::ORIGIN_VERSION ) { 59 // Upgrade from an older fingerprint formula: re-key, keep device, credential and sequence. 60 // If the old-formula value does not match either, this may be a clone made before the 61 // upgrade; flag it and let the server's sequence verdict decide (Transport). 62 $values = array( 63 'origin_fingerprint' => $current_origin, 64 'origin_fingerprint_version' => self::ORIGIN_VERSION, 65 ); 66 if ( ! hash_equals( $stored_origin, $this->legacy_origin_fingerprint() ) ) { 67 $values['rekey_unverified_at'] = time(); 68 } 69 $this->state->set_many( $values ); 70 return $device_id; 71 } 72 73 if ( ! hash_equals( $stored_origin, $current_origin ) ) { 74 if ( $this->rotation_allowed() ) { 75 return $this->rotate_device( 'origin_changed' ); 76 } 77 $this->state->set_many( 78 array( 79 'rotation_suppressed_at' => time(), 80 'rotation_suppressed_reason' => 'origin_changed', 81 ) 82 ); 83 $this->notify( 'signls_sdk_v1_identity_rotation_suppressed', 'origin_changed', $stored_origin, $current_origin ); 37 84 } 38 85 … … 40 87 } 41 88 42 public function rotate_device(): string { 43 $device_id = self::uuid4(); 89 public function rotation_allowed(): bool { 90 return time() - (int) $this->state->get( 'last_device_rotation_at', 0 ) >= self::ROTATION_INTERVAL; 91 } 92 93 public function rekey_unverified(): bool { 94 return (int) $this->state->get( 'rekey_unverified_at', 0 ) > 0; 95 } 96 97 public function rotate_device( string $reason = 'manual' ): string { 98 $old_origin = (string) $this->state->get( 'origin_fingerprint', '' ); 99 $new_origin = $this->origin_fingerprint(); 100 $device_id = self::uuid4(); 101 $reason = Sanitizer::slug( $reason, 48, 'manual' ); 44 102 $this->state->set_many( 45 103 array( 46 'device_id' => $device_id, 47 'origin_fingerprint' => $this->origin_fingerprint(), 104 'device_id' => $device_id, 105 'origin_fingerprint' => $new_origin, 106 'origin_fingerprint_version' => self::ORIGIN_VERSION, 107 'last_device_rotation_at' => time(), 108 'last_device_rotation_reason' => $reason, 109 'device_rotation_count' => (int) $this->state->get( 'device_rotation_count', 0 ) + 1, 48 110 ) 49 111 ); 50 112 $this->clear_credential(); 113 $this->notify( 'signls_sdk_v1_identity_rotated', $reason, $old_origin, $new_origin ); 51 114 return $device_id; 52 115 } … … 67 130 'retry_attempt', 68 131 'clock_offset', 132 'rekey_unverified_at', 133 'sequence_resync_at', 134 'rotation_suppressed_at', 135 'rotation_suppressed_reason', 69 136 ), 70 137 StateStore::pending_keys() … … 74 141 75 142 public function forget_device(): void { 76 $this->state->delete_keys( array( 'device_id', 'origin_fingerprint' ) );143 $this->state->delete_keys( array( 'device_id', 'origin_fingerprint', 'origin_fingerprint_version' ) ); 77 144 $this->clear_credential(); 78 145 } 79 146 80 private function origin_fingerprint(): string { 147 /** 148 * Version-2 fingerprint: site origin (stored home option) plus the table prefix. 149 */ 150 public function origin_fingerprint(): string { 151 return $this->fingerprint( ( new SiteIdentity() )->origin_fingerprint() ); 152 } 153 154 /** 155 * Version-1 fingerprint as SDK 1.1.11 and 1.1.12 stored it; used only during the upgrade re-key. 156 */ 157 public function legacy_origin_fingerprint(): string { 158 return $this->fingerprint( ( new SiteIdentity() )->legacy_origin_fingerprint() ); 159 } 160 161 private function fingerprint( string $site_origin ): string { 81 162 $salt = function_exists( 'wp_salt' ) ? (string) wp_salt( 'auth' ) : 'signls-test-salt'; 82 163 global $wpdb; 83 164 $prefix = isset( $wpdb->prefix ) ? (string) $wpdb->prefix : 'wp_'; 84 return hash_hmac( 'sha256', ( new SiteIdentity() )->origin_fingerprint() . '|' . $prefix, $salt ); 165 return hash_hmac( 'sha256', $site_origin . '|' . $prefix, $salt ); 166 } 167 168 /** 169 * Notify observers of an identity transition. 170 * 171 * @param non-empty-string $hook Fixed identity event hook. 172 */ 173 private function notify( string $hook, string $reason, string $old_fingerprint, string $new_fingerprint ): void { 174 if ( function_exists( 'do_action' ) ) { 175 do_action( $hook, $this->state->product(), $reason, substr( $old_fingerprint, 0, 12 ), substr( $new_fingerprint, 0, 12 ) ); 176 } 85 177 } 86 178 -
clicksyncr-for-kit/trunk/includes/signls/src/Sanitizer.php
r3726852 r3760857 100 100 */ 101 101 private static function normalized_url( $value, int $maximum ): string { 102 if ( ! is_string( $value ) || '' === $value || self::text( $value, strlen( $value ) ) !== $value) {102 if ( ! is_string( $value ) || $maximum < 1 || '' === $value || 1 !== preg_match( '//u', $value ) || preg_match( '/[\x00-\x20\x7F]/', $value ) ) { 103 103 return ''; 104 104 } … … 108 108 } 109 109 $scheme = strtolower( (string) $parts['scheme'] ); 110 $host = strtolower( (string) $parts['host']);110 $host = strtolower( rtrim( (string) $parts['host'], '.' ) ); 111 111 if ( ! in_array( $scheme, array( 'http', 'https' ), true ) || '' === $host || preg_match( '/\s/u', $host ) ) { 112 112 return ''; 113 113 } 114 114 if ( 1 === preg_match( '/[^\x00-\x7F]/', $host ) ) { 115 $ascii = function_exists( 'idn_to_ascii' ) ? idn_to_ascii( $host ) : false;115 $ascii = function_exists( 'idn_to_ascii' ) ? idn_to_ascii( $host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 ) : false; 116 116 if ( false === $ascii || '' === $ascii ) { 117 117 return ''; … … 130 130 return ''; 131 131 } 132 $base = $scheme . '://' . $host . ( $port > 0? ':' . $port : '' );132 $base = $scheme . '://' . $host . ( isset( $parts['port'] ) ? ':' . $port : '' ); 133 133 if ( strlen( $base ) > $maximum ) { 134 134 return ''; … … 143 143 } 144 144 $path = preg_replace( '/%(?:[a-f0-9]?)$/i', '', $path ); 145 $url = $base . $path; 146 if ( isset( $parts['query'] ) && '' !== $parts['query'] ) { 147 $query = '?' . $parts['query']; 148 if ( strlen( $url . $query ) <= $maximum && self::text( $query, strlen( $query ) ) === $query ) { 149 $url .= $query; 150 } 151 } 152 return $url; 145 return rtrim( $base . $path, '/' ); 153 146 } 154 147 -
clicksyncr-for-kit/trunk/includes/signls/src/SiteIdentity.php
r3726852 r3760857 2 2 /** 3 3 * Shared random WordPress-origin identity. 4 * 5 * The origin fingerprint is derived from the stored `home` option, read 6 * unfiltered from the options table, so request-scoped values (WPML or 7 * Polylang language prefixes and domains, dynamic WP_HOME, proxies that flip 8 * the scheme, scanners hitting the server by IP) can never rotate the site. 9 * A fingerprint-version marker lets an upgrade re-key the stored value 10 * without rotating, and a 24-hour cap bounds rotations on a real change. 4 11 * 5 12 * @package signls-sdk … … 17 24 private const ORIGIN_OPTION = 'signls_sdk_site_origin'; 18 25 26 private const ORIGIN_VERSION_OPTION = 'signls_sdk_site_origin_version'; 27 28 private const ROTATED_AT_OPTION = 'signls_sdk_site_rotated_at'; 29 30 /** Fingerprint formula version. 1 = home_url('/') (SDK <= 1.1.12), 2 = stored home option (SDK >= 1.1.13). */ 31 public const ORIGIN_VERSION = 2; 32 19 33 public function site_id(): string { 20 34 $current_origin = $this->origin_fingerprint(); 21 35 $stored_origin_candidate = get_option( self::ORIGIN_OPTION, '' ); 22 36 $site_id_candidate = get_option( self::ID_OPTION, '' ); 37 $version_candidate = get_option( self::ORIGIN_VERSION_OPTION, 0 ); 23 38 $stored_origin = is_scalar( $stored_origin_candidate ) ? (string) $stored_origin_candidate : ''; 24 39 $site_id = is_scalar( $site_id_candidate ) ? (string) $site_id_candidate : ''; 40 $version = is_scalar( $version_candidate ) ? (int) $version_candidate : 0; 25 41 26 if ( ! self::valid( $site_id ) || ( '' !== $stored_origin && ! hash_equals( $stored_origin, $current_origin ) )) {42 if ( ! self::valid( $site_id ) ) { 27 43 $site_id = bin2hex( random_bytes( 16 ) ); 28 44 self::replace_option( self::ID_OPTION, $site_id ); 29 45 self::replace_option( self::ORIGIN_OPTION, $current_origin ); 30 } elseif ( '' === $stored_origin ) { 46 self::replace_option( self::ORIGIN_VERSION_OPTION, (string) self::ORIGIN_VERSION ); 47 return $site_id; 48 } 49 50 if ( '' === $stored_origin || $version < self::ORIGIN_VERSION ) { 51 // First run on this formula, or an upgrade from an older formula: re-key, never rotate. 31 52 self::replace_option( self::ORIGIN_OPTION, $current_origin ); 53 self::replace_option( self::ORIGIN_VERSION_OPTION, (string) self::ORIGIN_VERSION ); 54 return $site_id; 55 } 56 57 if ( ! hash_equals( $stored_origin, $current_origin ) ) { 58 $rotated_at_candidate = get_option( self::ROTATED_AT_OPTION, 0 ); 59 $rotated_at = is_scalar( $rotated_at_candidate ) ? (int) $rotated_at_candidate : 0; 60 if ( time() - $rotated_at < DAY_IN_SECONDS ) { 61 self::notify( 'signls_sdk_v1_site_rotation_suppressed', 'origin_changed', $stored_origin, $current_origin ); 62 return $site_id; 63 } 64 $site_id = bin2hex( random_bytes( 16 ) ); 65 self::replace_option( self::ID_OPTION, $site_id ); 66 self::replace_option( self::ORIGIN_OPTION, $current_origin ); 67 self::replace_option( self::ORIGIN_VERSION_OPTION, (string) self::ORIGIN_VERSION ); 68 self::replace_option( self::ROTATED_AT_OPTION, (string) time() ); 69 self::notify( 'signls_sdk_v1_site_rotated', 'origin_changed', $stored_origin, $current_origin ); 32 70 } 33 71 … … 35 73 } 36 74 75 /** 76 * Version-2 fingerprint: HMAC of the normalized stored home option. 77 */ 37 78 public function origin_fingerprint(): string { 79 $salt = function_exists( 'wp_salt' ) ? (string) wp_salt( 'auth' ) : 'signls-test-salt'; 80 return hash_hmac( 'sha256', 'v2|' . self::normalized_origin( self::stored_home() ), $salt ); 81 } 82 83 /** 84 * Version-1 fingerprint (SDK 1.1.11 and 1.1.12): HMAC of scheme://host:port/path of home_url('/'). 85 * Kept only so an upgrade can tell a continued install from a pre-upgrade clone. 86 */ 87 public function legacy_origin_fingerprint(): string { 38 88 $origin = function_exists( 'home_url' ) ? (string) home_url( '/' ) : 'http://local/'; 39 89 $parts = wp_parse_url( $origin ); … … 46 96 $salt = function_exists( 'wp_salt' ) ? (string) wp_salt( 'auth' ) : 'signls-test-salt'; 47 97 return hash_hmac( 'sha256', $scheme . '://' . $host . ':' . $port . $path, $salt ); 98 } 99 100 /** 101 * The `home` option as stored in this blog's options table, bypassing WP_HOME and option filters. 102 * Falls back to get_option() and then home_url() only when the table cannot be read. 103 */ 104 public static function stored_home(): string { 105 global $wpdb; 106 $raw = null; 107 if ( is_object( $wpdb ) && isset( $wpdb->options ) && method_exists( $wpdb, 'get_var' ) && method_exists( $wpdb, 'prepare' ) ) { 108 try { 109 $raw = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Unfiltered read of the stored origin is the point. 110 $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'home' ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core table name. 111 ); 112 } catch ( \Throwable $error ) { 113 $raw = null; 114 } 115 } 116 if ( ! is_string( $raw ) || '' === trim( $raw ) ) { 117 $fallback = function_exists( 'get_option' ) ? get_option( 'home', '' ) : ''; 118 $raw = is_string( $fallback ) ? $fallback : ''; 119 } 120 if ( '' === trim( $raw ) ) { 121 $raw = function_exists( 'home_url' ) ? (string) home_url( '/' ) : 'http://local/'; 122 } 123 return trim( $raw ); 124 } 125 126 /** 127 * host[:port][/path] in lowercase, without scheme, without the default ports, without edge slashes. 128 * An http to https move, a trailing slash or a language prefix on the request must not change it. 129 */ 130 public static function normalized_origin( string $url ): string { 131 $parts = wp_parse_url( trim( $url ) ); 132 $parts = is_array( $parts ) ? $parts : array(); 133 if ( ! isset( $parts['host'] ) && isset( $parts['path'] ) && false === strpos( (string) $parts['path'], '/' ) ) { 134 $parts = wp_parse_url( 'http://' . trim( $url ) ); 135 $parts = is_array( $parts ) ? $parts : array(); 136 } 137 $host = isset( $parts['host'] ) ? strtolower( rtrim( (string) $parts['host'], '.' ) ) : 'local'; 138 $port = isset( $parts['port'] ) ? (int) $parts['port'] : 0; 139 $path = isset( $parts['path'] ) ? trim( (string) $parts['path'], '/' ) : ''; 140 return $host . ( $port > 0 && ! in_array( $port, array( 80, 443 ), true ) ? ':' . $port : '' ) . ( '' !== $path ? '/' . $path : '' ); 141 } 142 143 /** 144 * Notify observers of an identity transition. 145 * 146 * @param non-empty-string $hook Fixed identity event hook. 147 */ 148 private static function notify( string $hook, string $reason, string $old_fingerprint, string $new_fingerprint ): void { 149 if ( function_exists( 'do_action' ) ) { 150 do_action( $hook, $reason, substr( $old_fingerprint, 0, 12 ), substr( $new_fingerprint, 0, 12 ) ); 151 } 48 152 } 49 153 -
clicksyncr-for-kit/trunk/includes/signls/src/Transport.php
r3726852 r3760857 15 15 private const DEFAULT_ENDPOINT = 'https://signls.dev'; 16 16 17 private const SDK_VERSION = '1.1.12'; 17 private const SDK_VERSION = '1.1.15'; 18 19 /** A second sequence conflict this soon after a resync means two installs share one credential. */ 20 private const SHARED_CREDENTIAL_WINDOW = 1800; 18 21 19 22 private const CLOCK_SKEW_SECONDS = 300; … … 45 48 46 49 public function prepare( ProductAdapterInterface $adapter ) { 50 $this->identity->device_id(); 47 51 $revision = $this->payload_revision( $adapter ); 48 52 $pending = $this->reconcile_pending_identity( $adapter, $revision ); … … 70 74 71 75 public function deliver( ProductAdapterInterface $adapter, array $payload ): array { 76 $this->identity->device_id(); 72 77 $started = time(); 73 78 $this->state->set( 'last_attempt_started_at', $started ); 74 $this->identity->device_id();75 79 76 80 if ( '' === (string) $this->state->get( 'credential_id', '' ) ) { … … 236 240 ); 237 241 $this->state->clear_pending(); 242 $this->state->delete_keys( array( 'rekey_unverified_at' ) ); 238 243 return array( 239 244 'ok' => true, … … 249 254 if ( 'sequence_conflict' === $response['class'] ) { 250 255 $response['permanent'] = false; 251 $expected = isset( $response['data']['expected_sequence'] ) && is_int( $response['data']['expected_sequence'] ) ? $response['data']['expected_sequence'] : 0; 256 $now = time(); 257 // A conflict while the post-upgrade re-key is unverified: this install may be a clone made 258 // before the upgrade that kept production's credential. Separate it now. 259 if ( $this->identity->rekey_unverified() && ! $sequence_retry ) { 260 if ( $this->identity->rotation_allowed() ) { 261 $this->identity->rotate_device( 'clone_suspected_after_rekey' ); 262 $response['rotated'] = true; 263 return $response; 264 } 265 $this->state->delete_keys( array( 'rekey_unverified_at' ) ); 266 } 267 // A second conflict shortly after a resync: two installs are sending on one credential. 268 $last_resync = (int) $this->state->get( 'sequence_resync_at', 0 ); 269 if ( ! $sequence_retry && $last_resync > 0 && $now - $last_resync <= self::SHARED_CREDENTIAL_WINDOW && $this->identity->rotation_allowed() ) { 270 $this->identity->rotate_device( 'credential_shared' ); 271 $response['rotated'] = true; 272 return $response; 273 } 274 $expected = isset( $response['data']['expected_sequence'] ) && is_int( $response['data']['expected_sequence'] ) ? $response['data']['expected_sequence'] : 0; 252 275 if ( ! $sequence_retry && $expected > 0 ) { 253 $old_body = json_decode( $pending, true ); 254 if ( ! is_array( $old_body ) || ! isset( $old_body['payload'] ) || ! is_array( $old_body['payload'] ) ) { 255 return array( 256 'ok' => false, 257 'class' => 'invalid_pending', 258 'status' => 0, 259 'permanent' => false, 276 $old_body = json_decode( $pending ); 277 if ( ! is_object( $old_body ) || ! isset( $old_body->payload ) || ! is_object( $old_body->payload ) || array() === get_object_vars( $old_body->payload ) ) { 278 return $this->quarantine( 279 array( 280 'ok' => false, 281 'class' => 'invalid_payload', 282 'status' => 0, 283 'permanent' => true, 284 ) 260 285 ); 261 286 } 262 287 if ( $this->state->reconcile_sequence( $expected ) ) { 263 return $this->snapshot( $adapter, $old_body['payload'], $clock_retry, true ); 288 $this->state->set( 'sequence_resync_at', $now ); 289 return $this->snapshot( $adapter, get_object_vars( $old_body->payload ), $clock_retry, true ); 264 290 } 265 291 } … … 366 392 $values['retry_attempt'] = $attempt + 1; 367 393 if ( in_array( $values['failure_class'], array( 'invalid_credential', 'device_already_enrolled' ), true ) ) { 368 $last_rotation = (int) $this->state->get( 'last_device_rotation_at', 0 ); 369 if ( time() - $last_rotation >= DAY_IN_SECONDS ) { 370 $this->identity->rotate_device(); 371 $values['last_device_rotation_at'] = time(); 394 if ( $this->identity->rotation_allowed() ) { 395 $this->identity->rotate_device( $values['failure_class'] ); 372 396 } 373 397 $result['permanent'] = false; 398 } 399 if ( ! empty( $result['rotated'] ) ) { 400 // The device was just rotated; the next run enrolls afresh. Do not keep a stale retry schedule. 401 $values['retry_attempt'] = 0; 374 402 } 375 403 if ( empty( $result['permanent'] ) ) { -
clicksyncr-for-kit/trunk/includes/src/Admin/AdminBar.php
r3726852 r3760857 279 279 'parent' => self::MENU, 280 280 'id' => 'clicksyncr-licence', 281 'title' => esc_html__( 'Activate your Pro licen ce', 'clicksyncr-for-kit' ),281 'title' => esc_html__( 'Activate your Pro license', 'clicksyncr-for-kit' ), 282 282 'href' => admin_url( 'options-general.php?page=clicksyncr-pro' ), 283 283 'meta' => array( 'class' => 'syncr-kit-ab-update' ), -
clicksyncr-for-kit/trunk/includes/src/Admin/PanelHeader.php
r3726852 r3760857 32 32 return array( 'neutral', sprintf( __( '%s not connected', 'clicksyncr-for-kit' ), Brand::provider() ) ); 33 33 } 34 $texts = self::connected_texts(); 34 35 if ( '' === $destination ) { 36 return array( 'pending', $texts['pending'] ); 37 } 38 return array( 'connected', $texts['connected'] ); 39 } 40 41 /** @return array{pending:string,connected:string} */ 42 public static function connected_texts(): array { 43 return array( 35 44 /* translators: 1: email platform name, 2: list noun. */ 36 return array( 'pending', sprintf( __( '%1$s connected, no %2$s saved', 'clicksyncr-for-kit' ), Brand::provider(), Provider::destination() ) );37 }38 /* translators: %s: the email platform name. */39 return array( 'connected', sprintf( __( '%s connected', 'clicksyncr-for-kit' ), Brand::provider() ));45 'pending' => sprintf( __( '%1$s connected, no %2$s saved', 'clicksyncr-for-kit' ), Brand::provider(), Provider::destination() ), 46 /* translators: %s: the email platform name. */ 47 'connected' => sprintf( __( '%s connected', 'clicksyncr-for-kit' ), Brand::provider() ), 48 ); 40 49 } 41 50 -
clicksyncr-for-kit/trunk/includes/src/Admin/PanelRows.php
r3726852 r3760857 259 259 } 260 260 $body = $locked 261 ? '<p class="description syncr-locked">' . ( Edition::pro_active() ? sprintf( /* translators: %s: link to ClickSyncr Pro */ esc_html__( 'A %s licen ce unlocks this.', 'clicksyncr-for-kit' ), Brand::pro_link( 'panel', self::$builder . '-' . Val::str( $row['id'] ?? '' ) ) ) : sprintf( /* translators: %s: link to ClickSyncr Pro */ esc_html__( 'Available in %s.', 'clicksyncr-for-kit' ), Brand::pro_link( 'panel', self::$builder . '-' . Val::str( $row['id'] ?? '' ) ) ) ) . '</p>'261 ? '<p class="description syncr-locked">' . ( Edition::pro_active() ? sprintf( /* translators: %s: link to ClickSyncr Pro */ esc_html__( 'A %s license unlocks this.', 'clicksyncr-for-kit' ), Brand::pro_link( 'panel', self::$builder . '-' . Val::str( $row['id'] ?? '' ) ) ) : sprintf( /* translators: %s: link to ClickSyncr Pro */ esc_html__( 'Available in %s.', 'clicksyncr-for-kit' ), Brand::pro_link( 'panel', self::$builder . '-' . Val::str( $row['id'] ?? '' ) ) ) ) . '</p>' 262 262 : self::control( $row ); 263 263 printf( … … 358 358 self::tile_items( $options, $chosen, $ticked, $name, $extras, $locked ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in tile_items(). 359 359 ); 360 echo '<p id="clicksyncr-for-kit-destination-status" role="status" class="syncr-destination-status"></p>'; 360 361 printf( 361 362 '<div class="syncr-actions syncr-actions--end"><span id="clicksyncr-for-kit-destination-feedback" class="syncr-feedback" aria-live="polite"></span> <button type="button" class="button" id="clicksyncr-for-kit-destination-refresh">%s</button></div>', … … 392 393 $out .= '<span class="syncr-main-caption">' . esc_html( $caption ) . '</span>'; 393 394 $extra_name = 'wpforms' === self::$builder ? substr( $extras, 0, -2 ) . '[' . $i . ']' : $extras; 394 $out .= '<label class="syncr-destination-extra"' . ( $primary ? ' hidden' : '' ) . ( $locked ? ' tabindex="0" role="button" data-syncr-destination-tip aria-disabled="true"' : '' ) . '>';395 $out .= '<input type="checkbox"' . ( $locked ? ' tabindex="-1" aria-disabled="true" ' : ' name="' . esc_attr( $extra_name ) . '"' ) . ' value="' . esc_attr( $value ) . '"' . ( $saved && ! $primary ? ' checked' : '' ) . ' /> ' . esc_html__( 'Also add here', 'clicksyncr-for-kit' ) . ( $locked ? ' <span class="syncr-pro-chip">PRO</span>' : '' ) . '</label>';395 $out .= '<label class="syncr-destination-extra"' . ( $primary ? ' hidden' : '' ) . ( $locked ? ' tabindex="0" role="button" data-syncr-destination-tip' : '' ) . '>'; 396 $out .= '<input type="checkbox"' . ( $locked ? ' tabindex="-1" aria-disabled="true" disabled' : ' name="' . esc_attr( $extra_name ) . '"' ) . ' value="' . esc_attr( $value ) . '"' . ( $saved && ! $primary ? ' checked' : '' ) . ' /> ' . esc_html__( 'Also add here', 'clicksyncr-for-kit' ) . ( $locked ? ' <span class="syncr-pro-chip">PRO</span>' : '' ) . '</label>'; 396 397 $out .= '<span class="syncr-destination-paused"' . ( $locked && $saved && ! $primary ? '' : ' hidden' ) . '>' . esc_html__( 'Saved. Renew your license to send here too.', 'clicksyncr-for-kit' ) . '</span></div>'; 397 398 ++$i; … … 425 426 esc_html( Val::str( $row['label'] ?? '' ) ), 426 427 esc_html( Val::str( $row['hint'] ?? '' ) ), 427 $locked ? '<button type="button" class="button syncr-destination-extra" data-syncr-destination-tip aria-disabled="true">' . esc_html__( 'Add rule', 'clicksyncr-for-kit' ) . '</button> <span class="syncr-pro-chip">PRO</span>' : self::control( $row ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built by control() from escaped parts.428 $locked ? '<button type="button" class="button syncr-destination-extra" data-syncr-destination-tip>' . esc_html__( 'Add rule', 'clicksyncr-for-kit' ) . '</button> <span class="syncr-pro-chip">PRO</span>' : self::control( $row ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built by control() from escaped parts. 428 429 $locked ? '<p class="syncr-notice">' . sprintf( /* translators: %s: link to ClickSyncr Pro */ esc_html__( 'Answer-based rules require %s.', 'clicksyncr-for-kit' ), Brand::pro_link( 'panel', self::$builder . '-destination_rules' ) ) . '</p>' : '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pro_link() returns escaped markup. 429 430 ); -
clicksyncr-for-kit/trunk/includes/src/Builders/Adapter.php
r3726852 r3760857 42 42 public function key( int $form_id ): string; 43 43 44 public function own_key( int $form_id ): string; 45 46 /** 47 * @return list<int> 48 */ 49 public function configured_forms(): array; 50 44 51 public function hook(): string; 45 52 -
clicksyncr-for-kit/trunk/includes/src/Builders/CF7.php
r3726852 r3760857 247 247 248 248 public function key( int $form_id ): string { 249 $own = Val::str( Settings::get( $form_id, 'api_key', '' ) ); 250 251 return '' !== $own ? $own : Credentials::shared(); 249 return Credentials::resolve( $this->own_key( $form_id ) ); 250 } 251 252 public function own_key( int $form_id ): string { 253 return trim( Val::str( Settings::get( $form_id, 'api_key', '' ) ) ); 254 } 255 256 /** 257 * @return list<int> 258 */ 259 public function configured_forms(): array { 260 $out = array(); 261 $posts = get_posts( 262 array( 263 'post_type' => 'wpcf7_contact_form', 264 'post_status' => 'any', 265 'numberposts' => -1, 266 'fields' => 'ids', 267 ) 268 ); 269 foreach ( $posts as $form_id ) { 270 if ( '' !== Val::str( $this->values( Val::int( $form_id ) )['group_id'] ?? '' ) ) { 271 $out[] = Val::int( $form_id ); 272 } 273 } 274 275 return $out; 252 276 } 253 277 … … 278 302 $posted = isset( $_POST[ self::POST_KEY ] ) ? self::clean( wp_unslash( $_POST[ self::POST_KEY ] ) ) : array(); 279 303 if ( array() === $posted ) { 304 return; 305 } 306 if ( $form_id <= 0 ) { 307 add_action( 'wpcf7_after_save', array( $this, 'save_panel' ) ); 280 308 return; 281 309 } … … 365 393 'connecting' => __( 'Connecting…', 'clicksyncr-for-kit' ), 366 394 'connected' => __( 'Connected. Choose where people go.', 'clicksyncr-for-kit' ), 395 'header_pending' => \ClickSyncr\Kit\Admin\PanelHeader::connected_texts()['pending'], 396 'header_connected' => \ClickSyncr\Kit\Admin\PanelHeader::connected_texts()['connected'], 367 397 'failed' => __( 'That key was not accepted.', 'clicksyncr-for-kit' ), 368 398 'loading' => __( 'Loading…', 'clicksyncr-for-kit' ), -
clicksyncr-for-kit/trunk/includes/src/Builders/FluentForms.php
r3726852 r3760857 253 253 254 254 public function key( int $form_id ): string { 255 unset( $form_id ); 256 257 return Credentials::shared(); 255 return Credentials::resolve( $this->own_key( $form_id ) ); 256 } 257 258 public function own_key( int $form_id ): string { 259 return Credentials::stored( Builder::FluentForms, $form_id ); 260 } 261 262 /** 263 * @return list<int> 264 */ 265 public function configured_forms(): array { 266 global $wpdb; 267 if ( ! $wpdb instanceof \wpdb ) { 268 return array(); 269 } 270 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one read of a Fluent Forms table during the key upgrade. 271 $ids = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT form_id FROM %i WHERE meta_key = %s', $wpdb->prefix . 'fluentform_form_meta', self::FEEDS ) ); 272 273 return array_values( array_filter( array_map( array( Val::class, 'int' ), is_array( $ids ) ? $ids : array() ) ) ); 258 274 } 259 275 … … 262 278 return false; 263 279 } 264 unset( $form_id ); 265 Credentials::store_shared( $key, true, 'fluentforms' ); 266 267 return true; 280 281 return Credentials::store_form( Builder::FluentForms, $form_id, $key ); 268 282 } 269 283 -
clicksyncr-for-kit/trunk/includes/src/Builders/FluentIntegration.php
r3726852 r3760857 35 35 final class FluentIntegration extends IntegrationManagerController { 36 36 37 private const KEY_MASK = '••••••••';38 39 public static function masked_key( string $api_key ): string {40 return '' === $api_key ? '' : self::KEY_MASK . substr( $api_key, -4 );41 }42 43 private static function is_mask( string $value ): bool {44 return 1 === preg_match( '/^[\x{2022}*]{4,}/u', $value );45 }46 47 37 /** 48 38 * @param mixed $application … … 62 52 $this->logo = 'data:image/svg+xml;base64,' . base64_encode( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="8" fill="#2563eb"/><text x="20" y="27" font-size="17" fill="#fff" text-anchor="middle" font-family="Arial, sans-serif">' . esc_html( strtoupper( 'kit' ) ) . '</text></svg>' ); 63 53 54 $this->hasGlobalMenu = false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Fluent Forms property contract. 64 55 $this->registerAdminHooks(); 65 56 66 add_filter( 'fluentform/notifying_async_' . FluentForms::INTEGRATION, '__return_false' );67 57 add_filter( 'fluentform/save_integration_value_' . FluentForms::INTEGRATION, array( $this, 'sanitizeSettings' ), 10, 3 ); 68 58 } … … 71 61 72 62 /** 73 * @return array<array-key,mixed> 74 * @param mixed $fields 75 */ 76 public function getGlobalFields( $fields ): array { 77 unset( $fields ); 78 79 $config = array( 80 'logo' => $this->logo, 81 'menu_title' => __( 'ClickSyncr for Kit Settings', 'clicksyncr-for-kit' ), 82 /* translators: %s: provider name */ 83 'menu_description' => sprintf( __( 'Enter your %s API key to connect.', 'clicksyncr-for-kit' ), Provider::label() ), 84 /* translators: %s: provider name */ 85 'valid_message' => sprintf( __( 'Your %s API key is valid', 'clicksyncr-for-kit' ), Provider::label() ), 86 /* translators: %s: provider name */ 87 'invalid_message' => sprintf( __( 'Your %s API key is not valid', 'clicksyncr-for-kit' ), Provider::label() ), 88 'save_button_text' => __( 'Save Settings', 'clicksyncr-for-kit' ), 89 'fields' => array( 90 'apiKey' => array( 91 'type' => 'password', 92 /* translators: %s: provider name */ 93 'label_tips' => sprintf( __( 'Paste your %s API key.', 'clicksyncr-for-kit' ), Provider::label() ), 94 'label' => Provider::key_label(), 95 ), 96 ), 97 'hide_on_valid' => true, 98 'discard_settings' => array( 99 /* translators: %s: provider name */ 100 'section_description' => sprintf( __( 'Your %s integration is up and running.', 'clicksyncr-for-kit' ), Provider::label() ), 101 /* translators: %s: provider name */ 102 'button_text' => sprintf( __( 'Disconnect %s', 'clicksyncr-for-kit' ), Provider::label() ), 103 'data' => array( 'apiKey' => '' ), 104 'show_verify' => true, 105 ), 106 ); 107 return Val::arr( apply_filters( 'clicksyncr_kit_credential_fields', $config ) ); 108 } 109 110 /** 111 * @return array<array-key,mixed> 112 * @param mixed $settings 113 */ 114 public function getGlobalSettings( $settings ): array { 115 unset( $settings ); 116 117 $stored = wp_parse_args( 118 Val::arr( get_option( Val::str( $this->optionKey ) ) ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Fluent Forms property contract. 119 array( 120 'apiKey' => '', 121 'status' => '', 122 ) 123 ); 124 $stored['apiKey'] = self::masked_key( Val::str( $stored['apiKey'] ) ); 125 126 return $stored; 127 } 128 129 /** 130 * @param mixed $settings 131 */ 132 public function saveGlobalSettings( $settings ): void { 133 if ( ! \ClickSyncr\Kit\Core\Access::credentials() ) { 134 wp_send_json_error( array( 'message' => __( 'Administrator access required.', 'clicksyncr-for-kit' ) ), 403 ); 135 } 136 $api_key = Val::str( Val::arr( $settings )['apiKey'] ?? '' ); 137 138 if ( self::is_mask( $api_key ) ) { 139 wp_send_json_success( 140 array( 141 /* translators: %s: provider name */ 142 'message' => sprintf( __( 'Your saved %s API key was kept', 'clicksyncr-for-kit' ), Provider::label() ), 143 'status' => '' !== Credentials::shared(), 144 ), 145 200 146 ); 147 } 148 149 if ( '' === $api_key ) { 150 Credentials::store_shared( '', false, 'fluentforms' ); 151 wp_send_json_success( 152 array( 153 'message' => __( 'Your settings have been updated and disconnected', 'clicksyncr-for-kit' ), 154 'status' => false, 155 ), 156 200 157 ); 158 } 159 160 $client = Provider::CLIENT; 161 $result = class_exists( $client ) ? Val::arr( ( new $client( $api_key ) )->test() ) : array( 162 'success' => false, 163 'message' => '', 164 ); 165 166 $result = Val::arr( apply_filters( 'clicksyncr_kit_credential_validation', $result, $api_key, Val::arr( $settings ) ) ); 167 168 if ( empty( $result['success'] ) ) { 169 wp_send_json_error( array( 'message' => Val::str( $result['message'] ?? '' ) ), 400 ); 170 } 171 172 Credentials::store_shared( $api_key, true, 'fluentforms' ); 173 174 wp_send_json_success( 175 array( 176 /* translators: %s: provider name */ 177 'message' => sprintf( __( 'Your %s API key has been verified and saved', 'clicksyncr-for-kit' ), Provider::label() ), 178 'status' => true, 179 ), 180 200 181 ); 182 } 183 184 63 * @return bool 64 */ 65 public function isConfigured() { 66 return true; 67 } 185 68 186 69 /** … … 214 97 public static function describe( int $form_id, array $values ): array { 215 98 $adapter = new FluentForms(); 216 $key = Credentials::shared();99 $key = $adapter->key( $form_id ); 217 100 218 101 return Panel::describe( … … 345 228 */ 346 229 public function getMergeFields( $list, $listId, $formId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- Fluent Forms named-parameter contract. 347 unset( $list, $listId , $formId); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- Fluent Forms named-parameter contract.230 unset( $list, $listId ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- Fluent Forms named-parameter contract. 348 231 349 232 if ( ! $this->isConfigured() ) { … … 352 235 353 236 $fields = array(); 354 foreach ( Catalog::mappable_fields_typed( Credentials::shared() ) as $key => $row ) {237 foreach ( Catalog::mappable_fields_typed( ( new FluentForms() )->key( Val::int( $formId ) ) ) as $key => $row ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- Fluent Forms named-parameter contract. 355 238 $row = Val::arr( $row ); 356 239 $fields[ $key ] = Catalog::label_with_type( Val::str( $row['label'] ?? (string) $key ), Val::str( $row['type'] ?? '' ) ); … … 487 370 488 371 $result = SubscriberSync::push( 489 Credentials::shared(),372 ( new FluentForms() )->key( Val::int( is_object( $form ) ? ( $form->id ?? 0 ) : ( Val::arr( $form )['id'] ?? 0 ) ) ), 490 373 Val::str( $feed_data['list_id'] ?? '' ), 491 374 array( 'email' => $email ) + Val::str_map( $feed_data['merge_fields'] ?? null ), … … 493 376 StatusPreference::from_setting( $feed_data['status_preference'] ?? '' ), 494 377 Val::str( $feed_data['consent_field'] ?? '' ), 495 $form_data 378 $form_data, 379 StatusResolver::kind( ( new FluentForms() )->fields( Val::int( is_object( $form ) ? ( $form->id ?? 0 ) : ( Val::arr( $form )['id'] ?? 0 ) ) ), Val::str( $feed_data['consent_field'] ?? '' ) ) 496 380 ), 497 381 Builder::FluentForms, … … 507 391 $message = $succeeded 508 392 /* translators: %s: provider name */ 509 ? sprintf( __( ' %s feed has been processed successfully', 'clicksyncr-for-kit' ), Provider::label() )393 ? sprintf( __( 'Added to %s by ClickSyncr', 'clicksyncr-for-kit' ), Provider::label() ) 510 394 /* translators: %s: provider name */ 511 395 : Val::str_or( Val::str( $result['message'] ?? '' ), sprintf( __( '%s feed failed', 'clicksyncr-for-kit' ), Provider::label() ) ); -
clicksyncr-for-kit/trunk/includes/src/Builders/Formidable.php
r3726852 r3760857 194 194 return; 195 195 } 196 $vars = get_object_vars( $action ); 197 $content = self::apply( Val::arr( $vars['post_content'] ?? null ), $own ); 198 wp_update_post( 199 array( 200 'ID' => Val::int( $vars['ID'] ?? 0 ), 201 'post_content' => (string) wp_json_encode( $content ), 202 ) 196 $vars = get_object_vars( $action ); 197 self::write( Val::int( $vars['ID'] ?? 0 ), self::apply( Val::arr( $vars['post_content'] ?? null ), $own ) ); 198 } 199 200 /** 201 * @param array<array-key,mixed> $content 202 * @return array<array-key,mixed>|null 203 */ 204 private static function write( int $action_id, array $content ): ?array { 205 if ( $action_id <= 0 ) { 206 return null; 207 } 208 $result = wp_update_post( 209 wp_slash( 210 array( 211 'ID' => $action_id, 212 'post_content' => (string) wp_json_encode( $content ), 213 ) 214 ), 215 true 203 216 ); 217 if ( $result !== $action_id ) { 218 return null; 219 } 220 clean_post_cache( $action_id ); 221 $post = get_post( $action_id ); 222 $stored = is_object( $post ) ? json_decode( Val::str( get_object_vars( $post )['post_content'] ?? '' ), true ) : null; 223 224 return is_array( $stored ) ? $stored : null; 204 225 } 205 226 … … 250 271 251 272 public function key( int $form_id ): string { 252 return Credentials::resolve( Val::str( $this->values( $form_id )['api_key'] ?? '' ) ); 273 return Credentials::resolve( $this->own_key( $form_id ) ); 274 } 275 276 public function own_key( int $form_id ): string { 277 $action = self::first_action( $form_id ); 278 279 return null === $action ? '' : trim( Val::str( Val::arr( get_object_vars( $action )['post_content'] ?? null )['api_key'] ?? '' ) ); 280 } 281 282 /** 283 * @return list<int> 284 */ 285 public function configured_forms(): array { 286 $out = array(); 287 $posts = get_posts( 288 array( 289 'post_type' => 'frm_form_actions', 290 'post_status' => 'any', 291 'numberposts' => -1, 292 'post_excerpt' => self::SLUG, 293 'suppress_filters' => false, 294 ) 295 ); 296 foreach ( $posts as $post ) { 297 if ( $post instanceof \WP_Post && self::SLUG === $post->post_excerpt && $post->menu_order > 0 ) { 298 $out[] = (int) $post->menu_order; 299 } 300 } 301 302 return array_values( array_unique( $out ) ); 253 303 } 254 304 … … 257 307 return false; 258 308 } 259 unset( $form_id ); 260 Credentials::store_shared( $key, true, 'formidable' ); 261 262 return true; 309 $action = self::first_action( $form_id ); 310 if ( null === $action ) { 311 return false; 312 } 313 $vars = get_object_vars( $action ); 314 $content = Val::arr( $vars['post_content'] ?? null ); 315 $content['api_key'] = sanitize_text_field( $key ); 316 $stored = self::write( Val::int( $vars['ID'] ?? 0 ), $content ); 317 318 return null !== $stored && Val::str( $stored['api_key'] ?? '' ) === $content['api_key']; 263 319 } 264 320 265 321 public static function first_action( int $form_id ): ?object { 266 if ( $form_id <= 0 || ! class_exists( '\\FrmFormAction' ) || ! method_exists( '\\FrmFormAction', 'get_action_for_form' )) {322 if ( $form_id <= 0 || ! class_exists( '\\FrmFormAction' ) ) { 267 323 return null; 268 324 } -
clicksyncr-for-kit/trunk/includes/src/Builders/FormidableAction.php
r3726852 r3760857 94 94 $vars = is_object( $form_action ) ? get_object_vars( $form_action ) : Val::arr( $form_action ); 95 95 $settings = Val::arr( $vars['post_content'] ?? null ); 96 $form_id = Val::int( $this->form_id?? 0 );96 $form_id = Val::int( get_object_vars( $this )['form_id'] ?? 0 ); 97 97 $form_id = 0 !== $form_id ? $form_id : Val::int( $vars['menu_order'] ?? 0 ); 98 98 $values = Formidable::values_from( $settings ); … … 145 145 unset( $new_instance ); 146 146 $inst_vars = is_object( $instance ) ? get_object_vars( $instance ) : Val::arr( $instance ); 147 $form_id = Val::int( $inst_vars['menu_order'] ?? ( $this->form_id?? 0 ) );147 $form_id = Val::int( $inst_vars['menu_order'] ?? Val::int( get_object_vars( $this )['form_id'] ?? 0 ) ); 148 148 \ClickSyncr\Kit\Core\Access::require_form( 'formidable', $form_id ); 149 149 $content = Val::arr( $post_content ); -
clicksyncr-for-kit/trunk/includes/src/Builders/Forminator.php
r3726852 r3760857 78 78 79 79 return array( 80 'api_key' => '',80 'api_key' => Val::str( $settings['api_key'] ?? '' ), 81 81 'group_id' => Val::str( $settings['kit_group'] ?? ( $settings['ml_group'] ?? ( $settings['group_id'] ?? '' ) ) ), 82 82 'field_map' => $map, … … 181 181 182 182 public function key( int $form_id ): string { 183 unset( $form_id ); 184 185 return Credentials::shared(); 183 return Credentials::resolve( $this->own_key( $form_id ) ); 184 } 185 186 public function own_key( int $form_id ): string { 187 return trim( Val::str( self::settings( $form_id )['api_key'] ?? '' ) ); 188 } 189 190 /** 191 * @return list<int> 192 */ 193 public function configured_forms(): array { 194 $out = array(); 195 $posts = get_posts( 196 array( 197 'post_type' => 'forminator_forms', 198 'post_status' => 'any', 199 'numberposts' => -1, 200 'fields' => 'ids', 201 ) 202 ); 203 foreach ( $posts as $form_id ) { 204 if ( '' !== Val::str( self::values_from( self::settings( Val::int( $form_id ) ) )['group_id'] ?? '' ) ) { 205 $out[] = Val::int( $form_id ); 206 } 207 } 208 209 return $out; 186 210 } 187 211 … … 190 214 return false; 191 215 } 192 unset( $form_id ); 193 Credentials::store_shared( $key, true, 'forminator' ); 194 195 return true; 216 $meta = self::meta_key( $form_id ); 217 if ( '' === $meta ) { 218 return false; 219 } 220 $settings = self::settings( $form_id ); 221 $settings['api_key'] = $key; 222 update_post_meta( $form_id, $meta, $settings ); 223 return hash_equals( $key, Val::str( self::settings( $form_id )['api_key'] ?? '' ) ); 196 224 } 197 225 -
clicksyncr-for-kit/trunk/includes/src/Builders/ForminatorFormSettings.php
r3726852 r3760857 55 55 public static function describe( int $form_id, array $values ): array { 56 56 $adapter = new Forminator(); 57 $key = Credentials::shared();57 $key = $adapter->key( $form_id ); 58 58 59 59 $values['api_key'] = $key; … … 95 95 $form_id = Val::int( $this->module_id ); 96 96 $posted = Val::arr( $submitted['syncr'] ?? null ); 97 $key = Credentials::shared();97 $key = ( new Forminator() )->key( $form_id ); 98 98 $error = ''; 99 99 -
clicksyncr-for-kit/trunk/includes/src/Builders/Gravity.php
r3726852 r3760857 181 181 182 182 public function key( int $form_id ): string { 183 unset( $form_id ); 184 185 return Credentials::shared(); 183 return Credentials::resolve( $this->own_key( $form_id ) ); 184 } 185 186 public function own_key( int $form_id ): string { 187 return Credentials::stored( Builder::Gravity, $form_id ); 188 } 189 190 /** 191 * @return list<int> 192 */ 193 public function configured_forms(): array { 194 $out = array(); 195 $feeds = class_exists( '\\GFAPI' ) ? \GFAPI::get_feeds( null, null, GravityFeed::SLUG, null ) : array(); 196 foreach ( is_array( $feeds ) ? $feeds : array() as $feed ) { 197 $form_id = Val::int( Val::arr( $feed )['form_id'] ?? 0 ); 198 if ( $form_id > 0 ) { 199 $out[] = $form_id; 200 } 201 } 202 203 return array_values( array_unique( $out ) ); 186 204 } 187 205 … … 190 208 return false; 191 209 } 192 unset( $form_id ); 193 Credentials::store_shared( $key, true, 'gravity' ); 194 195 return true; 210 211 return Credentials::store_form( Builder::Gravity, $form_id, $key ); 196 212 } 197 213 -
clicksyncr-for-kit/trunk/includes/src/Builders/GravityFeed.php
r3726852 r3760857 37 37 public const SLUG = 'clicksyncr_kit'; 38 38 39 /** 40 * @var bool 41 */ 42 protected $_async_feed_processing = true; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore -- Gravity Forms add-on API. 43 39 44 protected $_version = SYNCR_KIT_VER; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore -- Gravity Forms add-on API. 40 45 protected $_min_gravityforms_version = '2.5'; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore -- Gravity Forms add-on API. … … 74 79 /* translators: %s: provider name */ 75 80 'title' => esc_html( sprintf( __( '%s account', 'clicksyncr-for-kit' ), Provider::label() ) ), 76 'description' => '<p>' . esc_html__( 'The key is shared with every form builder ClickSyncr supports on this site.', 'clicksyncr-for-kit' ) . '</p>',77 81 'fields' => array( 78 82 array( 79 'name' => 'kit_ key_status',83 'name' => 'kit_per_form_keys', 80 84 'type' => 'html', 81 'html' => static fn (): string => self::key_status_html(), 82 ), 83 array( 84 'name' => 'kit_api_key', 85 'label' => esc_html( Provider::key_label() ), 86 'type' => 'text', 87 'input_type' => 'password', 88 'class' => 'medium', 89 'description' => '' !== Credentials::shared() 90 ? esc_html__( 'Paste a new key to replace the saved one, or leave this empty to keep it.', 'clicksyncr-for-kit' ) 91 /* translators: %s: provider name */ 92 : esc_html( sprintf( __( 'Create an API key in %s, then paste it here.', 'clicksyncr-for-kit' ), Provider::label() ) ), 93 ), 94 array( 95 'name' => 'kit_disconnect', 96 'label' => esc_html__( 'Disconnect', 'clicksyncr-for-kit' ), 97 'type' => 'checkbox', 98 'choices' => array( 99 array( 100 'name' => 'kit_disconnect', 101 /* translators: %s: provider name */ 102 'label' => esc_html( sprintf( __( 'Remove the saved %s key from this site', 'clicksyncr-for-kit' ), Provider::label() ) ), 103 ), 104 ), 85 'html' => '<p>' . esc_html( Provider::per_form_line() ) . '</p>', 105 86 ), 106 87 ), 107 88 ), 108 89 ); 109 }110 111 private static function key_status_html(): string {112 $shared = Credentials::shared();113 if ( '' === $shared ) {114 return '';115 }116 117 return '<p>' . esc_html(118 sprintf(119 /* translators: %s: the last four characters of the saved key */120 __( 'A key ending in %s is saved.', 'clicksyncr-for-kit' ),121 '••••' . substr( $shared, -4 )122 )123 ) . '</p>';124 90 } 125 91 … … 132 98 return $this->get_plugin_settings(); 133 99 } 134 $settings = (array) $settings; 135 $submitted = Val::str( rgar( $settings, 'kit_api_key' ) ); 136 $disconnect = ! empty( rgar( $settings, 'kit_disconnect' ) ); 137 $client = Provider::CLIENT; 138 139 if ( $disconnect ) { 140 Credentials::store_shared( '', false, 'gravity' ); 141 } elseif ( '' !== $submitted && class_exists( $client ) && ! empty( ( new $client( $submitted ) )->test()['success'] ) ) { 142 Credentials::store_shared( $submitted, true, 'gravity' ); 143 } 144 100 $settings = (array) $settings; 145 101 $settings['kit_api_key'] = ''; 146 102 unset( $settings['kit_disconnect'] ); … … 173 129 */ 174 130 public function can_create_feed() { 175 return '' !== Credentials::shared();131 return true; 176 132 } 177 133 … … 195 151 public function describe( int $form_id, array $values ): array { 196 152 $adapter = new Gravity(); 197 $key = Credentials::shared();153 $key = $adapter->key( $form_id ); 198 154 199 155 return Panel::describe( … … 352 308 353 309 $consent_field = Val::str( rgars( (array) $feed, 'meta/consent_field' ) ); 310 $consent_kind = '' === $consent_field ? '' : StatusResolver::kind( ( new Gravity() )->fields( Val::int( rgar( (array) $form, 'id' ) ) ), $consent_field ); 354 311 $answers = array(); 355 312 foreach ( (array) $entry as $entry_key => $entry_value ) { … … 359 316 } 360 317 361 SubscriberSync::push(362 Credentials::shared(),318 $result = SubscriberSync::push( 319 ( new Gravity() )->key( Val::int( rgar( (array) $form, 'id' ) ) ), 363 320 Val::str( rgars( (array) $feed, 'meta/kit_group' ) ), 364 321 $subscriber_data, … … 366 323 StatusPreference::from_setting( rgars( (array) $feed, 'meta/status_preference' ) ), 367 324 $consent_field, 368 '' === $consent_field ? '' : rgar( (array) $entry, $consent_field ) 325 self::answer( (array) $entry, $consent_field, $consent_kind ), 326 $consent_kind 369 327 ), 370 328 Builder::Gravity, … … 372 330 $answers 373 331 ); 332 if ( ! empty( $result['skipped'] ) ) { 333 return; 334 } 335 $succeeded = ! empty( $result['success'] ); 336 $text = $succeeded 337 /* translators: %s: provider name */ 338 ? sprintf( __( 'Added to %s by ClickSyncr', 'clicksyncr-for-kit' ), Provider::label() ) 339 /* translators: %s: provider name */ 340 : Val::str_or( Val::str( $result['message'] ?? '' ), sprintf( __( '%s sync failed', 'clicksyncr-for-kit' ), Provider::label() ) ); 341 $this->add_note( Val::int( rgar( (array) $entry, 'id' ) ), $text, $succeeded ? 'success' : 'error' ); 374 342 } 375 343 … … 389 357 public function get_column_value_kit_group( $feed ): string { 390 358 $group_id = Val::str( rgars( (array) $feed, 'meta/kit_group' ) ); 391 $options = Catalog::group_options( Credentials::shared() );359 $options = Catalog::group_options( ( new Gravity() )->key( Val::int( rgar( (array) $feed, 'form_id' ) ) ) ); 392 360 393 361 return Val::str( $options[ $group_id ] ?? $group_id ); 394 362 } 363 364 /** 365 * @param array<array-key,mixed> $entry 366 * @return mixed 367 */ 368 public static function answer( array $entry, string $field_id, string $kind ) { 369 if ( '' === $field_id ) { 370 return ''; 371 } 372 $value = $entry[ $field_id ] ?? ''; 373 if ( '' !== Val::flat( $value ) || false !== strpos( $field_id, '.' ) ) { 374 return $value; 375 } 376 if ( 'consent' === $kind ) { 377 return $entry[ $field_id . '.1' ] ?? ''; 378 } 379 $picked = array(); 380 foreach ( $entry as $key => $item ) { 381 if ( strncmp( (string) $key, $field_id . '.', strlen( $field_id ) + 1 ) === 0 ) { 382 $picked[] = $item; 383 } 384 } 385 386 return $picked; 387 } 395 388 } -
clicksyncr-for-kit/trunk/includes/src/Builders/NinjaAction.php
r3726852 r3760857 14 14 use ClickSyncr\Kit\Core\Brand; 15 15 use ClickSyncr\Kit\Core\Catalog; 16 use ClickSyncr\Kit\Core\Dispatch; 16 17 use ClickSyncr\Kit\Core\Edition; 17 18 use ClickSyncr\Kit\Core\Panel; … … 135 136 } 136 137 137 SubscriberSync::push( 138 $api_key, 139 Val::str( $action_settings[ NinjaForms::S . 'group_id' ] ?? '' ), 140 $subscriber_data, 141 self::consent_status( $action_settings, Val::int( $form_id ), $answers ), 142 Builder::NinjaForms, 143 Val::int( $form_id ), 144 $answers 138 Dispatch::later( 139 array( SubscriberSync::class, 'push' ), 140 array( 141 $api_key, 142 Val::str( $action_settings[ NinjaForms::S . 'group_id' ] ?? '' ), 143 $subscriber_data, 144 self::consent_status( $action_settings, Val::int( $form_id ), $answers ), 145 Builder::NinjaForms, 146 Val::int( $form_id ), 147 $answers, 148 ) 145 149 ); 146 150 … … 173 177 StatusPreference::from_setting( $settings[ NinjaForms::S . 'status_preference' ] ?? '' ), 174 178 $field, 175 $answers[ $field ] ?? '' 179 $answers[ $field ] ?? '', 180 StatusResolver::kind( ( new NinjaForms() )->fields( $form_id ), $field ) 176 181 ); 177 182 } -
clicksyncr-for-kit/trunk/includes/src/Builders/NinjaForms.php
r3726852 r3760857 29 29 public const ACTION = 'clicksyncr_kit'; 30 30 public const SETTING = 'clicksyncr_kit_api_key'; 31 public const DISCONNECT = 'clicksyncr_kit_disconnect';32 31 public const S = 'cskit_'; 33 32 … … 37 36 38 37 public function register(): void { 39 add_filter( 'ninja_forms_plugin_settings_groups', array( $this, 'settings_group' ) );40 add_filter( 'ninja_forms_plugin_settings', array( $this, 'settings_fields' ) );41 add_filter( 'ninja_forms_update_setting_' . self::SETTING, array( $this, 'store_setting' ) );42 add_filter( 'ninja_forms_update_setting_' . self::DISCONNECT, array( $this, 'disconnect' ) );43 38 44 39 add_action( 'init', array( $this, 'register_action' ), 0 ); … … 92 87 93 88 94 /** 95 * @param mixed $value 96 */ 97 public function store_setting( $value ): string { 98 if ( ! \ClickSyncr\Kit\Core\Access::credentials() ) { 99 return ''; 100 } 101 $api_key = Val::str( $value ); 102 $client = Provider::CLIENT; 103 if ( '' !== $api_key && class_exists( $client ) && ! empty( ( new $client( $api_key ) )->test()['success'] ) ) { 104 Credentials::store_shared( $api_key, true, 'ninjaforms' ); 105 } 106 107 return ''; 108 } 109 110 /** 111 * @param mixed $value 112 */ 113 public function disconnect( $value ): string { 114 if ( ! \ClickSyncr\Kit\Core\Access::credentials() ) { 115 return ''; 116 } 117 if ( ! empty( $value ) ) { 118 Credentials::store_shared( '', false, 'ninjaforms' ); 119 } 120 121 return ''; 122 } 123 124 private static function migrate_legacy_setting(): void { 89 public static function migrate_legacy_setting(): void { 125 90 if ( ! function_exists( 'Ninja_Forms' ) ) { 126 91 return; … … 136 101 } 137 102 138 /**139 * @return array<mixed>140 * @param mixed $groups141 */142 public function settings_group( $groups ): array {143 $groups = Val::arr( $groups );144 $groups[ self::ACTION ] = array(145 'id' => self::ACTION,146 'label' => __( 'ClickSyncr for Kit', 'clicksyncr-for-kit' ),147 );148 149 return $groups;150 }151 152 /**153 * @return array<mixed>154 * @param mixed $settings155 */156 public function settings_fields( $settings ): array {157 $settings = Val::arr( $settings );158 self::migrate_legacy_setting();159 $shared = Credentials::shared();160 161 $settings[ self::ACTION ] = array(162 self::SETTING => array(163 'id' => self::SETTING,164 'type' => 'textbox',165 'label' => Provider::key_label(),166 'desc' => '' !== $shared167 ? sprintf(168 /* translators: %s: the last four characters of the saved key */169 __( 'A key ending in %s is saved. Paste a new key to replace it, or leave this empty to keep it.', 'clicksyncr-for-kit' ),170 '••••' . substr( $shared, -4 )171 )172 /* translators: %s: provider name */173 : sprintf( __( 'Paste your %s API key to sync Ninja Forms submissions.', 'clicksyncr-for-kit' ), Provider::label() ),174 ),175 );176 if ( '' !== $shared ) {177 $settings[ self::ACTION ][ self::DISCONNECT ] = array(178 'id' => self::DISCONNECT,179 'type' => 'checkbox',180 /* translators: %s: provider name */181 'label' => sprintf( __( 'Remove the saved %s key from this site', 'clicksyncr-for-kit' ), Provider::label() ),182 );183 }184 185 return $settings;186 }187 188 103 public function key( int $form_id ): string { 189 unset( $form_id ); 190 $setting = function_exists( 'Ninja_Forms' ) ? Val::str( Ninja_Forms()->get_setting( self::SETTING ) ) : ''; 191 192 return Credentials::resolve( $setting ); 104 return Credentials::resolve( $this->own_key( $form_id ) ); 105 } 106 107 public function own_key( int $form_id ): string { 108 return Credentials::stored( Builder::NinjaForms, $form_id ); 109 } 110 111 /** 112 * @return list<int> 113 */ 114 public function configured_forms(): array { 115 global $wpdb; 116 if ( ! $wpdb instanceof \wpdb ) { 117 return array(); 118 } 119 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one read of the Ninja Forms actions table during the key upgrade. 120 $ids = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT parent_id FROM %i WHERE type = %s', $wpdb->prefix . 'nf3_actions', self::ACTION ) ); 121 122 return array_values( array_filter( array_map( array( Val::class, 'int' ), is_array( $ids ) ? $ids : array() ) ) ); 193 123 } 194 124 … … 197 127 return false; 198 128 } 199 unset( $form_id ); 200 Credentials::store_shared( $key, true, 'ninjaforms' ); 201 202 return true; 129 130 return Credentials::store_form( Builder::NinjaForms, $form_id, $key ); 203 131 } 204 132 -
clicksyncr-for-kit/trunk/includes/src/Builders/WPForms.php
r3726852 r3760857 196 196 if ( class_exists( $client ) && ! empty( ( new $client( $typed ) )->test()['success'] ) ) { 197 197 $settings[ self::FLAT . 'api_key' ] = $typed; 198 if ( '' === Credentials::shared() ) {199 Credentials::store_shared( $typed, true, 'wpforms' );200 }201 198 } 202 199 } … … 255 252 } 256 253 257 $decoded = is_array( $data ) && array() !== $data 258 ? $data 259 : Val::arr( json_decode( wp_unslash( Val::str( $form['post_content'] ?? '' ) ), true ) ); 254 $decoded = Val::arr( json_decode( wp_unslash( Val::str( $form['post_content'] ?? '' ) ), true ) ); 255 if ( array() === $decoded && is_array( $data ) ) { 256 $decoded = $data; 257 } 260 258 if ( array() === $decoded ) { 261 259 return $form; … … 340 338 341 339 public function key( int $form_id ): string { 342 $own = Val::str( $this->values( $form_id )['api_key'] ?? '' ); 343 344 return '' !== $own ? $own : Credentials::shared(); 340 return Credentials::resolve( $this->own_key( $form_id ) ); 341 } 342 343 public function own_key( int $form_id ): string { 344 return trim( Val::str( $this->values( $form_id )['api_key'] ?? '' ) ); 345 } 346 347 /** 348 * @return list<int> 349 */ 350 public function configured_forms(): array { 351 $out = array(); 352 $posts = get_posts( 353 array( 354 'post_type' => 'wpforms', 355 'post_status' => 'any', 356 'numberposts' => -1, 357 'fields' => 'ids', 358 ) 359 ); 360 foreach ( $posts as $form_id ) { 361 if ( '' !== Val::str( $this->values( Val::int( $form_id ) )['group_id'] ?? '' ) ) { 362 $out[] = Val::int( $form_id ); 363 } 364 } 365 366 return $out; 345 367 } 346 368 -
clicksyncr-for-kit/trunk/includes/src/CF7/SubmissionHandler.php
r3726852 r3760857 13 13 use ClickSyncr\Kit\Core\Builder; 14 14 use ClickSyncr\Kit\Core\Credentials; 15 use ClickSyncr\Kit\Core\Dispatch; 15 16 use ClickSyncr\Kit\Core\Settings; 16 17 use ClickSyncr\Kit\Core\StatusPreference; … … 48 49 $posted_data = Val::arr( $submission->get_posted_data() ); 49 50 50 SubscriberSync::push( 51 Credentials::resolve( Val::str( Settings::get( $form_id, 'api_key' ) ) ), 52 Val::str( Settings::get( $form_id, 'group_id' ) ), 53 FieldMapper::map_fields( $posted_data, $field_map ), 54 StatusResolver::resolve( 55 StatusPreference::from_setting( Settings::get( $form_id, 'status_preference' ) ), 56 Val::str( Settings::get( $form_id, 'consent_field', '' ) ), 57 $posted_data 58 ), 59 Builder::CF7, 60 $form_id, 61 $posted_data 51 Dispatch::later( 52 array( SubscriberSync::class, 'push' ), 53 array( 54 Credentials::resolve( Val::str( Settings::get( $form_id, 'api_key' ) ) ), 55 Val::str( Settings::get( $form_id, 'group_id' ) ), 56 FieldMapper::map_fields( $posted_data, $field_map ), 57 StatusResolver::resolve( 58 StatusPreference::from_setting( Settings::get( $form_id, 'status_preference' ) ), 59 Val::str( Settings::get( $form_id, 'consent_field', '' ) ), 60 $posted_data, 61 StatusResolver::kind( ( new \ClickSyncr\Kit\Builders\CF7() )->fields( $form_id ), Val::str( Settings::get( $form_id, 'consent_field', '' ) ) ) 62 ), 63 Builder::CF7, 64 $form_id, 65 $posted_data, 66 ) 62 67 ); 63 68 } -
clicksyncr-for-kit/trunk/includes/src/Core/Access.php
r3726852 r3760857 65 65 return Current::allowed( '' === $builder ? 'cf7' : $builder ); 66 66 } 67 if ( '/advanced/sharing' === $route ) { 68 $builder = Val::str( $request->get_param( 'builder' ) ); 69 return class_exists( \ClickSyncr\Kit\Signals\Sharing::class ) && \ClickSyncr\Kit\Signals\Sharing::allowed( '' === $builder ? 'cf7' : $builder ); 70 } 67 71 if ( '/notices/act' === $route ) { 68 72 return current_user_can( 'manage_options' ); … … 178 182 return 'wpforms' === get_post_type( $form_id ); 179 183 case 'fluentforms': 180 return class_exists( '\FluentForm\App\ Models\Form' ) && null !== \FluentForm\App\Models\Form::find( $form_id );184 return class_exists( '\FluentForm\App\Api\Form' ) && null !== ( new \FluentForm\App\Api\Form() )->find( $form_id ); 181 185 case 'gravity': 182 186 $form = \GFAPI::get_form( $form_id ); -
clicksyncr-for-kit/trunk/includes/src/Core/Advanced.php
r3726852 r3760857 10 10 11 11 namespace ClickSyncr\Kit\Core; 12 13 use ClickSyncr\Kit\Signals\Sharing; 12 14 13 15 if ( ! defined( 'ABSPATH' ) ) { … … 308 310 } 309 311 $host = Val::str( wp_parse_url( $url, PHP_URL_HOST ) ); 310 if ( '' === self::API_HOST || false === stripos( $host, self::API_HOST) ) {312 if ( ! ApiHost::allows( $url ) ) { 311 313 return; 312 314 } … … 389 391 $row( 'Connector', 'clicksyncr-for-kit ' . ( defined( 'SYNCR_KIT_VER' ) ? SYNCR_KIT_VER : '' ) ); 390 392 $row( 'Pro', Edition::pro_installed() ? Edition::pro_version() . ( Edition::pro_active() ? ' (active)' : ' (installed, not active)' ) : 'not installed' ); 391 $row( 'Key stored', '' !== Credentials::shared());393 $row( 'Keys', Credentials::per_form() ? 'one per form' : 'site key until an administrator opens the dashboard' ); 392 394 $row( 'Debug log', self::logging() ? 'on until ' . gmdate( 'Y-m-d H:i', self::settings()['debug_until'] ) . ' UTC' : 'off' ); 393 395 $row( 'Site log', '' === self::site_log_path() ? 'none' : basename( self::site_log_path() ) ); … … 401 403 $start = microtime( true ); 402 404 $probe = wp_remote_head( 403 'https://' . self::API_HOST. '/',405 ApiHost::base() . '/', 404 406 array( 405 407 'timeout' => 5, … … 407 409 ) 408 410 ); 409 $row( 'Host', self::API_HOST);410 $row( 'Answer', is_wp_error( $probe ) || ! is_array( $probe ) ? 'error: ' . ( is_object( $probe ) && method_exists( $probe, 'get_error_message' ) ? (string) $probe->get_error_message() : 'no response') : 'HTTP ' . wp_remote_retrieve_response_code( $probe ) . ' in ' . (int) round( ( microtime( true ) - $start ) * 1000 ) . ' ms' );411 $row( 'Host', Val::str( wp_parse_url( ApiHost::base(), PHP_URL_HOST ) ) ); 412 $row( 'Answer', $probe instanceof \WP_Error ? 'error: ' . $probe->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code( $probe ) . ' in ' . (int) round( ( microtime( true ) - $start ) * 1000 ) . ' ms' ); 411 413 412 414 $section( 'Form builders' ); … … 496 498 ) 497 499 ); 500 if ( class_exists( Sharing::class ) ) { 501 register_rest_route( 502 self::REST_NAMESPACE, 503 '/advanced/sharing', 504 array( 505 'methods' => 'POST', 506 'callback' => array( Sharing::class, 'save' ), 507 'permission_callback' => array( self::class, 'permitted' ), 508 ) 509 ); 510 } 498 511 register_rest_route( 499 512 self::REST_NAMESPACE, … … 607 620 'success' => true, 608 621 'html' => self::stats_html( $builder, $form_id ), 622 'ready' => self::test_ready( $builder, $form_id ), 609 623 ) 610 624 ); … … 673 687 array( 674 688 'success' => false, 675 /* translators: %s: list noun. */ 676 'message' => sprintf( __( 'Save an API key and a %s for this form first.', 'clicksyncr-for-kit' ), Provider::destination() ), 689 'message' => self::not_ready_message(), 677 690 ), 678 691 400 … … 708 721 remove_action( 'clicksyncr_kit_synced', $observe, 1 ); 709 722 710 $reason = trim( Val::str( $result['message'] ?? '' ));723 $reason = rtrim( trim( Val::str( $result['message'] ?? '' ) ), '.' ); 711 724 if ( '' === $reason ) { 712 725 $reason = __( 'Nothing was sent', 'clicksyncr-for-kit' ); … … 715 728 return new \WP_REST_Response( 716 729 array( 717 'success' => ! empty( $result['success'] ) ,730 'success' => ! empty( $result['success'] ) || ! empty( $result['skipped'] ), 718 731 'skipped' => ! empty( $result['skipped'] ), 719 732 'sent' => self::scalars( $sent ), … … 749 762 ) . '</p>' 750 763 . '<div class="syncr-adv__viewer">' 751 . '<div class="syncr-adv__tabs" role="tablist">'752 . '<button type="button" class="button button-small is-active" data-syncr-log="own">' . esc_html__( 'Plugin log', 'clicksyncr-for-kit' ) . '</button> '753 . '<button type="button" class="button button-small" data-syncr-log="site">' . esc_html__( 'Site errors', 'clicksyncr-for-kit' ) . '</button> '764 . '<div class="syncr-adv__tabs">' 765 . '<button type="button" class="button button-small is-active" aria-pressed="true" data-syncr-log="own">' . esc_html__( 'Plugin log', 'clicksyncr-for-kit' ) . '</button> ' 766 . '<button type="button" class="button button-small" aria-pressed="false" data-syncr-log="site">' . esc_html__( 'Site errors', 'clicksyncr-for-kit' ) . '</button> ' 754 767 . '<label class="syncr-adv__auto"><input type="checkbox" id="' . $id( 'log-auto' ) . '" /> ' . esc_html__( 'Auto-refresh', 'clicksyncr-for-kit' ) . '</label>' 755 768 . '</div>' … … 790 803 791 804 $current_html = Current::html( $builder ); 805 $sharing_html = class_exists( Sharing::class ) ? Sharing::html( $builder ) : ''; 792 806 793 807 $rows = array_merge( … … 802 816 Panel::row( 'adv_reset', __( 'Reset this form', 'clicksyncr-for-kit' ), sprintf( __( 'This form only. Forgets the %1$s key, %2$s, mapping, consent and status saved here, and its counters. The form itself is untouched.', 'clicksyncr-for-kit' ), 'Kit', Provider::destination() ), 'html', 'adv_reset', '', array( 'html' => $reset_html ) ), 803 817 ...( '' === $current_html ? array() : array( Panel::row( 'adv_current', __( 'New versions', 'clicksyncr-for-kit' ), '', 'html', 'adv_current', '', array( 'html' => $current_html ) ) ) ), 818 ...( '' === $sharing_html ? array() : array( Panel::row( 'adv_sharing', __( 'Help Us Improve', 'clicksyncr-for-kit' ), '', 'html', 'adv_sharing', '', array( 'html' => $sharing_html ) ) ) ), 804 819 Panel::row( 'adv_purge', __( 'On uninstall', 'clicksyncr-for-kit' ), $site, 'html', 'adv_purge', '', array( 'html' => $purge_html ) ), 805 820 ) … … 932 947 } 933 948 949 public static function test_ready( string $builder, int $form_id ): bool { 950 if ( $form_id <= 0 || null === Builder::tryFrom( $builder ) ) { 951 return false; 952 } 953 $class = Builder::adapter( $builder ); 954 if ( ! class_exists( $class ) ) { 955 return false; 956 } 957 /** 958 * @var \ClickSyncr\Kit\Builders\Adapter $adapter 959 */ 960 $adapter = new $class(); 961 962 return '' !== $adapter->key( $form_id ) && '' !== Val::str( $adapter->values( $form_id )['group_id'] ?? '' ); 963 } 964 965 public static function not_ready_message(): string { 966 /* translators: %s: list noun. */ 967 return sprintf( __( 'Save an API key and a %s for this form first.', 'clicksyncr-for-kit' ), Provider::destination() ); 968 } 969 934 970 /** @return array<array-key,mixed> */ 935 public static function test_row(): array { 936 $id = 'clicksyncr-for-kit-adv-test'; 937 $html = '<div class="syncr-tool-field"><div><label for="' . $id . '-email">' . esc_html__( 'Send a test to', 'clicksyncr-for-kit' ) . '</label>' 971 public static function test_row( string $builder = '', int $form_id = 0 ): array { 972 $id = 'clicksyncr-for-kit-adv-test'; 973 $ready = self::test_ready( $builder, $form_id ); 974 $html = '<div class="syncr-tool-field"><div><label for="' . $id . '-email">' . esc_html__( 'Send a test to', 'clicksyncr-for-kit' ) . '</label>' 938 975 . '<input type="email" id="' . $id . '-email" class="regular-text" placeholder="you@example.com" autocomplete="off" /></div>' 939 . '<div class="syncr-actions"><button type="button" class="button button-primary" id="' . $id . '" >' . esc_html__( 'Send test', 'clicksyncr-for-kit' ) . '</button></div></div>'940 . '<div class="syncr-tool-results"><span id="' . $id . '-feedback" aria-live="polite"> </span></div>';976 . '<div class="syncr-actions"><button type="button" class="button button-primary" id="' . $id . '"' . ( $ready ? '' : ' disabled' ) . '>' . esc_html__( 'Send test', 'clicksyncr-for-kit' ) . '</button></div></div>' 977 . '<div class="syncr-tool-results"><span id="' . $id . '-feedback" aria-live="polite">' . ( $ready ? '' : esc_html( self::not_ready_message() ) ) . '</span></div>'; 941 978 942 979 return Panel::row( 'adv_test', __( 'Send a test', 'clicksyncr-for-kit' ), __( 'A real subscriber, added through this form\'s saved settings, so you see exactly what a visitor gets.', 'clicksyncr-for-kit' ), 'html', 'adv_test', '', array( 'html' => $html ) ); -
clicksyncr-for-kit/trunk/includes/src/Core/Context.php
r3726852 r3760857 259 259 return $subscriber; 260 260 } 261 $parts = preg_split( '/\s+/', $full, 2 ); 261 $parts = preg_split( '/\s+/', $full, 2 ); 262 if ( false === $parts ) { 263 return $subscriber; 264 } 262 265 $subscriber[ $first ] = $parts[0]; 263 266 $subscriber[ $last ] = $parts[1] ?? ''; … … 407 410 return $message; 408 411 } 409 $plain = class_exists( '\\WPForms\\Emails\\Helpers' ) && method_exists( '\\WPForms\\Emails\\Helpers', 'is_plain_text_template' ) &&\WPForms\Emails\Helpers::is_plain_text_template( Val::str( $template ) );412 $plain = class_exists( '\\WPForms\\Emails\\Helpers' ) && \WPForms\Emails\Helpers::is_plain_text_template( Val::str( $template ) ); 410 413 411 414 return $message . self::block( Builder::WPForms, $form_id, ! $plain ); -
clicksyncr-for-kit/trunk/includes/src/Core/Credentials.php
r3726852 r3760857 13 13 private const OPTION = '_clicksyncr_kit_settings'; 14 14 15 public const SCHEMA = 'syncr_kit_key_schema'; 16 17 private const LOCK = 'syncr_kit_key_upgrade'; 18 19 private const FORM = 'syncr_kit_key_'; 20 21 public static function boot(): void { 22 add_action( 'admin_init', array( self::class, 'upgrade' ) ); 23 } 24 15 25 public static function shared(): string { 16 26 $settings = Val::arr( get_option( self::OPTION ) ); 17 27 return Val::str( $settings['apiKey'] ?? '' ); 28 } 29 30 public static function per_form(): bool { 31 return 2 <= Val::int( get_option( self::SCHEMA, 0 ) ); 18 32 } 19 33 … … 25 39 } 26 40 } 27 return self::shared(); 41 return self::per_form() ? '' : self::shared(); 42 } 43 44 public static function stored( string $builder, int $form_id ): string { 45 return $form_id > 0 ? trim( Val::str( get_option( self::name( $builder, $form_id ), '' ) ) ) : ''; 46 } 47 48 public static function store_form( string $builder, int $form_id, string $key ): bool { 49 if ( $form_id <= 0 || ! current_user_can( 'manage_options' ) ) { 50 return false; 51 } 52 $name = self::name( $builder, $form_id ); 53 $key = sanitize_text_field( $key ); 54 if ( '' === $key ) { 55 delete_option( $name ); 56 } else { 57 update_option( $name, $key, false ); 58 } 59 60 return self::stored( $builder, $form_id ) === $key; 61 } 62 63 public static function upgrade(): void { 64 if ( self::per_form() || ! current_user_can( 'manage_options' ) ) { 65 return; 66 } 67 if ( ! add_option( self::LOCK, (string) time(), '', false ) ) { 68 if ( time() - Val::int( get_option( self::LOCK, 0 ) ) < 300 ) { 69 return; 70 } 71 update_option( self::LOCK, (string) time(), false ); 72 } 73 if ( class_exists( \ClickSyncr\Kit\Builders\NinjaForms::class ) ) { 74 \ClickSyncr\Kit\Builders\NinjaForms::migrate_legacy_setting(); 75 } 76 if ( class_exists( '\\GFForms' ) && class_exists( \ClickSyncr\Kit\Builders\GravityFeed::class ) ) { 77 \ClickSyncr\Kit\Builders\GravityFeed::get_instance()->get_plugin_settings(); 78 } 79 $shared = self::shared(); 80 if ( '' !== $shared ) { 81 foreach ( Builder::adapters() as $adapter ) { 82 foreach ( $adapter->configured_forms() as $form_id ) { 83 if ( '' === $adapter->own_key( $form_id ) ) { 84 $adapter->store_key( $form_id, $shared ); 85 } 86 } 87 } 88 } 89 update_option( self::SCHEMA, 2, false ); 90 delete_option( self::LOCK ); 28 91 } 29 92 … … 41 104 ); 42 105 } 106 107 private static function name( string $builder, int $form_id ): string { 108 return self::FORM . sanitize_key( $builder ) . '_' . $form_id; 109 } 43 110 } -
clicksyncr-for-kit/trunk/includes/src/Core/Panel.php
r3726852 r3760857 197 197 /* translators: 1: record noun, 2: provider name */ 198 198 'intent' => sprintf( __( 'Inspect a %1$s at %2$s or run an occasional task without leaving this screen.', 'clicksyncr-for-kit' ), $r, $label ), 199 'rows' => array( Advanced::test_row( ) ),199 'rows' => array( Advanced::test_row( $builder, $form_id ) ), 200 200 ), 201 201 array( -
clicksyncr-for-kit/trunk/includes/src/Core/Provider.php
r3726852 r3760857 34 34 public static function label(): string { 35 35 return 'Kit'; 36 } 37 38 public static function per_form_line(): string { 39 /* translators: %s: provider name */ 40 return sprintf( __( 'Each form has its own %s API key. Enter it in the ClickSyncr panel of the form.', 'clicksyncr-for-kit' ), self::label() ); 36 41 } 37 42 -
clicksyncr-for-kit/trunk/includes/src/Core/StatusResolver.php
r3726852 r3760857 15 15 } 16 16 17 final class StatusResolver { 17 18 18 final class StatusResolver { 19 private const YES = array( '1', 'true', 'on', 'yes', 'checked' ); 20 21 private const NO = array( '0', 'false', 'off', 'no', 'unchecked', 'not checked' ); 22 23 private const BOXES = array( 'acceptance', 'checkbox', 'gdpr-checkbox', 'gdprcheckbox', 'consent', 'listcheckbox', 'input_checkbox', 'terms_and_condition', 'gdpr_agreement' ); 19 24 20 25 /** … … 24 29 string $preference, 25 30 string $consent_field, 26 array $posted_data 31 array $posted_data, 32 string $kind = '' 27 33 ): ?string { 28 return self::resolve_value( $preference, $consent_field, $posted_data[ $consent_field ] ?? '' );34 return self::resolve_value( $preference, $consent_field, $posted_data[ $consent_field ] ?? '', $kind ); 29 35 } 30 36 … … 35 41 string $preference, 36 42 string $consent_field, 37 $consent_value 43 $consent_value, 44 string $kind = '' 38 45 ): ?string { 39 46 if ( '' === $consent_field ) { … … 41 48 } 42 49 43 return self::consent_given( $consent_value )50 return self::consent_given( $consent_value, $kind ) 44 51 ? SubscriberStatus::Active 45 52 : SubscriberStatus::Unconfirmed; … … 47 54 48 55 /** 56 * @param array<array-key,mixed> $fields 57 */ 58 public static function kind( array $fields, string $consent_field ): string { 59 return Val::str( Val::arr( $fields[ $consent_field ] ?? null )['kind'] ?? '' ); 60 } 61 62 /** 49 63 * @param mixed $raw 50 64 */ 51 private static function consent_given( $raw ): bool { 52 $raw = Val::flat( $raw ); 65 public static function consent_given( $raw, string $kind = '' ): bool { 66 $answers = is_array( $raw ) ? $raw : array( $raw ); 67 foreach ( $answers as $answer ) { 68 if ( ! is_scalar( $answer ) ) { 69 continue; 70 } 71 $answer = strtolower( trim( (string) $answer ) ); 72 if ( '' === $answer || in_array( $answer, self::NO, true ) ) { 73 continue; 74 } 75 if ( in_array( $answer, self::YES, true ) || in_array( $kind, self::BOXES, true ) ) { 76 return true; 77 } 78 } 53 79 54 return '' !== $raw && false !== filter_var( $raw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );80 return false; 55 81 } 56 82 } -
clicksyncr-for-kit/trunk/includes/src/Core/SubscriberSync.php
r3726852 r3760857 21 21 /** 22 22 * @param array<string,string> $subscriber_data 23 * @param array<array-key,mixed> $form_data The builder's posted values, when it has them. 23 24 * @return array<array-key,mixed> 24 25 */ … … 64 65 $email = $subscriber_data['email'] ?? ''; 65 66 66 if ( '' === $api_key || '' === $group_id || ! is_email( $email ) ) { 67 if ( '' === $api_key ) { 68 $result = array( 69 'success' => false, 70 'skipped' => true, 71 'message' => 'Nothing was sent. This form has no API key.', 72 ); 73 } elseif ( '' === $group_id || ! is_email( $email ) ) { 67 74 $result = array( 68 75 'success' => false, -
clicksyncr-for-kit/trunk/includes/src/Formidable/Handler.php
r3726852 r3760857 14 14 use ClickSyncr\Kit\Core\Catalog; 15 15 use ClickSyncr\Kit\Core\Credentials; 16 use ClickSyncr\Kit\Core\Dispatch; 16 17 use ClickSyncr\Kit\Core\StatusPreference; 17 18 use ClickSyncr\Kit\Core\StatusResolver; … … 58 59 $consent_field = Val::str( $settings['consent_field'] ?? '' ); 59 60 60 SubscriberSync::push( 61 $api_key, 62 Val::str( $settings['group_id'] ?? '' ), 63 $subscriber_data, 64 StatusResolver::resolve_value( 65 StatusPreference::from_setting( $settings['status_preference'] ?? '' ), 66 $consent_field, 67 self::field_value( $entry, $consent_field ) 68 ), 69 Builder::Formidable, 70 Val::int( $entry_vars['form_id'] ?? 0 ), 71 self::answers( $entry_vars ) 61 Dispatch::later( 62 array( SubscriberSync::class, 'push' ), 63 array( 64 $api_key, 65 Val::str( $settings['group_id'] ?? '' ), 66 $subscriber_data, 67 StatusResolver::resolve_value( 68 StatusPreference::from_setting( $settings['status_preference'] ?? '' ), 69 $consent_field, 70 self::field_value( $entry, $consent_field ), 71 StatusResolver::kind( ( new \ClickSyncr\Kit\Builders\Formidable() )->fields( Val::int( $entry_vars['form_id'] ?? 0 ) ), $consent_field ) 72 ), 73 Builder::Formidable, 74 Val::int( $entry_vars['form_id'] ?? 0 ), 75 self::answers( $entry_vars ), 76 ) 72 77 ); 73 78 } -
clicksyncr-for-kit/trunk/includes/src/Forminator/FormHooks.php
r3726852 r3760857 12 12 use ClickSyncr\Kit\Core\Builder; 13 13 use ClickSyncr\Kit\Core\Catalog; 14 use ClickSyncr\Kit\Core\Dispatch; 14 15 use ClickSyncr\Kit\Core\Credentials; 15 16 use ClickSyncr\Kit\Core\StatusPreference; … … 37 38 */ 38 39 public function add_entry_fields( $submitted_data, $current_entry_fields = array(), $entry = null ) { 39 unset( $current_entry_fields , $entry);40 unset( $current_entry_fields ); 40 41 41 42 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Forminator validates the submission before invoking this hook. 42 43 $submitted_data = Submission::normalize( Val::arr( $submitted_data ), wp_unslash( $_POST ) ); 43 $api_key = Credentials::shared();44 $api_key = ( new \ClickSyncr\Kit\Builders\Forminator() )->key( (int) $this->module_id ); 44 45 $settings = Val::arr( $this->settings_instance instanceof Forminator_Integration_Settings ? $this->settings_instance->get_settings_values() : array() ); 45 46 … … 58 59 $group_id = Val::str( Forminator::values_from( $settings )['group_id'] ); 59 60 60 $result = SubscriberSync::push( 61 $api_key, 62 $group_id, 63 $subscriber_data, 64 StatusResolver::resolve_value( 65 StatusPreference::from_setting( $settings['status_preference'] ?? '' ), 66 $consent_field, 67 '' === $consent_field ? '' : ( $submitted_data[ $consent_field ] ?? '' ) 68 ), 69 Builder::Forminator, 70 Val::int( $this->module_id ), 71 $submitted_data 61 Dispatch::later( 62 array( self::class, 'deliver' ), 63 array( 64 $api_key, 65 $group_id, 66 $subscriber_data, 67 StatusResolver::resolve_value( 68 StatusPreference::from_setting( $settings['status_preference'] ?? '' ), 69 $consent_field, 70 '' === $consent_field ? '' : ( $submitted_data[ $consent_field ] ?? '' ), 71 StatusResolver::kind( ( new \ClickSyncr\Kit\Builders\Forminator() )->fields( (int) $this->module_id ), $consent_field ) 72 ), 73 Val::int( $this->module_id ), 74 $submitted_data, 75 $entry, 76 ) 72 77 ); 78 79 return array(); 80 } 81 82 /** 83 * @param array<string,string> $subscriber_data 84 * @param array<array-key,mixed> $submitted_data 85 * @param mixed $entry 86 */ 87 public static function deliver( string $api_key, string $group_id, array $subscriber_data, ?string $status, int $form_id, array $submitted_data, $entry ): void { 88 $result = SubscriberSync::push( $api_key, $group_id, $subscriber_data, $status, Builder::Forminator, $form_id, $submitted_data ); 89 if ( ! empty( $result['skipped'] ) || ! is_object( $entry ) || ! method_exists( $entry, 'set_fields' ) ) { 90 return; 91 } 92 $rows = array(); 93 foreach ( self::entry_fields( $result, Val::str( Catalog::group_options( $api_key )[ $group_id ] ?? '' ) ) as $row ) { 94 $rows[] = array( 95 'name' => 'forminator_addon_clicksyncrkit_' . $row['name'], 96 'value' => $row['value'], 97 ); 98 } 99 $entry->set_fields( $rows ); 100 } 101 102 /** 103 * @param array<array-key,mixed> $result 104 * @return list<array{name:string,value:string}> 105 */ 106 public static function entry_fields( array $result, string $group_name ): array { 107 if ( ! empty( $result['skipped'] ) ) { 108 return array(); 109 } 73 110 74 111 return array( … … 80 117 'name' => 'description', 81 118 'value' => empty( $result['success'] ) 82 ? Val::str_or( $result['message'] ?? '', esc_html__( 'Kit sync failed', 'clicksyncr-for-kit' ) )83 : esc_html__( 'Added to Kit ', 'clicksyncr-for-kit' ),119 ? Val::str_or( Val::str( $result['message'] ?? '' ), esc_html__( 'Kit sync failed', 'clicksyncr-for-kit' ) ) 120 : esc_html__( 'Added to Kit by ClickSyncr', 'clicksyncr-for-kit' ), 84 121 ), 85 122 array( 86 123 'name' => 'group_name', 87 'value' => Val::str( Catalog::group_options( $api_key )[ $group_id ] ?? '' ),124 'value' => $group_name, 88 125 ), 89 126 ); -
clicksyncr-for-kit/trunk/includes/src/Forminator/Integration.php
r3726852 r3760857 9 9 declare(strict_types=1); 10 10 11 use ClickSyncr\Kit\API\KitClient;12 11 use ClickSyncr\Kit\Core\Assets; 13 use ClickSyncr\Kit\Core\ Credentials;12 use ClickSyncr\Kit\Core\Provider; 14 13 use ClickSyncr\Kit\Core\Val; 15 14 … … 80 79 */ 81 80 public function is_authorized() { 82 return '' !== Credentials::shared();81 return array() !== Val::arr( $this->get_settings_values() ); 83 82 } 84 83 … … 109 108 110 109 $submitted_data = Val::arr( $submitted_data ); 111 $error = ''; 110 if ( isset( $submitted_data['activate'] ) ) { 111 $this->save_settings_values( array( 'per_form_keys' => true ) ); 112 112 113 if ( isset( $submitted_data['api_key'] ) ) { 114 $api_key = Val::str( $submitted_data['api_key'] ); 113 if ( ! forminator_addon_is_active( $this->get_slug() ) ) { 114 Forminator_Integration_Loader::get_instance()->activate_addon( $this->get_slug() ); 115 } 115 116 116 if ( '' === $api_key ) { 117 $error = esc_html__( 'Please add a Kit API key.', 'clicksyncr-for-kit' ); 118 } elseif ( empty( ( new KitClient( $api_key ) )->test()['success'] ) ) { 119 $error = esc_html__( 'Invalid API key. Please check and try again.', 'clicksyncr-for-kit' ); 120 } else { 121 Credentials::store_shared( $api_key, true, 'forminator' ); 122 $this->save_settings_values( array( 'api_key' => $api_key ) ); 117 $this->adopt_orphaned_form_settings(); 123 118 124 if ( ! forminator_addon_is_active( $this->get_slug() ) ) { 125 Forminator_Integration_Loader::get_instance()->activate_addon( $this->get_slug() ); 126 } 127 128 $this->adopt_orphaned_form_settings(); 129 130 return array( 131 'html' => $this->success_authorize(), 132 'redirect' => false, 133 'has_errors' => false, 134 'notification' => array( 135 'type' => 'success', 136 'text' => '<strong>' . $this->get_title() . '</strong> ' . esc_html__( 'is connected successfully.', 'clicksyncr-for-kit' ), 137 ), 138 ); 139 } 119 return array( 120 'html' => $this->success_authorize(), 121 'redirect' => false, 122 'has_errors' => false, 123 'notification' => array( 124 'type' => 'success', 125 'text' => '<strong>' . $this->get_title() . '</strong> ' . esc_html__( 'is ready. Add your API key on each form.', 'clicksyncr-for-kit' ), 126 ), 127 ); 140 128 } 141 142 $buttons = array(143 'submit' => array(144 'markup' => self::get_button_markup(145 $this->is_connected()146 ? esc_html__( 'Save', 'clicksyncr-for-kit' )147 : esc_html__( 'Connect', 'clicksyncr-for-kit' ),148 'forminator-addon-connect'149 ),150 ),151 );152 129 153 130 $html = '<div class="forminator-integration-popup__header">'; 154 131 $html .= '<h3 class="sui-box-title sui-lg">' . esc_html__( 'Configure ClickSyncr for Kit', 'clicksyncr-for-kit' ) . '</h3>'; 155 $html .= '<p>' . esc_html __( 'The key is shared with every form builder ClickSyncr supports on this site.', 'clicksyncr-for-kit') . '</p>';132 $html .= '<p>' . esc_html( Provider::per_form_line() ) . '</p>'; 156 133 $html .= '</div>'; 157 $html .= '<form>'; 158 $html .= '<div class="sui-form-field' . ( '' !== $error ? ' sui-form-field-error' : '' ) . '">'; 159 $html .= '<label class="sui-label">' . esc_html__( 'Kit API Key', 'clicksyncr-for-kit' ) . '</label>'; 160 $html .= '<input name="api_key" value="' . esc_attr( Credentials::shared() ) . '" placeholder="' . esc_attr__( 'Enter your Kit API key', 'clicksyncr-for-kit' ) . '" class="sui-form-control" />'; 161 $html .= '' !== $error ? '<span class="sui-error-message">' . esc_html( $error ) . '</span>' : ''; 162 $html .= '</div>'; 163 $html .= '</form>'; 134 $html .= '<form><input type="hidden" name="activate" value="1" /></form>'; 164 135 165 136 return array( 166 137 'html' => $html, 167 138 'redirect' => false, 168 'buttons' => $buttons, 169 'has_errors' => '' !== $error, 139 'buttons' => array( 140 'submit' => array( 141 'markup' => self::get_button_markup( esc_html__( 'Activate', 'clicksyncr-for-kit' ), 'forminator-addon-connect' ), 142 ), 143 ), 144 'has_errors' => false, 170 145 'size' => 'small', 171 146 ); -
clicksyncr-for-kit/trunk/includes/src/REST/Connection.php
r3726852 r3760857 6 6 7 7 use ClickSyncr\Kit\API\KitClient; 8 use ClickSyncr\Kit\Core\Credentials;9 8 use ClickSyncr\Kit\Core\Settings; 10 9 use ClickSyncr\Kit\Core\Val; … … 28 27 $api_key = sanitize_text_field( $api_key ); 29 28 $client = new KitClient( $api_key ); 30 $verified = $client->test();29 $verified = Val::arr( apply_filters( 'clicksyncr_kit_credential_validation', $client->test(), $api_key, $params ) ); 31 30 if ( empty( $verified['success'] ) ) { 32 31 return new \WP_REST_Response( … … 58 57 if ( $groups_response['success'] ) { 59 58 $groups = Val::arr( $groups_response['data']['data'] ?? null ); 60 61 if ( '' === Credentials::shared() ) {62 $builder = Val::str( $request->get_param( 'builder' ) );63 Credentials::store_shared( $api_key, true, '' !== $builder ? $builder : 'cf7' );64 }65 59 66 60 $first_group_id = Val::str( Val::arr( $groups[0] ?? null )['id'] ?? '' ); -
clicksyncr-for-kit/trunk/includes/src/Signals/Sharing.php
r3726852 r3760857 9 9 namespace ClickSyncr\Kit\Signals; 10 10 11 use ClickSyncr\Kit\Core\Access; 12 use ClickSyncr\Kit\Core\Val; 11 13 use Signls\Sdk\V1\Consent; 12 14 use Signls\Sdk\V1\Runtime; … … 26 28 ) 27 29 ); 28 add_action( 'admin_menu', array( self::class, 'menu' ) ); 29 add_action( 'admin_post_syncr_kit_signal_choice', array( self::class, 'save' ) ); 30 add_action( 'init', array( self::class, 'default_on' ) ); 30 31 add_filter( 'http_request_args', array( self::class, 'request_metadata' ), 10, 2 ); 31 32 add_action( … … 58 59 } 59 60 60 public static function menu(): void {61 add_options_page( __( 'ClickSyncr signal sharing', 'clicksyncr-for-kit' ), __( 'ClickSyncr for Kit signal sharing', 'clicksyncr-for-kit' ), 'manage_options', 'clicksyncr-for-kit-signals', array( self::class, 'screen' ) );62 }63 64 61 public static function status(): string { 62 if ( ! class_exists( Consent::class, false ) ) { 63 return 'unset'; 64 } 65 65 return ( new Consent( new StateStore( 'clicksyncr-for-kit' ) ) )->status(); 66 66 } 67 67 68 public static function screen(): void { 69 if ( ! current_user_can( 'manage_options' ) ) { 70 return; 71 } 72 ?> 73 <div class="wrap"> 74 <h1><?php esc_html_e( 'ClickSyncr signal sharing', 'clicksyncr-for-kit' ); ?></h1> 75 <p><?php esc_html_e( 'Help improve ClickSyncr by sharing an optional diagnostic report with Signls at signls.dev. It includes software versions, aggregate form and mapping counts, delivery totals, and whether ClickSyncr Pro is active. Random installation and device identifiers link reports. No contact or submission values, API keys, license keys, site URLs, or admin email addresses are shared.', 'clicksyncr-for-kit' ); ?></p> 76 <p><?php esc_html_e( 'Sharing is off until you enable it. You can turn it off here at any time. Turning it off cancels scheduled reports and removes the local device credential.', 'clicksyncr-for-kit' ); ?></p> 77 <p><a href="https://clicksyncr.com/external-services"><?php esc_html_e( 'External services and privacy details', 'clicksyncr-for-kit' ); ?></a></p> 78 <p id="syncr-signal-status"><?php echo esc_html( 'enabled' === self::status() ? __( 'Sharing enabled', 'clicksyncr-for-kit' ) : __( 'Sharing off', 'clicksyncr-for-kit' ) ); ?></p> 79 <form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post"> 80 <input type="hidden" name="action" value="syncr_kit_signal_choice"> 81 <?php wp_nonce_field( 'syncr_kit_signal_choice' ); ?> 82 <button class="button button-primary" name="choice" value="enable" type="submit"><?php esc_html_e( 'Enable sharing', 'clicksyncr-for-kit' ); ?></button> 83 <button class="button" name="choice" value="disable" type="submit"><?php esc_html_e( 'Keep sharing off', 'clicksyncr-for-kit' ); ?></button> 84 </form> 85 </div> 86 <?php 68 public static function enabled(): bool { 69 return 'disabled' !== self::status(); 87 70 } 88 71 89 public static function save(): void {90 if ( ! current_user_can( 'manage_options') ) {91 wp_die( esc_html__( 'You cannot change signal sharing.', 'clicksyncr-for-kit' ), '', array( 'response' => 403 ));72 public static function default_on(): void { 73 if ( class_exists( Runtime::class, false ) && 'unset' === self::status() ) { 74 Runtime::enable( 'clicksyncr-for-kit', 'network_install_default', 1 ); 92 75 } 93 check_admin_referer( 'syncr_kit_signal_choice' ); 94 $choice = isset( $_POST['choice'] ) && is_string( $_POST['choice'] ) ? sanitize_key( wp_unslash( $_POST['choice'] ) ) : ''; 95 if ( ! in_array( $choice, array( 'enable', 'disable' ), true ) ) { 96 wp_die( esc_html__( 'Choose whether to enable sharing.', 'clicksyncr-for-kit' ), '', array( 'response' => 400 ) ); 76 } 77 78 public static function allowed( string $builder ): bool { 79 return Access::account( $builder ); 80 } 81 82 public static function html( string $builder ): string { 83 if ( ! class_exists( Runtime::class, false ) || ! self::allowed( $builder ) ) { 84 return ''; 97 85 } 98 $saved = 'enable' === $choice ? Runtime::enable( 'clicksyncr-for-kit', 'settings', 1 ) : Runtime::disable( 'clicksyncr-for-kit', 'settings', 1 ); 99 if ( ! $saved ) { 100 wp_die( esc_html__( 'Your choice could not be saved. Please try again.', 'clicksyncr-for-kit' ), '', array( 'response' => 503 ) ); 86 return '<label class="syncr-adv__toggle"><input type="checkbox" id="clicksyncr-for-kit-adv-sharing"' 87 . checked( self::enabled(), true, false ) . ' /> ' 88 . esc_html__( 'Help us improve the plugin.', 'clicksyncr-for-kit' ) . '</label> ' 89 . '<a href="https://clicksyncr.com/privacy" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Learn about it', 'clicksyncr-for-kit' ) . '</a>' 90 . '<span id="clicksyncr-for-kit-adv-sharing-feedback" aria-live="polite"></span>'; 91 } 92 93 /** @return \WP_REST_Response|\WP_Error */ 94 public static function save( \WP_REST_Request $request ) { 95 $builder = Val::str( $request->get_param( 'builder' ) ); 96 $builder = '' === $builder ? 'cf7' : $builder; 97 $nonce = Val::str( $request->get_header( 'X-WP-Nonce' ) ); 98 if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) || ! self::allowed( $builder ) ) { 99 return new \WP_Error( 'clicksyncr_forbidden', __( 'You do not have permission to perform this action.', 'clicksyncr-for-kit' ), array( 'status' => 403 ) ); 101 100 } 102 wp_safe_redirect( admin_url( 'options-general.php?page=clicksyncr-for-kit-signals' ) ); 103 exit; 101 $body = $request->get_json_params(); 102 if ( ! is_array( $body ) || ! array_key_exists( 'enabled', $body ) || ! is_bool( $body['enabled'] ) ) { 103 return new \WP_Error( 'clicksyncr_invalid_choice', __( 'Choose on or off.', 'clicksyncr-for-kit' ), array( 'status' => 400 ) ); 104 } 105 $saved = $body['enabled'] ? Runtime::enable( 'clicksyncr-for-kit', 'settings', 1 ) : Runtime::disable( 'clicksyncr-for-kit', 'settings', 1 ); 106 $success = $saved && self::enabled() === $body['enabled']; 107 return new \WP_REST_Response( 108 array( 109 'success' => $success, 110 'enabled' => self::enabled(), 111 ), 112 $success ? 200 : 503 113 ); 104 114 } 105 115 } -
clicksyncr-for-kit/trunk/includes/src/WPForms/SubmissionHandler.php
r3726852 r3760857 13 13 use ClickSyncr\Kit\Core\Builder; 14 14 use ClickSyncr\Kit\Core\Credentials; 15 use ClickSyncr\Kit\Core\Dispatch; 15 16 use ClickSyncr\Kit\Core\StatusPreference; 16 17 use ClickSyncr\Kit\Core\StatusResolver; … … 26 27 private const MAP_PREFIX = 'syncr_kit_map_'; 27 28 29 public const TASK = 'clicksyncr_kit_wpforms_send'; 30 28 31 public function __construct() { 29 32 add_action( 'wpforms_process_complete', array( $this, 'handle_submission' ), 10, 4 ); 33 add_action( self::TASK, array( self::class, 'run_task' ) ); 30 34 } 31 35 … … 61 65 $consent_field = Val::str( $settings['syncr_kit_consent_field'] ?? '' ); 62 66 67 $form_id = Val::int( $form_data['id'] ?? 0 ); 68 $status = StatusResolver::resolve_value( 69 StatusPreference::from_setting( $settings['syncr_kit_status_preference'] ?? '' ), 70 $consent_field, 71 Val::arr( $fields[ $consent_field ] ?? null )['value'] ?? '', 72 Val::str( Val::arr( Val::arr( $form_data['fields'] ?? null )[ $consent_field ] ?? null )['type'] ?? '' ) 73 ); 74 $answers = self::answers( $fields ); 75 $tasks = function_exists( 'wpforms' ) ? wpforms()->obj( 'tasks' ) : null; 76 if ( is_object( $tasks ) && method_exists( $tasks, 'create' ) && null !== $tasks->create( self::TASK )->async()->params( $form_id, $subscriber_data, $status, $answers )->register() ) { 77 return; 78 } 79 Dispatch::later( array( self::class, 'send' ), array( $form_id, $subscriber_data, $status, $answers ) ); 80 } 81 82 /** 83 * @param mixed $meta_id 84 */ 85 public static function run_task( $meta_id ): void { 86 if ( ! class_exists( '\\WPForms\\Tasks\\Meta' ) ) { 87 return; 88 } 89 $meta = ( new \WPForms\Tasks\Meta() )->get( Val::int( $meta_id ) ); 90 $args = is_object( $meta ) ? Val::arr( $meta->data ?? null ) : array(); 91 if ( 4 !== count( $args ) ) { 92 return; 93 } 94 $status = $args[2] ?? null; 95 self::send( Val::int( $args[0] ?? 0 ), Val::str_map( $args[1] ?? null ), is_string( $status ) ? $status : null, Val::arr( $args[3] ?? null ) ); 96 } 97 98 /** 99 * @param array<string,string> $subscriber_data 100 * @param array<array-key,mixed> $answers 101 */ 102 public static function send( int $form_id, array $subscriber_data, ?string $status, array $answers ): void { 103 $handler = function_exists( 'wpforms' ) ? wpforms()->obj( 'form' ) : null; 104 $args = array( 105 'content_only' => true, 106 'cap' => false, 107 ); 108 $form_data = is_object( $handler ) && method_exists( $handler, 'get' ) ? Val::arr( $handler->get( $form_id, $args ) ) : array(); 109 $settings = Val::arr( $form_data['settings'] ?? null ); 110 63 111 SubscriberSync::push( 64 112 Credentials::resolve( Val::str( $settings['syncr_kit_api_key'] ?? '' ) ), 65 113 Val::str( $settings['syncr_kit_group_id'] ?? '' ), 66 114 $subscriber_data, 67 StatusResolver::resolve_value( 68 StatusPreference::from_setting( $settings['syncr_kit_status_preference'] ?? '' ), 69 $consent_field, 70 Val::arr( $fields[ $consent_field ] ?? null )['value'] ?? '' 71 ), 115 $status, 72 116 Builder::WPForms, 73 Val::int( $form_data['id'] ?? 0 ),74 self::answers( $fields )117 $form_id, 118 $answers 75 119 ); 76 120 } -
clicksyncr-for-kit/trunk/readme.txt
r3726852 r3760857 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 26.10. 03.137 Stable tag: 26.10.22.23 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 34 34 * **Your Kit forms load automatically**: choose which [Kit form](https://clicksyncr.com/integrations/kit) new subscribers join, right inside the form editor. 35 35 * **Map any field**: send first names and your Kit custom fields straight from your form. 36 * **Built-in opt-in**: add a [consent checkbox](https://clicksyncr.com/help) and ClickSyncr adds subscribers to your Kit form when they tick it, or saves them as a subscriber only when they don't.36 * **Built-in opt-in**: add a [consent checkbox](https://clicksyncr.com/help) and ClickSyncr adds the subscriber when a visitor ticks it, and sends nothing at all when they leave it unticked. 37 37 * **Subscriber status control**: add to the Kit form or save as a subscriber only, per form. 38 38 * **Honest double opt-in**: your Kit form's own setting decides whether Kit sends its confirmation email, and the plugin says so instead of pretending. … … 69 69 == External services == 70 70 71 Optional signal sharing sends software versions, aggregate configuration and delivery counts, and random installation/device identifiers to Signls only after an administrator enables it in this plugin's signal-sharing settings. It excludes contact/submission values and credentials. You can turn sharing off at any time. See [external services and privacy details](https://clicksyncr.com/external-services).71 Diagnostic sharing is on by default and can be turned off in the Advanced section of any connected form. It sends software versions, aggregate configuration and delivery counts, and random installation/device identifiers to Signls. It excludes contact/submission values and credentials. See [privacy details](https://clicksyncr.com/privacy). 72 72 73 73 This plugin connects to Kit. When you connect, refresh or open a configured form's settings, it authenticates with your API key and requests the available lists, fields and account settings. A configured form submission sends the email address and the fields you map to your selected destination. A test submission sends the test address and configured fields. Subscription and consent settings determine whether a submission is sent. … … 126 126 = How does the opt-in consent field work? = 127 127 128 Pick a consent field (a checkbox, acceptance, or radio) from your form. When a visitor ticks it, the y are added to the Kit form you selected; if they leave it unticked, they are saved in Kit as a subscriber only, without joining the form, so no form emails or automations fire. Leave it as "None" to use the subscriber status you chose for the form.128 Pick a consent field (a checkbox, acceptance, or radio) from your form. When a visitor ticks it, the subscriber is sent to Kit; if they leave it unticked, nothing is sent at all, so nobody is added without agreeing. Leave it as "None" to use the subscriber status you chose for the form. 129 129 130 130 = Does it work with Kit double opt-in? = … … 146 146 == Screenshots == 147 147 148 1. The ClickSyncr for Kit – Contact Form 7, WPForms, Gravity Forms, Ninja Forms, Fluent Forms, Formidable & Forminator panel in the Contact Form 7 editor: connect your Kit API key, choose a Kit form, map form fields to Kit subscriberfields, and set an opt-in consent field.148 1. The ClickSyncr for Kit – Contact Form 7, WPForms, Gravity Forms, Ninja Forms, Fluent Forms, Formidable & Forminator panel in the Contact Form 7 editor: connect your Kit API key, choose a form, map form fields to Kit fields, and set an opt-in consent field. 149 149 150 150 == Changelog == 151 151 152 = 26.10.22.23 = 153 154 [Version 26.10.22.23 release notes](https://clicksyncr.com/integrations/kit#26.10.22.23) 155 152 156 For the full changelog with details, see the [release history](https://clicksyncr.com/integrations/kit#changelog). 157 158 = 26.10.18.15 = 159 160 [Version 26.10.18.15 release notes](https://clicksyncr.com/integrations/kit#26.10.18.15) 153 161 154 162 = 26.10.03.13 =
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)