From 585736965d9d3d941d8849073e6135b5f0152de4 Mon Sep 17 00:00:00 2001 From: Spencer Finnell Date: Fri, 6 Oct 2023 11:26:32 +0200 Subject: [PATCH 1/6] Sync from Pro --- includes/core/abstracts/abstract-form.php | 11 +- includes/core/assets/css/admin/admin.scss | 10 + includes/core/assets/css/simpay-admin.min.css | 2 +- includes/core/class-rest-api.php | 3 +- includes/core/functions/admin.php | 39 -- includes/core/functions/shared.php | 40 ++ includes/core/recaptcha/index.php | 4 + .../core/settings/class-setting-input.php | 9 + includes/core/settings/class-setting.php | 5 +- includes/core/settings/register-general.php | 6 +- includes/core/utils/abstract-collection.php | 57 +-- .../migrations/class-single-migration.php | 6 + .../action-scheduler/action-scheduler.php | 17 +- .../action-scheduler/changelog.txt | 74 +++ .../classes/ActionScheduler_ActionFactory.php | 283 ++++++++--- .../classes/ActionScheduler_AdminView.php | 100 +++- .../classes/ActionScheduler_Compatibility.php | 18 +- .../classes/ActionScheduler_DateTime.php | 3 + .../classes/ActionScheduler_Exception.php | 2 +- .../classes/ActionScheduler_ListTable.php | 35 +- .../classes/ActionScheduler_OptionLock.php | 90 +++- .../classes/ActionScheduler_QueueCleaner.php | 127 ++++- .../classes/ActionScheduler_QueueRunner.php | 62 ++- .../ActionScheduler_WPCLI_Clean_Command.php | 125 +++++ .../ActionScheduler_WPCLI_QueueRunner.php | 2 +- ...ctionScheduler_WPCLI_Scheduler_command.php | 80 +++- .../classes/abstracts/ActionScheduler.php | 45 +- .../ActionScheduler_Abstract_ListTable.php | 22 +- .../ActionScheduler_Abstract_QueueRunner.php | 178 ++++++- .../ActionScheduler_Abstract_Schema.php | 31 +- .../abstracts/ActionScheduler_Lock.php | 2 + .../abstracts/ActionScheduler_Logger.php | 2 +- .../abstracts/ActionScheduler_Store.php | 36 +- .../actions/ActionScheduler_Action.php | 62 ++- .../data-stores/ActionScheduler_DBStore.php | 444 +++++++++++++++--- .../ActionScheduler_wpPostStore.php | 26 +- .../classes/migration/Runner.php | 2 +- .../ActionScheduler_NullSchedule.php | 3 + .../schema/ActionScheduler_LoggerSchema.php | 14 +- .../schema/ActionScheduler_StoreSchema.php | 25 +- .../action-scheduler/functions.php | 285 ++++++++--- .../action-scheduler/lib/WP_Async_Request.php | 27 +- lib/woocommerce/action-scheduler/readme.txt | 80 +++- src/Admin/AdminServiceProvider.php | 7 + src/Admin/Education/InstantPayouts.php | 2 +- .../Education/PluginCustomersSettings.php | 4 +- src/Admin/Education/PluginTaxesSettings.php | 2 +- src/Admin/FormBuilder/LicenseCheck.php | 11 +- src/Admin/SetupWizard/SetupWizardLaunch.php | 2 +- .../SiteHealth/SiteHealthDebugInformation.php | 67 ++- src/Admin/UpeNotification.php | 139 ++++++ src/AdminNotice/LicenseExpiredNotice.php | 28 +- src/AdminNotice/LicenseMissingNotice.php | 6 +- .../CustomerSuccessServiceProvider.php | 11 + .../TelemetryData/AbstractTelemetryData.php | 30 ++ .../AbstractTransactionTelemetryData.php | 81 ++++ .../CustomerJourneyTelemetryData.php | 55 +++ .../EnvironmentTelemetryData.php | 112 +++++ .../IntegrationTelemetryData.php | 58 +++ .../PaymentFormTelemetryData.php | 161 +++++++ .../TelemetryData/PluginTelemetryData.php | 165 +++++++ .../TelemetryData/StatTelemetryData.php | 221 +++++++++ .../TransactionTelemetryData.php | 52 ++ src/CustomerSuccess/TelemetrySubscriber.php | 163 +++++++ src/License/LicenseNotificationSubscriber.php | 22 +- src/PaymentPage/PaymentPageOutput.php | 61 +-- .../Payment/Traits/PaymentIntentTrait.php | 6 +- .../Payment/Traits/SubscriptionTrait.php | 7 +- src/StripeConnect/ApplicationFee.php | 21 +- src/StripeConnect/ConnectionSubscriber.php | 8 +- src/Webhook/EndpointHealthCheck.php | 10 +- vendor/autoload.php | 20 +- vendor/composer/ClassLoader.php | 146 +----- vendor/composer/InstalledVersions.php | 44 +- vendor/composer/autoload_classmap.php | 2 +- vendor/composer/autoload_namespaces.php | 2 +- vendor/composer/autoload_psr4.php | 2 +- vendor/composer/autoload_real.php | 33 +- vendor/composer/autoload_static.php | 8 +- vendor/composer/installed.json | 14 +- vendor/composer/installed.php | 26 +- views/admin-notice-expired-license.php | 14 +- views/admin-payment-forms-license-expired.php | 15 +- 83 files changed, 3585 insertions(+), 747 deletions(-) create mode 100644 lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php create mode 100644 src/Admin/UpeNotification.php create mode 100644 src/CustomerSuccess/TelemetryData/AbstractTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/AbstractTransactionTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/CustomerJourneyTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/EnvironmentTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/IntegrationTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/PaymentFormTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/PluginTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/StatTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetryData/TransactionTelemetryData.php create mode 100644 src/CustomerSuccess/TelemetrySubscriber.php diff --git a/includes/core/abstracts/abstract-form.php b/includes/core/abstracts/abstract-form.php index f6d78a0d..b6225463 100644 --- a/includes/core/abstracts/abstract-form.php +++ b/includes/core/abstracts/abstract-form.php @@ -21,6 +21,12 @@ */ abstract class Form { + /** + * @since 3.0.0 + * @var \WP_Post + */ + public $post; + /** * Test mode. * @@ -156,7 +162,7 @@ abstract class Form { * @since 3.0.0 * @var string */ - public $decimal_separator = ''; + public $decimal_separator = ''; /** * Thousand separator. @@ -196,7 +202,7 @@ abstract class Form { * @since 3.0.0 * @var string */ - public $item_description = ''; + public $item_description = ''; /** * Stripe Checkout: Image URL (form logo). @@ -776,7 +782,6 @@ public function get_stripe_script_variables() { * * @since 4.7.0 * - * @param \SimplePay\Core\Abstracts\Form $form The payment form. * @return bool */ abstract public function has_fee_recovery(); diff --git a/includes/core/assets/css/admin/admin.scss b/includes/core/assets/css/admin/admin.scss index ba81a554..d451c22b 100644 --- a/includes/core/assets/css/admin/admin.scss +++ b/includes/core/assets/css/admin/admin.scss @@ -157,6 +157,7 @@ display: flex; align-items: center; margin-bottom: 8px; + position: relative; .spinner { float: none; @@ -2414,3 +2415,12 @@ body.post-type-simple-pay #wpbody-content { } } } + +.simpay-settings-is_upe td { + background: #fff; + border: 1px solid #c3c4c7; + border-left-width: 4px; + border-left-color: #2271b1; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + padding: 12px 18px; +} diff --git a/includes/core/assets/css/simpay-admin.min.css b/includes/core/assets/css/simpay-admin.min.css index c12055c4..23ca52f3 100644 --- a/includes/core/assets/css/simpay-admin.min.css +++ b/includes/core/assets/css/simpay-admin.min.css @@ -1 +1 @@ -.simpay-settings-subsections{display:flex;align-items:center;box-shadow:inset 0 -1px 0 0 #ccc}.simpay-settings-subsections__subsection{font-weight:500;text-decoration:none;padding:15px;display:flex;align-items:center}.simpay-settings-subsections__subsection .dashicons{width:18px;height:18px;font-size:18px;margin-right:4px}.simpay-settings-subsections__subsection.is-active{box-shadow:inset 0 -4px 0 0 currentColor;position:relative;z-index:1}.simpay-settings-subsections__subsection:not(.is-active){color:#23282d}.simpay-settings form>h2:not(.nav-tab-wrapper){clip:rect(1px, 1px, 1px, 1px);height:1px;overflow:hidden;position:absolute !important;width:1px}.simpay-settings .form-table td fieldset+p,.simpay-settings .form-table td label+p,.simpay-settings .form-table td select+p,.simpay-settings .form-table td input+p{color:#666;font-style:italic}.simpay-settings .simpay-settings-subsections__subsection{display:flex;align-items:center}.simpay-settings .simpay-settings-subsections__subsection .simpay-settings-bubble{margin-left:5px}.simpay-settings .simpay-settings-visual-toggles{margin:30px 0 0;display:flex}.simpay-settings .simpay-settings-visual-toggles input[type=radio]{display:none}.simpay-settings .simpay-settings-visual-toggles__toggle{-webkit-user-select:none;-moz-user-select:none;user-select:none;min-width:180px;margin:0 30px 0 0 !important;position:relative;display:block;background-color:#fff;border-radius:4px;border:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04)}.simpay-settings .simpay-settings-visual-toggles__toggle:hover{border:1px solid #999;box-shadow:0 1px 2px rgba(0,0,0,.1)}.simpay-settings .simpay-settings-visual-toggles input[type=radio]:checked+.simpay-settings-visual-toggles__toggle{border-color:#007cba;border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px #007cba;box-shadow:0 0 0 1px var(--wp-admin-theme-color)}.simpay-settings .simpay-settings-visual-toggles__toggle-recommended,.simpay-settings .simpay-settings-visual-toggles__toggle-not-recommended{text-align:center;font-size:12px;text-transform:uppercase;font-weight:bold;margin:0;padding:5px 0;display:block;border-top-right-radius:4px;border-top-left-radius:4px}.simpay-settings .simpay-settings-visual-toggles__toggle-recommended{color:#0f8569;background:#f4f9f7}.simpay-settings .simpay-settings-visual-toggles__toggle-not-recommended{color:#b91c1b;background:#fef2f2}.simpay-settings .simpay-settings-visual-toggles__toggle-icon{margin:20px auto 15px;padding:0 15px;display:block}.simpay-settings .simpay-settings-visual-toggles__toggle-label{line-height:1.5;text-align:center;font-size:16px;font-weight:500;margin:15px;display:block}.simpay-settings .simpay-settings-visual-toggles__toggle-label small{color:#666;font-weight:normal;font-size:13px;line-height:1;display:block;margin:4px 0}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type .simpay-settings-visual-toggles__toggle{min-height:160px}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type .simpay-settings-visual-toggles__toggle-icon{width:80px;height:80px}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type input[type=radio].simpay-settings-captcha-type--is-recommended:checked+.simpay-settings-visual-toggles__toggle{border-color:#0f8569;box-shadow:0 0 0 1px #0f8569}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type input[type=radio].simpay-settings-captcha-type--is-not-recommended:checked+.simpay-settings-visual-toggles__toggle{border-color:#b91c1b;box-shadow:0 0 0 1px #b91c1b}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type label[for=simpay-settings-captcha-type-cloudflare-turnstile] .simpay-settings-visual-toggles__toggle-icon{width:120px}.simpay-settings-general-recaptcha-no_captcha_warning th,.simpay-settings-general-recaptcha-no_captcha_warning td,.simpay-settings-hcaptcha_secret_key th,.simpay-settings-hcaptcha_secret_key td,.simpay-settings-cloudflare_turnstile_secret_key th,.simpay-settings-cloudflare_turnstile_secret_key td,.simpay-settings-recaptcha_score_threshold th,.simpay-settings-recaptcha_score_threshold td{padding-bottom:50px !important}.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-summary-report,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-payment-confirmation,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-payment-notification,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-upcoming-invoice,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-invoice-confirmation{display:none}.simpay-settings .simpay-settings-subsection-emails-tools{margin-left:auto}.simpay-admin-charts-period-over-period{position:relative;padding-bottom:12px}.simpay-admin-charts-period-over-period *{box-sizing:border-box}.simpay-admin-charts-period-over-period__tooltip{position:absolute;background:#fff;border:1px solid #c3c3c3;box-shadow:0 2px 6px rgba(0,0,0,.05);border-radius:2px;padding:10px 14px;display:flex;flex-direction:column;z-Index:10000;min-width:175px}.simpay-admin-charts-period-over-period__tooltip-data{white-space:nowrap;margin-bottom:8px;display:grid;grid-template-columns:1fr auto;grid-auto-rows:auto;-moz-column-gap:16px;column-gap:16px}.simpay-admin-charts-period-over-period__tooltip-data:last-child{margin-bottom:0}.simpay-admin-charts-period-over-period__tooltip-data[data-dataset="1"]{opacity:.65}.simpay-admin-charts-period-over-period__tooltip-data em{font-style:normal;text-align:right}.simpay-admin-charts-period-over-period__tooltip-delta{font-size:12px;margin:0 -14px -10px;padding:8px 14px;border-top:1px solid #eee;background:#fdfdfd;border-radius:2px;display:flex;align-items:center;justify-content:center}.simpay-admin-charts-period-over-period__tooltip-delta .simpay-admin-charts-badge{margin-right:4px}.simpay-admin-charts-period-over-period__tooltip-delta strong.is-positive{color:#006908}.simpay-admin-charts-period-over-period__tooltip-delta strong.is-negative{color:#b3093c}.simpay-admin-charts-badge{color:#2f2f2f;font-size:12px;font-weight:500;font-style:normal;line-height:1;padding:3px 6px;display:inline-flex;align-items:center;background:#f0f0f0;border-radius:100px}.simpay-admin-charts-badge.is-positive{color:#006908;background-color:#d7f7c2}.simpay-admin-charts-badge.is-negative{color:#b3093c;background-color:#ffe7f2}.simpay-admin-charts-badge__icon{width:15px;height:15px}.simpay-admin-charts-no-data{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;background:rgba(255,255,255,.5);z-index:2}.simpay-admin-charts-no-data>div{text-align:center;padding:24px;background:#fff;border:1px solid #c3c3c3;box-shadow:0 2px 6px rgba(0,0,0,.1);border-radius:2px;max-width:60%}.simpay-admin-charts-no-data strong{font-size:15px;margin-bottom:8px;display:block}.button.button-large.simpay-button-large{font-size:14px;line-height:30px;padding:4px 12px}.simpay-copy-hidden-input{clip:rect(1px, 1px, 1px, 1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.fixed .column-livemode{width:15%;text-align:right}@media screen and (max-width: 782px){.fixed .column-livemode{text-align:left}}.fixed .column-livemode .simpay-badge{margin-top:3px}.fixed .column-shortcode{width:25%}.fixed .column-shortcode .simpay-shortcode{clip:rect(1px, 1px, 1px, 1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.post-type-simple-pay .misc-pub-curtime,.post-type-simple-pay .misc-pub-visibility{display:none}.simpay-metabox-title{border:1px solid #eee}.simpay-shortcode-section{border-top:1px solid #ddd;border-width:1px 0;padding-top:15px;padding-bottom:15px;position:relative}.simpay-shortcode-section label{display:block;margin-bottom:6px}.simpay-shortcode-section label .dashicons{color:#8c8f94;margin-right:3px}.simpay-shortcode-section .simpay-copy-button{line-height:normal;position:absolute;right:20px;bottom:20px;border:0;background:none;box-shadow:none;padding:0}.simpay-shortcode-section .simpay-copy-button:hover{border:0;background:none;box-shadow:none}.simpay-shortcode-section .simpay-copy-button .dashicons{color:#3c434a}.simpay-shortcode{width:100%;padding:8px;line-height:1;margin:0;height:32px;resize:none}.simpay-badge{color:#3f3f46;text-align:center;line-height:1;padding:5px 7px;border-radius:3px;background:#e4e4e7;border:0;box-shadow:none;display:inline-flex;align-items:center}button.simpay-badge{cursor:pointer}button.simpay-badge:hover{background:#d4d4d8}.simpay-badge__icon{opacity:.8;margin:2px 5px 0 0}.simpay-badge--green{color:#0e6245;background:#cbf4c9}.simpay-badge--yellow{color:#983705;background:#f8e5b9}.simpay-stripe-account-info{display:flex;align-items:center;margin-bottom:8px}.simpay-stripe-account-info .spinner{float:none;margin-top:0;margin-left:0}.simple-pay_page_simpay_settings .simpay-settings-upgrade,.post-type-simple-pay .simpay-settings-upgrade{margin-top:20px;padding:1px;position:relative;background:#fff;border-radius:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2),0 5px 10px rgba(0,0,0,.1);max-width:677px}.simple-pay_page_simpay_settings .simpay-settings-upgrade__inner,.post-type-simple-pay .simpay-settings-upgrade__inner{text-align:center;margin:0;padding:30px}.simple-pay_page_simpay_settings .simpay-settings-upgrade h3,.post-type-simple-pay .simpay-settings-upgrade h3{line-height:1.5;font-size:22px;margin:0 0 1.5rem}.simple-pay_page_simpay_settings .simpay-settings-upgrade ul,.post-type-simple-pay .simpay-settings-upgrade ul{margin:1.5rem 0 calc(1.5rem - 6px);display:flex;flex-wrap:wrap;justify-content:center}@media screen and (min-width: 782px){.simple-pay_page_simpay_settings .simpay-settings-upgrade ul,.post-type-simple-pay .simpay-settings-upgrade ul{margin-left:4rem;margin-right:4rem}}.simple-pay_page_simpay_settings .simpay-settings-upgrade li,.post-type-simple-pay .simpay-settings-upgrade li{font-size:15px;margin:6px 0;width:100%}.simple-pay_page_simpay_settings .simpay-settings-upgrade li a,.post-type-simple-pay .simpay-settings-upgrade li a{color:#3c434a;text-decoration:none}.simple-pay_page_simpay_settings .simpay-settings-upgrade li a:hover,.post-type-simple-pay .simpay-settings-upgrade li a:hover{color:var(--wp-admin-theme-color);text-decoration:underline}@media screen and (min-width: 782px){.simple-pay_page_simpay_settings .simpay-settings-upgrade li,.post-type-simple-pay .simpay-settings-upgrade li{text-align:left;width:50%}}.simple-pay_page_simpay_settings .simpay-settings-upgrade .button.button-large,.post-type-simple-pay .simpay-settings-upgrade .button.button-large{font-size:17px;line-height:30px;padding:10px 20px}.simple-pay_page_simpay_settings .simpay-settings-upgrade small,.post-type-simple-pay .simpay-settings-upgrade small{color:#666;margin:15px 0 0;display:block}.simple-pay_page_simpay_settings .simpay-settings-upgrade .dashicons-yes,.post-type-simple-pay .simpay-settings-upgrade .dashicons-yes{color:#428bca}.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link,.post-type-simple-pay .simpay-settings-upgrade .button-link{position:absolute;top:0;right:0;font-size:20px;color:#666;font-weight:bold;text-decoration:none;margin-left:5px;padding:6px 10px;z-index:2}.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link:hover,.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link:active,.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link:focus,.post-type-simple-pay .simpay-settings-upgrade .button-link:hover,.post-type-simple-pay .simpay-settings-upgrade .button-link:active,.post-type-simple-pay .simpay-settings-upgrade .button-link:focus{color:#666;text-decoration:none}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext{text-align:center;margin:0;padding:30px 20px 20px;background-color:#fcf9e8;border:1px solid #edeac9;border-width:1px 0 0;position:relative;border-radius:0;border-bottom-left-radius:4px;border-bottom-right-radius:4px}@media screen and (min-width: 782px){.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext{padding-left:4rem;padding-right:4rem}}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext svg,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext svg{background:#00a32a;fill:#fff;border-radius:50%;border:4px solid #fff;box-shadow:0 0 0 1px #edeac9;width:28px;height:28px;position:absolute;top:-18px;left:50%;margin-left:-18px}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext u,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext u{text-decoration:none;font-weight:bold;color:#00a32a}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext a,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext a{font-weight:normal;display:inline-block}#simpay-payment-form-settings table{width:100%;border-collapse:collapse}#simpay-payment-form-settings ::-webkit-input-placeholder{color:#9ba1a9}#simpay-payment-form-settings ::-moz-placeholder{color:#9ba1a9;opacity:1}#simpay-payment-form-settings :-ms-input-placeholder{color:#9ba1a9}#simpay-payment-form-settings .inside{margin:0;padding:0}#simpay-payment-form-settings .simpay-panel-field .toolbar{margin-bottom:-4px}#simpay-payment-form-settings .simpay-panel-field .toolbar .simpay-field-select{margin:0 0 4px;width:auto;max-width:70%}#simpay-payment-form-settings .simpay-tabs{margin:0;padding:0;list-style:none;background:#fafafa;border-right:1px solid #ccd0d4;line-height:1em;position:relative;flex:0 0 25%}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs{flex-basis:100%;flex-grow:1;border-right:0}}#simpay-payment-form-settings .simpay-tabs li{margin:0;padding:0}#simpay-payment-form-settings .simpay-tabs li:first-child{margin-top:12px}#simpay-payment-form-settings .simpay-tabs li:last-child{margin-bottom:20px}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs li:last-child{margin-bottom:0}}#simpay-payment-form-settings .simpay-tabs li.active{margin-left:-1px;box-shadow:0 1px 1px rgba(0,0,0,.04);position:relative}#simpay-payment-form-settings .simpay-tabs li.active:focus:after{display:none}#simpay-payment-form-settings .simpay-tabs li.active:before,#simpay-payment-form-settings .simpay-tabs li.active:after{content:"";width:calc(100% + 1px);height:1px;background:#ccd0d4;position:absolute;top:0;left:0;right:0;z-index:2}#simpay-payment-form-settings .simpay-tabs li.active:after{top:auto;bottom:0}#simpay-payment-form-settings .simpay-tabs li.active a{font-weight:bold;background-color:#fff;position:relative;margin-right:-1px}#simpay-payment-form-settings .simpay-tabs li.active a:before{content:"";position:absolute;top:0;left:0;bottom:0;width:4px;height:100%;background:currentColor;z-index:3}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs li.active a{margin-right:0}}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item{display:flex;align-items:center;line-height:20px;margin:0;padding:8px 10px 8px 14px;text-decoration:none;transition:all .05s ease-in-out}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item svg,#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item .dashicons{margin-right:6px}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item{padding:18px}}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item:focus{outline:0;position:relative;z-index:3;box-shadow:inset 0 0 0 1px currentColor,0 0 0 1px currentColor}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#purchase-restrictions-settings-panel"],#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#payment-page-settings-panel"]{margin-bottom:20px;position:relative}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#purchase-restrictions-settings-panel"]:after,#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#payment-page-settings-panel"]:after{content:"";position:absolute;left:14px;right:14px;bottom:-10px;width:calc(100% - 28px);height:1px;background:#eaeaea}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item span>span{color:#f18500;font-size:12px;font-weight:600;margin:0 0 0 5px;display:inline-block}#simpay-payment-form-settings .simpay-tabs li:not(.active) .simpay-tab-item{color:inherit}#simpay-payment-form-settings .simpay-panels-wrap{background:#fff;display:flex}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-panels-wrap{flex-direction:column}}#simpay-payment-form-settings .simpay-panels{flex:0 0 75%;display:flex}@media screen and (min-width: 1400px){#simpay-payment-form-settings .simpay-panels{flex-basis:75%}}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-panels{flex-basis:100%}}#simpay-payment-form-settings .simpay-panel,#simpay-payment-form-settings .simpay-panel-section{width:100%}#simpay-payment-form-settings .simpay-panel>table,#simpay-payment-form-settings .simpay-panel>table>tr,#simpay-payment-form-settings .simpay-panel>table>tbody,#simpay-payment-form-settings .simpay-panel>table>tbody>tr,#simpay-payment-form-settings .simpay-panel>table>thead,#simpay-payment-form-settings .simpay-panel>table>thead>tr{display:block;width:100%}#simpay-payment-form-settings .simpay-panel>table:last-child>tbody:last-child>tr:last-child>td{border-bottom:0}#simpay-payment-form-settings .simpay-panel-field--requires-upgrade{position:relative}#simpay-payment-form-settings .simpay-panel-field--requires-upgrade td>div{margin-right:80px}#simpay-payment-form-settings .simpay-panel-field--requires-upgrade td>div .button-small{position:absolute;top:calc(50% - 13px);right:18px}#simpay-payment-form-settings .simpay-panel-field,#simpay-payment-form-settings .simpay-panel-field>td,#simpay-payment-form-settings .simpay-panel-field>th{text-align:left;display:block}#simpay-payment-form-settings .simpay-panel-field>td,#simpay-payment-form-settings .simpay-panel-field>th{width:calc(100% - 36px);margin-left:18px;margin-right:18px}#simpay-payment-form-settings .simpay-panel-field th{font-weight:bold;padding-top:18px;padding-bottom:5px}#simpay-payment-form-settings .simpay-panel-field td{border-bottom:1px solid #ddd;padding-bottom:18px}#simpay-payment-form-settings .simpay-panel-field p.description{margin-top:4px}#simpay-payment-form-settings .simpay-panel-field p.description:last-of-type{margin-bottom:0}#simpay-payment-form-settings .simpay-panel-field .simpay-panel-field__nested{margin-top:18px}#simpay-payment-form-settings .simpay-panel-field .simpay-panel-field__nested label{font-weight:bold;display:block;margin-bottom:4px}#simpay-payment-form-settings .simpay-panel-field .simpay-field-select,#simpay-payment-form-settings .simpay-panel-field .simpay-field-text{min-width:75%;max-width:100%}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-panel-field .simpay-field-select,#simpay-payment-form-settings .simpay-panel-field .simpay-field-text{min-width:0;width:100%}}#simpay-payment-form-settings .simpay-panel-field .simpay-field-textarea{width:100%;max-width:100%}#simpay-payment-form-settings .simpay-panel-field .notice:last-of-type{margin-bottom:0}#simpay-payment-form-settings .simpay-panel-field .error,#simpay-payment-form-settings .simpay-panel-field .simpay-important{color:#a94442;font-weight:normal}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap{position:relative;margin-top:12px}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap .simpay-remove-image-preview{position:absolute;top:-15px;left:-15px;cursor:pointer;background-color:#fff}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap .simpay-remove-image-preview::before{font-size:22px;line-height:26px}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap .simpay-image-preview{max-height:128px;max-width:128px;border:1px solid #ddd}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box{background-color:#f4f4f4;border:1px solid #e5e5e5;padding:18px;margin-top:18px;position:relative}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box__dismiss{color:inherit;text-decoration:none;position:absolute;top:8px;right:8px}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box__dismiss .dashicons-dismiss{font-size:16px;width:16px;height:16px}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box h3{font-size:18px;font-weight:600;margin:0;padding:0}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box p{font-size:14px}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box p:last-child{margin-bottom:0}#simpay-payment-form-settings .simpay-metabox-content{margin-bottom:-1px;background-color:#f5f5f5;border:1px solid #c3c4c7;border-width:1px 0;box-shadow:0 1px 1px rgba(0,0,0,.04);position:relative}#simpay-payment-form-settings .simpay-show-if,#simpay-payment-form-settings .simpay-panel-hidden{display:none}#simpay-payment-form-settings .simpay-payment-methods{border:1px solid #ccd0d4;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,.04)}#simpay-payment-form-settings .simpay-panel-field-payment-method{display:block;border-top:1px solid #ccd0d4;padding:7px;box-sizing:border-box}#simpay-payment-form-settings .simpay-panel-field-payment-method:first-child{border-top:0;border-top-left-radius:4px;border-top-right-radius:4px}#simpay-payment-form-settings .simpay-panel-field-payment-method__enable{display:flex;align-items:center}#simpay-payment-form-settings .simpay-panel-field-payment-method__enable input[type=checkbox]{margin-top:0;margin-right:8px}#simpay-payment-form-settings .simpay-panel-field-payment-method__help{text-decoration:none}#simpay-payment-form-settings .simpay-panel-field-payment-method__help .dashicons{font-size:18px;width:18px;height:18px}#simpay-payment-form-settings .simpay-panel-field-payment-method__restrictions,#simpay-payment-form-settings .simpay-panel-field-payment-method__restrictions-ach{margin-left:72px}#simpay-payment-form-settings .simpay-panel-field-payment-method__icon{border-radius:3px;overflow:hidden;margin:0 8px 0 5px;width:30px;height:30px;flex-shrink:0}#simpay-payment-form-settings .simpay-panel-field-payment-method__icon svg{width:30px;height:30px}#simpay-payment-form-settings .simpay-panel-field-payment-method__configure{display:flex;align-items:center;justify-content:center}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metaboxes:not(.is-empty),#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metaboxes:not(.is-empty){border:1px solid #ccd0d4;box-shadow:0 1px 1px rgba(0,0,0,.04);border-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-handlediv,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-handlediv{display:none;float:right;width:36px;height:36px;margin:0;padding:0;border:0;background:none;cursor:pointer;display:block}#simpay-global-settings .simpay-metaboxes-wrapper .postbox.closed .simpay-handlediv .toggle-indicator:before,#simpay-form-settings .simpay-metaboxes-wrapper .postbox.closed .simpay-handlediv .toggle-indicator:before{content:""}#simpay-global-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus,#simpay-form-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus{outline:0}#simpay-global-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus .toggle-indicator:before,#simpay-form-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus .toggle-indicator:before{box-shadow:0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8)}#simpay-global-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv .toggle-indicator:before,#simpay-form-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv .toggle-indicator:before{margin-top:4px;width:20px;border-radius:50%;text-indent:-1px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox{background:#fff;border:1px solid #ccd0d4;margin:0 -1px -1px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .hndle,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .hndle{border:0}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox select,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox select{font-weight:400}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox:first-of-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox:first-of-type{margin-top:-1px;border-top-left-radius:4px;border-top-right-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type{margin-bottom:-1px;border-bottom-left-radius:4px;border-bottom-right-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type .simpay-metabox-content,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type .simpay-metabox-content{border-bottom-left-radius:4px;border-bottom-right-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2{cursor:pointer;display:flex;align-items:center;padding:10px 0 10px 12px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type{font-size:90%;color:gray;font-weight:normal;text-decoration:none;margin-left:10px}@media screen and (max-width: 782px){#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type{display:none}}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-handle,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-handle{cursor:move}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 strong,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 strong{font-size:95%;margin-left:8px;display:flex;align-items:center;flex-grow:1}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 svg,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 svg{border-radius:3px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 select,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 select{font-family:sans-serif;max-width:20%;margin:.25em .25em .25em 0}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2.fixed,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2.fixed{cursor:pointer !important}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-menu,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-menu{cursor:move}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions{padding:9px 18px;justify-content:space-between;display:flex;align-items:center}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id{display:flex;align-items:center}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id input,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id input{margin:0 2px 0 5px;width:50px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id a,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id a{text-decoration:none}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link{color:#a00}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link:hover,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link:hover{color:#dc3232}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox table,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox table{border-spacing:0;width:100%}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox table.simpay-inner-table,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox table.simpay-inner-table{border:none;padding:0 1em}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox table tr td,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox table tr td{border-bottom-color:#ccd0d4}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-remove-plan,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-remove-plan{color:#a00}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-custom-field-payment-button .dashicons-menu,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-custom-field-payment-button .dashicons-menu{visibility:hidden}#simpay-global-settings .sortable-placeholder,#simpay-form-settings .sortable-placeholder{margin:5px;display:block;min-height:36px}#simpay-global-settings .chosen-container,#simpay-form-settings .chosen-container{min-width:20em;max-width:30em}#simpay-global-settings .simpay-field.simpay-small-text,#simpay-form-settings .simpay-field.simpay-small-text{width:7em}#simpay-global-settings .simpay-field.simpay-medium-text,#simpay-form-settings .simpay-field.simpay-medium-text{width:15em}#simpay-global-settings .simpay-field-radios ul,#simpay-form-settings .simpay-field-radios ul{margin:0}#simpay-global-settings .simpay-field-radios>i,#simpay-form-settings .simpay-field-radios>i{margin-left:5px;vertical-align:middle}#simpay-global-settings ul.simpay-field-radios-inline,#simpay-form-settings ul.simpay-field-radios-inline{margin:0 0 -10px}#simpay-global-settings ul.simpay-field-radios-inline li,#simpay-form-settings ul.simpay-field-radios-inline li{display:inline-block;margin:0 10px 10px 0}#simpay-global-settings ul.simpay-field-radios-inline li:last-child,#simpay-form-settings ul.simpay-field-radios-inline li:last-child{margin-right:0}#simpay-global-settings .simpay-currency-field,#simpay-form-settings .simpay-currency-field{display:flex;align-items:center}>#simpay-global-settings .simpay-currency-field:focus,>#simpay-form-settings .simpay-currency-field:focus{position:relative;z-index:5}#simpay-global-settings .simpay-currency-symbol,#simpay-form-settings .simpay-currency-symbol{margin:0;padding-left:8px;padding-right:8px;line-height:28px;font-size:14px}@media screen and (max-width: 782px){#simpay-global-settings .simpay-currency-symbol,#simpay-form-settings .simpay-currency-symbol{line-height:38px}}#simpay-global-settings .simpay-currency-symbol-left,#simpay-form-settings .simpay-currency-symbol-left{border-top-left-radius:4px;border-bottom-left-radius:4px}#simpay-global-settings .simpay-currency-symbol-right,#simpay-form-settings .simpay-currency-symbol-right{border-top-right-radius:4px;border-bottom-right-radius:4px}#simpay-global-settings div.simpay-currency-symbol,#simpay-form-settings div.simpay-currency-symbol{border-color:#7e8993;border-style:solid;background-color:#fff}#simpay-global-settings select.simpay-currency-symbol,#simpay-form-settings select.simpay-currency-symbol{padding-right:25px}#simpay-global-settings .simpay-currency-symbol-left,#simpay-form-settings .simpay-currency-symbol-left{border-width:1px 0 1px 1px}#simpay-global-settings .simpay-currency-symbol-left+.simpay-field-amount,#simpay-form-settings .simpay-currency-symbol-left+.simpay-field-amount{border-radius:0 4px 4px 0}#simpay-global-settings .simpay-currency-symbol-right,#simpay-form-settings .simpay-currency-symbol-right{border-width:1px 1px 1px 0}#simpay-global-settings .simpay-field-amount,#simpay-form-settings .simpay-field-amount{margin:0;padding-left:8px;padding-right:8px;font-size:14px;width:6em;position:relative;z-index:2;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px}#simpay-global-settings .simpay-error,#simpay-form-settings .simpay-error{color:red}#simpay-global-settings .simpay-docs-link-wrap,#simpay-form-settings .simpay-docs-link-wrap{position:absolute;right:0;bottom:0;color:#666;font-size:13px;font-style:italic;padding:15px 18px}#simpay-global-settings .simpay-docs-link-wrap a .dashicons-editor-help,#simpay-form-settings .simpay-docs-link-wrap a .dashicons-editor-help{color:#666;text-decoration:none;width:17px;height:17px;font-size:17px;padding-left:4px}#simpay-global-settings .simpay-docs-icon,#simpay-form-settings .simpay-docs-icon{color:#666}#simpay-global-settings .simpay-docs-icon,#simpay-global-settings .simpay-docs-icon .dashicons-editor-help,#simpay-form-settings .simpay-docs-icon,#simpay-form-settings .simpay-docs-icon .dashicons-editor-help{text-decoration:none;width:17px;height:17px;font-size:17px}.button.button-primary.simpay-upgrade-btn{background-color:#428bca;border:1px solid #428bca;color:#fff;display:inline-block}.button.button-primary.simpay-upgrade-btn:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px #2d6ca2}.button.button-primary.simpay-upgrade-btn:hover{background-color:#037ad0;border:1px solid #428bca}.simpay-upgrade-btn-subtext{color:#3c434a;font-size:14px;line-height:1.5;text-align:center;margin:40px 0 0;padding:30px 35px 20px;background-color:#fcf9e8;border:3px solid #ebe29a;border-radius:4px;position:relative}.simpay-upgrade-btn-subtext svg{background:#00a32a;fill:#fff;border-radius:50%;border:3px solid #ebe29a;width:28px;height:28px;position:absolute;top:-14px;left:50%;margin-left:-14px}.simpay-upgrade-btn-subtext u{text-decoration:none;font-weight:bold;color:#00a32a}.simpay-upgrade-btn-subtext a{text-decoration:none;display:block;margin-top:6px;font-weight:bold}.post-type-simple-pay #post-body-content{display:none}.simpay-card{margin:0 0 20px;padding:30px;background:#fff;border:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04)}.simpay-card,.simpay-card p{line-height:1.5;font-size:16px}.simpay-card h3{line-height:1.6;font-size:18px;margin:0 0 20px;color:#23282c}.simpay-card p{margin:0 0 20px}.simpay-card p:last-child,.simpay-card ul:last-child{margin-bottom:0}.simpay-card figure{float:right;margin:0 0 30px 30px;max-width:400px}.simpay-card figure iframe,.simpay-card figure img{max-width:100%}.simpay-card figure figcaption{text-align:center}@media screen and (max-width: 782px){.simpay-card figure{margin:0 0 30px;max-width:100%;float:none}}.simpay-doc-suggestions{width:100%;display:flex;flex-wrap:wrap;padding:0}.simpay-doc-suggestion{text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;flex:0 1 33.333%;padding:30px;border-right:1px solid #c3c4c7;box-sizing:border-box}.simpay-doc-suggestion:nth-child(3n){border-right:0}@media screen and (max-width: 782px){.simpay-doc-suggestion{flex:0 1 100%;border-bottom:1px solid #c3c4c7;border-right:0}.simpay-doc-suggestion:last-child{border-bottom:0}}.simpay-doc-suggestion h3{font-size:20px;margin-bottom:10px}.simpay-doc-suggestion p{font-size:15px}.simpay-doc-suggestion .dashicons{font-size:40px;width:40px;height:40px;display:block;margin-bottom:10px}.simpay-doc-suggestion .button-large{font-size:16px}.simpay-addons{display:flex;flex-wrap:wrap;justify-content:space-between;margin:20px 0}.simpay-addon{background:#fff;border:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04);margin-bottom:20px;display:flex;flex-direction:column;flex-basis:calc(33% - 10px);box-sizing:border-box}@media screen and (max-width: 782px){.simpay-addon{flex-basis:100%}}.simpay-addon img{float:left;max-width:75px}.simpay-addon h5{margin:0 0 10px 100px;font-size:16px}.simpay-addon__details{padding:20px;flex:1 0 auto}.simpay-addon__actions{display:flex;align-items:center;justify-content:space-between;flex:0 1 auto;background-color:#f7f7f7;border-top:1px solid #ddd;margin-top:auto;padding:20px;position:relative}.simpay-addon__actions .msg{text-align:center;justify-content:center;display:flex;align-items:center;position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;background-color:#f7f7f7;z-index:3}.simpay-addon .error,.simpay-addon .status-label.status-installed{color:#d63638}.simpay-addon .success,.simpay-addon .status-label.status-active{color:#00a32a}.simpay-addon .addon-desc{margin:0 0 0 100px}.form-table td .simpay-stripe-connect-help{margin:15px 0;display:flex;align-items:center}.form-table td .simpay-stripe-connect-help .dashicons{margin-right:4px}.simpay-currency-field{display:flex;align-items:center}>.simpay-currency-field:focus{position:relative;z-index:5}.simpay-currency-symbol{margin:0;padding-left:8px;padding-right:8px;line-height:28px;font-size:14px}@media screen and (max-width: 782px){.simpay-currency-symbol{line-height:38px}}.simpay-currency-symbol-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.simpay-currency-symbol-right{border-top-right-radius:4px;border-bottom-right-radius:4px}div.simpay-currency-symbol{border-color:#7e8993;border-style:solid;background-color:#fff}select.simpay-currency-symbol{padding-right:25px}.simpay-currency-symbol-left{border-width:1px 0 1px 1px}.simpay-currency-symbol-left+.simpay-field-amount{border-radius:0 4px 4px 0}.simpay-currency-symbol-right{border-width:1px 1px 1px 0}.simpay-field-amount{margin:0;padding-left:8px;padding-right:8px;font-size:14px;width:6em;position:relative;z-index:2;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.simpay-template-explorer-open .show-settings{display:none !important}.simpay-branding-bar{margin:0 -20px;padding:14px 22px;background:#fff;border-bottom:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04);width:calc(100% + 20px);display:flex;justify-content:space-between;box-sizing:border-box}.site-health .simpay-branding-bar{display:none}.simpay-branding-bar__title{display:flex;align-items:center;margin:8px 0}.simpay-branding-bar__logo{width:200px}.simpay-branding-bar__divider{color:#dadbdf;font-size:23px;font-weight:400;margin:0 15px}.simpay-branding-bar__actions{display:flex;align-items:center}.simpay-branding-bar__actions>div{margin-left:10px;min-width:40px}.simpay-branding-bar__actions-button{color:#000;cursor:pointer;padding:10px;width:40px;height:40px;background-color:#f3f4f5;border-radius:50%;border:0;box-shadow:none;position:relative;transition:background-color .2s ease;box-sizing:border-box;display:block}.simpay-branding-bar__actions-button:hover{background-color:#e5e5e5}.simpay-branding-bar__actions-button:active,.simpay-branding-bar__actions-button:focus{box-shadow:0 0 0 2px var(--wp-admin-theme-color)}.simpay-branding-bar__actions-button-count{font-weight:600;font-size:10px;line-height:16px;color:#fff;margin:0;background-color:#df2a4a;border-radius:100%;width:16px;height:16px;position:absolute;top:-8px;left:50%;margin-left:-8px}.simpay-branding-bar .wp-heading-inline{font-size:23px;font-weight:400;margin:0}.simpay-branding-bar .page-title-action{font-weight:600;font-size:13px;line-height:normal;cursor:pointer;text-shadow:none;text-decoration:none;margin-left:10px;padding:4px 8px;border:1px solid currentColor;border-radius:2px;background:#f6f7f7}.simpay-landing-zone{text-align:center;max-width:700px;margin:40px auto}.simpay-landing-zone__title{font-size:26px;font-weight:600;margin:0 0 1.5rem;padding:0}.simpay-landing-zone__subtitle{font-size:17px;color:#666;margin:.25rem 0}.simpay-landing-zone__subtitle strong{color:#444}.simpay-landing-zone section{margin:2rem 0}.simpay-landing-zone__empty-state-graphic img{width:425px}.simpay-landing-zone__screenshot>*{vertical-align:middle}.simpay-landing-zone__screenshot-img{display:inline-block;position:relative;width:315px;padding:5px;background-color:#fff;box-shadow:0px 2px 5px 0px rgba(0,0,0,.05);border-radius:3px}.simpay-landing-zone__screenshot-img img{max-width:100%;display:block}.simpay-landing-zone__screenshot-img .hover{position:absolute;opacity:0;height:calc(100% - 10px);width:calc(100% - 10px);top:0;left:0;border:5px solid #fff;background-color:rgba(0,0,0,.15);background-image:url(data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik01MCAwYzI3LjYxNCAwIDUwIDIyLjM4NiA1MCA1MHMtMjIuMzg2IDUwLTUwIDUwUzAgNzcuNjE0IDAgNTAgMjIuMzg2IDAgNTAgMHoiIGZpbGw9IiMxNjg1YjciLz48cGF0aCBkPSJNNzMuMDIzIDY3LjA2N2wtOS4zNDQtOS4zNDRhMi4yNDcgMi4yNDcgMCAwMC0xLjU5My0uNjU2aC0xLjUyOGExOS4zOTkgMTkuMzk5IDAgMDA0LjEyNC0xMS45OTZjMC0xMC43Ny04LjcyNS0xOS40OTUtMTkuNDk0LTE5LjQ5NS0xMC43NjggMC0xOS40OTQgOC43MjYtMTkuNDk0IDE5LjQ5NSAwIDEwLjc2OCA4LjcyNiAxOS40OTMgMTkuNDk0IDE5LjQ5M2ExOS40IDE5LjQgMCAwMDExLjk5Ny00LjEyM3YxLjUyN2MwIC42LjIzNCAxLjE3Mi42NTUgMS41OTRsOS4zNDQgOS4zNDRhMi4yNCAyLjI0IDAgMDAzLjE3OCAwbDIuNjUyLTIuNjUzYTIuMjYgMi4yNiAwIDAwLjAxLTMuMTg2em0tMjcuODM1LTEwYy02LjYyNiAwLTExLjk5Ni01LjM2MS0xMS45OTYtMTEuOTk2IDAtNi42MjcgNS4zNi0xMS45OTcgMTEuOTk2LTExLjk5NyA2LjYyNiAwIDExLjk5NyA1LjM2MSAxMS45OTcgMTEuOTk3IDAgNi42MjYtNS4zNjEgMTEuOTk2LTExLjk5NyAxMS45OTZ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+);background-repeat:no-repeat;background-position:center;background-size:50px;transition:all .3s}.simpay-landing-zone__screenshot-img:hover .hover{opacity:1;transition:all .3s}.simpay-landing-zone__screenshot ul{text-align:left;display:inline-block;margin:0 0 0 30px;list-style-type:none;max-width:calc(100% - 350px)}@media screen and (max-width: 782px){.simpay-landing-zone__screenshot ul{text-align:center;margin:30px auto;max-width:100%;display:block}}.simpay-landing-zone__screenshot li{margin:16px 0;padding:0;font-size:15px;color:#777;position:relative}.simpay-landing-zone__screenshot li:before{content:"";background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjc3NjIgNi40NTYwNUw4LjcyMDMyIDEuNDAwMDVDOC41NzU5OSAxLjI1NTcyIDguMzgzNjQgMS4xNzY1IDguMTc4NTMgMS4xNzY1QzcuOTczMiAxLjE3NjUgNy43ODA5NiAxLjI1NTg0IDcuNjM2NjQgMS40MDAwNUw3LjE3NzYgMS44NTkyQzcuMDMzMzkgMi4wMDMyOSA2Ljk1Mzk1IDIuMTk1NzYgNi45NTM5NSAyLjQwMDk4QzYuOTUzOTUgMi42MDYwOCA3LjAzMzM5IDIuODA1MDQgNy4xNzc2IDIuOTQ5MTRMMTAuMTI3MSA1LjkwNTE2SDAuNzU2MzM0QzAuMzMzODM0IDUuOTA1MTYgMCA2LjIzNTkyIDAgNi42NTg1NFY3LjMwNzY1QzAgNy43MzAyNyAwLjMzMzgzNCA4LjA5NDM4IDAuNzU2MzM0IDguMDk0MzhIMTAuMTYwNkw3LjE3NzcyIDExLjA2NjlDNy4wMzM1MSAxMS4yMTEyIDYuOTU0MDYgMTEuMzk4NSA2Ljk1NDA2IDExLjYwMzdDNi45NTQwNiAxMS44MDg3IDcuMDMzNTEgMTEuOTk4NiA3LjE3NzcyIDEyLjE0MjhMNy42MzY3NSAxMi42MDA1QzcuNzgxMDcgMTIuNzQ0OCA3Ljk3MzMyIDEyLjgyMzUgOC4xNzg2NSAxMi44MjM1QzguMzgzNzUgMTIuODIzNSA4LjU3NjExIDEyLjc0MzggOC43MjA0MyAxMi41OTk1TDEzLjc3NjMgNy41NDM2QzEzLjkyMSA3LjM5ODgyIDE0LjAwMDUgNy4yMDU1NiAxNCA3LjAwMDExQzE0LjAwMDQgNi43OTM5OCAxMy45MjEgNi42MDA2IDEzLjc3NjIgNi40NTYwNVY2LjQ1NjA1WiIgZmlsbD0iIzQyOEJDQSIvPgo8L3N2Zz4K);background-position:center;background-repeat:no-repeat;background-size:14px;width:14px;height:14px;display:inline-block;margin:-3px 5px 0 0;vertical-align:middle}.simpay-landing-zone .button.button-large{font-size:17px;line-height:30px;padding:10px 20px}.simpay-landing-zone__purchased{display:block;margin:15px 0 0}.simpay-upgrade-modal,.simpay-upgrade-upe-modal{padding:0 !important}.simpay-upgrade-modal *,.simpay-upgrade-upe-modal *{box-sizing:border-box}.simpay-upgrade-modal__content,.simpay-upgrade-upe-modal__content{text-align:center;display:flex;flex-direction:column;align-items:center;padding:16px 32px 32px}.simpay-upgrade-modal__content>.dashicons,.simpay-upgrade-upe-modal__content>.dashicons{color:#333;font-size:48px;width:48px;height:48px}.simpay-upgrade-modal__title,.simpay-upgrade-upe-modal__title{font-size:22px;line-height:1.5;display:block;margin:12px 0 0}.simpay-upgrade-modal__description,.simpay-upgrade-upe-modal__description{color:#777;font-size:16px;margin:16px 0 24px}.simpay-upgrade-modal__description strong,.simpay-upgrade-upe-modal__description strong{color:#333}.simpay-upgrade-modal__discount,.simpay-upgrade-upe-modal__discount{font-size:15px;text-align:center;margin:32px -32px -32px;padding:24px 40px;background-color:#fcf9e8;position:relative}.simpay-upgrade-modal__discount svg,.simpay-upgrade-upe-modal__discount svg{background:#00a32a;fill:#fff;border-radius:50%;border:4px solid #fff;width:32px;height:32px;position:absolute;top:-16px;left:50%;margin-left:-16px}.simpay-upgrade-modal__discount u,.simpay-upgrade-upe-modal__discount u{text-decoration:none;font-weight:bold;color:#00a32a}.simpay-upgrade-modal .button-large,.simpay-upgrade-upe-modal .button-large{font-size:16px;font-weight:bold;margin:0 0 15px;padding:8px 30px !important;height:auto}.simpay-teaser-float{margin:50px;position:relative}.simpay-teaser-float__card{text-align:center;padding:30px;background:#fff;border-radius:4px;box-shadow:0 0 30px 15px rgba(0,0,0,.18);position:relative;z-index:2}#poststuff .simpay-teaser-float h2,.simpay-teaser-float h2{font-size:24px;font-weight:600;margin:0;padding:0}.simpay-teaser-float p{font-size:15px;line-height:1.35;color:#666}.simpay-teaser-float p strong{color:#444}.simpay-teaser-float ul{text-align:left;display:inline-block;margin:-10px 0 20px;list-style-type:none}.simpay-teaser-float li{margin:16px 0;padding:0 0 0 24px;font-size:15px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjc3NjIgNi40NTYwNUw4LjcyMDMyIDEuNDAwMDVDOC41NzU5OSAxLjI1NTcyIDguMzgzNjQgMS4xNzY1IDguMTc4NTMgMS4xNzY1QzcuOTczMiAxLjE3NjUgNy43ODA5NiAxLjI1NTg0IDcuNjM2NjQgMS40MDAwNUw3LjE3NzYgMS44NTkyQzcuMDMzMzkgMi4wMDMyOSA2Ljk1Mzk1IDIuMTk1NzYgNi45NTM5NSAyLjQwMDk4QzYuOTUzOTUgMi42MDYwOCA3LjAzMzM5IDIuODA1MDQgNy4xNzc2IDIuOTQ5MTRMMTAuMTI3MSA1LjkwNTE2SDAuNzU2MzM0QzAuMzMzODM0IDUuOTA1MTYgMCA2LjIzNTkyIDAgNi42NTg1NFY3LjMwNzY1QzAgNy43MzAyNyAwLjMzMzgzNCA4LjA5NDM4IDAuNzU2MzM0IDguMDk0MzhIMTAuMTYwNkw3LjE3NzcyIDExLjA2NjlDNy4wMzM1MSAxMS4yMTEyIDYuOTU0MDYgMTEuMzk4NSA2Ljk1NDA2IDExLjYwMzdDNi45NTQwNiAxMS44MDg3IDcuMDMzNTEgMTEuOTk4NiA3LjE3NzcyIDEyLjE0MjhMNy42MzY3NSAxMi42MDA1QzcuNzgxMDcgMTIuNzQ0OCA3Ljk3MzMyIDEyLjgyMzUgOC4xNzg2NSAxMi44MjM1QzguMzgzNzUgMTIuODIzNSA4LjU3NjExIDEyLjc0MzggOC43MjA0MyAxMi41OTk1TDEzLjc3NjMgNy41NDM2QzEzLjkyMSA3LjM5ODgyIDE0LjAwMDUgNy4yMDU1NiAxNCA3LjAwMDExQzE0LjAwMDQgNi43OTM5OCAxMy45MjEgNi42MDA2IDEzLjc3NjIgNi40NTYwNVY2LjQ1NjA1WiIgZmlsbD0iIzQyOEJDQSIvPgo8L3N2Zz4K);background-position:left 3px;background-repeat:no-repeat;background-size:14px;color:#777}.simpay-teaser-float .button.button-large{font-size:17px;line-height:30px;padding:10px 20px}.simpay-teaser-float .simpay-notice-dismiss{position:absolute;top:0;right:0;font-size:20px;color:#666;font-weight:bold;text-decoration:none;margin-left:5px;padding:6px 10px;z-index:2}.simpay-teaser-float .simpay-notice-dismiss:hover,.simpay-teaser-float .simpay-notice-dismiss:active,.simpay-teaser-float .simpay-notice-dismiss:focus{color:#666;text-decoration:none}.simpay-teaser-float:after,.simpay-teaser-float:before{opacity:.75;z-index:0;content:"";position:absolute;left:-30px;right:-30px;top:-35px;width:calc(100% + 60px);height:170px;background-image:linear-gradient(#ddd, #ddd),linear-gradient(#eee, #eee),linear-gradient(#ddd, #ddd),linear-gradient(#eee, #eee);background-repeat:no-repeat;background-size:100% 20px,100% 40px,100% 20px,100% 40px;background-position:0 0,0 30px,0 100px,0 130px}.simpay-teaser-float:before{top:170px}.simpay-teaser-float .simpay-upgrade-btn-subtext{margin:24px -30px -30px;padding:32px 40px;border-bottom-left-radius:2px;border-bottom-right-radius:2px;border:0}.simpay-teaser-float .simpay-upgrade-btn-subtext svg{border-color:#fff}.simpay-teaser-float--inline{margin:40px 30px 30px}.simpay-teaser-float--inline .simpay-teaser-float__card{padding:30px;box-shadow:0 0 12px 6px rgba(0,0,0,.16)}#poststuff .simpay-teaser-float--inline h2,.simpay-teaser-float--inline h2{font-size:20px}.simpay-teaser-float--inline p{font-size:15px;margin:.75rem 0}.simpay-teaser-float--inline .button.button-large{font-size:15px;line-height:24px;margin:1rem 0;padding:8px 14px;display:inline-block}.simpay-teaser-float--inline:before,.simpay-teaser-float--inline:after{opacity:.6}.simpay-teaser-float--inline:before{display:none}.simpay-teaser-float--inline:after{top:15px}.simpay-form-settings-notice{font-weight:normal;color:#1d2327;position:relative;margin:18px 18px 0;padding:14px;border-radius:4px;background:#f5f5ff}.simpay-form-settings-notice a{color:#635aff}.simpay-form-settings-notice .simpay-external-link .dashicons-external{margin:1px 0 0 2px}.simpay-form-settings-notice strong{font-size:14px}.simpay-form-settings-notice p{margin:5px 0 0 23px}.simpay-form-settings-notice .simpay-notice-dismiss{font-size:20px;color:#b0b0f0;font-weight:bold;line-height:1;position:absolute;top:0;right:5px;text-decoration:none;padding:0 5px;z-index:2}.simpay-form-settings-notice .simpay-notice-dismiss:hover,.simpay-form-settings-notice .simpay-notice-dismiss:active,.simpay-form-settings-notice .simpay-notice-dismiss:focus{color:#9191ef;text-decoration:none;background:none}.simpay-settings .simpay-form-settings-notice{border:2px solid #645aff;box-shadow:0 1px 1px rgba(0,0,0,.04)}.post-type-simple-pay .lity{z-index:999999999;padding:20px}.post-type-simple-pay .lity-close{margin:10px}.post-type-simple-pay .lity-content{max-width:80vw;max-height:80vh}.post-type-simple-pay .lity-content img{max-height:80vh !important;max-width:80vw !important}#wpsp-api-keys-row-hide{display:none}.post-type-simple-pay #post-preview:not(.simpay-preview-button){display:none}.simpay-license-field-wrapper{margin:20px 0}#simpay-settings-license-key-license-key{margin:0}#simpay-settings-license-key-license-key[readonly]{background:#fff}.simpay-license-message__loading,.simpay-license-field{display:flex;align-items:center}.wp-core-ui .button.simpay-license-button.button-primary,.wp-core-ui .button.simpay-license-button.button-secondary{font-size:1rem;line-height:1;margin:0 8px;padding:13px}.simpay-license-message{font-size:15px;margin-top:8px}.simpay-license-message--valid{color:#15803d}.simpay-license-message--invalid{color:#b91c1c}.simpay-recaptcha-payment-form-feedback .dashicons{margin-top:-1px;display:inline-block}.simpay-recaptcha-payment-form-feedback .dashicons-update-alt{animation:rotation 2s infinite linear}.simpay-form-builder-inset-settings{margin:3px 0 0 24px}fieldset.simpay-form-builder-inset-settings>*:not(legend){margin:5px 0 8px}.simpay-form-builder-inset-settings>*:last-child{margin-bottom:0}.simpay-form-builder-inventory-control{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;padding:6px 8px;box-shadow:0 0 0 rgba(0,0,0,0);transition:box-shadow .1s linear;border-radius:2px;border:1px solid #757575;font-size:16px;line-height:normal;margin:0;padding:0;width:auto;display:inline-flex;align-items:center}@media(prefers-reduced-motion: reduce){.simpay-form-builder-inventory-control{transition-duration:0s;transition-delay:0s}}@media(min-width: 600px){.simpay-form-builder-inventory-control{font-size:13px;line-height:normal}}.simpay-form-builder-inventory-control:focus{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-inventory-control::-webkit-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-inventory-control::-moz-placeholder{opacity:1;color:rgba(30,30,30,.62)}.simpay-form-builder-inventory-control:-ms-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-inventory-control:focus-within{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-inventory-control input[type=number]{border:0;box-shadow:none;background:none;border-radius:0;width:75px}.simpay-form-builder-inventory-control input[type=number]:focus{border:0;box-shadow:none;outline:0}.simpay-form-builder-inventory-control+label{margin-left:8px}.simpay-form-builder-inventory-control__initial{color:#757575;font-size:16px;line-height:normal;padding-right:8px}@media(min-width: 600px){.simpay-form-builder-inventory-control__initial{font-size:13px;line-height:normal}}.simpay-form-builder-inventory-control__initial:before{content:"/";display:inline-block;vertical-align:top;margin-top:-1px}.simpay-form-builder-purchase-restrictions__restriction-item{display:flex;align-items:center;margin-top:8px;margin-bottom:8px}.simpay-form-builder-purchase-restrictions__restriction-item label{margin-left:8px}.simpay-form-builder-purchase-restrictions__restriction-item-datetime{display:flex;align-items:center}.simpay-form-builder-purchase-restrictions__restriction-item-datetime>*{margin-right:10px}.simpay-form-builder-purchase-restrictions__restriction-item-datetime span{color:#757575}.simpay-form-builder-fee-recovery__amounts{display:flex;align-items:center;margin-top:5px}.simpay-form-builder-fee-recovery ::-webkit-input-placeholder{color:#b7bec7}.simpay-form-builder-fee-recovery ::-moz-placeholder{color:#b7bec7;opacity:1}.simpay-form-builder-fee-recovery :-ms-input-placeholder{color:#b7bec7}.simpay-form-builder-fee-recovery>*{margin-right:8px}.simpay-form-builder-fee-percent-control{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;padding:6px 8px;box-shadow:0 0 0 rgba(0,0,0,0);transition:box-shadow .1s linear;border-radius:2px;border:1px solid #757575;font-size:16px;line-height:normal;margin:0;padding:0;width:auto;display:inline-flex;align-items:center}@media(prefers-reduced-motion: reduce){.simpay-form-builder-fee-percent-control{transition-duration:0s;transition-delay:0s}}@media(min-width: 600px){.simpay-form-builder-fee-percent-control{font-size:13px;line-height:normal}}.simpay-form-builder-fee-percent-control:focus{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-fee-percent-control::-webkit-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-fee-percent-control::-moz-placeholder{opacity:1;color:rgba(30,30,30,.62)}.simpay-form-builder-fee-percent-control:-ms-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-fee-percent-control:focus-within{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-fee-percent-control input[type=number]{border:0;box-shadow:none;background:none;border-radius:0;width:75px}.simpay-form-builder-fee-percent-control input[type=number]:focus{border:0;box-shadow:none;outline:0}.simpay-form-builder-fee-percent-control+label{margin-left:8px}.simpay-form-builder-fee-percent-control__suffix{color:#757575;font-size:16px;line-height:normal;padding-right:8px}@media(min-width: 600px){.simpay-form-builder-fee-percent-control__suffix{font-size:13px;line-height:normal}}.simpay-form-builder-automations__cta{margin:24px 0 6px;text-align:center}.simpay-form-builder-automator{padding:16px 4px 26px !important}.simpay-form-builder-automator,.simpay-form-builder-automator *{box-sizing:border-box}.simpay-form-builder-automator input[type=search]{font-size:15px;padding:2px 8px}.simpay-form-builder-automator__integrations{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));grid-column-gap:16px;grid-row-gap:16px;margin:18px -18px 0;padding:0 18px 10px;width:calc(100% + 36px);max-height:400px;overflow-y:scroll}.simpay-form-builder-automator__integrations-integration{color:initial;text-align:center;text-decoration:none;background:#fff;border:1px solid #eee;border-radius:4px;box-shadow:0 1px 3px 0 rgba(0,0,0,.03);transition:all ease-in .15s}.simpay-form-builder-automator__integrations-integration img{display:block;margin:16px auto;height:50px}.simpay-form-builder-automator__integrations-integration span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:8px 16px;display:block;border-top:1px solid #eee}.simpay-form-builder-automator__integrations-integration:hover{cursor:pointer;border-color:#ddd;box-shadow:0 2px 4px 0 rgba(0,0,0,.06)}body.site-health #wpbody-content,body.post-type-simple-pay #wpbody-content{padding-bottom:200px}#wpfooter .simpay-footer-promotion{text-align:center;font-weight:400;font-size:13px;line-height:16px;color:#787c82;padding:20px 0 30px 0;margin-bottom:20px}#wpfooter .simpay-footer-promotion p{font-weight:600}#wpfooter .simpay-footer-promotion-links,#wpfooter .simpay-footer-promotion-social{display:flex;justify-content:center;align-items:center}#wpfooter .simpay-footer-promotion-links{margin:9px 0 0}#wpfooter .simpay-footer-promotion-links span{color:#c3c4c7;padding:0 7px}#wpfooter .simpay-footer-promotion-social{margin:10px 0 0 0;gap:10px}#wpfooter .simpay-footer-promotion-social li{margin-bottom:0}#wpfooter .simpay-footer-promotion-social li:hover path{fill:#50575e}#wpfooter .simpay-footer-promotion-social a{display:block;height:16px} +.simpay-settings-subsections{display:flex;align-items:center;box-shadow:inset 0 -1px 0 0 #ccc}.simpay-settings-subsections__subsection{font-weight:500;text-decoration:none;padding:15px;display:flex;align-items:center}.simpay-settings-subsections__subsection .dashicons{width:18px;height:18px;font-size:18px;margin-right:4px}.simpay-settings-subsections__subsection.is-active{box-shadow:inset 0 -4px 0 0 currentColor;position:relative;z-index:1}.simpay-settings-subsections__subsection:not(.is-active){color:#23282d}.simpay-settings form>h2:not(.nav-tab-wrapper){clip:rect(1px, 1px, 1px, 1px);height:1px;overflow:hidden;position:absolute !important;width:1px}.simpay-settings .form-table td fieldset+p,.simpay-settings .form-table td label+p,.simpay-settings .form-table td select+p,.simpay-settings .form-table td input+p{color:#666;font-style:italic}.simpay-settings .simpay-settings-subsections__subsection{display:flex;align-items:center}.simpay-settings .simpay-settings-subsections__subsection .simpay-settings-bubble{margin-left:5px}.simpay-settings .simpay-settings-visual-toggles{margin:30px 0 0;display:flex}.simpay-settings .simpay-settings-visual-toggles input[type=radio]{display:none}.simpay-settings .simpay-settings-visual-toggles__toggle{-webkit-user-select:none;-moz-user-select:none;user-select:none;min-width:180px;margin:0 30px 0 0 !important;position:relative;display:block;background-color:#fff;border-radius:4px;border:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04)}.simpay-settings .simpay-settings-visual-toggles__toggle:hover{border:1px solid #999;box-shadow:0 1px 2px rgba(0,0,0,.1)}.simpay-settings .simpay-settings-visual-toggles input[type=radio]:checked+.simpay-settings-visual-toggles__toggle{border-color:#007cba;border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px #007cba;box-shadow:0 0 0 1px var(--wp-admin-theme-color)}.simpay-settings .simpay-settings-visual-toggles__toggle-recommended,.simpay-settings .simpay-settings-visual-toggles__toggle-not-recommended{text-align:center;font-size:12px;text-transform:uppercase;font-weight:bold;margin:0;padding:5px 0;display:block;border-top-right-radius:4px;border-top-left-radius:4px}.simpay-settings .simpay-settings-visual-toggles__toggle-recommended{color:#0f8569;background:#f4f9f7}.simpay-settings .simpay-settings-visual-toggles__toggle-not-recommended{color:#b91c1b;background:#fef2f2}.simpay-settings .simpay-settings-visual-toggles__toggle-icon{margin:20px auto 15px;padding:0 15px;display:block}.simpay-settings .simpay-settings-visual-toggles__toggle-label{line-height:1.5;text-align:center;font-size:16px;font-weight:500;margin:15px;display:block}.simpay-settings .simpay-settings-visual-toggles__toggle-label small{color:#666;font-weight:normal;font-size:13px;line-height:1;display:block;margin:4px 0}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type .simpay-settings-visual-toggles__toggle{min-height:160px}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type .simpay-settings-visual-toggles__toggle-icon{width:80px;height:80px}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type input[type=radio].simpay-settings-captcha-type--is-recommended:checked+.simpay-settings-visual-toggles__toggle{border-color:#0f8569;box-shadow:0 0 0 1px #0f8569}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type input[type=radio].simpay-settings-captcha-type--is-not-recommended:checked+.simpay-settings-visual-toggles__toggle{border-color:#b91c1b;box-shadow:0 0 0 1px #b91c1b}.simpay-settings .simpay-settings-visual-toggles.simpay-settings-captcha-type label[for=simpay-settings-captcha-type-cloudflare-turnstile] .simpay-settings-visual-toggles__toggle-icon{width:120px}.simpay-settings-general-recaptcha-no_captcha_warning th,.simpay-settings-general-recaptcha-no_captcha_warning td,.simpay-settings-hcaptcha_secret_key th,.simpay-settings-hcaptcha_secret_key td,.simpay-settings-cloudflare_turnstile_secret_key th,.simpay-settings-cloudflare_turnstile_secret_key td,.simpay-settings-recaptcha_score_threshold th,.simpay-settings-recaptcha_score_threshold td{padding-bottom:50px !important}.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-summary-report,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-payment-confirmation,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-payment-notification,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-upcoming-invoice,.simpay-settings .simpay-settings-subsections__subsection.simpay-settings-subsection-invoice-confirmation{display:none}.simpay-settings .simpay-settings-subsection-emails-tools{margin-left:auto}.simpay-admin-charts-period-over-period{position:relative;padding-bottom:12px}.simpay-admin-charts-period-over-period *{box-sizing:border-box}.simpay-admin-charts-period-over-period__tooltip{position:absolute;background:#fff;border:1px solid #c3c3c3;box-shadow:0 2px 6px rgba(0,0,0,.05);border-radius:2px;padding:10px 14px;display:flex;flex-direction:column;z-Index:10000;min-width:175px}.simpay-admin-charts-period-over-period__tooltip-data{white-space:nowrap;margin-bottom:8px;display:grid;grid-template-columns:1fr auto;grid-auto-rows:auto;-moz-column-gap:16px;column-gap:16px}.simpay-admin-charts-period-over-period__tooltip-data:last-child{margin-bottom:0}.simpay-admin-charts-period-over-period__tooltip-data[data-dataset="1"]{opacity:.65}.simpay-admin-charts-period-over-period__tooltip-data em{font-style:normal;text-align:right}.simpay-admin-charts-period-over-period__tooltip-delta{font-size:12px;margin:0 -14px -10px;padding:8px 14px;border-top:1px solid #eee;background:#fdfdfd;border-radius:2px;display:flex;align-items:center;justify-content:center}.simpay-admin-charts-period-over-period__tooltip-delta .simpay-admin-charts-badge{margin-right:4px}.simpay-admin-charts-period-over-period__tooltip-delta strong.is-positive{color:#006908}.simpay-admin-charts-period-over-period__tooltip-delta strong.is-negative{color:#b3093c}.simpay-admin-charts-badge{color:#2f2f2f;font-size:12px;font-weight:500;font-style:normal;line-height:1;padding:3px 6px;display:inline-flex;align-items:center;background:#f0f0f0;border-radius:100px}.simpay-admin-charts-badge.is-positive{color:#006908;background-color:#d7f7c2}.simpay-admin-charts-badge.is-negative{color:#b3093c;background-color:#ffe7f2}.simpay-admin-charts-badge__icon{width:15px;height:15px}.simpay-admin-charts-no-data{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;background:rgba(255,255,255,.5);z-index:2}.simpay-admin-charts-no-data>div{text-align:center;padding:24px;background:#fff;border:1px solid #c3c3c3;box-shadow:0 2px 6px rgba(0,0,0,.1);border-radius:2px;max-width:60%}.simpay-admin-charts-no-data strong{font-size:15px;margin-bottom:8px;display:block}.button.button-large.simpay-button-large{font-size:14px;line-height:30px;padding:4px 12px}.simpay-copy-hidden-input{clip:rect(1px, 1px, 1px, 1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.fixed .column-livemode{width:15%;text-align:right}@media screen and (max-width: 782px){.fixed .column-livemode{text-align:left}}.fixed .column-livemode .simpay-badge{margin-top:3px}.fixed .column-shortcode{width:25%}.fixed .column-shortcode .simpay-shortcode{clip:rect(1px, 1px, 1px, 1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.post-type-simple-pay .misc-pub-curtime,.post-type-simple-pay .misc-pub-visibility{display:none}.simpay-metabox-title{border:1px solid #eee}.simpay-shortcode-section{border-top:1px solid #ddd;border-width:1px 0;padding-top:15px;padding-bottom:15px;position:relative}.simpay-shortcode-section label{display:block;margin-bottom:6px}.simpay-shortcode-section label .dashicons{color:#8c8f94;margin-right:3px}.simpay-shortcode-section .simpay-copy-button{line-height:normal;position:absolute;right:20px;bottom:20px;border:0;background:none;box-shadow:none;padding:0}.simpay-shortcode-section .simpay-copy-button:hover{border:0;background:none;box-shadow:none}.simpay-shortcode-section .simpay-copy-button .dashicons{color:#3c434a}.simpay-shortcode{width:100%;padding:8px;line-height:1;margin:0;height:32px;resize:none}.simpay-badge{color:#3f3f46;text-align:center;line-height:1;padding:5px 7px;border-radius:3px;background:#e4e4e7;border:0;box-shadow:none;display:inline-flex;align-items:center}button.simpay-badge{cursor:pointer}button.simpay-badge:hover{background:#d4d4d8}.simpay-badge__icon{opacity:.8;margin:2px 5px 0 0}.simpay-badge--green{color:#0e6245;background:#cbf4c9}.simpay-badge--yellow{color:#983705;background:#f8e5b9}.simpay-stripe-account-info{display:flex;align-items:center;margin-bottom:8px;position:relative}.simpay-stripe-account-info .spinner{float:none;margin-top:0;margin-left:0}.simple-pay_page_simpay_settings .simpay-settings-upgrade,.post-type-simple-pay .simpay-settings-upgrade{margin-top:20px;padding:1px;position:relative;background:#fff;border-radius:4px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2),0 5px 10px rgba(0,0,0,.1);max-width:677px}.simple-pay_page_simpay_settings .simpay-settings-upgrade__inner,.post-type-simple-pay .simpay-settings-upgrade__inner{text-align:center;margin:0;padding:30px}.simple-pay_page_simpay_settings .simpay-settings-upgrade h3,.post-type-simple-pay .simpay-settings-upgrade h3{line-height:1.5;font-size:22px;margin:0 0 1.5rem}.simple-pay_page_simpay_settings .simpay-settings-upgrade ul,.post-type-simple-pay .simpay-settings-upgrade ul{margin:1.5rem 0 calc(1.5rem - 6px);display:flex;flex-wrap:wrap;justify-content:center}@media screen and (min-width: 782px){.simple-pay_page_simpay_settings .simpay-settings-upgrade ul,.post-type-simple-pay .simpay-settings-upgrade ul{margin-left:4rem;margin-right:4rem}}.simple-pay_page_simpay_settings .simpay-settings-upgrade li,.post-type-simple-pay .simpay-settings-upgrade li{font-size:15px;margin:6px 0;width:100%}.simple-pay_page_simpay_settings .simpay-settings-upgrade li a,.post-type-simple-pay .simpay-settings-upgrade li a{color:#3c434a;text-decoration:none}.simple-pay_page_simpay_settings .simpay-settings-upgrade li a:hover,.post-type-simple-pay .simpay-settings-upgrade li a:hover{color:var(--wp-admin-theme-color);text-decoration:underline}@media screen and (min-width: 782px){.simple-pay_page_simpay_settings .simpay-settings-upgrade li,.post-type-simple-pay .simpay-settings-upgrade li{text-align:left;width:50%}}.simple-pay_page_simpay_settings .simpay-settings-upgrade .button.button-large,.post-type-simple-pay .simpay-settings-upgrade .button.button-large{font-size:17px;line-height:30px;padding:10px 20px}.simple-pay_page_simpay_settings .simpay-settings-upgrade small,.post-type-simple-pay .simpay-settings-upgrade small{color:#666;margin:15px 0 0;display:block}.simple-pay_page_simpay_settings .simpay-settings-upgrade .dashicons-yes,.post-type-simple-pay .simpay-settings-upgrade .dashicons-yes{color:#428bca}.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link,.post-type-simple-pay .simpay-settings-upgrade .button-link{position:absolute;top:0;right:0;font-size:20px;color:#666;font-weight:bold;text-decoration:none;margin-left:5px;padding:6px 10px;z-index:2}.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link:hover,.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link:active,.simple-pay_page_simpay_settings .simpay-settings-upgrade .button-link:focus,.post-type-simple-pay .simpay-settings-upgrade .button-link:hover,.post-type-simple-pay .simpay-settings-upgrade .button-link:active,.post-type-simple-pay .simpay-settings-upgrade .button-link:focus{color:#666;text-decoration:none}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext{text-align:center;margin:0;padding:30px 20px 20px;background-color:#fcf9e8;border:1px solid #edeac9;border-width:1px 0 0;position:relative;border-radius:0;border-bottom-left-radius:4px;border-bottom-right-radius:4px}@media screen and (min-width: 782px){.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext{padding-left:4rem;padding-right:4rem}}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext svg,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext svg{background:#00a32a;fill:#fff;border-radius:50%;border:4px solid #fff;box-shadow:0 0 0 1px #edeac9;width:28px;height:28px;position:absolute;top:-18px;left:50%;margin-left:-18px}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext u,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext u{text-decoration:none;font-weight:bold;color:#00a32a}.simple-pay_page_simpay_settings .simpay-settings-upgrade .simpay-upgrade-btn-subtext a,.post-type-simple-pay .simpay-settings-upgrade .simpay-upgrade-btn-subtext a{font-weight:normal;display:inline-block}#simpay-payment-form-settings table{width:100%;border-collapse:collapse}#simpay-payment-form-settings ::-webkit-input-placeholder{color:#9ba1a9}#simpay-payment-form-settings ::-moz-placeholder{color:#9ba1a9;opacity:1}#simpay-payment-form-settings :-ms-input-placeholder{color:#9ba1a9}#simpay-payment-form-settings .inside{margin:0;padding:0}#simpay-payment-form-settings .simpay-panel-field .toolbar{margin-bottom:-4px}#simpay-payment-form-settings .simpay-panel-field .toolbar .simpay-field-select{margin:0 0 4px;width:auto;max-width:70%}#simpay-payment-form-settings .simpay-tabs{margin:0;padding:0;list-style:none;background:#fafafa;border-right:1px solid #ccd0d4;line-height:1em;position:relative;flex:0 0 25%}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs{flex-basis:100%;flex-grow:1;border-right:0}}#simpay-payment-form-settings .simpay-tabs li{margin:0;padding:0}#simpay-payment-form-settings .simpay-tabs li:first-child{margin-top:12px}#simpay-payment-form-settings .simpay-tabs li:last-child{margin-bottom:20px}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs li:last-child{margin-bottom:0}}#simpay-payment-form-settings .simpay-tabs li.active{margin-left:-1px;box-shadow:0 1px 1px rgba(0,0,0,.04);position:relative}#simpay-payment-form-settings .simpay-tabs li.active:focus:after{display:none}#simpay-payment-form-settings .simpay-tabs li.active:before,#simpay-payment-form-settings .simpay-tabs li.active:after{content:"";width:calc(100% + 1px);height:1px;background:#ccd0d4;position:absolute;top:0;left:0;right:0;z-index:2}#simpay-payment-form-settings .simpay-tabs li.active:after{top:auto;bottom:0}#simpay-payment-form-settings .simpay-tabs li.active a{font-weight:bold;background-color:#fff;position:relative;margin-right:-1px}#simpay-payment-form-settings .simpay-tabs li.active a:before{content:"";position:absolute;top:0;left:0;bottom:0;width:4px;height:100%;background:currentColor;z-index:3}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs li.active a{margin-right:0}}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item{display:flex;align-items:center;line-height:20px;margin:0;padding:8px 10px 8px 14px;text-decoration:none;transition:all .05s ease-in-out}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item svg,#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item .dashicons{margin-right:6px}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item{padding:18px}}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item:focus{outline:0;position:relative;z-index:3;box-shadow:inset 0 0 0 1px currentColor,0 0 0 1px currentColor}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#purchase-restrictions-settings-panel"],#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#payment-page-settings-panel"]{margin-bottom:20px;position:relative}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#purchase-restrictions-settings-panel"]:after,#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item[href="#payment-page-settings-panel"]:after{content:"";position:absolute;left:14px;right:14px;bottom:-10px;width:calc(100% - 28px);height:1px;background:#eaeaea}#simpay-payment-form-settings .simpay-tabs li .simpay-tab-item span>span{color:#f18500;font-size:12px;font-weight:600;margin:0 0 0 5px;display:inline-block}#simpay-payment-form-settings .simpay-tabs li:not(.active) .simpay-tab-item{color:inherit}#simpay-payment-form-settings .simpay-panels-wrap{background:#fff;display:flex}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-panels-wrap{flex-direction:column}}#simpay-payment-form-settings .simpay-panels{flex:0 0 75%;display:flex}@media screen and (min-width: 1400px){#simpay-payment-form-settings .simpay-panels{flex-basis:75%}}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-panels{flex-basis:100%}}#simpay-payment-form-settings .simpay-panel,#simpay-payment-form-settings .simpay-panel-section{width:100%}#simpay-payment-form-settings .simpay-panel>table,#simpay-payment-form-settings .simpay-panel>table>tr,#simpay-payment-form-settings .simpay-panel>table>tbody,#simpay-payment-form-settings .simpay-panel>table>tbody>tr,#simpay-payment-form-settings .simpay-panel>table>thead,#simpay-payment-form-settings .simpay-panel>table>thead>tr{display:block;width:100%}#simpay-payment-form-settings .simpay-panel>table:last-child>tbody:last-child>tr:last-child>td{border-bottom:0}#simpay-payment-form-settings .simpay-panel-field--requires-upgrade{position:relative}#simpay-payment-form-settings .simpay-panel-field--requires-upgrade td>div{margin-right:80px}#simpay-payment-form-settings .simpay-panel-field--requires-upgrade td>div .button-small{position:absolute;top:calc(50% - 13px);right:18px}#simpay-payment-form-settings .simpay-panel-field,#simpay-payment-form-settings .simpay-panel-field>td,#simpay-payment-form-settings .simpay-panel-field>th{text-align:left;display:block}#simpay-payment-form-settings .simpay-panel-field>td,#simpay-payment-form-settings .simpay-panel-field>th{width:calc(100% - 36px);margin-left:18px;margin-right:18px}#simpay-payment-form-settings .simpay-panel-field th{font-weight:bold;padding-top:18px;padding-bottom:5px}#simpay-payment-form-settings .simpay-panel-field td{border-bottom:1px solid #ddd;padding-bottom:18px}#simpay-payment-form-settings .simpay-panel-field p.description{margin-top:4px}#simpay-payment-form-settings .simpay-panel-field p.description:last-of-type{margin-bottom:0}#simpay-payment-form-settings .simpay-panel-field .simpay-panel-field__nested{margin-top:18px}#simpay-payment-form-settings .simpay-panel-field .simpay-panel-field__nested label{font-weight:bold;display:block;margin-bottom:4px}#simpay-payment-form-settings .simpay-panel-field .simpay-field-select,#simpay-payment-form-settings .simpay-panel-field .simpay-field-text{min-width:75%;max-width:100%}@media screen and (max-width: 782px){#simpay-payment-form-settings .simpay-panel-field .simpay-field-select,#simpay-payment-form-settings .simpay-panel-field .simpay-field-text{min-width:0;width:100%}}#simpay-payment-form-settings .simpay-panel-field .simpay-field-textarea{width:100%;max-width:100%}#simpay-payment-form-settings .simpay-panel-field .notice:last-of-type{margin-bottom:0}#simpay-payment-form-settings .simpay-panel-field .error,#simpay-payment-form-settings .simpay-panel-field .simpay-important{color:#a94442;font-weight:normal}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap{position:relative;margin-top:12px}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap .simpay-remove-image-preview{position:absolute;top:-15px;left:-15px;cursor:pointer;background-color:#fff}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap .simpay-remove-image-preview::before{font-size:22px;line-height:26px}#simpay-payment-form-settings .simpay-panel-field .simpay-image-preview-wrap .simpay-image-preview{max-height:128px;max-width:128px;border:1px solid #ddd}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box{background-color:#f4f4f4;border:1px solid #e5e5e5;padding:18px;margin-top:18px;position:relative}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box__dismiss{color:inherit;text-decoration:none;position:absolute;top:8px;right:8px}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box__dismiss .dashicons-dismiss{font-size:16px;width:16px;height:16px}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box h3{font-size:18px;font-weight:600;margin:0;padding:0}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box p{font-size:14px}#simpay-payment-form-settings .simpay-panel .simpay-promo-under-box p:last-child{margin-bottom:0}#simpay-payment-form-settings .simpay-metabox-content{margin-bottom:-1px;background-color:#f5f5f5;border:1px solid #c3c4c7;border-width:1px 0;box-shadow:0 1px 1px rgba(0,0,0,.04);position:relative}#simpay-payment-form-settings .simpay-show-if,#simpay-payment-form-settings .simpay-panel-hidden{display:none}#simpay-payment-form-settings .simpay-payment-methods{border:1px solid #ccd0d4;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,.04)}#simpay-payment-form-settings .simpay-panel-field-payment-method{display:block;border-top:1px solid #ccd0d4;padding:7px;box-sizing:border-box}#simpay-payment-form-settings .simpay-panel-field-payment-method:first-child{border-top:0;border-top-left-radius:4px;border-top-right-radius:4px}#simpay-payment-form-settings .simpay-panel-field-payment-method__enable{display:flex;align-items:center}#simpay-payment-form-settings .simpay-panel-field-payment-method__enable input[type=checkbox]{margin-top:0;margin-right:8px}#simpay-payment-form-settings .simpay-panel-field-payment-method__help{text-decoration:none}#simpay-payment-form-settings .simpay-panel-field-payment-method__help .dashicons{font-size:18px;width:18px;height:18px}#simpay-payment-form-settings .simpay-panel-field-payment-method__restrictions,#simpay-payment-form-settings .simpay-panel-field-payment-method__restrictions-ach{margin-left:72px}#simpay-payment-form-settings .simpay-panel-field-payment-method__icon{border-radius:3px;overflow:hidden;margin:0 8px 0 5px;width:30px;height:30px;flex-shrink:0}#simpay-payment-form-settings .simpay-panel-field-payment-method__icon svg{width:30px;height:30px}#simpay-payment-form-settings .simpay-panel-field-payment-method__configure{display:flex;align-items:center;justify-content:center}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metaboxes:not(.is-empty),#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metaboxes:not(.is-empty){border:1px solid #ccd0d4;box-shadow:0 1px 1px rgba(0,0,0,.04);border-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-handlediv,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-handlediv{display:none;float:right;width:36px;height:36px;margin:0;padding:0;border:0;background:none;cursor:pointer;display:block}#simpay-global-settings .simpay-metaboxes-wrapper .postbox.closed .simpay-handlediv .toggle-indicator:before,#simpay-form-settings .simpay-metaboxes-wrapper .postbox.closed .simpay-handlediv .toggle-indicator:before{content:""}#simpay-global-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus,#simpay-form-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus{outline:0}#simpay-global-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus .toggle-indicator:before,#simpay-form-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv:focus .toggle-indicator:before{box-shadow:0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8)}#simpay-global-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv .toggle-indicator:before,#simpay-form-settings .simpay-metaboxes-wrapper .postbox .simpay-handlediv .toggle-indicator:before{margin-top:4px;width:20px;border-radius:50%;text-indent:-1px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox{background:#fff;border:1px solid #ccd0d4;margin:0 -1px -1px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .hndle,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .hndle{border:0}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox select,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox select{font-weight:400}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox:first-of-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox:first-of-type{margin-top:-1px;border-top-left-radius:4px;border-top-right-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type{margin-bottom:-1px;border-bottom-left-radius:4px;border-bottom-right-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type .simpay-metabox-content,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox:last-of-type .simpay-metabox-content{border-bottom-left-radius:4px;border-bottom-right-radius:4px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2{cursor:pointer;display:flex;align-items:center;padding:10px 0 10px 12px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type{font-size:90%;color:gray;font-weight:normal;text-decoration:none;margin-left:10px}@media screen and (max-width: 782px){#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .simpay-field-type{display:none}}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-handle,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-handle{cursor:move}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 strong,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 strong{font-size:95%;margin-left:8px;display:flex;align-items:center;flex-grow:1}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 svg,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 svg{border-radius:3px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 select,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 select{font-family:sans-serif;max-width:20%;margin:.25em .25em .25em 0}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2.fixed,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2.fixed{cursor:pointer !important}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-menu,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox h2 .dashicons-menu{cursor:move}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions{padding:9px 18px;justify-content:space-between;display:flex;align-items:center}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id{display:flex;align-items:center}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id input,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id input{margin:0 2px 0 5px;width:50px}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id a,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-metabox-content-actions__field-id a{text-decoration:none}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link{color:#a00}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link:hover,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .button-link.simpay-remove-field-link:hover{color:#dc3232}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox table,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox table{border-spacing:0;width:100%}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox table.simpay-inner-table,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox table.simpay-inner-table{border:none;padding:0 1em}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox table tr td,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox table tr td{border-bottom-color:#ccd0d4}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-remove-plan,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-metabox .simpay-remove-plan{color:#a00}#simpay-global-settings .simpay-metaboxes-wrapper .simpay-custom-field-payment-button .dashicons-menu,#simpay-form-settings .simpay-metaboxes-wrapper .simpay-custom-field-payment-button .dashicons-menu{visibility:hidden}#simpay-global-settings .sortable-placeholder,#simpay-form-settings .sortable-placeholder{margin:5px;display:block;min-height:36px}#simpay-global-settings .chosen-container,#simpay-form-settings .chosen-container{min-width:20em;max-width:30em}#simpay-global-settings .simpay-field.simpay-small-text,#simpay-form-settings .simpay-field.simpay-small-text{width:7em}#simpay-global-settings .simpay-field.simpay-medium-text,#simpay-form-settings .simpay-field.simpay-medium-text{width:15em}#simpay-global-settings .simpay-field-radios ul,#simpay-form-settings .simpay-field-radios ul{margin:0}#simpay-global-settings .simpay-field-radios>i,#simpay-form-settings .simpay-field-radios>i{margin-left:5px;vertical-align:middle}#simpay-global-settings ul.simpay-field-radios-inline,#simpay-form-settings ul.simpay-field-radios-inline{margin:0 0 -10px}#simpay-global-settings ul.simpay-field-radios-inline li,#simpay-form-settings ul.simpay-field-radios-inline li{display:inline-block;margin:0 10px 10px 0}#simpay-global-settings ul.simpay-field-radios-inline li:last-child,#simpay-form-settings ul.simpay-field-radios-inline li:last-child{margin-right:0}#simpay-global-settings .simpay-currency-field,#simpay-form-settings .simpay-currency-field{display:flex;align-items:center}>#simpay-global-settings .simpay-currency-field:focus,>#simpay-form-settings .simpay-currency-field:focus{position:relative;z-index:5}#simpay-global-settings .simpay-currency-symbol,#simpay-form-settings .simpay-currency-symbol{margin:0;padding-left:8px;padding-right:8px;line-height:28px;font-size:14px}@media screen and (max-width: 782px){#simpay-global-settings .simpay-currency-symbol,#simpay-form-settings .simpay-currency-symbol{line-height:38px}}#simpay-global-settings .simpay-currency-symbol-left,#simpay-form-settings .simpay-currency-symbol-left{border-top-left-radius:4px;border-bottom-left-radius:4px}#simpay-global-settings .simpay-currency-symbol-right,#simpay-form-settings .simpay-currency-symbol-right{border-top-right-radius:4px;border-bottom-right-radius:4px}#simpay-global-settings div.simpay-currency-symbol,#simpay-form-settings div.simpay-currency-symbol{border-color:#7e8993;border-style:solid;background-color:#fff}#simpay-global-settings select.simpay-currency-symbol,#simpay-form-settings select.simpay-currency-symbol{padding-right:25px}#simpay-global-settings .simpay-currency-symbol-left,#simpay-form-settings .simpay-currency-symbol-left{border-width:1px 0 1px 1px}#simpay-global-settings .simpay-currency-symbol-left+.simpay-field-amount,#simpay-form-settings .simpay-currency-symbol-left+.simpay-field-amount{border-radius:0 4px 4px 0}#simpay-global-settings .simpay-currency-symbol-right,#simpay-form-settings .simpay-currency-symbol-right{border-width:1px 1px 1px 0}#simpay-global-settings .simpay-field-amount,#simpay-form-settings .simpay-field-amount{margin:0;padding-left:8px;padding-right:8px;font-size:14px;width:6em;position:relative;z-index:2;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px}#simpay-global-settings .simpay-error,#simpay-form-settings .simpay-error{color:red}#simpay-global-settings .simpay-docs-link-wrap,#simpay-form-settings .simpay-docs-link-wrap{position:absolute;right:0;bottom:0;color:#666;font-size:13px;font-style:italic;padding:15px 18px}#simpay-global-settings .simpay-docs-link-wrap a .dashicons-editor-help,#simpay-form-settings .simpay-docs-link-wrap a .dashicons-editor-help{color:#666;text-decoration:none;width:17px;height:17px;font-size:17px;padding-left:4px}#simpay-global-settings .simpay-docs-icon,#simpay-form-settings .simpay-docs-icon{color:#666}#simpay-global-settings .simpay-docs-icon,#simpay-global-settings .simpay-docs-icon .dashicons-editor-help,#simpay-form-settings .simpay-docs-icon,#simpay-form-settings .simpay-docs-icon .dashicons-editor-help{text-decoration:none;width:17px;height:17px;font-size:17px}.button.button-primary.simpay-upgrade-btn{background-color:#428bca;border:1px solid #428bca;color:#fff;display:inline-block}.button.button-primary.simpay-upgrade-btn:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px #2d6ca2}.button.button-primary.simpay-upgrade-btn:hover{background-color:#037ad0;border:1px solid #428bca}.simpay-upgrade-btn-subtext{color:#3c434a;font-size:14px;line-height:1.5;text-align:center;margin:40px 0 0;padding:30px 35px 20px;background-color:#fcf9e8;border:3px solid #ebe29a;border-radius:4px;position:relative}.simpay-upgrade-btn-subtext svg{background:#00a32a;fill:#fff;border-radius:50%;border:3px solid #ebe29a;width:28px;height:28px;position:absolute;top:-14px;left:50%;margin-left:-14px}.simpay-upgrade-btn-subtext u{text-decoration:none;font-weight:bold;color:#00a32a}.simpay-upgrade-btn-subtext a{text-decoration:none;display:block;margin-top:6px;font-weight:bold}.post-type-simple-pay #post-body-content{display:none}.simpay-card{margin:0 0 20px;padding:30px;background:#fff;border:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04)}.simpay-card,.simpay-card p{line-height:1.5;font-size:16px}.simpay-card h3{line-height:1.6;font-size:18px;margin:0 0 20px;color:#23282c}.simpay-card p{margin:0 0 20px}.simpay-card p:last-child,.simpay-card ul:last-child{margin-bottom:0}.simpay-card figure{float:right;margin:0 0 30px 30px;max-width:400px}.simpay-card figure iframe,.simpay-card figure img{max-width:100%}.simpay-card figure figcaption{text-align:center}@media screen and (max-width: 782px){.simpay-card figure{margin:0 0 30px;max-width:100%;float:none}}.simpay-doc-suggestions{width:100%;display:flex;flex-wrap:wrap;padding:0}.simpay-doc-suggestion{text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;flex:0 1 33.333%;padding:30px;border-right:1px solid #c3c4c7;box-sizing:border-box}.simpay-doc-suggestion:nth-child(3n){border-right:0}@media screen and (max-width: 782px){.simpay-doc-suggestion{flex:0 1 100%;border-bottom:1px solid #c3c4c7;border-right:0}.simpay-doc-suggestion:last-child{border-bottom:0}}.simpay-doc-suggestion h3{font-size:20px;margin-bottom:10px}.simpay-doc-suggestion p{font-size:15px}.simpay-doc-suggestion .dashicons{font-size:40px;width:40px;height:40px;display:block;margin-bottom:10px}.simpay-doc-suggestion .button-large{font-size:16px}.simpay-addons{display:flex;flex-wrap:wrap;justify-content:space-between;margin:20px 0}.simpay-addon{background:#fff;border:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04);margin-bottom:20px;display:flex;flex-direction:column;flex-basis:calc(33% - 10px);box-sizing:border-box}@media screen and (max-width: 782px){.simpay-addon{flex-basis:100%}}.simpay-addon img{float:left;max-width:75px}.simpay-addon h5{margin:0 0 10px 100px;font-size:16px}.simpay-addon__details{padding:20px;flex:1 0 auto}.simpay-addon__actions{display:flex;align-items:center;justify-content:space-between;flex:0 1 auto;background-color:#f7f7f7;border-top:1px solid #ddd;margin-top:auto;padding:20px;position:relative}.simpay-addon__actions .msg{text-align:center;justify-content:center;display:flex;align-items:center;position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;background-color:#f7f7f7;z-index:3}.simpay-addon .error,.simpay-addon .status-label.status-installed{color:#d63638}.simpay-addon .success,.simpay-addon .status-label.status-active{color:#00a32a}.simpay-addon .addon-desc{margin:0 0 0 100px}.form-table td .simpay-stripe-connect-help{margin:15px 0;display:flex;align-items:center}.form-table td .simpay-stripe-connect-help .dashicons{margin-right:4px}.simpay-currency-field{display:flex;align-items:center}>.simpay-currency-field:focus{position:relative;z-index:5}.simpay-currency-symbol{margin:0;padding-left:8px;padding-right:8px;line-height:28px;font-size:14px}@media screen and (max-width: 782px){.simpay-currency-symbol{line-height:38px}}.simpay-currency-symbol-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.simpay-currency-symbol-right{border-top-right-radius:4px;border-bottom-right-radius:4px}div.simpay-currency-symbol{border-color:#7e8993;border-style:solid;background-color:#fff}select.simpay-currency-symbol{padding-right:25px}.simpay-currency-symbol-left{border-width:1px 0 1px 1px}.simpay-currency-symbol-left+.simpay-field-amount{border-radius:0 4px 4px 0}.simpay-currency-symbol-right{border-width:1px 1px 1px 0}.simpay-field-amount{margin:0;padding-left:8px;padding-right:8px;font-size:14px;width:6em;position:relative;z-index:2;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.simpay-template-explorer-open .show-settings{display:none !important}.simpay-branding-bar{margin:0 -20px;padding:14px 22px;background:#fff;border-bottom:1px solid #c3c4c7;box-shadow:0 1px 1px rgba(0,0,0,.04);width:calc(100% + 20px);display:flex;justify-content:space-between;box-sizing:border-box}.site-health .simpay-branding-bar{display:none}.simpay-branding-bar__title{display:flex;align-items:center;margin:8px 0}.simpay-branding-bar__logo{width:200px}.simpay-branding-bar__divider{color:#dadbdf;font-size:23px;font-weight:400;margin:0 15px}.simpay-branding-bar__actions{display:flex;align-items:center}.simpay-branding-bar__actions>div{margin-left:10px;min-width:40px}.simpay-branding-bar__actions-button{color:#000;cursor:pointer;padding:10px;width:40px;height:40px;background-color:#f3f4f5;border-radius:50%;border:0;box-shadow:none;position:relative;transition:background-color .2s ease;box-sizing:border-box;display:block}.simpay-branding-bar__actions-button:hover{background-color:#e5e5e5}.simpay-branding-bar__actions-button:active,.simpay-branding-bar__actions-button:focus{box-shadow:0 0 0 2px var(--wp-admin-theme-color)}.simpay-branding-bar__actions-button-count{font-weight:600;font-size:10px;line-height:16px;color:#fff;margin:0;background-color:#df2a4a;border-radius:100%;width:16px;height:16px;position:absolute;top:-8px;left:50%;margin-left:-8px}.simpay-branding-bar .wp-heading-inline{font-size:23px;font-weight:400;margin:0}.simpay-branding-bar .page-title-action{font-weight:600;font-size:13px;line-height:normal;cursor:pointer;text-shadow:none;text-decoration:none;margin-left:10px;padding:4px 8px;border:1px solid currentColor;border-radius:2px;background:#f6f7f7}.simpay-landing-zone{text-align:center;max-width:700px;margin:40px auto}.simpay-landing-zone__title{font-size:26px;font-weight:600;margin:0 0 1.5rem;padding:0}.simpay-landing-zone__subtitle{font-size:17px;color:#666;margin:.25rem 0}.simpay-landing-zone__subtitle strong{color:#444}.simpay-landing-zone section{margin:2rem 0}.simpay-landing-zone__empty-state-graphic img{width:425px}.simpay-landing-zone__screenshot>*{vertical-align:middle}.simpay-landing-zone__screenshot-img{display:inline-block;position:relative;width:315px;padding:5px;background-color:#fff;box-shadow:0px 2px 5px 0px rgba(0,0,0,.05);border-radius:3px}.simpay-landing-zone__screenshot-img img{max-width:100%;display:block}.simpay-landing-zone__screenshot-img .hover{position:absolute;opacity:0;height:calc(100% - 10px);width:calc(100% - 10px);top:0;left:0;border:5px solid #fff;background-color:rgba(0,0,0,.15);background-image:url(data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik01MCAwYzI3LjYxNCAwIDUwIDIyLjM4NiA1MCA1MHMtMjIuMzg2IDUwLTUwIDUwUzAgNzcuNjE0IDAgNTAgMjIuMzg2IDAgNTAgMHoiIGZpbGw9IiMxNjg1YjciLz48cGF0aCBkPSJNNzMuMDIzIDY3LjA2N2wtOS4zNDQtOS4zNDRhMi4yNDcgMi4yNDcgMCAwMC0xLjU5My0uNjU2aC0xLjUyOGExOS4zOTkgMTkuMzk5IDAgMDA0LjEyNC0xMS45OTZjMC0xMC43Ny04LjcyNS0xOS40OTUtMTkuNDk0LTE5LjQ5NS0xMC43NjggMC0xOS40OTQgOC43MjYtMTkuNDk0IDE5LjQ5NSAwIDEwLjc2OCA4LjcyNiAxOS40OTMgMTkuNDk0IDE5LjQ5M2ExOS40IDE5LjQgMCAwMDExLjk5Ny00LjEyM3YxLjUyN2MwIC42LjIzNCAxLjE3Mi42NTUgMS41OTRsOS4zNDQgOS4zNDRhMi4yNCAyLjI0IDAgMDAzLjE3OCAwbDIuNjUyLTIuNjUzYTIuMjYgMi4yNiAwIDAwLjAxLTMuMTg2em0tMjcuODM1LTEwYy02LjYyNiAwLTExLjk5Ni01LjM2MS0xMS45OTYtMTEuOTk2IDAtNi42MjcgNS4zNi0xMS45OTcgMTEuOTk2LTExLjk5NyA2LjYyNiAwIDExLjk5NyA1LjM2MSAxMS45OTcgMTEuOTk3IDAgNi42MjYtNS4zNjEgMTEuOTk2LTExLjk5NyAxMS45OTZ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+);background-repeat:no-repeat;background-position:center;background-size:50px;transition:all .3s}.simpay-landing-zone__screenshot-img:hover .hover{opacity:1;transition:all .3s}.simpay-landing-zone__screenshot ul{text-align:left;display:inline-block;margin:0 0 0 30px;list-style-type:none;max-width:calc(100% - 350px)}@media screen and (max-width: 782px){.simpay-landing-zone__screenshot ul{text-align:center;margin:30px auto;max-width:100%;display:block}}.simpay-landing-zone__screenshot li{margin:16px 0;padding:0;font-size:15px;color:#777;position:relative}.simpay-landing-zone__screenshot li:before{content:"";background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjc3NjIgNi40NTYwNUw4LjcyMDMyIDEuNDAwMDVDOC41NzU5OSAxLjI1NTcyIDguMzgzNjQgMS4xNzY1IDguMTc4NTMgMS4xNzY1QzcuOTczMiAxLjE3NjUgNy43ODA5NiAxLjI1NTg0IDcuNjM2NjQgMS40MDAwNUw3LjE3NzYgMS44NTkyQzcuMDMzMzkgMi4wMDMyOSA2Ljk1Mzk1IDIuMTk1NzYgNi45NTM5NSAyLjQwMDk4QzYuOTUzOTUgMi42MDYwOCA3LjAzMzM5IDIuODA1MDQgNy4xNzc2IDIuOTQ5MTRMMTAuMTI3MSA1LjkwNTE2SDAuNzU2MzM0QzAuMzMzODM0IDUuOTA1MTYgMCA2LjIzNTkyIDAgNi42NTg1NFY3LjMwNzY1QzAgNy43MzAyNyAwLjMzMzgzNCA4LjA5NDM4IDAuNzU2MzM0IDguMDk0MzhIMTAuMTYwNkw3LjE3NzcyIDExLjA2NjlDNy4wMzM1MSAxMS4yMTEyIDYuOTU0MDYgMTEuMzk4NSA2Ljk1NDA2IDExLjYwMzdDNi45NTQwNiAxMS44MDg3IDcuMDMzNTEgMTEuOTk4NiA3LjE3NzcyIDEyLjE0MjhMNy42MzY3NSAxMi42MDA1QzcuNzgxMDcgMTIuNzQ0OCA3Ljk3MzMyIDEyLjgyMzUgOC4xNzg2NSAxMi44MjM1QzguMzgzNzUgMTIuODIzNSA4LjU3NjExIDEyLjc0MzggOC43MjA0MyAxMi41OTk1TDEzLjc3NjMgNy41NDM2QzEzLjkyMSA3LjM5ODgyIDE0LjAwMDUgNy4yMDU1NiAxNCA3LjAwMDExQzE0LjAwMDQgNi43OTM5OCAxMy45MjEgNi42MDA2IDEzLjc3NjIgNi40NTYwNVY2LjQ1NjA1WiIgZmlsbD0iIzQyOEJDQSIvPgo8L3N2Zz4K);background-position:center;background-repeat:no-repeat;background-size:14px;width:14px;height:14px;display:inline-block;margin:-3px 5px 0 0;vertical-align:middle}.simpay-landing-zone .button.button-large{font-size:17px;line-height:30px;padding:10px 20px}.simpay-landing-zone__purchased{display:block;margin:15px 0 0}.simpay-upgrade-modal,.simpay-upgrade-upe-modal{padding:0 !important}.simpay-upgrade-modal *,.simpay-upgrade-upe-modal *{box-sizing:border-box}.simpay-upgrade-modal__content,.simpay-upgrade-upe-modal__content{text-align:center;display:flex;flex-direction:column;align-items:center;padding:16px 32px 32px}.simpay-upgrade-modal__content>.dashicons,.simpay-upgrade-upe-modal__content>.dashicons{color:#333;font-size:48px;width:48px;height:48px}.simpay-upgrade-modal__title,.simpay-upgrade-upe-modal__title{font-size:22px;line-height:1.5;display:block;margin:12px 0 0}.simpay-upgrade-modal__description,.simpay-upgrade-upe-modal__description{color:#777;font-size:16px;margin:16px 0 24px}.simpay-upgrade-modal__description strong,.simpay-upgrade-upe-modal__description strong{color:#333}.simpay-upgrade-modal__discount,.simpay-upgrade-upe-modal__discount{font-size:15px;text-align:center;margin:32px -32px -32px;padding:24px 40px;background-color:#fcf9e8;position:relative}.simpay-upgrade-modal__discount svg,.simpay-upgrade-upe-modal__discount svg{background:#00a32a;fill:#fff;border-radius:50%;border:4px solid #fff;width:32px;height:32px;position:absolute;top:-16px;left:50%;margin-left:-16px}.simpay-upgrade-modal__discount u,.simpay-upgrade-upe-modal__discount u{text-decoration:none;font-weight:bold;color:#00a32a}.simpay-upgrade-modal .button-large,.simpay-upgrade-upe-modal .button-large{font-size:16px;font-weight:bold;margin:0 0 15px;padding:8px 30px !important;height:auto}.simpay-teaser-float{margin:50px;position:relative}.simpay-teaser-float__card{text-align:center;padding:30px;background:#fff;border-radius:4px;box-shadow:0 0 30px 15px rgba(0,0,0,.18);position:relative;z-index:2}#poststuff .simpay-teaser-float h2,.simpay-teaser-float h2{font-size:24px;font-weight:600;margin:0;padding:0}.simpay-teaser-float p{font-size:15px;line-height:1.35;color:#666}.simpay-teaser-float p strong{color:#444}.simpay-teaser-float ul{text-align:left;display:inline-block;margin:-10px 0 20px;list-style-type:none}.simpay-teaser-float li{margin:16px 0;padding:0 0 0 24px;font-size:15px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjc3NjIgNi40NTYwNUw4LjcyMDMyIDEuNDAwMDVDOC41NzU5OSAxLjI1NTcyIDguMzgzNjQgMS4xNzY1IDguMTc4NTMgMS4xNzY1QzcuOTczMiAxLjE3NjUgNy43ODA5NiAxLjI1NTg0IDcuNjM2NjQgMS40MDAwNUw3LjE3NzYgMS44NTkyQzcuMDMzMzkgMi4wMDMyOSA2Ljk1Mzk1IDIuMTk1NzYgNi45NTM5NSAyLjQwMDk4QzYuOTUzOTUgMi42MDYwOCA3LjAzMzM5IDIuODA1MDQgNy4xNzc2IDIuOTQ5MTRMMTAuMTI3MSA1LjkwNTE2SDAuNzU2MzM0QzAuMzMzODM0IDUuOTA1MTYgMCA2LjIzNTkyIDAgNi42NTg1NFY3LjMwNzY1QzAgNy43MzAyNyAwLjMzMzgzNCA4LjA5NDM4IDAuNzU2MzM0IDguMDk0MzhIMTAuMTYwNkw3LjE3NzcyIDExLjA2NjlDNy4wMzM1MSAxMS4yMTEyIDYuOTU0MDYgMTEuMzk4NSA2Ljk1NDA2IDExLjYwMzdDNi45NTQwNiAxMS44MDg3IDcuMDMzNTEgMTEuOTk4NiA3LjE3NzcyIDEyLjE0MjhMNy42MzY3NSAxMi42MDA1QzcuNzgxMDcgMTIuNzQ0OCA3Ljk3MzMyIDEyLjgyMzUgOC4xNzg2NSAxMi44MjM1QzguMzgzNzUgMTIuODIzNSA4LjU3NjExIDEyLjc0MzggOC43MjA0MyAxMi41OTk1TDEzLjc3NjMgNy41NDM2QzEzLjkyMSA3LjM5ODgyIDE0LjAwMDUgNy4yMDU1NiAxNCA3LjAwMDExQzE0LjAwMDQgNi43OTM5OCAxMy45MjEgNi42MDA2IDEzLjc3NjIgNi40NTYwNVY2LjQ1NjA1WiIgZmlsbD0iIzQyOEJDQSIvPgo8L3N2Zz4K);background-position:left 3px;background-repeat:no-repeat;background-size:14px;color:#777}.simpay-teaser-float .button.button-large{font-size:17px;line-height:30px;padding:10px 20px}.simpay-teaser-float .simpay-notice-dismiss{position:absolute;top:0;right:0;font-size:20px;color:#666;font-weight:bold;text-decoration:none;margin-left:5px;padding:6px 10px;z-index:2}.simpay-teaser-float .simpay-notice-dismiss:hover,.simpay-teaser-float .simpay-notice-dismiss:active,.simpay-teaser-float .simpay-notice-dismiss:focus{color:#666;text-decoration:none}.simpay-teaser-float:after,.simpay-teaser-float:before{opacity:.75;z-index:0;content:"";position:absolute;left:-30px;right:-30px;top:-35px;width:calc(100% + 60px);height:170px;background-image:linear-gradient(#ddd, #ddd),linear-gradient(#eee, #eee),linear-gradient(#ddd, #ddd),linear-gradient(#eee, #eee);background-repeat:no-repeat;background-size:100% 20px,100% 40px,100% 20px,100% 40px;background-position:0 0,0 30px,0 100px,0 130px}.simpay-teaser-float:before{top:170px}.simpay-teaser-float .simpay-upgrade-btn-subtext{margin:24px -30px -30px;padding:32px 40px;border-bottom-left-radius:2px;border-bottom-right-radius:2px;border:0}.simpay-teaser-float .simpay-upgrade-btn-subtext svg{border-color:#fff}.simpay-teaser-float--inline{margin:40px 30px 30px}.simpay-teaser-float--inline .simpay-teaser-float__card{padding:30px;box-shadow:0 0 12px 6px rgba(0,0,0,.16)}#poststuff .simpay-teaser-float--inline h2,.simpay-teaser-float--inline h2{font-size:20px}.simpay-teaser-float--inline p{font-size:15px;margin:.75rem 0}.simpay-teaser-float--inline .button.button-large{font-size:15px;line-height:24px;margin:1rem 0;padding:8px 14px;display:inline-block}.simpay-teaser-float--inline:before,.simpay-teaser-float--inline:after{opacity:.6}.simpay-teaser-float--inline:before{display:none}.simpay-teaser-float--inline:after{top:15px}.simpay-form-settings-notice{font-weight:normal;color:#1d2327;position:relative;margin:18px 18px 0;padding:14px;border-radius:4px;background:#f5f5ff}.simpay-form-settings-notice a{color:#635aff}.simpay-form-settings-notice .simpay-external-link .dashicons-external{margin:1px 0 0 2px}.simpay-form-settings-notice strong{font-size:14px}.simpay-form-settings-notice p{margin:5px 0 0 23px}.simpay-form-settings-notice .simpay-notice-dismiss{font-size:20px;color:#b0b0f0;font-weight:bold;line-height:1;position:absolute;top:0;right:5px;text-decoration:none;padding:0 5px;z-index:2}.simpay-form-settings-notice .simpay-notice-dismiss:hover,.simpay-form-settings-notice .simpay-notice-dismiss:active,.simpay-form-settings-notice .simpay-notice-dismiss:focus{color:#9191ef;text-decoration:none;background:none}.simpay-settings .simpay-form-settings-notice{border:2px solid #645aff;box-shadow:0 1px 1px rgba(0,0,0,.04)}.post-type-simple-pay .lity{z-index:999999999;padding:20px}.post-type-simple-pay .lity-close{margin:10px}.post-type-simple-pay .lity-content{max-width:80vw;max-height:80vh}.post-type-simple-pay .lity-content img{max-height:80vh !important;max-width:80vw !important}#wpsp-api-keys-row-hide{display:none}.post-type-simple-pay #post-preview:not(.simpay-preview-button){display:none}.simpay-license-field-wrapper{margin:20px 0}#simpay-settings-license-key-license-key{margin:0}#simpay-settings-license-key-license-key[readonly]{background:#fff}.simpay-license-message__loading,.simpay-license-field{display:flex;align-items:center}.wp-core-ui .button.simpay-license-button.button-primary,.wp-core-ui .button.simpay-license-button.button-secondary{font-size:1rem;line-height:1;margin:0 8px;padding:13px}.simpay-license-message{font-size:15px;margin-top:8px}.simpay-license-message--valid{color:#15803d}.simpay-license-message--invalid{color:#b91c1c}.simpay-recaptcha-payment-form-feedback .dashicons{margin-top:-1px;display:inline-block}.simpay-recaptcha-payment-form-feedback .dashicons-update-alt{animation:rotation 2s infinite linear}.simpay-form-builder-inset-settings{margin:3px 0 0 24px}fieldset.simpay-form-builder-inset-settings>*:not(legend){margin:5px 0 8px}.simpay-form-builder-inset-settings>*:last-child{margin-bottom:0}.simpay-form-builder-inventory-control{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;padding:6px 8px;box-shadow:0 0 0 rgba(0,0,0,0);transition:box-shadow .1s linear;border-radius:2px;border:1px solid #757575;font-size:16px;line-height:normal;margin:0;padding:0;width:auto;display:inline-flex;align-items:center}@media(prefers-reduced-motion: reduce){.simpay-form-builder-inventory-control{transition-duration:0s;transition-delay:0s}}@media(min-width: 600px){.simpay-form-builder-inventory-control{font-size:13px;line-height:normal}}.simpay-form-builder-inventory-control:focus{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-inventory-control::-webkit-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-inventory-control::-moz-placeholder{opacity:1;color:rgba(30,30,30,.62)}.simpay-form-builder-inventory-control:-ms-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-inventory-control:focus-within{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-inventory-control input[type=number]{border:0;box-shadow:none;background:none;border-radius:0;width:75px}.simpay-form-builder-inventory-control input[type=number]:focus{border:0;box-shadow:none;outline:0}.simpay-form-builder-inventory-control+label{margin-left:8px}.simpay-form-builder-inventory-control__initial{color:#757575;font-size:16px;line-height:normal;padding-right:8px}@media(min-width: 600px){.simpay-form-builder-inventory-control__initial{font-size:13px;line-height:normal}}.simpay-form-builder-inventory-control__initial:before{content:"/";display:inline-block;vertical-align:top;margin-top:-1px}.simpay-form-builder-purchase-restrictions__restriction-item{display:flex;align-items:center;margin-top:8px;margin-bottom:8px}.simpay-form-builder-purchase-restrictions__restriction-item label{margin-left:8px}.simpay-form-builder-purchase-restrictions__restriction-item-datetime{display:flex;align-items:center}.simpay-form-builder-purchase-restrictions__restriction-item-datetime>*{margin-right:10px}.simpay-form-builder-purchase-restrictions__restriction-item-datetime span{color:#757575}.simpay-form-builder-fee-recovery__amounts{display:flex;align-items:center;margin-top:5px}.simpay-form-builder-fee-recovery ::-webkit-input-placeholder{color:#b7bec7}.simpay-form-builder-fee-recovery ::-moz-placeholder{color:#b7bec7;opacity:1}.simpay-form-builder-fee-recovery :-ms-input-placeholder{color:#b7bec7}.simpay-form-builder-fee-recovery>*{margin-right:8px}.simpay-form-builder-fee-percent-control{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;padding:6px 8px;box-shadow:0 0 0 rgba(0,0,0,0);transition:box-shadow .1s linear;border-radius:2px;border:1px solid #757575;font-size:16px;line-height:normal;margin:0;padding:0;width:auto;display:inline-flex;align-items:center}@media(prefers-reduced-motion: reduce){.simpay-form-builder-fee-percent-control{transition-duration:0s;transition-delay:0s}}@media(min-width: 600px){.simpay-form-builder-fee-percent-control{font-size:13px;line-height:normal}}.simpay-form-builder-fee-percent-control:focus{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-fee-percent-control::-webkit-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-fee-percent-control::-moz-placeholder{opacity:1;color:rgba(30,30,30,.62)}.simpay-form-builder-fee-percent-control:-ms-input-placeholder{color:rgba(30,30,30,.62)}.simpay-form-builder-fee-percent-control:focus-within{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color);outline:2px solid rgba(0,0,0,0)}.simpay-form-builder-fee-percent-control input[type=number]{border:0;box-shadow:none;background:none;border-radius:0;width:75px}.simpay-form-builder-fee-percent-control input[type=number]:focus{border:0;box-shadow:none;outline:0}.simpay-form-builder-fee-percent-control+label{margin-left:8px}.simpay-form-builder-fee-percent-control__suffix{color:#757575;font-size:16px;line-height:normal;padding-right:8px}@media(min-width: 600px){.simpay-form-builder-fee-percent-control__suffix{font-size:13px;line-height:normal}}.simpay-form-builder-automations__cta{margin:24px 0 6px;text-align:center}.simpay-form-builder-automator{padding:16px 4px 26px !important}.simpay-form-builder-automator,.simpay-form-builder-automator *{box-sizing:border-box}.simpay-form-builder-automator input[type=search]{font-size:15px;padding:2px 8px}.simpay-form-builder-automator__integrations{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));grid-column-gap:16px;grid-row-gap:16px;margin:18px -18px 0;padding:0 18px 10px;width:calc(100% + 36px);max-height:400px;overflow-y:scroll}.simpay-form-builder-automator__integrations-integration{color:initial;text-align:center;text-decoration:none;background:#fff;border:1px solid #eee;border-radius:4px;box-shadow:0 1px 3px 0 rgba(0,0,0,.03);transition:all ease-in .15s}.simpay-form-builder-automator__integrations-integration img{display:block;margin:16px auto;height:50px}.simpay-form-builder-automator__integrations-integration span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:8px 16px;display:block;border-top:1px solid #eee}.simpay-form-builder-automator__integrations-integration:hover{cursor:pointer;border-color:#ddd;box-shadow:0 2px 4px 0 rgba(0,0,0,.06)}body.site-health #wpbody-content,body.post-type-simple-pay #wpbody-content{padding-bottom:200px}#wpfooter .simpay-footer-promotion{text-align:center;font-weight:400;font-size:13px;line-height:16px;color:#787c82;padding:20px 0 30px 0;margin-bottom:20px}#wpfooter .simpay-footer-promotion p{font-weight:600}#wpfooter .simpay-footer-promotion-links,#wpfooter .simpay-footer-promotion-social{display:flex;justify-content:center;align-items:center}#wpfooter .simpay-footer-promotion-links{margin:9px 0 0}#wpfooter .simpay-footer-promotion-links span{color:#c3c4c7;padding:0 7px}#wpfooter .simpay-footer-promotion-social{margin:10px 0 0 0;gap:10px}#wpfooter .simpay-footer-promotion-social li{margin-bottom:0}#wpfooter .simpay-footer-promotion-social li:hover path{fill:#50575e}#wpfooter .simpay-footer-promotion-social a{display:block;height:16px}.simpay-settings-is_upe td{background:#fff;border:1px solid #c3c4c7;border-left-width:4px;border-left-color:#2271b1;box-shadow:0 1px 1px rgba(0,0,0,.04);padding:12px 18px} diff --git a/includes/core/class-rest-api.php b/includes/core/class-rest-api.php index 535af3a6..cd2d0eac 100644 --- a/includes/core/class-rest-api.php +++ b/includes/core/class-rest-api.php @@ -53,8 +53,7 @@ public function register_routes() { $controllers = apply_filters( 'simpay_rest_api_controllers', $controllers ); foreach ( $controllers as $controller ) { - $this->$controller = new $controller(); - $this->$controller->register_routes(); + ( new $controller() )->register_routes(); } } diff --git a/includes/core/functions/admin.php b/includes/core/functions/admin.php index bd34dc9c..416c8844 100644 --- a/includes/core/functions/admin.php +++ b/includes/core/functions/admin.php @@ -265,45 +265,6 @@ function simpay_is_admin_screen() { return false; } -/** - * Link with HTML to docs site article & GA campaign values. - * - * @since 3.0.0 - * @since 4.4.0 Rename $ga_content to $utm_medium to work with simpay_ga_url(). - * - * @param string $text Link text. - * @param string $slug Link slug. - * @param string $utm_medium utm_medium link parameter. - * @param bool $plain If the link should have an icon. Default false. - * @return string - */ -function simpay_docs_link( $text, $slug, $utm_medium, $plain = false ) { - - // Articles on docs site currently require a base slug themselves. - $base_url = 'https://wpsimplepay.com/doc/'; - - // Ensure ending slash is included for consistency. - $url = trailingslashit( $base_url . $slug ); - - // If $plain is true we want to return ONLY the link, otherwise return the full HTML. - // Add GA campaign params in both cases. - if ( $plain ) { - - return simpay_ga_url( $url, $utm_medium, $text ); - - } else { - - $html = ''; - $html .= ''; - - return $html; - } -} - /** * Output the copy/paste shortcode on the forms page. * diff --git a/includes/core/functions/shared.php b/includes/core/functions/shared.php index bc33e20c..9f283831 100644 --- a/includes/core/functions/shared.php +++ b/includes/core/functions/shared.php @@ -1904,3 +1904,43 @@ function simpay_is_upe() { function simpay_list_separator() { return apply_filters( 'simpay_list_separator', ',' ); } + +/** + * Link with HTML to docs site article & GA campaign values. + * + * @since 3.0.0 + * @since 4.4.0 Rename $ga_content to $utm_medium to work with simpay_ga_url(). + * @since 4.7.10 Available globally (not just admin). + * + * @param string $text Link text. + * @param string $slug Link slug. + * @param string $utm_medium utm_medium link parameter. + * @param bool $plain If the link should have an icon. Default false. + * @return string + */ +function simpay_docs_link( $text, $slug, $utm_medium, $plain = false ) { + + // Articles on docs site currently require a base slug themselves. + $base_url = 'https://wpsimplepay.com/doc/'; + + // Ensure ending slash is included for consistency. + $url = trailingslashit( $base_url . $slug ); + + // If $plain is true we want to return ONLY the link, otherwise return the full HTML. + // Add GA campaign params in both cases. + if ( $plain ) { + + return simpay_ga_url( $url, $utm_medium, $text ); + + } else { + + $html = ''; + $html .= ''; + + return $html; + } +} diff --git a/includes/core/recaptcha/index.php b/includes/core/recaptcha/index.php index e1b1ccc2..26763fd8 100644 --- a/includes/core/recaptcha/index.php +++ b/includes/core/recaptcha/index.php @@ -645,6 +645,10 @@ function validate_hcaptcha_order_submit( $request, $form ) { * @return void */ function maybe_add_inbox_notification() { + if ( ! simpay_is_livemode() ) { + return; + } + // Notification Inbox is only available in WP 5.7+. global $wp_version; diff --git a/includes/core/settings/class-setting-input.php b/includes/core/settings/class-setting-input.php index c9044638..a77704c9 100644 --- a/includes/core/settings/class-setting-input.php +++ b/includes/core/settings/class-setting-input.php @@ -69,6 +69,14 @@ class Setting_Input extends Setting { */ public $step; + /** + * Classes. + * + * @since 4.7.10 + * @var array + */ + public $classes; + /** * Additional input attributes (readonly, disabled, etc) * @@ -90,6 +98,7 @@ class Setting_Input extends Setting { * @type string $min Setting minimum. * @type string $max Setting maximum. * @type string $step Setting step. + * @type array $classes * } */ public function __construct( $args ) { diff --git a/includes/core/settings/class-setting.php b/includes/core/settings/class-setting.php index e151ae65..6a2d5aac 100644 --- a/includes/core/settings/class-setting.php +++ b/includes/core/settings/class-setting.php @@ -97,8 +97,9 @@ class Setting { * Setting toggles. * * @since 4.0.0 - * @var array + * @var array> */ + public $toggles = array(); /** * Constructs the settings section. @@ -130,7 +131,7 @@ public function __construct( $args ) { 'section' => '', 'subsection' => '', 'label' => '', - 'schema' => array(), + 'schema' => array(), 'priority' => 10, 'output' => null, 'toggles' => array(), diff --git a/includes/core/settings/register-general.php b/includes/core/settings/register-general.php index 87b4cf0c..ce583d52 100644 --- a/includes/core/settings/register-general.php +++ b/includes/core/settings/register-general.php @@ -270,13 +270,11 @@ function register_advanced_settings( $settings ) { : wpautop( wp_kses( sprintf( - /* translators: %3$s Opening tag, do not translate. %4$s Closing tag, do not translate. */ + /* translators: %1$s Opening tag, do not translate. %2$s Closing tag, do not translate. */ __( - 'Enable the new, smarter payment experience. Have questions about opting in or managing customizations? %3$sContact us%4$s', + 'Enable the new, smarter payment experience. Have questions about opting in or managing customizations? %1$sContact us%2$s', 'stripe' ), - '', - '', '', Utils\get_external_link_markup() . '' ), diff --git a/includes/core/utils/abstract-collection.php b/includes/core/utils/abstract-collection.php index f15bd9ef..71076ecd 100644 --- a/includes/core/utils/abstract-collection.php +++ b/includes/core/utils/abstract-collection.php @@ -20,7 +20,7 @@ * @since 3.8.0 * @abstract */ -abstract class Collection extends \ArrayObject { +abstract class Collection { /** * Array of registry items. @@ -110,59 +110,4 @@ public function _reset_items() { } } - /** - * Determines whether an item exists. - * - * Defined only for compatibility with ArrayAccess, use has_item() directly. - * - * @since 3.8.0 - * - * @param string $offset Item ID. - * @return bool True if the item exists, false on failure. - */ - public function offsetExists( $offset ) { - return $this->has_item( $offset ); - } - - /** - * Retrieves an item by its ID. - * - * Defined only for compatibility with ArrayAccess, use get_item() directly. - * - * @since 3.8.0 - * - * @param mixed $offset Item ID. - * @return mixed|false Item attributes if registered, otherwise false. - */ - public function offsetGet( $offset ) { - return $this->get_item( $offset ); - } - - /** - * Adds/overwrites an item in the registry. - * - * Defined only for compatibility with ArrayAccess, use add_item() directly. - * - * @since 3.8.0 - * - * @param string $offset Item ID. - * @param mixed $value Item attributes. - */ - public function offsetSet( $offset, $value ) { - $this->add_item( $offset, $value ); - } - - /** - * Removes an item from the registry. - * - * Defined only for compatibility with ArrayAccess, use remove_item() directly. - * - * @since 3.8.0 - * - * @param string $offset Item ID. - */ - public function offsetUnset( $offset ) { - $this->remove_item( $offset ); - } - } diff --git a/includes/core/utils/migrations/class-single-migration.php b/includes/core/utils/migrations/class-single-migration.php index 2c2d38f8..9d5031fa 100644 --- a/includes/core/utils/migrations/class-single-migration.php +++ b/includes/core/utils/migrations/class-single-migration.php @@ -23,6 +23,12 @@ */ class Single_Migration extends Migration { + /** + * @since 4.7.10 + * @var bool + */ + public $automatic; + /** * Constructs the migration. * diff --git a/lib/woocommerce/action-scheduler/action-scheduler.php b/lib/woocommerce/action-scheduler/action-scheduler.php index 859e4c9a..960e439b 100644 --- a/lib/woocommerce/action-scheduler/action-scheduler.php +++ b/lib/woocommerce/action-scheduler/action-scheduler.php @@ -5,8 +5,11 @@ * Description: A robust scheduling library for use in WordPress plugins. * Author: Automattic * Author URI: https://automattic.com/ - * Version: 3.4.0 + * Version: 3.6.2 * License: GPLv3 + * Tested up to: 6.3 + * Requires at least: 5.2 + * Requires PHP: 5.6 * * Copyright 2019 Automattic, Inc. (https://automattic.com/contact/) * @@ -26,27 +29,27 @@ * @package ActionScheduler */ -if ( ! function_exists( 'action_scheduler_register_3_dot_4_dot_0' ) && function_exists( 'add_action' ) ) { +if ( ! function_exists( 'action_scheduler_register_3_dot_6_dot_2' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION. if ( ! class_exists( 'ActionScheduler_Versions', false ) ) { require_once __DIR__ . '/classes/ActionScheduler_Versions.php'; add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); } - add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_4_dot_0', 0, 0 ); + add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_6_dot_2', 0, 0 ); // WRCS: DEFINED_VERSION. /** * Registers this version of Action Scheduler. */ - function action_scheduler_register_3_dot_4_dot_0() { + function action_scheduler_register_3_dot_6_dot_2() { // WRCS: DEFINED_VERSION. $versions = ActionScheduler_Versions::instance(); - $versions->register( '3.4.0', 'action_scheduler_initialize_3_dot_4_dot_0' ); + $versions->register( '3.6.2', 'action_scheduler_initialize_3_dot_6_dot_2' ); // WRCS: DEFINED_VERSION. } /** * Initializes this version of Action Scheduler. */ - function action_scheduler_initialize_3_dot_4_dot_0() { + function action_scheduler_initialize_3_dot_6_dot_2() { // WRCS: DEFINED_VERSION. // A final safety check is required even here, because historic versions of Action Scheduler // followed a different pattern (in some unusual cases, we could reach this point and the // ActionScheduler class is already defined—so we need to guard against that). @@ -58,7 +61,7 @@ function action_scheduler_initialize_3_dot_4_dot_0() { // Support usage in themes - load this version if no plugin has loaded a version yet. if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) { - action_scheduler_initialize_3_dot_4_dot_0(); + action_scheduler_initialize_3_dot_6_dot_2(); // WRCS: DEFINED_VERSION. do_action( 'action_scheduler_pre_theme_init' ); ActionScheduler_Versions::initialize_latest_version(); } diff --git a/lib/woocommerce/action-scheduler/changelog.txt b/lib/woocommerce/action-scheduler/changelog.txt index 4bb2650b..239ae058 100644 --- a/lib/woocommerce/action-scheduler/changelog.txt +++ b/lib/woocommerce/action-scheduler/changelog.txt @@ -1,5 +1,79 @@ *** Changelog *** += 3.6.2 - 2023-08-09 = +* Add guidance about passing arguments. +* Atomic option locking. +* Improve bulk delete handling. +* Include database error in the exception message. +* Tweak - WP 6.3 compatibility. + += 3.6.1 - 2023-06-14 = +* Document new optional `$priority` arg for various API functions. +* Document the new `--exclude-groups` WP CLI option. +* Document the new `action_scheduler_init` hook. +* Ensure actions within each claim are executed in the expected order. +* Fix incorrect text domain. +* Remove SHOW TABLES usage when checking if tables exist. + += 3.6.0 - 2023-05-10 = +* Add $unique parameter to function signatures. +* Add a cast-to-int for extra safety before forming new DateTime object. +* Add a hook allowing exceptions for consistently failing recurring actions. +* Add action priorities. +* Add init hook. +* Always raise the time limit. +* Bump minimatch from 3.0.4 to 3.0.8. +* Bump yaml from 2.2.1 to 2.2.2. +* Defensive coding relating to gaps in declared schedule types. +* Do not process an action if it cannot be set to `in-progress`. +* Filter view labels (status names) should be translatable | #919. +* Fix WPCLI progress messages. +* Improve data-store initialization flow. +* Improve error handling across all supported PHP versions. +* Improve logic for flushing the runtime cache. +* Support exclusion of multiple groups. +* Update lint-staged and Node/NPM requirements. +* add CLI clean command. +* add CLI exclude-group filter. +* exclude past-due from list table all filter count. +* throwing an exception if as_schedule_recurring_action interval param is not of type integer. + += 3.5.4 - 2023-01-17 = +* Add pre filters during action registration. +* Async scheduling. +* Calculate timeouts based on total actions. +* Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`. +* Fetch action in memory first before releasing claim to avoid deadlock. +* PHP 8.2: declare property to fix creation of dynamic property warning. +* PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead". +* Prevent `undefined variable` warning for `$num_pastdue_actions`. + += 3.5.3 - 2022-11-09 = +* Query actions with partial match. + += 3.5.2 - 2022-09-16 = +* Fix - erroneous 3.5.1 release. + += 3.5.1 - 2022-09-13 = +* Maintenance on A/S docs. +* fix: PHP 8.2 deprecated notice. + += 3.5.0 - 2022-08-25 = +* Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable. +* Add - A warning when there are past-due actions. +* Enhancement - Added the ability to schedule unique actions via an atomic operation. +* Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+). +* Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled. +* Enhancement - Adds a new "Past Due" view to the scheduled actions list table. + += 3.4.2 - 2022-06-08 = +* Fix - Change the include for better linting. +* Fix - update: Added Action scheduler completed action hook. + += 3.4.1 - 2022-05-24 = +* Fix - Change the include for better linting. +* Fix - Fix the documented return type. + = 3.4.0 - 2021-10-29 = * Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771 * Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755 diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_ActionFactory.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_ActionFactory.php index 545277f8..2fd46a73 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_ActionFactory.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_ActionFactory.php @@ -6,27 +6,33 @@ class ActionScheduler_ActionFactory { /** - * @param string $status The action's status in the data store - * @param string $hook The hook to trigger when this action runs - * @param array $args Args to pass to callbacks when the hook is triggered - * @param ActionScheduler_Schedule $schedule The action's schedule - * @param string $group A group to put the action in + * Return stored actions for given params. * - * @return ActionScheduler_Action An instance of the stored action + * @param string $status The action's status in the data store. + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass to callbacks when the hook is triggered. + * @param ActionScheduler_Schedule $schedule The action's schedule. + * @param string $group A group to put the action in. + * @param int $priority The action priority. + * + * @return ActionScheduler_Action An instance of the stored action. */ public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) { + // The 6th parameter ($priority) is not formally declared in the method signature to maintain compatibility with + // third-party subclasses created before this param was added. + $priority = func_num_args() >= 6 ? (int) func_get_arg( 5 ) : 10; switch ( $status ) { - case ActionScheduler_Store::STATUS_PENDING : + case ActionScheduler_Store::STATUS_PENDING: $action_class = 'ActionScheduler_Action'; break; - case ActionScheduler_Store::STATUS_CANCELED : + case ActionScheduler_Store::STATUS_CANCELED: $action_class = 'ActionScheduler_CanceledAction'; if ( ! is_null( $schedule ) && ! is_a( $schedule, 'ActionScheduler_CanceledSchedule' ) && ! is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) { $schedule = new ActionScheduler_CanceledSchedule( $schedule->get_date() ); } break; - default : + default: $action_class = 'ActionScheduler_FinishedAction'; break; } @@ -34,99 +40,164 @@ public function get_stored_action( $status, $hook, array $args = array(), Action $action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group ); $action = new $action_class( $hook, $args, $schedule, $group ); + $action->set_priority( $priority ); /** * Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group. * - * @param ActionScheduler_Action $action The instantiated action. - * @param string $hook The instantiated action's hook. - * @param array $args The instantiated action's args. + * @param ActionScheduler_Action $action The instantiated action. + * @param string $hook The instantiated action's hook. + * @param array $args The instantiated action's args. * @param ActionScheduler_Schedule $schedule The instantiated action's schedule. - * @param string $group The instantiated action's group. + * @param string $group The instantiated action's group. + * @param int $priority The action priority. */ - return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group ); + return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group, $priority ); } /** * Enqueue an action to run one time, as soon as possible (rather a specific scheduled time). * - * This method creates a new action with the NULLSchedule. This schedule maps to a MySQL datetime string of - * 0000-00-00 00:00:00. This is done to create a psuedo "async action" type that is fully backward compatible. - * Existing queries to claim actions claim by date, meaning actions scheduled for 0000-00-00 00:00:00 will - * always be claimed prior to actions scheduled for a specific date. This makes sure that any async action is - * given priority in queue processing. This has the added advantage of making sure async actions can be - * claimed by both the existing WP Cron and WP CLI runners, as well as a new async request runner. + * This method creates a new action using the NullSchedule. In practice, this results in an action scheduled to + * execute "now". Therefore, it will generally run as soon as possible but is not prioritized ahead of other actions + * that are already past-due. * - * @param string $hook The hook to trigger when this action runs - * @param array $args Args to pass when the hook is triggered - * @param string $group A group to put the action in + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param string $group A group to put the action in. * - * @return int The ID of the stored action + * @return int The ID of the stored action. */ public function async( $hook, $args = array(), $group = '' ) { + return $this->async_unique( $hook, $args, $group, false ); + } + + /** + * Same as async, but also supports $unique param. + * + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param string $group A group to put the action in. + * @param bool $unique Whether to ensure the action is unique. + * + * @return int The ID of the stored action. + */ + public function async_unique( $hook, $args = array(), $group = '', $unique = true ) { $schedule = new ActionScheduler_NullSchedule(); - $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); - return $this->store( $action ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $unique ? $this->store_unique_action( $action, $unique ) : $this->store( $action ); } /** - * @param string $hook The hook to trigger when this action runs - * @param array $args Args to pass when the hook is triggered - * @param int $when Unix timestamp when the action will run - * @param string $group A group to put the action in + * Create single action. * - * @return int The ID of the stored action + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param int $when Unix timestamp when the action will run. + * @param string $group A group to put the action in. + * + * @return int The ID of the stored action. */ public function single( $hook, $args = array(), $when = null, $group = '' ) { - $date = as_get_datetime_object( $when ); + return $this->single_unique( $hook, $args, $when, $group, false ); + } + + /** + * Create single action only if there is no pending or running action with same name and params. + * + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param int $when Unix timestamp when the action will run. + * @param string $group A group to put the action in. + * @param bool $unique Whether action scheduled should be unique. + * + * @return int The ID of the stored action. + */ + public function single_unique( $hook, $args = array(), $when = null, $group = '', $unique = true ) { + $date = as_get_datetime_object( $when ); $schedule = new ActionScheduler_SimpleSchedule( $date ); - $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); - return $this->store( $action ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $unique ? $this->store_unique_action( $action ) : $this->store( $action ); } /** * Create the first instance of an action recurring on a given interval. * - * @param string $hook The hook to trigger when this action runs - * @param array $args Args to pass when the hook is triggered - * @param int $first Unix timestamp for the first run - * @param int $interval Seconds between runs - * @param string $group A group to put the action in + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param int $first Unix timestamp for the first run. + * @param int $interval Seconds between runs. + * @param string $group A group to put the action in. * - * @return int The ID of the stored action + * @return int The ID of the stored action. */ public function recurring( $hook, $args = array(), $first = null, $interval = null, $group = '' ) { - if ( empty($interval) ) { - return $this->single( $hook, $args, $first, $group ); + return $this->recurring_unique( $hook, $args, $first, $interval, $group, false ); + } + + /** + * Create the first instance of an action recurring on a given interval only if there is no pending or running action with same name and params. + * + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param int $first Unix timestamp for the first run. + * @param int $interval Seconds between runs. + * @param string $group A group to put the action in. + * @param bool $unique Whether action scheduled should be unique. + * + * @return int The ID of the stored action. + */ + public function recurring_unique( $hook, $args = array(), $first = null, $interval = null, $group = '', $unique = true ) { + if ( empty( $interval ) ) { + return $this->single_unique( $hook, $args, $first, $group, $unique ); } - $date = as_get_datetime_object( $first ); + $date = as_get_datetime_object( $first ); $schedule = new ActionScheduler_IntervalSchedule( $date, $interval ); - $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); - return $this->store( $action ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $unique ? $this->store_unique_action( $action ) : $this->store( $action ); } /** * Create the first instance of an action recurring on a Cron schedule. * - * @param string $hook The hook to trigger when this action runs - * @param array $args Args to pass when the hook is triggered - * @param int $base_timestamp The first instance of the action will be scheduled + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param int $base_timestamp The first instance of the action will be scheduled * to run at a time calculated after this timestamp matching the cron * expression. This can be used to delay the first instance of the action. - * @param int $schedule A cron definition string - * @param string $group A group to put the action in + * @param int $schedule A cron definition string. + * @param string $group A group to put the action in. * - * @return int The ID of the stored action + * @return int The ID of the stored action. */ public function cron( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '' ) { - if ( empty($schedule) ) { - return $this->single( $hook, $args, $base_timestamp, $group ); + return $this->cron_unique( $hook, $args, $base_timestamp, $schedule, $group, false ); + } + + + /** + * Create the first instance of an action recurring on a Cron schedule only if there is no pending or running action with same name and params. + * + * @param string $hook The hook to trigger when this action runs. + * @param array $args Args to pass when the hook is triggered. + * @param int $base_timestamp The first instance of the action will be scheduled + * to run at a time calculated after this timestamp matching the cron + * expression. This can be used to delay the first instance of the action. + * @param int $schedule A cron definition string. + * @param string $group A group to put the action in. + * @param bool $unique Whether action scheduled should be unique. + * + * @return int The ID of the stored action. + **/ + public function cron_unique( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '', $unique = true ) { + if ( empty( $schedule ) ) { + return $this->single_unique( $hook, $args, $base_timestamp, $group, $unique ); } - $date = as_get_datetime_object( $base_timestamp ); - $cron = CronExpression::factory( $schedule ); + $date = as_get_datetime_object( $base_timestamp ); + $cron = CronExpression::factory( $schedule ); $schedule = new ActionScheduler_CronSchedule( $date, $cron ); - $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); - return $this->store( $action ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $unique ? $this->store_unique_action( $action ) : $this->store( $action ); } /** @@ -162,13 +233,92 @@ public function repeat( $action ) { } $schedule_class = get_class( $schedule ); - $new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() ); - $new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() ); + $new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() ); + $new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() ); + $new_action->set_priority( $action->get_priority() ); return $this->store( $new_action ); } /** - * @param ActionScheduler_Action $action + * Creates a scheduled action. + * + * This general purpose method can be used in place of specific methods such as async(), + * async_unique(), single() or single_unique(), etc. + * + * @internal Not intended for public use, should not be overriden by subclasses. + * @throws Exception May be thrown if invalid options are passed. + * + * @param array $options { + * Describes the action we wish to schedule. + * + * @type string $type Must be one of 'async', 'cron', 'recurring', or 'single'. + * @type string $hook The hook to be executed. + * @type array $arguments Arguments to be passed to the callback. + * @type string $group The action group. + * @type bool $unique If the action should be unique. + * @type int $when Timestamp. Indicates when the action, or first instance of the action in the case + * of recurring or cron actions, becomes due. + * @type int|string $pattern Recurrence pattern. This is either an interval in seconds for recurring actions + * or a cron expression for cron actions. + * @type int $priority Lower values means higher priority. Should be in the range 0-255. + * } + * + * @return int + */ + public function create( array $options = array() ) { + $defaults = array( + 'type' => 'single', + 'hook' => '', + 'arguments' => array(), + 'group' => '', + 'unique' => false, + 'when' => time(), + 'pattern' => null, + 'priority' => 10, + ); + + $options = array_merge( $defaults, $options ); + + // Cron/recurring actions without a pattern are treated as single actions (this gives calling code the ability + // to use functions like as_schedule_recurring_action() to schedule recurring as well as single actions). + if ( ( 'cron' === $options['type'] || 'recurring' === $options['type'] ) && empty( $options['pattern'] ) ) { + $options['type'] = 'single'; + } + + switch ( $options['type'] ) { + case 'async': + $schedule = new ActionScheduler_NullSchedule(); + break; + + case 'cron': + $date = as_get_datetime_object( $options['when'] ); + $cron = CronExpression::factory( $options['pattern'] ); + $schedule = new ActionScheduler_CronSchedule( $date, $cron ); + break; + + case 'recurring': + $date = as_get_datetime_object( $options['when'] ); + $schedule = new ActionScheduler_IntervalSchedule( $date, $options['pattern'] ); + break; + + case 'single': + $date = as_get_datetime_object( $options['when'] ); + $schedule = new ActionScheduler_SimpleSchedule( $date ); + break; + + default: + throw new Exception( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." ); + } + + $action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] ); + $action->set_priority( $options['priority'] ); + return $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action ); + } + + /** + * Save action to database. + * + * @param ActionScheduler_Action $action Action object to save. * * @return int The ID of the stored action */ @@ -176,4 +326,17 @@ protected function store( ActionScheduler_Action $action ) { $store = ActionScheduler_Store::instance(); return $store->save_action( $action ); } + + /** + * Store action if it's unique. + * + * @param ActionScheduler_Action $action Action object to store. + * + * @return int ID of the created action. Will be 0 if action was not created. + */ + protected function store_unique_action( ActionScheduler_Action $action ) { + $store = ActionScheduler_Store::instance(); + return method_exists( $store, 'save_unique_action' ) ? + $store->save_unique_action( $action ) : $store->save_action( $action ); + } } diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_AdminView.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_AdminView.php index c1fd0d72..b747b0a1 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_AdminView.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_AdminView.php @@ -40,7 +40,7 @@ public function init() { } add_action( 'admin_menu', array( $this, 'register_menu' ) ); - + add_action( 'admin_notices', array( $this, 'maybe_check_pastdue_actions' ) ); add_action( 'current_screen', array( $this, 'add_help_tabs' ) ); } } @@ -110,6 +110,104 @@ protected function get_list_table() { return $this->list_table; } + /** + * Action: admin_notices + * + * Maybe check past-due actions, and print notice. + * + * @uses $this->check_pastdue_actions() + */ + public function maybe_check_pastdue_actions() { + + # Filter to prevent checking actions (ex: inappropriate user). + if ( ! apply_filters( 'action_scheduler_check_pastdue_actions', current_user_can( 'manage_options' ) ) ) { + return; + } + + # Get last check transient. + $last_check = get_transient( 'action_scheduler_last_pastdue_actions_check' ); + + # If transient exists, we're within interval, so bail. + if ( ! empty( $last_check ) ) { + return; + } + + # Perform the check. + $this->check_pastdue_actions(); + } + + /** + * Check past-due actions, and print notice. + * + * @todo update $link_url to "Past-due" filter when released (see issue #510, PR #511) + */ + protected function check_pastdue_actions() { + + # Set thresholds. + $threshold_seconds = ( int ) apply_filters( 'action_scheduler_pastdue_actions_seconds', DAY_IN_SECONDS ); + $threshhold_min = ( int ) apply_filters( 'action_scheduler_pastdue_actions_min', 1 ); + + // Set fallback value for past-due actions count. + $num_pastdue_actions = 0; + + // Allow third-parties to preempt the default check logic. + $check = apply_filters( 'action_scheduler_pastdue_actions_check_pre', null ); + + // If no third-party preempted and there are no past-due actions, return early. + if ( ! is_null( $check ) ) { + return; + } + + # Scheduled actions query arguments. + $query_args = array( + 'date' => as_get_datetime_object( time() - $threshold_seconds ), + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'per_page' => $threshhold_min, + ); + + # If no third-party preempted, run default check. + if ( is_null( $check ) ) { + $store = ActionScheduler_Store::instance(); + $num_pastdue_actions = ( int ) $store->query_actions( $query_args, 'count' ); + + # Check if past-due actions count is greater than or equal to threshold. + $check = ( $num_pastdue_actions >= $threshhold_min ); + $check = ( bool ) apply_filters( 'action_scheduler_pastdue_actions_check', $check, $num_pastdue_actions, $threshold_seconds, $threshhold_min ); + } + + # If check failed, set transient and abort. + if ( ! boolval( $check ) ) { + $interval = apply_filters( 'action_scheduler_pastdue_actions_check_interval', round( $threshold_seconds / 4 ), $threshold_seconds ); + set_transient( 'action_scheduler_last_pastdue_actions_check', time(), $interval ); + + return; + } + + $actions_url = add_query_arg( array( + 'page' => 'action-scheduler', + 'status' => 'past-due', + 'order' => 'asc', + ), admin_url( 'tools.php' ) ); + + # Print notice. + echo '

'; + printf( + _n( + // translators: 1) is the number of affected actions, 2) is a link to an admin screen. + 'Action Scheduler: %1$d past-due action found; something may be wrong. Read documentation »', + 'Action Scheduler: %1$d past-due actions found; something may be wrong. Read documentation »', + $num_pastdue_actions, + 'action-scheduler' + ), + $num_pastdue_actions, + esc_attr( esc_url( $actions_url ) ) + ); + echo '

'; + + # Facilitate third-parties to evaluate and print notices. + do_action( 'action_scheduler_pastdue_actions_extra_notices', $query_args ); + } + /** * Provide more information about the screen and its data in the help tab. */ diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_Compatibility.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_Compatibility.php index 85e0ed9d..bb28023b 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_Compatibility.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_Compatibility.php @@ -4,7 +4,6 @@ * Class ActionScheduler_Compatibility */ class ActionScheduler_Compatibility { - /** * Converts a shorthand byte value to an integer byte value. * @@ -89,21 +88,18 @@ public static function raise_time_limit( $limit = 0 ) { $limit = (int) $limit; $max_execution_time = (int) ini_get( 'max_execution_time' ); - /* - * If the max execution time is already unlimited (zero), or if it exceeds or is equal to the proposed - * limit, there is no reason for us to make further changes (we never want to lower it). - */ - if ( - 0 === $max_execution_time - || ( $max_execution_time >= $limit && $limit !== 0 ) - ) { + // If the max execution time is already set to zero (unlimited), there is no reason to make a further change. + if ( 0 === $max_execution_time ) { return; } + // Whichever of $max_execution_time or $limit is higher is the amount by which we raise the time limit. + $raise_by = 0 === $limit || $limit > $max_execution_time ? $limit : $max_execution_time; + if ( function_exists( 'wc_set_time_limit' ) ) { - wc_set_time_limit( $limit ); + wc_set_time_limit( $raise_by ); } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved - @set_time_limit( $limit ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @set_time_limit( $raise_by ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } } diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_DateTime.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_DateTime.php index 5e8743ca..b142ca81 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_DateTime.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_DateTime.php @@ -24,6 +24,7 @@ class ActionScheduler_DateTime extends DateTime { * * @return int */ + #[\ReturnTypeWillChange] public function getTimestamp() { return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); } @@ -45,6 +46,7 @@ public function setUtcOffset( $offset ) { * @return int * @link http://php.net/manual/en/datetime.getoffset.php */ + #[\ReturnTypeWillChange] public function getOffset() { return $this->utcOffset ? $this->utcOffset : parent::getOffset(); } @@ -57,6 +59,7 @@ public function getOffset() { * @return static * @link http://php.net/manual/en/datetime.settimezone.php */ + #[\ReturnTypeWillChange] public function setTimezone( $timezone ) { $this->utcOffset = 0; parent::setTimezone( $timezone ); diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_Exception.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_Exception.php index 353d3c09..08e4faef 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_Exception.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_Exception.php @@ -6,6 +6,6 @@ * Facilitates catching Exceptions unique to Action Scheduler. * * @package ActionScheduler - * @since %VERSION% + * @since 2.1.0 */ interface ActionScheduler_Exception {} diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php index 501c0da2..a21fdbe3 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_ListTable.php @@ -252,7 +252,7 @@ private static function human_interval( $interval, $periods_to_include = 2 ) { */ protected function get_recurrence( $action ) { $schedule = $action->get_schedule(); - if ( $schedule->is_recurring() ) { + if ( $schedule->is_recurring() && method_exists( $schedule, 'get_recurrence' ) ) { $recurrence = $schedule->get_recurrence(); if ( is_numeric( $recurrence ) ) { @@ -467,7 +467,11 @@ protected function get_schedule_display_string( ActionScheduler_Schedule $schedu $schedule_display_string = ''; - if ( ! $schedule->get_date() ) { + if ( is_a( $schedule, 'ActionScheduler_NullSchedule' ) ) { + return __( 'async', 'action-scheduler' ); + } + + if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) { return '0000-00-00 00:00:00'; } @@ -498,7 +502,20 @@ protected function get_schedule_display_string( ActionScheduler_Schedule $schedu */ protected function bulk_delete( array $ids, $ids_sql ) { foreach ( $ids as $id ) { - $this->store->delete_action( $id ); + try { + $this->store->delete_action( $id ); + } catch ( Exception $e ) { + // A possible reason for an exception would include a scenario where the same action is deleted by a + // concurrent request. + error_log( + sprintf( + /* translators: 1: action ID 2: exception message. */ + __( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'action-scheduler' ), + $id, + $e->getMessage() + ) + ); + } } } @@ -583,6 +600,16 @@ public function prepare_items() { 'search' => $this->get_request_search_query(), ); + /** + * Change query arguments to query for past-due actions. + * Past-due actions have the 'pending' status and are in the past. + * This is needed because registering 'past-due' as a status is overkill. + */ + if ( 'past-due' === $this->get_request_status() ) { + $query['status'] = ActionScheduler_Store::STATUS_PENDING; + $query['date'] = as_get_datetime_object(); + } + $this->items = array(); $total_items = $this->store->query_actions( $query, 'count' ); @@ -623,7 +650,7 @@ public function prepare_items() { * Prints the available statuses so the user can click to filter. */ protected function display_filter_by_status() { - $this->status_counts = $this->store->action_counts(); + $this->status_counts = $this->store->action_counts() + $this->store->extra_action_counts(); parent::display_filter_by_status(); } diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_OptionLock.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_OptionLock.php index 4bc9a3fc..911f9b77 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_OptionLock.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_OptionLock.php @@ -24,7 +24,37 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock { * @bool True if lock value has changed, false if not or if set failed. */ public function set( $lock_type ) { - return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $lock_type ) ); + global $wpdb; + + $lock_key = $this->get_key( $lock_type ); + $existing_lock_value = $this->get_existing_lock( $lock_type ); + $new_lock_value = $this->new_lock_value( $lock_type ); + + // The lock may not exist yet, or may have been deleted. + if ( empty( $existing_lock_value ) ) { + return (bool) $wpdb->insert( + $wpdb->options, + array( + 'option_name' => $lock_key, + 'option_value' => $new_lock_value, + 'autoload' => 'no', + ) + ); + } + + if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) { + return false; + } + + // Otherwise, try to obtain the lock. + return (bool) $wpdb->update( + $wpdb->options, + array( 'option_value' => $new_lock_value ), + array( + 'option_name' => $lock_key, + 'option_value' => $existing_lock_value, + ) + ); } /** @@ -34,7 +64,30 @@ public function set( $lock_type ) { * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. */ public function get_expiration( $lock_type ) { - return get_option( $this->get_key( $lock_type ) ); + return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) ); + } + + /** + * Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined). + * + * @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp. + * + * @return false|int + */ + private function get_expiration_from( $lock_value ) { + $lock_string = explode( '|', $lock_value ); + + // Old style lock? + if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) { + return (int) $lock_string[0]; + } + + // New style lock? + if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) { + return (int) $lock_string[1]; + } + + return false; } /** @@ -46,4 +99,37 @@ public function get_expiration( $lock_type ) { protected function get_key( $lock_type ) { return sprintf( 'action_scheduler_lock_%s', $lock_type ); } + + /** + * Supplies the existing lock value, or an empty string if not set. + * + * @param string $lock_type A string to identify different lock types. + * + * @return string + */ + private function get_existing_lock( $lock_type ) { + global $wpdb; + + // Now grab the existing lock value, if there is one. + return (string) $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM $wpdb->options WHERE option_name = %s", + $this->get_key( $lock_type ) + ) + ); + } + + /** + * Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe + * character. + * + * Example: (string) "649de012e6b262.09774912|1688068114" + * + * @param string $lock_type A string to identify different lock types. + * + * @return string + */ + private function new_lock_value( $lock_type ) { + return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) ); + } } diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueCleaner.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueCleaner.php index 49cd44bb..6f2a696d 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueCleaner.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueCleaner.php @@ -18,6 +18,14 @@ class ActionScheduler_QueueCleaner { */ private $month_in_seconds = 2678400; + /** + * @var string[] Default list of statuses purged by the cleaner process. + */ + private $default_statuses_to_purge = [ + ActionScheduler_Store::STATUS_COMPLETE, + ActionScheduler_Store::STATUS_CANCELED, + ]; + /** * ActionScheduler_QueueCleaner constructor. * @@ -29,46 +37,113 @@ public function __construct( ActionScheduler_Store $store = null, $batch_size = $this->batch_size = $batch_size; } + /** + * Default queue cleaner process used by queue runner. + * + * @return array + */ public function delete_old_actions() { + /** + * Filter the minimum scheduled date age for action deletion. + * + * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted. + */ $lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds ); - $cutoff = as_get_datetime_object($lifespan.' seconds ago'); - $statuses_to_purge = array( - ActionScheduler_Store::STATUS_COMPLETE, - ActionScheduler_Store::STATUS_CANCELED, - ); + try { + $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' ); + } catch ( Exception $e ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* Translators: %s is the exception message. */ + esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'action-scheduler' ), + esc_html( $e->getMessage() ) + ), + '3.5.5' + ); + + return array(); + } + + + /** + * Filter the statuses when cleaning the queue. + * + * @param string[] $default_statuses_to_purge Action statuses to clean. + */ + $statuses_to_purge = (array) apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge ); + + return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() ); + } + + /** + * Delete selected actions limited by status and date. + * + * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete. + * @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago. + * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20. + * @param string $context Calling process context. Defaults to `old`. + * @return array Actions deleted. + */ + public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) { + $batch_size = $batch_size !== null ? $batch_size : $this->batch_size; + $cutoff = $cutoff_date !== null ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' ); + $lifespan = time() - $cutoff->getTimestamp(); + if ( empty( $statuses_to_purge ) ) { + $statuses_to_purge = $this->default_statuses_to_purge; + } + $deleted_actions = []; foreach ( $statuses_to_purge as $status ) { $actions_to_delete = $this->store->query_actions( array( 'status' => $status, 'modified' => $cutoff, 'modified_compare' => '<=', - 'per_page' => $this->get_batch_size(), + 'per_page' => $batch_size, 'orderby' => 'none', ) ); - foreach ( $actions_to_delete as $action_id ) { - try { - $this->store->delete_action( $action_id ); - } catch ( Exception $e ) { - - /** - * Notify 3rd party code of exceptions when deleting a completed action older than the retention period - * - * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their - * actions. - * - * @since 2.0.0 - * - * @param int $action_id The scheduled actions ID in the data store - * @param Exception $e The exception thrown when attempting to delete the action from the data store - * @param int $lifespan The retention period, in seconds, for old actions - * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch - */ - do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) ); - } + $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) ); + } + + return $deleted_actions; + } + + /** + * @param int[] $actions_to_delete List of action IDs to delete. + * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted. + * @param string $context Context of the delete request. + * @return array Deleted action IDs. + */ + private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) { + $deleted_actions = []; + if ( $lifespan === null ) { + $lifespan = $this->month_in_seconds; + } + + foreach ( $actions_to_delete as $action_id ) { + try { + $this->store->delete_action( $action_id ); + $deleted_actions[] = $action_id; + } catch ( Exception $e ) { + /** + * Notify 3rd party code of exceptions when deleting a completed action older than the retention period + * + * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their + * actions. + * + * @param int $action_id The scheduled actions ID in the data store + * @param Exception $e The exception thrown when attempting to delete the action from the data store + * @param int $lifespan The retention period, in seconds, for old actions + * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch + * @since 2.0.0 + * + */ + do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) ); } } + return $deleted_actions; } /** diff --git a/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueRunner.php b/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueRunner.php index cd76807e..1ec3eab2 100644 --- a/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueRunner.php +++ b/lib/woocommerce/action-scheduler/classes/ActionScheduler_QueueRunner.php @@ -14,6 +14,9 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { /** @var ActionScheduler_QueueRunner */ private static $runner = null; + /** @var int */ + private $processed_actions_count = 0; + /** * @return ActionScheduler_QueueRunner * @codeCoverageIgnore @@ -100,9 +103,12 @@ public function unhook_dispatch_async_request() { * should dispatch a request to process pending actions. */ public function maybe_dispatch_async_request() { - if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) { - // Only start an async queue at most once every 60 seconds - ActionScheduler::lock()->set( 'async-request-runner' ); + // Only start an async queue at most once every 60 seconds. + if ( + is_admin() + && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) + && ActionScheduler::lock()->set( 'async-request-runner' ) + ) { $this->async_request->maybe_dispatch(); } } @@ -125,17 +131,18 @@ public function run( $context = 'WP Cron' ) { ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); do_action( 'action_scheduler_before_process_queue' ); $this->run_cleanup(); - $processed_actions = 0; + + $this->processed_actions_count = 0; if ( false === $this->has_maximum_concurrent_batches() ) { $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 ); do { - $processed_actions_in_batch = $this->do_batch( $batch_size, $context ); - $processed_actions += $processed_actions_in_batch; - } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $processed_actions ) ); // keep going until we run out of actions, time, or memory + $processed_actions_in_batch = $this->do_batch( $batch_size, $context ); + $this->processed_actions_count += $processed_actions_in_batch; + } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $this->processed_actions_count ) ); // keep going until we run out of actions, time, or memory } do_action( 'action_scheduler_after_process_queue' ); - return $processed_actions; + return $this->processed_actions_count; } /** @@ -162,7 +169,7 @@ protected function do_batch( $size = 100, $context = '' ) { $this->process_action( $action_id, $context ); $processed_actions++; - if ( $this->batch_limits_exceeded( $processed_actions ) ) { + if ( $this->batch_limits_exceeded( $processed_actions + $this->processed_actions_count ) ) { break; } } @@ -173,15 +180,40 @@ protected function do_batch( $size = 100, $context = '' ) { } /** - * Running large batches can eat up memory, as WP adds data to its object cache. - * - * If using a persistent object store, this has the side effect of flushing that - * as well, so this is disabled by default. To enable: + * Flush the cache if possible (intended for use after a batch of actions has been processed). * - * add_filter( 'action_scheduler_queue_runner_flush_cache', '__return_true' ); + * This is useful because running large batches can eat up memory and because invalid data can accrue in the + * runtime cache, which may lead to unexpected results. */ protected function clear_caches() { - if ( ! wp_using_ext_object_cache() || apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) ) { + /* + * Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object + * cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available. + * + * However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if + * it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it. + */ + $flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ); + $flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' ); + + if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) { + wp_cache_flush_runtime(); + } elseif ( + ! wp_using_ext_object_cache() + /** + * When an external object cache is in use, and when wp_cache_flush_runtime() is not available, then + * normally the cache will not be flushed after processing a batch of actions (to avoid a performance + * penalty for other processes). + * + * This filter makes it possible to override this behavior and always flush the cache, even if an external + * object cache is in use. + * + * @since 1.0 + * + * @param bool $flush_cache If the cache should be flushed. + */ + || apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) + ) { wp_cache_flush(); } } diff --git a/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php b/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php new file mode 100644 index 00000000..ff6e57aa --- /dev/null +++ b/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php @@ -0,0 +1,125 @@ +] + * : The maximum number of actions to delete per batch. Defaults to 20. + * + * [--batches=] + * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue all eligible actions are deleted. + * + * [--status=] + * : Only clean actions with the specified status. Defaults to Canceled, Completed. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled` + * + * [--before=] + * : Only delete actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'` + * + * [--pause=] + * : The number of seconds to pause between batches. Default no pause. + * + * @param array $args Positional arguments. + * @param array $assoc_args Keyed arguments. + * @throws \WP_CLI\ExitException When an error occurs. + * + * @subcommand clean + */ + public function clean( $args, $assoc_args ) { + // Handle passed arguments. + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 20 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $status = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'status', '' ) ); + $status = array_filter( array_map( 'trim', $status ) ); + $before = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before', '' ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + + $batches_completed = 0; + $actions_deleted = 0; + $unlimited = $batches === 0; + try { + $lifespan = as_get_datetime_object( $before ); + } catch ( Exception $e ) { + $lifespan = null; + } + + try { + // Custom queue cleaner instance. + $cleaner = new ActionScheduler_QueueCleaner( null, $batch ); + + // Clean actions for as long as possible. + while ( $unlimited || $batches_completed < $batches ) { + if ( $sleep && $batches_completed > 0 ) { + sleep( $sleep ); + } + + $deleted = count( $cleaner->clean_actions( $status, $lifespan, null,'CLI' ) ); + if ( $deleted <= 0 ) { + break; + } + $actions_deleted += $deleted; + $batches_completed++; + $this->print_success( $deleted ); + } + } catch ( Exception $e ) { + $this->print_error( $e ); + } + + $this->print_total_batches( $batches_completed ); + if ( $batches_completed > 1 ) { + $this->print_success( $actions_deleted ); + } + } + + /** + * Print WP CLI message about how many batches of actions were processed. + * + * @param int $batches_processed + */ + protected function print_total_batches( int $batches_processed ) { + WP_CLI::log( + sprintf( + /* translators: %d refers to the total number of batches processed */ + _n( '%d batch processed.', '%d batches processed.', $batches_processed, 'action-scheduler' ), + $batches_processed + ) + ); + } + + /** + * Convert an exception into a WP CLI error. + * + * @param Exception $e The error object. + * + * @throws \WP_CLI\ExitException + */ + protected function print_error( Exception $e ) { + WP_CLI::error( + sprintf( + /* translators: %s refers to the exception error message */ + __( 'There was an error deleting an action: %s', 'action-scheduler' ), + $e->getMessage() + ) + ); + } + + /** + * Print a success message with the number of completed actions. + * + * @param int $actions_deleted + */ + protected function print_success( int $actions_deleted ) { + WP_CLI::success( + sprintf( + /* translators: %d refers to the total number of actions deleted */ + _n( '%d action deleted.', '%d actions deleted.', $actions_deleted, 'action-scheduler' ), + $actions_deleted + ) + ); + } +} diff --git a/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php b/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php index c33de686..4681daa4 100644 --- a/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php +++ b/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php @@ -90,7 +90,7 @@ protected function setup_progress_bar() { $count = count( $this->actions ); $this->progress_bar = new ProgressBar( /* translators: %d: amount of actions */ - sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), number_format_i18n( $count ) ), + sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), $count ), $count ); } diff --git a/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php b/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php index 6cf27d42..2c68a386 100644 --- a/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php +++ b/lib/woocommerce/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php @@ -5,6 +5,36 @@ */ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command { + /** + * Force tables schema creation for Action Scheduler + * + * ## OPTIONS + * + * @param array $args Positional arguments. + * @param array $assoc_args Keyed arguments. + * + * @subcommand fix-schema + */ + public function fix_schema( $args, $assoc_args ) { + $schema_classes = array( ActionScheduler_LoggerSchema::class, ActionScheduler_StoreSchema::class ); + + foreach ( $schema_classes as $classname ) { + if ( is_subclass_of( $classname, ActionScheduler_Abstract_Schema::class ) ) { + $obj = new $classname(); + $obj->init(); + $obj->register_tables( true ); + + WP_CLI::success( + sprintf( + /* translators: %s refers to the schema name*/ + __( 'Registered schema for %s', 'action-scheduler' ), + $classname + ) + ); + } + } + } + /** * Run the Action Scheduler * @@ -25,6 +55,9 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command { * [--group=] * : Only run actions from the specified group. Omitting this option runs actions from all groups. * + * [--exclude-groups=] + * : Run actions from all groups except the specified group(s). Define multiple groups as a comma separated string (without spaces), e.g. '--group_a,group_b'. This option is ignored when `--group` is used. + * * [--free-memory-on=] * : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50. * @@ -42,15 +75,16 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command { */ public function run( $args, $assoc_args ) { // Handle passed arguments. - $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); - $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); - $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); - $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); - $hooks = array_filter( array_map( 'trim', $hooks ) ); - $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); - $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); - $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); - $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); + $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); + $hooks = array_filter( array_map( 'trim', $hooks ) ); + $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); + $exclude_groups = \WP_CLI\Utils\get_flag_value( $assoc_args, 'exclude-groups', '' ); + $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); ActionScheduler_DataController::set_free_ticks( $free_on ); ActionScheduler_DataController::set_sleep_time( $sleep ); @@ -58,6 +92,13 @@ public function run( $args, $assoc_args ) { $batches_completed = 0; $actions_completed = 0; $unlimited = $batches === 0; + if ( is_callable( [ ActionScheduler::store(), 'set_claim_filter' ] ) ) { + $exclude_groups = $this->parse_comma_separated_string( $exclude_groups ); + + if ( ! empty( $exclude_groups ) ) { + ActionScheduler::store()->set_claim_filter('exclude-groups', $exclude_groups ); + } + } try { // Custom queue cleaner instance. @@ -86,6 +127,17 @@ public function run( $args, $assoc_args ) { $this->print_success( $actions_completed ); } + /** + * Converts a string of comma-separated values into an array of those same values. + * + * @param string $string The string of one or more comma separated values. + * + * @return array + */ + private function parse_comma_separated_string( $string ): array { + return array_filter( str_getcsv( $string ) ); + } + /** * Print WP CLI message about how many actions are about to be processed. * @@ -96,9 +148,9 @@ public function run( $args, $assoc_args ) { protected function print_total_actions( $total ) { WP_CLI::log( sprintf( - /* translators: %d refers to how many scheduled taks were found to run */ + /* translators: %d refers to how many scheduled tasks were found to run */ _n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'action-scheduler' ), - number_format_i18n( $total ) + $total ) ); } @@ -115,7 +167,7 @@ protected function print_total_batches( $batches_completed ) { sprintf( /* translators: %d refers to the total number of batches executed */ _n( '%d batch executed.', '%d batches executed.', $batches_completed, 'action-scheduler' ), - number_format_i18n( $batches_completed ) + $batches_completed ) ); } @@ -149,9 +201,9 @@ protected function print_error( Exception $e ) { protected function print_success( $actions_completed ) { WP_CLI::success( sprintf( - /* translators: %d refers to the total number of taskes completed */ + /* translators: %d refers to the total number of tasks completed */ _n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'action-scheduler' ), - number_format_i18n( $actions_completed ) + $actions_completed ) ); } diff --git a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler.php b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler.php index a5a6161a..8a0109ee 100644 --- a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler.php +++ b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler.php @@ -118,8 +118,8 @@ public static function autoload( $class ) { return; } - if ( file_exists( "{$dir}{$class}.php" ) ) { - include( "{$dir}{$class}.php" ); + if ( file_exists( $dir . "{$class}.php" ) ) { + include( $dir . "{$class}.php" ); return; } } @@ -153,11 +153,41 @@ public static function init( $plugin_file ) { add_action( 'init', array( $store, 'init' ), 1, 0 ); add_action( 'init', array( $logger, 'init' ), 1, 0 ); add_action( 'init', array( $runner, 'init' ), 1, 0 ); + + add_action( + 'init', + /** + * Runs after the active store's init() method has been called. + * + * It would probably be preferable to have $store->init() (or it's parent method) set this itself, + * once it has initialized, however that would cause problems in cases where a custom data store is in + * use and it has not yet been updated to follow that same logic. + */ + function () { + self::$data_store_initialized = true; + + /** + * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. + * + * @since 3.5.5 + */ + do_action( 'action_scheduler_init' ); + }, + 1 + ); } else { $admin_view->init(); $store->init(); $logger->init(); $runner->init(); + self::$data_store_initialized = true; + + /** + * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. + * + * @since 3.5.5 + */ + do_action( 'action_scheduler_init' ); } if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) { @@ -166,14 +196,13 @@ public static function init( $plugin_file ) { if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' ); + WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Clean_Command' ); if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) { $command = new Migration_Command(); $command->register(); } } - self::$data_store_initialized = true; - /** * Handle WP comment cleanup after migration. */ @@ -192,8 +221,12 @@ public static function init( $plugin_file ) { */ public static function is_initialized( $function_name = null ) { if ( ! self::$data_store_initialized && ! empty( $function_name ) ) { - $message = sprintf( __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), esc_attr( $function_name ) ); - error_log( $message, E_WARNING ); + $message = sprintf( + /* translators: %s function name. */ + __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), + esc_attr( $function_name ) + ); + error_log( $message ); } return self::$data_store_initialized; diff --git a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php index 4018599d..8d1465fc 100644 --- a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php +++ b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php @@ -673,24 +673,34 @@ protected function display_filter_by_status() { // Helper to set 'all' filter when not set on status counts passed in. if ( ! isset( $this->status_counts['all'] ) ) { - $this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts; + $all_count = array_sum( $this->status_counts ); + if ( isset( $this->status_counts['past-due'] ) ) { + $all_count -= $this->status_counts['past-due']; + } + $this->status_counts = array( 'all' => $all_count ) + $this->status_counts; } - foreach ( $this->status_counts as $status_name => $count ) { + // Translated status labels. + $status_labels = ActionScheduler_Store::instance()->get_status_labels(); + $status_labels['all'] = _x( 'All', 'status labels', 'action-scheduler' ); + $status_labels['past-due'] = _x( 'Past-due', 'status labels', 'action-scheduler' ); + + foreach ( $this->status_counts as $status_slug => $count ) { if ( 0 === $count ) { continue; } - if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) { - $status_list_item = '
  • %3$s (%4$d)
  • '; + if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) { + $status_list_item = '
  • %3$s (%4$d)
  • '; } else { $status_list_item = '
  • %3$s (%4$d)
  • '; } - $status_filter_url = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name ); + $status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug ); + $status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug ); $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url ); - $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) ); + $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) ); } if ( $status_list_items ) { diff --git a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php index 82ecbc67..2f957020 100644 --- a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php +++ b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php @@ -48,30 +48,56 @@ public function __construct( ActionScheduler_Store $store = null, ActionSchedule * Generally, this should be capitalised and not localised as it's a proper noun. */ public function process_action( $action_id, $context = '' ) { + // Temporarily override the error handler while we process the current action. + set_error_handler( + /** + * Temporary error handler which can catch errors and convert them into exceptions. This faciliates more + * robust error handling across all supported PHP versions. + * + * @throws Exception + * + * @param int $type Error level expressed as an integer. + * @param string $message Error message. + */ + function ( $type, $message ) { + throw new Exception( $message ); + }, + E_USER_ERROR | E_RECOVERABLE_ERROR + ); + + /* + * The nested try/catch structure is required because we potentially need to convert thrown errors into + * exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same* + * structure). + */ try { - $valid_action = false; - do_action( 'action_scheduler_before_execute', $action_id, $context ); - - if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { - do_action( 'action_scheduler_execution_ignored', $action_id, $context ); - return; + try { + $valid_action = false; + do_action( 'action_scheduler_before_execute', $action_id, $context ); + + if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { + do_action( 'action_scheduler_execution_ignored', $action_id, $context ); + return; + } + + $valid_action = true; + do_action( 'action_scheduler_begin_execute', $action_id, $context ); + + $action = $this->store->fetch_action( $action_id ); + $this->store->log_execution( $action_id ); + $action->execute(); + do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); + $this->store->mark_complete( $action_id ); + } catch ( Throwable $e ) { + // Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for + // compatibility with ActionScheduler_Logger. + throw new Exception( $e->getMessage(), $e->getCode(), $e->getPrevious() ); } - - $valid_action = true; - do_action( 'action_scheduler_begin_execute', $action_id, $context ); - - $action = $this->store->fetch_action( $action_id ); - $this->store->log_execution( $action_id ); - $action->execute(); - do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); - $this->store->mark_complete( $action_id ); } catch ( Exception $e ) { - if ( $valid_action ) { - $this->store->mark_failure( $action_id ); - do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); - } else { - do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); - } + // This catch block exists for compatibility with PHP 5.6. + $this->handle_action_error( $action_id, $e, $context, $valid_action ); + } finally { + restore_error_handler(); } if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) { @@ -79,6 +105,39 @@ public function process_action( $action_id, $context = '' ) { } } + /** + * Marks actions as either having failed execution or failed validation, as appropriate. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + * @param bool $valid_action If the action is valid. + * + * @return void + */ + private function handle_action_error( $action_id, $e, $context, $valid_action ) { + if ( $valid_action ) { + $this->store->mark_failure( $action_id ); + /** + * Runs when action execution fails. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + */ + do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); + } else { + /** + * Runs when action validation fails. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + */ + do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); + } + } + /** * Schedule the next instance of the action if necessary. * @@ -86,6 +145,19 @@ public function process_action( $action_id, $context = '' ) { * @param int $action_id */ protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) { + // If a recurring action has been consistently failing, we may wish to stop rescheduling it. + if ( + ActionScheduler_Store::STATUS_FAILED === $this->store->get_status( $action_id ) + && $this->recurring_action_is_consistently_failing( $action, $action_id ) + ) { + ActionScheduler_Logger::instance()->log( + $action_id, + __( 'This action appears to be consistently failing. A new instance will not be scheduled.', 'action-scheduler' ) + ); + + return; + } + try { ActionScheduler::factory()->repeat( $action ); } catch ( Exception $e ) { @@ -93,6 +165,61 @@ protected function schedule_next_instance( ActionScheduler_Action $action, $acti } } + /** + * Determine if the specified recurring action has been consistently failing. + * + * @param ActionScheduler_Action $action The recurring action to be rescheduled. + * @param int $action_id The ID of the recurring action. + * + * @return bool + */ + private function recurring_action_is_consistently_failing( ActionScheduler_Action $action, $action_id ) { + /** + * Controls the failure threshold for recurring actions. + * + * Before rescheduling a recurring action, we look at its status. If it failed, we then check if all of the most + * recent actions (upto the threshold set by this filter) sharing the same hook have also failed: if they have, + * that is considered consistent failure and a new instance of the action will not be scheduled. + * + * @param int $failure_threshold Number of actions of the same hook to examine for failure. Defaults to 5. + */ + $consistent_failure_threshold = (int) apply_filters( 'action_scheduler_recurring_action_failure_threshold', 5 ); + + // This query should find the earliest *failing* action (for the hook we are interested in) within our threshold. + $query_args = array( + 'hook' => $action->get_hook(), + 'status' => ActionScheduler_Store::STATUS_FAILED, + 'date' => date_create( 'now', timezone_open( 'UTC' ) )->format( 'Y-m-d H:i:s' ), + 'date_compare' => '<', + 'per_page' => 1, + 'offset' => $consistent_failure_threshold - 1 + ); + + $first_failing_action_id = $this->store->query_actions( $query_args ); + + // If we didn't retrieve an action ID, then there haven't been enough failures for us to worry about. + if ( empty( $first_failing_action_id ) ) { + return false; + } + + // Now let's fetch the first action (having the same hook) of *any status* within the same window. + unset( $query_args['status'] ); + $first_action_id_with_the_same_hook = $this->store->query_actions( $query_args ); + + /** + * If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a + * way to observe and optionally override that assessment. + * + * @param bool $is_consistently_failing If the action is considered to be consistently failing. + * @param ActionScheduler_Action $action The action being assessed. + */ + return (bool) apply_filters( + 'action_scheduler_recurring_action_is_consistently_failing', + $first_action_id_with_the_same_hook === $first_failing_action_id, + $action + ); + } + /** * Run the queue cleaner. * @@ -165,9 +292,14 @@ protected function get_execution_time() { * @return bool */ protected function time_likely_to_be_exceeded( $processed_actions ) { + $execution_time = $this->get_execution_time(); + $max_execution_time = $this->get_time_limit(); + + // Safety against division by zero errors. + if ( 0 === $processed_actions ) { + return $execution_time >= $max_execution_time; + } - $execution_time = $this->get_execution_time(); - $max_execution_time = $this->get_time_limit(); $time_per_action = $execution_time / $processed_actions; $estimated_time = $execution_time + ( $time_per_action * 3 ); $likely_to_be_exceeded = $estimated_time > $max_execution_time; diff --git a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php index 2334fda1..3fd259ea 100644 --- a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php +++ b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php @@ -25,7 +25,7 @@ abstract class ActionScheduler_Abstract_Schema { /** * @var array Names of tables that will be registered by this class. */ - protected $tables = []; + protected $tables = array(); /** * Can optionally be used by concrete classes to carry out additional initialization work @@ -90,10 +90,10 @@ private function schema_update_required() { $plugin_option_name = 'schema-'; switch ( static::class ) { - case 'ActionScheduler_StoreSchema' : + case 'ActionScheduler_StoreSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker'; break; - case 'ActionScheduler_LoggerSchema' : + case 'ActionScheduler_LoggerSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker'; break; } @@ -129,7 +129,7 @@ private function mark_schema_update_complete() { * @return void */ private function update_table( $table ) { - require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $definition = $this->get_table_definition( $table ); if ( $definition ) { $updated = dbDelta( $definition ); @@ -148,7 +148,7 @@ private function update_table( $table ) { * table prefix for the current blog */ protected function get_full_table_name( $table ) { - return $GLOBALS[ 'wpdb' ]->prefix . $table; + return $GLOBALS['wpdb']->prefix . $table; } /** @@ -159,14 +159,19 @@ protected function get_full_table_name( $table ) { public function tables_exist() { global $wpdb; - $existing_tables = $wpdb->get_col( 'SHOW TABLES' ); - $expected_tables = array_map( - function ( $table_name ) use ( $wpdb ) { - return $wpdb->prefix . $table_name; - }, - $this->tables - ); + $tables_exist = true; - return count( array_intersect( $existing_tables, $expected_tables ) ) === count( $expected_tables ); + foreach ( $this->tables as $table_name ) { + $table_name = $wpdb->prefix . $table_name; + $pattern = str_replace( '_', '\\_', $table_name ); + $existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) ); + + if ( $existing_table !== $table_name ) { + $tables_exist = false; + break; + } + } + + return $tables_exist; } } diff --git a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Lock.php b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Lock.php index 86e85285..e388a58f 100644 --- a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Lock.php +++ b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Lock.php @@ -26,6 +26,8 @@ public function is_locked( $lock_type ) { /** * Set a lock. * + * To prevent race conditions, implementations should avoid setting the lock if the lock is already held. + * * @param string $lock_type A string to identify different lock types. * @return bool */ diff --git a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Logger.php b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Logger.php index 3e7252c5..c3afd04b 100644 --- a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Logger.php +++ b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Logger.php @@ -109,7 +109,7 @@ public function log_failed_action( $action_id, Exception $exception, $context = public function log_timed_out_action( $action_id, $timeout ) { /* translators: %s: amount of time */ - $this->log( $action_id, sprintf( __( 'action timed out after %s seconds', 'action-scheduler' ), $timeout ) ); + $this->log( $action_id, sprintf( __( 'action marked as failed after %s seconds. Unknown error occurred. Check server, PHP and database error logs to diagnose cause.', 'action-scheduler' ), $timeout ) ); } public function log_unexpected_shutdown( $action_id, $error ) { diff --git a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Store.php b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Store.php index 6b71a779..a5552933 100644 --- a/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Store.php +++ b/lib/woocommerce/action-scheduler/classes/abstracts/ActionScheduler_Store.php @@ -76,7 +76,7 @@ public function find_action( $hook, $params = array() ) { /** * Query for action count or list of action IDs. * - * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @param array $query { * Query filtering options. @@ -104,7 +104,7 @@ abstract public function query_actions( $query = array(), $query_type = 'select' /** * Run query to get a single action ID. * - * @since x.x.x + * @since 3.3.0 * * @see ActionScheduler_Store::query_actions for $query arg usage but 'per_page' and 'offset' can't be used. * @@ -131,6 +131,34 @@ public function query_action( $query ) { */ abstract public function action_counts(); + /** + * Get additional action counts. + * + * - add past-due actions + * + * @return array + */ + public function extra_action_counts() { + $extra_actions = array(); + + $pastdue_action_counts = ( int ) $this->query_actions( array( + 'status' => self::STATUS_PENDING, + 'date' => as_get_datetime_object(), + ), 'count' ); + + if ( $pastdue_action_counts ) { + $extra_actions['past-due'] = $pastdue_action_counts; + } + + /** + * Allows 3rd party code to add extra action counts (used in filters in the list table). + * + * @since 3.5.0 + * @param $extra_actions array Array with format action_count_identifier => action count. + */ + return apply_filters( 'action_scheduler_extra_action_counts', $extra_actions ); + } + /** * @param string $action_id */ @@ -229,7 +257,7 @@ protected function validate_sql_comparator( $comparison_operator ) { protected function get_scheduled_date_string( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) { $next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date; if ( ! $next ) { - return '0000-00-00 00:00:00'; + $next = date_create(); } $next->setTimezone( new DateTimeZone( 'UTC' ) ); @@ -246,7 +274,7 @@ protected function get_scheduled_date_string( ActionScheduler_Action $action, Da protected function get_scheduled_date_string_local( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) { $next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date; if ( ! $next ) { - return '0000-00-00 00:00:00'; + $next = date_create(); } ActionScheduler_TimezoneHelper::set_local_timezone( $next ); diff --git a/lib/woocommerce/action-scheduler/classes/actions/ActionScheduler_Action.php b/lib/woocommerce/action-scheduler/classes/actions/ActionScheduler_Action.php index 520f932a..ddf33d5d 100644 --- a/lib/woocommerce/action-scheduler/classes/actions/ActionScheduler_Action.php +++ b/lib/woocommerce/action-scheduler/classes/actions/ActionScheduler_Action.php @@ -10,6 +10,19 @@ class ActionScheduler_Action { protected $schedule = NULL; protected $group = ''; + /** + * Priorities are conceptually similar to those used for regular WordPress actions. + * Like those, a lower priority takes precedence over a higher priority and the default + * is 10. + * + * Unlike regular WordPress actions, the priority of a scheduled action is strictly an + * integer and should be kept within the bounds 0-255 (anything outside the bounds will + * be brought back into the acceptable range). + * + * @var int + */ + protected $priority = 10; + public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = NULL, $group = '' ) { $schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule; $this->set_hook($hook); @@ -18,8 +31,29 @@ public function __construct( $hook, array $args = array(), ActionScheduler_Sched $this->set_group($group); } + /** + * Executes the action. + * + * If no callbacks are registered, an exception will be thrown and the action will not be + * fired. This is useful to help detect cases where the code responsible for setting up + * a scheduled action no longer exists. + * + * @throws Exception If no callbacks are registered for this action. + */ public function execute() { - return do_action_ref_array( $this->get_hook(), array_values( $this->get_args() ) ); + $hook = $this->get_hook(); + + if ( ! has_action( $hook ) ) { + throw new Exception( + sprintf( + /* translators: 1: action hook. */ + __( 'Scheduled action for %1$s will not be executed as no callbacks are registered.', 'action-scheduler' ), + $hook + ) + ); + } + + do_action_ref_array( $hook, array_values( $this->get_args() ) ); } /** @@ -72,4 +106,30 @@ public function get_group() { public function is_finished() { return FALSE; } + + /** + * Sets the priority of the action. + * + * @param int $priority Priority level (lower is higher priority). Should be in the range 0-255. + * + * @return void + */ + public function set_priority( $priority ) { + if ( $priority < 0 ) { + $priority = 0; + } elseif ( $priority > 255 ) { + $priority = 255; + } + + $this->priority = (int) $priority; + } + + /** + * Gets the action priority. + * + * @return int + */ + public function get_priority() { + return $this->priority; + } } diff --git a/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php b/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php index f0837641..f4d824eb 100644 --- a/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php +++ b/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php @@ -25,6 +25,13 @@ class ActionScheduler_DBStore extends ActionScheduler_Store { /** @var int */ protected static $max_index_length = 191; + /** @var array List of claim filters. */ + protected $claim_filters = [ + 'group' => '', + 'hooks' => '', + 'exclude-groups' => '', + ]; + /** * Initialize the data store * @@ -36,30 +43,58 @@ public function init() { $table_maker->register_tables(); } + /** + * Save an action, checks if this is a unique action before actually saving. + * + * @param ActionScheduler_Action $action Action object. + * @param \DateTime $scheduled_date Optional schedule date. Default null. + * + * @return int Action ID. + * @throws RuntimeException Throws exception when saving the action fails. + */ + public function save_unique_action( ActionScheduler_Action $action, \DateTime $scheduled_date = null ) { + return $this->save_action_to_db( $action, $scheduled_date, true ); + } + + /** + * Save an action. Can save duplicate action as well, prefer using `save_unique_action` instead. + * + * @param ActionScheduler_Action $action Action object. + * @param \DateTime $scheduled_date Optional schedule date. Default null. + * + * @return int Action ID. + * @throws RuntimeException Throws exception when saving the action fails. + */ + public function save_action( ActionScheduler_Action $action, \DateTime $scheduled_date = null ) { + return $this->save_action_to_db( $action, $scheduled_date, false ); + } + /** * Save an action. * * @param ActionScheduler_Action $action Action object. - * @param DateTime $date Optional schedule date. Default null. + * @param ?DateTime $date Optional schedule date. Default null. + * @param bool $unique Whether the action should be unique. * * @return int Action ID. * @throws RuntimeException Throws exception when saving the action fails. */ - public function save_action( ActionScheduler_Action $action, \DateTime $date = null ) { - try { + private function save_action_to_db( ActionScheduler_Action $action, DateTime $date = null, $unique = false ) { + global $wpdb; + try { $this->validate_action( $action ); - /** @var \wpdb $wpdb */ - global $wpdb; $data = array( 'hook' => $action->get_hook(), 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ), 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ), 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ), 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - 'group_id' => $this->get_group_id( $action->get_group() ), + 'group_id' => current( $this->get_group_ids( $action->get_group() ) ), + 'priority' => $action->get_priority(), ); + $args = wp_json_encode( $action->get_args() ); if ( strlen( $args ) <= static::$max_index_length ) { $data['args'] = $args; @@ -68,13 +103,18 @@ public function save_action( ActionScheduler_Action $action, \DateTime $date = n $data['extended_args'] = $args; } - $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions'; - $wpdb->insert( $table_name, $data ); + $insert_sql = $this->build_insert_sql( $data, $unique ); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_sql should be already prepared. + $wpdb->query( $insert_sql ); $action_id = $wpdb->insert_id; if ( is_wp_error( $action_id ) ) { throw new \RuntimeException( $action_id->get_error_message() ); } elseif ( empty( $action_id ) ) { + if ( $unique ) { + return 0; + } throw new \RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) ); } @@ -87,6 +127,104 @@ public function save_action( ActionScheduler_Action $action, \DateTime $date = n } } + /** + * Helper function to build insert query. + * + * @param array $data Row data for action. + * @param bool $unique Whether the action should be unique. + * + * @return string Insert query. + */ + private function build_insert_sql( array $data, $unique ) { + global $wpdb; + $columns = array_keys( $data ); + $values = array_values( $data ); + $placeholders = array_map( array( $this, 'get_placeholder_for_column' ), $columns ); + + $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions'; + + $column_sql = '`' . implode( '`, `', $columns ) . '`'; + $placeholder_sql = implode( ', ', $placeholders ); + $where_clause = $this->build_where_clause_for_insert( $data, $table_name, $unique ); + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $column_sql and $where_clause are already prepared. $placeholder_sql is hardcoded. + $insert_query = $wpdb->prepare( + " +INSERT INTO $table_name ( $column_sql ) +SELECT $placeholder_sql FROM DUAL +WHERE ( $where_clause ) IS NULL", + $values + ); + // phpcs:enable + + return $insert_query; + } + + /** + * Helper method to build where clause for action insert statement. + * + * @param array $data Row data for action. + * @param string $table_name Action table name. + * @param bool $unique Where action should be unique. + * + * @return string Where clause to be used with insert. + */ + private function build_where_clause_for_insert( $data, $table_name, $unique ) { + global $wpdb; + + if ( ! $unique ) { + return 'SELECT NULL FROM DUAL'; + } + + $pending_statuses = array( + ActionScheduler_Store::STATUS_PENDING, + ActionScheduler_Store::STATUS_RUNNING, + ); + $pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) ); + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $pending_status_placeholders is hardcoded. + $where_clause = $wpdb->prepare( + " +SELECT action_id FROM $table_name +WHERE status IN ( $pending_status_placeholders ) +AND hook = %s +AND `group_id` = %d +", + array_merge( + $pending_statuses, + array( + $data['hook'], + $data['group_id'], + ) + ) + ); + // phpcs:enable + + return "$where_clause" . ' LIMIT 1'; + } + + /** + * Helper method to get $wpdb->prepare placeholder for a given column name. + * + * @param string $column_name Name of column in actions table. + * + * @return string Placeholder to use for given column. + */ + private function get_placeholder_for_column( $column_name ) { + $string_columns = array( + 'hook', + 'status', + 'scheduled_date_gmt', + 'scheduled_date_local', + 'args', + 'schedule', + 'last_attempt_gmt', + 'last_attempt_local', + 'extended_args', + ); + + return in_array( $column_name, $string_columns ) ? '%s' : '%d'; + } + /** * Generate a hash from json_encoded $args using MD5 as this isn't for security. * @@ -113,23 +251,35 @@ protected function get_args_for_query( $args ) { /** * Get a group's ID based on its name/slug. * - * @param string $slug The string name of a group. - * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. + * @param string|array $slugs The string name of a group, or names for several groups. + * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. * - * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created. + * @return array The group IDs, if they exist or were successfully created. May be empty. */ - protected function get_group_id( $slug, $create_if_not_exists = true ) { - if ( empty( $slug ) ) { - return 0; + protected function get_group_ids( $slugs, $create_if_not_exists = true ) { + $slugs = (array) $slugs; + $group_ids = array(); + + if ( empty( $slugs ) ) { + return array(); } + /** @var \wpdb $wpdb */ global $wpdb; - $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); - if ( empty( $group_id ) && $create_if_not_exists ) { - $group_id = $this->create_group( $slug ); + + foreach ( $slugs as $slug ) { + $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); + + if ( empty( $group_id ) && $create_if_not_exists ) { + $group_id = $this->create_group( $slug ); + } + + if ( $group_id ) { + $group_ids[] = $group_id; + } } - return $group_id; + return $group_ids; } /** @@ -226,13 +376,13 @@ protected function make_action_from_db_record( $data ) { } $group = $data->group ? $data->group : ''; - return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group ); + return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority ); } /** * Returns the SQL statement to query (or count) actions. * - * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @param array $query Filtering options. * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count. @@ -246,49 +396,101 @@ protected function get_query_actions_sql( array $query, $select_or_count = 'sele throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'action-scheduler' ) ); } - $query = wp_parse_args( - $query, - array( - 'hook' => '', - 'args' => null, - 'date' => null, - 'date_compare' => '<=', - 'modified' => null, - 'modified_compare' => '<=', - 'group' => '', - 'status' => '', - 'claimed' => null, - 'per_page' => 5, - 'offset' => 0, - 'orderby' => 'date', - 'order' => 'ASC', - ) - ); + $query = wp_parse_args( $query, array( + 'hook' => '', + 'args' => null, + 'partial_args_matching' => 'off', // can be 'like' or 'json' + 'date' => null, + 'date_compare' => '<=', + 'modified' => null, + 'modified_compare' => '<=', + 'group' => '', + 'status' => '', + 'claimed' => null, + 'per_page' => 5, + 'offset' => 0, + 'orderby' => 'date', + 'order' => 'ASC', + ) ); /** @var \wpdb $wpdb */ global $wpdb; + + $db_server_info = is_callable( array( $wpdb, 'db_server_info' ) ) ? $wpdb->db_server_info() : $wpdb->db_version(); + if ( false !== strpos( $db_server_info, 'MariaDB' ) ) { + $supports_json = version_compare( + PHP_VERSION_ID >= 80016 ? $wpdb->db_version() : preg_replace( '/[^0-9.].*/', '', str_replace( '5.5.5-', '', $db_server_info ) ), + '10.2', + '>=' + ); + } else { + $supports_json = version_compare( $wpdb->db_version(), '5.7', '>=' ); + } + $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id'; - $sql .= " FROM {$wpdb->actionscheduler_actions} a"; + $sql .= " FROM {$wpdb->actionscheduler_actions} a"; $sql_params = array(); if ( ! empty( $query['group'] ) || 'group' === $query['orderby'] ) { $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id"; } - $sql .= ' WHERE 1=1'; + $sql .= " WHERE 1=1"; if ( ! empty( $query['group'] ) ) { - $sql .= ' AND g.slug=%s'; + $sql .= " AND g.slug=%s"; $sql_params[] = $query['group']; } - if ( $query['hook'] ) { - $sql .= ' AND a.hook=%s'; + if ( ! empty( $query['hook'] ) ) { + $sql .= " AND a.hook=%s"; $sql_params[] = $query['hook']; } + if ( ! is_null( $query['args'] ) ) { - $sql .= ' AND a.args=%s'; - $sql_params[] = $this->get_args_for_query( $query['args'] ); + switch ( $query['partial_args_matching'] ) { + case 'json': + if ( ! $supports_json ) { + throw new \RuntimeException( __( 'JSON partial matching not supported in your environment. Please check your MySQL/MariaDB version.', 'action-scheduler' ) ); + } + $supported_types = array( + 'integer' => '%d', + 'boolean' => '%s', + 'double' => '%f', + 'string' => '%s', + ); + foreach ( $query['args'] as $key => $value ) { + $value_type = gettype( $value ); + if ( 'boolean' === $value_type ) { + $value = $value ? 'true' : 'false'; + } + $placeholder = isset( $supported_types[ $value_type ] ) ? $supported_types[ $value_type ] : false; + if ( ! $placeholder ) { + throw new \RuntimeException( sprintf( + /* translators: %s: provided value type */ + __( 'The value type for the JSON partial matching is not supported. Must be either integer, boolean, double or string. %s type provided.', 'action-scheduler' ), + $value_type + ) ); + } + $sql .= ' AND JSON_EXTRACT(a.args, %s)='.$placeholder; + $sql_params[] = '$.'.$key; + $sql_params[] = $value; + } + break; + case 'like': + foreach ( $query['args'] as $key => $value ) { + $sql .= ' AND a.args LIKE %s'; + $json_partial = $wpdb->esc_like( trim( json_encode( array( $key => $value ) ), '{}' ) ); + $sql_params[] = "%{$json_partial}%"; + } + break; + case 'off': + $sql .= " AND a.args=%s"; + $sql_params[] = $this->get_args_for_query( $query['args'] ); + break; + default: + throw new \RuntimeException( __( 'Unknown partial args matching value.', 'action-scheduler' ) ); + } } if ( $query['status'] ) { @@ -384,7 +586,7 @@ protected function get_query_actions_sql( array $query, $select_or_count = 'sele /** * Query for action count or list of action IDs. * - * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @see ActionScheduler_Store::query_actions for $query arg usage. * @@ -446,7 +648,7 @@ public function cancel_action( $action_id ) { array( '%s' ), array( '%d' ) ); - if ( empty( $updated ) ) { + if ( false === $updated ) { /* translators: %s: action ID */ throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); } @@ -615,6 +817,33 @@ protected function generate_claim_id() { return $wpdb->insert_id; } + /** + * Set a claim filter. + * + * @param string $filter_name Claim filter name. + * @param mixed $filter_values Values to filter. + * @return void + */ + public function set_claim_filter( $filter_name, $filter_values ) { + if ( isset( $this->claim_filters[ $filter_name ] ) ) { + $this->claim_filters[ $filter_name ] = $filter_values; + } + } + + /** + * Get the claim filter value. + * + * @param string $filter_name Claim filter name. + * @return mixed + */ + public function get_claim_filter( $filter_name ) { + if ( isset( $this->claim_filters[ $filter_name ] ) ) { + return $this->claim_filters[ $filter_name ]; + } + + return ''; + } + /** * Mark actions claimed. * @@ -632,9 +861,8 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu /** @var \wpdb $wpdb */ global $wpdb; - $now = as_get_datetime_object(); - $date = is_null( $before_date ) ? $now : clone $before_date; - + $now = as_get_datetime_object(); + $date = is_null( $before_date ) ? $now : clone $before_date; // can't use $wpdb->update() because of the <= condition. $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; $params = array( @@ -643,44 +871,81 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu current_time( 'mysql' ), ); + // Set claim filters. + if ( ! empty( $hooks ) ) { + $this->set_claim_filter( 'hooks', $hooks ); + } else { + $hooks = $this->get_claim_filter( 'hooks' ); + } + if ( ! empty( $group ) ) { + $this->set_claim_filter( 'group', $group ); + } else { + $group = $this->get_claim_filter( 'group' ); + } + $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s'; $params[] = $date->format( 'Y-m-d H:i:s' ); $params[] = self::STATUS_PENDING; if ( ! empty( $hooks ) ) { $placeholders = array_fill( 0, count( $hooks ), '%s' ); - $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')'; + $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')'; $params = array_merge( $params, array_values( $hooks ) ); } - if ( ! empty( $group ) ) { - - $group_id = $this->get_group_id( $group, false ); + $group_operator = 'IN'; + if ( empty( $group ) ) { + $group = $this->get_claim_filter( 'exclude-groups' ); + $group_operator = 'NOT IN'; + } - // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour. - if ( empty( $group_id ) ) { - /* translators: %s: group name */ - throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) ); + if ( ! empty( $group ) ) { + $group_ids = $this->get_group_ids( $group, false ); + + // throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour. + if ( empty( $group_ids ) ) { + throw new InvalidArgumentException( + sprintf( + /* translators: %s: group name(s) */ + _n( + 'The group "%s" does not exist.', + 'The groups "%s" do not exist.', + is_array( $group ) ? count( $group ) : 1, + 'action-scheduler' + ), + $group + ) + ); } - $where .= ' AND group_id = %d'; - $params[] = $group_id; + $id_list = implode( ',', array_map( 'intval', $group_ids ) ); + $where .= " AND group_id {$group_operator} ( $id_list )"; } /** * Sets the order-by clause used in the action claim query. * - * @since x.x.x + * @since 3.4.0 * * @param string $order_by_sql */ - $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); + $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); $params[] = $limit; $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( false === $rows_affected ) { - throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); + $error = empty( $wpdb->last_error ) + ? _x( 'unknown', 'database error', 'action-scheduler' ) + : $wpdb->last_error; + + throw new \RuntimeException( + sprintf( + /* translators: %s database error. */ + __( 'Unable to claim actions. Database error: %s.', 'action-scheduler' ), + $error + ) + ); } return (int) $rows_affected; @@ -731,7 +996,7 @@ public function find_actions_by_claim_id( $claim_id ) { $cut_off = $before_date->format( 'Y-m-d H:i:s' ); $sql = $wpdb->prepare( - "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", + "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC", $claim_id ); @@ -754,8 +1019,31 @@ public function find_actions_by_claim_id( $claim_id ) { public function release_claim( ActionScheduler_ActionClaim $claim ) { /** @var \wpdb $wpdb */ global $wpdb; - $wpdb->update( $wpdb->actionscheduler_actions, array( 'claim_id' => 0 ), array( 'claim_id' => $claim->get_id() ), array( '%d' ), array( '%d' ) ); + /** + * Deadlock warning: This function modifies actions to release them from claims that have been processed. Earlier, we used to it in a atomic query, i.e. we would update all actions belonging to a particular claim_id with claim_id = 0. + * While this was functionally correct, it would cause deadlock, since this update query will hold a lock on the claim_id_.. index on the action table. + * This allowed the possibility of a race condition, where the claimer query is also running at the same time, then the claimer query will also try to acquire a lock on the claim_id_.. index, and in this case if claim release query has already progressed to the point of acquiring the lock, but have not updated yet, it would cause a deadlock. + * + * We resolve this by getting all the actions_id that we want to release claim from in a separate query, and then releasing the claim on each of them. This way, our lock is acquired on the action_id index instead of the claim_id index. Note that the lock on claim_id will still be acquired, but it will only when we actually make the update, rather than when we select the actions. + */ + $action_ids = $wpdb->get_col( $wpdb->prepare( "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", $claim->get_id() ) ); + + $row_updates = 0; + if ( count( $action_ids ) > 0 ) { + $action_id_string = implode( ',', array_map( 'absint', $action_ids ) ); + $row_updates = $wpdb->query( "UPDATE {$wpdb->actionscheduler_actions} SET claim_id = 0 WHERE action_id IN ({$action_id_string})" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + $wpdb->delete( $wpdb->actionscheduler_claims, array( 'claim_id' => $claim->get_id() ), array( '%d' ) ); + + if ( $row_updates < count( $action_ids ) ) { + throw new RuntimeException( + sprintf( + __( 'Unable to release actions from claim id %d.', 'action-scheduler' ), + $claim->get_id() + ) + ); + } } /** @@ -801,6 +1089,8 @@ public function mark_failure( $action_id ) { /** * Add execution message to action log. * + * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). + * * @param int $action_id Action ID. * * @return void @@ -811,7 +1101,20 @@ public function log_execution( $action_id ) { $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $status_updated = $wpdb->query( $sql ); + + if ( ! $status_updated ) { + throw new Exception( + sprintf( + /* translators: 1: action ID. 2: status slug. */ + __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), + $action_id, + self::STATUS_RUNNING + ) + ); + } } /** @@ -839,6 +1142,15 @@ public function mark_complete( $action_id ) { if ( empty( $updated ) ) { throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); //phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment } + + /** + * Fires after a scheduled action has been completed. + * + * @since 3.4.2 + * + * @param int $action_id Action ID. + */ + do_action( 'action_scheduler_completed_action', $action_id ); } /** diff --git a/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php b/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php index 24c1dffd..7c6b06d1 100644 --- a/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php +++ b/lib/woocommerce/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php @@ -452,7 +452,7 @@ protected function get_query_actions_sql( array $query, $select_or_count = 'sele /** * Query for action count or list of action IDs. * - * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * @since 3.3.0 $query['status'] accepts array of statuses instead of a single status. * * @see ActionScheduler_Store::query_actions for $query arg usage. * @@ -936,6 +936,8 @@ private function get_post_column( $action_id, $column_name ) { /** * Log Execution. * + * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). + * * @param string $action_id Action ID. */ public function log_execution( $action_id ) { @@ -947,7 +949,7 @@ public function log_execution( $action_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->query( + $status_updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s", self::STATUS_RUNNING, @@ -957,6 +959,17 @@ public function log_execution( $action_id ) { self::POST_TYPE ) ); + + if ( ! $status_updated ) { + throw new Exception( + sprintf( + /* translators: 1: action ID. 2: status slug. */ + __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), + $action_id, + self::STATUS_RUNNING + ) + ); + } } /** @@ -987,6 +1000,15 @@ public function mark_complete( $action_id ) { if ( is_wp_error( $result ) ) { throw new RuntimeException( $result->get_error_message() ); } + + /** + * Fires after a scheduled action has been completed. + * + * @since 3.4.2 + * + * @param int $action_id Action ID. + */ + do_action( 'action_scheduler_completed_action', $action_id ); } /** diff --git a/lib/woocommerce/action-scheduler/classes/migration/Runner.php b/lib/woocommerce/action-scheduler/classes/migration/Runner.php index 867c5de6..2304a79a 100644 --- a/lib/woocommerce/action-scheduler/classes/migration/Runner.php +++ b/lib/woocommerce/action-scheduler/classes/migration/Runner.php @@ -79,7 +79,7 @@ public function run( $batch_size = 10 ) { if ( $this->progress_bar ) { /* translators: %d: amount of actions */ - $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), number_format_i18n( $batch_size ) ) ); + $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), $batch_size ) ); $this->progress_bar->set_count( $batch_size ); } diff --git a/lib/woocommerce/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php b/lib/woocommerce/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php index 0ca9f7ca..1b1afec0 100644 --- a/lib/woocommerce/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php +++ b/lib/woocommerce/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php @@ -5,6 +5,9 @@ */ class ActionScheduler_NullSchedule extends ActionScheduler_SimpleSchedule { + /** @var DateTime|null */ + protected $scheduled_date; + /** * Make the $date param optional and default to null. * diff --git a/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php b/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php index af4aa5c5..c52d37ce 100644 --- a/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php +++ b/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php @@ -37,12 +37,12 @@ protected function get_table_definition( $table ) { case self::LOG_TABLE: $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; - return "CREATE TABLE {$table_name} ( + return "CREATE TABLE $table_name ( log_id bigint(20) unsigned NOT NULL auto_increment, action_id bigint(20) unsigned NOT NULL, message text NOT NULL, - log_date_gmt datetime NULL default '${default_date}', - log_date_local datetime NULL default '${default_date}', + log_date_gmt datetime NULL default '{$default_date}', + log_date_local datetime NULL default '{$default_date}', PRIMARY KEY (log_id), KEY action_id (action_id), KEY log_date_gmt (log_date_gmt) @@ -74,14 +74,14 @@ public function update_schema_3_0( $table, $db_version ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $table_name = $wpdb->prefix . 'actionscheduler_logs'; - $table_list = $wpdb->get_col( "SHOW TABLES LIKE '${table_name}'" ); + $table_list = $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ); $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; if ( ! empty( $table_list ) ) { $query = " - ALTER TABLE ${table_name} - MODIFY COLUMN log_date_gmt datetime NULL default '${default_date}', - MODIFY COLUMN log_date_local datetime NULL default '${default_date}' + ALTER TABLE {$table_name} + MODIFY COLUMN log_date_gmt datetime NULL default '{$default_date}', + MODIFY COLUMN log_date_local datetime NULL default '{$default_date}' "; $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } diff --git a/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php b/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php index 2506f018..a894d4ec 100644 --- a/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php +++ b/lib/woocommerce/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php @@ -16,7 +16,7 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema { /** * @var int Increment this value to trigger a schema update. */ - protected $schema_version = 6; + protected $schema_version = 7; public function __construct() { $this->tables = [ @@ -47,14 +47,15 @@ protected function get_table_definition( $table ) { action_id bigint(20) unsigned NOT NULL auto_increment, hook varchar(191) NOT NULL, status varchar(20) NOT NULL, - scheduled_date_gmt datetime NULL default '${default_date}', - scheduled_date_local datetime NULL default '${default_date}', + scheduled_date_gmt datetime NULL default '{$default_date}', + scheduled_date_local datetime NULL default '{$default_date}', + priority tinyint unsigned NOT NULL default '10', args varchar($max_index_length), schedule longtext, group_id bigint(20) unsigned NOT NULL default '0', attempts int(11) NOT NULL default '0', - last_attempt_gmt datetime NULL default '${default_date}', - last_attempt_local datetime NULL default '${default_date}', + last_attempt_gmt datetime NULL default '{$default_date}', + last_attempt_local datetime NULL default '{$default_date}', claim_id bigint(20) unsigned NOT NULL default '0', extended_args varchar(8000) DEFAULT NULL, PRIMARY KEY (action_id), @@ -71,7 +72,7 @@ protected function get_table_definition( $table ) { return "CREATE TABLE {$table_name} ( claim_id bigint(20) unsigned NOT NULL auto_increment, - date_created_gmt datetime NULL default '${default_date}', + date_created_gmt datetime NULL default '{$default_date}', PRIMARY KEY (claim_id), KEY date_created_gmt (date_created_gmt) ) $charset_collate"; @@ -111,16 +112,16 @@ public function update_schema_5_0( $table, $db_version ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $table_name = $wpdb->prefix . 'actionscheduler_actions'; - $table_list = $wpdb->get_col( "SHOW TABLES LIKE '${table_name}'" ); + $table_list = $wpdb->get_col( "SHOW TABLES LIKE '{$table_name}'" ); $default_date = self::DEFAULT_DATE; if ( ! empty( $table_list ) ) { $query = " - ALTER TABLE ${table_name} - MODIFY COLUMN scheduled_date_gmt datetime NULL default '${default_date}', - MODIFY COLUMN scheduled_date_local datetime NULL default '${default_date}', - MODIFY COLUMN last_attempt_gmt datetime NULL default '${default_date}', - MODIFY COLUMN last_attempt_local datetime NULL default '${default_date}' + ALTER TABLE {$table_name} + MODIFY COLUMN scheduled_date_gmt datetime NULL default '{$default_date}', + MODIFY COLUMN scheduled_date_local datetime NULL default '{$default_date}', + MODIFY COLUMN last_attempt_gmt datetime NULL default '{$default_date}', + MODIFY COLUMN last_attempt_local datetime NULL default '{$default_date}' "; $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } diff --git a/lib/woocommerce/action-scheduler/functions.php b/lib/woocommerce/action-scheduler/functions.php index 5f055467..9770f4fd 100644 --- a/lib/woocommerce/action-scheduler/functions.php +++ b/lib/woocommerce/action-scheduler/functions.php @@ -1,7 +1,8 @@ async( $hook, $args, $group ); + + /** + * Provides an opportunity to short-circuit the default process for enqueuing async + * actions. + * + * Returning a value other than null from the filter will short-circuit the normal + * process. The expectation in such a scenario is that callbacks will return an integer + * representing the enqueued action ID (enqueued using some alternative process) or else + * zero. + * + * @param int|null $pre_option The value to return instead of the option value. + * @param string $hook Action hook. + * @param array $args Action arguments. + * @param string $group Action group. + * @param int $priority Action priority. + */ + $pre = apply_filters( 'pre_as_enqueue_async_action', null, $hook, $args, $group, $priority ); + if ( null !== $pre ) { + return is_int( $pre ) ? $pre : 0; + } + + return ActionScheduler::factory()->create( + array( + 'type' => 'async', + 'hook' => $hook, + 'arguments' => $args, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** * Schedule an action to run one time * - * @param int $timestamp When the job will run. + * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. - * @param array $args Arguments to pass when the hook triggers. + * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. + * @param bool $unique Whether the action should be unique. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * * @return int The action ID. */ -function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { +function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } - return ActionScheduler::factory()->single( $hook, $args, $timestamp, $group ); + + /** + * Provides an opportunity to short-circuit the default process for enqueuing single + * actions. + * + * Returning a value other than null from the filter will short-circuit the normal + * process. The expectation in such a scenario is that callbacks will return an integer + * representing the scheduled action ID (scheduled using some alternative process) or else + * zero. + * + * @param int|null $pre_option The value to return instead of the option value. + * @param int $timestamp When the action will run. + * @param string $hook Action hook. + * @param array $args Action arguments. + * @param string $group Action group. + * @param int $priorities Action priority. + */ + $pre = apply_filters( 'pre_as_schedule_single_action', null, $timestamp, $hook, $args, $group, $priority ); + if ( null !== $pre ) { + return is_int( $pre ) ? $pre : 0; + } + + return ActionScheduler::factory()->create( + array( + 'type' => 'single', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** * Schedule a recurring action * - * @param int $timestamp When the first instance of the job will run. - * @param int $interval_in_seconds How long to wait between runs. + * @param int $timestamp When the first instance of the job will run. + * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. - * @param array $args Arguments to pass when the hook triggers. + * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. + * @param bool $unique Whether the action should be unique. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * * @return int The action ID. */ -function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { +function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } - return ActionScheduler::factory()->recurring( $hook, $args, $timestamp, $interval_in_seconds, $group ); + + $interval = (int) $interval_in_seconds; + + // We expect an integer and allow it to be passed using float and string types, but otherwise + // should reject unexpected values. + if ( ! is_numeric( $interval_in_seconds ) || $interval_in_seconds != $interval ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: provided value 2: provided type. */ + esc_html__( 'An integer was expected but "%1$s" (%2$s) was received.', 'action-scheduler' ), + esc_html( $interval_in_seconds ), + esc_html( gettype( $interval_in_seconds ) ) + ), + '3.6.0' + ); + + return 0; + } + + /** + * Provides an opportunity to short-circuit the default process for enqueuing recurring + * actions. + * + * Returning a value other than null from the filter will short-circuit the normal + * process. The expectation in such a scenario is that callbacks will return an integer + * representing the scheduled action ID (scheduled using some alternative process) or else + * zero. + * + * @param int|null $pre_option The value to return instead of the option value. + * @param int $timestamp When the action will run. + * @param int $interval_in_seconds How long to wait between runs. + * @param string $hook Action hook. + * @param array $args Action arguments. + * @param string $group Action group. + * @param int $priority Action priority. + */ + $pre = apply_filters( 'pre_as_schedule_recurring_action', null, $timestamp, $interval_in_seconds, $hook, $args, $group, $priority ); + if ( null !== $pre ) { + return is_int( $pre ) ? $pre : 0; + } + + return ActionScheduler::factory()->create( + array( + 'type' => 'recurring', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'pattern' => $interval_in_seconds, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** * Schedule an action that recurs on a cron-like schedule. * - * @param int $base_timestamp The first instance of the action will be scheduled - * to run at a time calculated after this timestamp matching the cron - * expression. This can be used to delay the first instance of the action. - * @param string $schedule A cron-link schedule string + * @param int $timestamp The first instance of the action will be scheduled + * to run at a time calculated after this timestamp matching the cron + * expression. This can be used to delay the first instance of the action. + * @param string $schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ @@ -72,16 +195,52 @@ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. - * @param array $args Arguments to pass when the hook triggers. + * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. + * @param bool $unique Whether the action should be unique. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * * @return int The action ID. */ -function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { +function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } - return ActionScheduler::factory()->cron( $hook, $args, $timestamp, $schedule, $group ); + + /** + * Provides an opportunity to short-circuit the default process for enqueuing cron + * actions. + * + * Returning a value other than null from the filter will short-circuit the normal + * process. The expectation in such a scenario is that callbacks will return an integer + * representing the scheduled action ID (scheduled using some alternative process) or else + * zero. + * + * @param int|null $pre_option The value to return instead of the option value. + * @param int $timestamp When the action will run. + * @param string $schedule Cron-like schedule string. + * @param string $hook Action hook. + * @param array $args Action arguments. + * @param string $group Action group. + * @param int $priority Action priority. + */ + $pre = apply_filters( 'pre_as_schedule_cron_action', null, $timestamp, $schedule, $hook, $args, $group, $priority ); + if ( null !== $pre ) { + return is_int( $pre ) ? $pre : 0; + } + + return ActionScheduler::factory()->create( + array( + 'type' => 'cron', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'pattern' => $schedule, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -95,10 +254,10 @@ function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), * by this method also. * * @param string $hook The hook that the job will trigger. - * @param array $args Args that would have been passed to the job. + * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to. * - * @return string|null The scheduled action ID if a scheduled action was found, or null if no matching action found. + * @return int|null The scheduled action ID if a scheduled action was found, or null if no matching action found. */ function as_unschedule_action( $hook, $args = array(), $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { @@ -116,8 +275,22 @@ function as_unschedule_action( $hook, $args = array(), $group = '' ) { } $action_id = ActionScheduler::store()->query_action( $params ); + if ( $action_id ) { - ActionScheduler::store()->cancel_action( $action_id ); + try { + ActionScheduler::store()->cancel_action( $action_id ); + } catch ( Exception $exception ) { + ActionScheduler::logger()->log( + $action_id, + sprintf( + /* translators: %s is the name of the hook to be cancelled. */ + __( 'Caught exception while cancelling action: %s', 'action-scheduler' ), + esc_attr( $hook ) + ) + ); + + $action_id = null; + } } return $action_id; @@ -127,7 +300,7 @@ function as_unschedule_action( $hook, $args = array(), $group = '' ) { * Cancel all occurrences of a scheduled action. * * @param string $hook The hook that the job will trigger. - * @param array $args Args that would have been passed to the job. + * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to. */ function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) { @@ -158,9 +331,9 @@ function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) { * returned. Or there may be no async, in-progress or pending action for this hook, in which case, * boolean false will be the return value. * - * @param string $hook - * @param array $args - * @param string $group + * @param string $hook Name of the hook to search for. + * @param array $args Arguments of the action to be searched. + * @param string $group Group of the action to be searched. * * @return int|bool The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action. */ @@ -196,7 +369,7 @@ function as_next_scheduled_action( $hook, $args = null, $group = '' ) { $scheduled_date = $action->get_schedule()->get_date(); if ( $scheduled_date ) { return (int) $scheduled_date->format( 'U' ); - } elseif ( null === $scheduled_date ) { // pending async action with NullSchedule + } elseif ( null === $scheduled_date ) { // pending async action with NullSchedule. return true; } @@ -209,7 +382,7 @@ function as_next_scheduled_action( $hook, $args = null, $group = '' ) { * It's recommended to use this function when you need to know whether a specific action is currently scheduled * (pending or in-progress). * - * @since x.x.x + * @since 3.3.0 * * @param string $hook The hook of the action. * @param array $args Args that have been passed to the action. Null will matches any args. @@ -223,10 +396,10 @@ function as_has_scheduled_action( $hook, $args = null, $group = '' ) { } $query_args = array( - 'hook' => $hook, - 'status' => array( ActionScheduler_Store::STATUS_RUNNING, ActionScheduler_Store::STATUS_PENDING ), - 'group' => $group, - 'orderby' => 'none', + 'hook' => $hook, + 'status' => array( ActionScheduler_Store::STATUS_RUNNING, ActionScheduler_Store::STATUS_PENDING ), + 'group' => $group, + 'orderby' => 'none', ); if ( null !== $args ) { @@ -235,26 +408,26 @@ function as_has_scheduled_action( $hook, $args = null, $group = '' ) { $action_id = ActionScheduler::store()->query_action( $query_args ); - return $action_id !== null; + return null !== $action_id; } /** * Find scheduled actions * - * @param array $args Possible arguments, with their default values: - * 'hook' => '' - the name of the action that will be triggered - * 'args' => NULL - the args array that will be passed with the action - * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. - * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' - * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. - * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' - * 'group' => '' - the group the action belongs to - * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING - * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID - * 'per_page' => 5 - Number of results to return - * 'offset' => 0 - * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', 'date' or 'none' - * 'order' => 'ASC' + * @param array $args Possible arguments, with their default values. + * 'hook' => '' - the name of the action that will be triggered. + * 'args' => NULL - the args array that will be passed with the action. + * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. + * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. + * 'group' => '' - the group the action belongs to. + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. + * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. + * 'per_page' => 5 - Number of results to return. + * 'offset' => 0. + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', 'date' or 'none'. + * 'order' => 'ASC'. * * @param string $return_format OBJECT, ARRAY_A, or ids. * @@ -265,25 +438,25 @@ function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { return array(); } $store = ActionScheduler::store(); - foreach ( array('date', 'modified') as $key ) { - if ( isset($args[$key]) ) { - $args[$key] = as_get_datetime_object($args[$key]); + foreach ( array( 'date', 'modified' ) as $key ) { + if ( isset( $args[ $key ] ) ) { + $args[ $key ] = as_get_datetime_object( $args[ $key ] ); } } $ids = $store->query_actions( $args ); - if ( $return_format == 'ids' || $return_format == 'int' ) { + if ( 'ids' === $return_format || 'int' === $return_format ) { return $ids; } $actions = array(); foreach ( $ids as $action_id ) { - $actions[$action_id] = $store->fetch_action( $action_id ); + $actions[ $action_id ] = $store->fetch_action( $action_id ); } - if ( $return_format == ARRAY_A ) { + if ( ARRAY_A == $return_format ) { foreach ( $actions as $action_id => $action_object ) { - $actions[$action_id] = get_object_vars($action_object); + $actions[ $action_id ] = get_object_vars( $action_object ); } } @@ -302,7 +475,7 @@ function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { * timezone when instantiating datetimes rather than leaving it up to * the PHP default. * - * @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php. + * @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php. * @param string $timezone A timezone identifier, like UTC or Europe/Lisbon. The list of valid identifiers is available http://php.net/manual/en/timezones.php. * * @return ActionScheduler_DateTime @@ -313,7 +486,7 @@ function as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) { } elseif ( is_numeric( $date_string ) ) { $date = new ActionScheduler_DateTime( '@' . $date_string, new DateTimeZone( $timezone ) ); } else { - $date = new ActionScheduler_DateTime( $date_string, new DateTimeZone( $timezone ) ); + $date = new ActionScheduler_DateTime( null === $date_string ? 'now' : $date_string, new DateTimeZone( $timezone ) ); } return $date; } diff --git a/lib/woocommerce/action-scheduler/lib/WP_Async_Request.php b/lib/woocommerce/action-scheduler/lib/WP_Async_Request.php index d7dea1c2..ff5e29b3 100644 --- a/lib/woocommerce/action-scheduler/lib/WP_Async_Request.php +++ b/lib/woocommerce/action-scheduler/lib/WP_Async_Request.php @@ -104,10 +104,17 @@ protected function get_query_args() { return $this->query_args; } - return array( + $args = array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); + + /** + * Filters the post arguments used during an async request. + * + * @param array $url + */ + return apply_filters( $this->identifier . '_query_args', $args ); } /** @@ -120,7 +127,14 @@ protected function get_query_url() { return $this->query_url; } - return admin_url( 'admin-ajax.php' ); + $url = admin_url( 'admin-ajax.php' ); + + /** + * Filters the post arguments used during an async request. + * + * @param string $url + */ + return apply_filters( $this->identifier . '_query_url', $url ); } /** @@ -133,13 +147,20 @@ protected function get_post_args() { return $this->post_args; } - return array( + $args = array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); + + /** + * Filters the post arguments used during an async request. + * + * @param array $args + */ + return apply_filters( $this->identifier . '_post_args', $args ); } /** diff --git a/lib/woocommerce/action-scheduler/readme.txt b/lib/woocommerce/action-scheduler/readme.txt index 2afd6278..5c707019 100644 --- a/lib/woocommerce/action-scheduler/readme.txt +++ b/lib/woocommerce/action-scheduler/readme.txt @@ -1,11 +1,9 @@ === Action Scheduler === Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1 Tags: scheduler, cron -Requires at least: 5.2 -Tested up to: 5.7 -Stable tag: 3.4.0 +Stable tag: 3.6.2 License: GPLv3 -Requires PHP: 5.6 +Tested up to: 6.3 Action Scheduler - Job Queue for WordPress @@ -47,6 +45,80 @@ Collaboration is cool. We'd love to work with you to improve Action Scheduler. [ == Changelog == += 3.6.2 - 2023-08-09 = +* Add guidance about passing arguments. +* Atomic option locking. +* Improve bulk delete handling. +* Include database error in the exception message. +* Tweak - WP 6.3 compatibility. + += 3.6.1 - 2023-06-14 = +* Document new optional `$priority` arg for various API functions. +* Document the new `--exclude-groups` WP CLI option. +* Document the new `action_scheduler_init` hook. +* Ensure actions within each claim are executed in the expected order. +* Fix incorrect text domain. +* Remove SHOW TABLES usage when checking if tables exist. + += 3.6.0 - 2023-05-10 = +* Add $unique parameter to function signatures. +* Add a cast-to-int for extra safety before forming new DateTime object. +* Add a hook allowing exceptions for consistently failing recurring actions. +* Add action priorities. +* Add init hook. +* Always raise the time limit. +* Bump minimatch from 3.0.4 to 3.0.8. +* Bump yaml from 2.2.1 to 2.2.2. +* Defensive coding relating to gaps in declared schedule types. +* Do not process an action if it cannot be set to `in-progress`. +* Filter view labels (status names) should be translatable | #919. +* Fix WPCLI progress messages. +* Improve data-store initialization flow. +* Improve error handling across all supported PHP versions. +* Improve logic for flushing the runtime cache. +* Support exclusion of multiple groups. +* Update lint-staged and Node/NPM requirements. +* add CLI clean command. +* add CLI exclude-group filter. +* exclude past-due from list table all filter count. +* throwing an exception if as_schedule_recurring_action interval param is not of type integer. + += 3.5.4 - 2023-01-17 = +* Add pre filters during action registration. +* Async scheduling. +* Calculate timeouts based on total actions. +* Correctly order the parameters for `ActionScheduler_ActionFactory`'s calls to `single_unique`. +* Fetch action in memory first before releasing claim to avoid deadlock. +* PHP 8.2: declare property to fix creation of dynamic property warning. +* PHP 8.2: fix "Using ${var} in strings is deprecated, use {$var} instead". +* Prevent `undefined variable` warning for `$num_pastdue_actions`. + += 3.5.3 - 2022-11-09 = +* Query actions with partial match. + += 3.5.2 - 2022-09-16 = +* Fix - erroneous 3.5.1 release. + += 3.5.1 - 2022-09-13 = +* Maintenance on A/S docs. +* fix: PHP 8.2 deprecated notice. + += 3.5.0 - 2022-08-25 = +* Add - The active view link within the "Tools > Scheduled Actions" screen is now clickable. +* Add - A warning when there are past-due actions. +* Enhancement - Added the ability to schedule unique actions via an atomic operation. +* Enhancement - Improvements to cache invalidation when processing batches (when running on WordPress 6.0+). +* Enhancement - If a recurring action is found to be consistently failing, it will stop being rescheduled. +* Enhancement - Adds a new "Past Due" view to the scheduled actions list table. + += 3.4.2 - 2022-06-08 = +* Fix - Change the include for better linting. +* Fix - update: Added Action scheduler completed action hook. + += 3.4.1 - 2022-05-24 = +* Fix - Change the include for better linting. +* Fix - Fix the documented return type. + = 3.4.0 - 2021-10-29 = * Enhancement - Number of items per page can now be set for the Scheduled Actions view (props @ovidiul). #771 * Fix - Do not lower the max_execution_time if it is already set to 0 (unlimited) (props @barryhughes). #755 diff --git a/src/Admin/AdminServiceProvider.php b/src/Admin/AdminServiceProvider.php index 91422a26..a76986c4 100644 --- a/src/Admin/AdminServiceProvider.php +++ b/src/Admin/AdminServiceProvider.php @@ -51,6 +51,7 @@ public function get_subscribers() { 'admin-page-subscriber', 'admin-notice-subscriber', 'admin-notification-five-star-rating', + 'admin-notification-upe-4710', ); } @@ -82,6 +83,12 @@ public function register() { 'admin-notification-five-star-rating', PluginRatingNotification::class ); + + // UPE (4.7.10) notification. + $container->share( + 'admin-notification-upe-4710', + UpeNotification::class + ); } /** diff --git a/src/Admin/Education/InstantPayouts.php b/src/Admin/Education/InstantPayouts.php index cb594b6f..615b6a58 100644 --- a/src/Admin/Education/InstantPayouts.php +++ b/src/Admin/Education/InstantPayouts.php @@ -48,7 +48,7 @@ public function get_subscribed_events() { * * @since 4.4.4 * - * @param \SimplePay\Core\Settings\Subsection_Collection<\SimplePay\Core\Settings\Subsection> $subsections Subsections collection. + * @param \SimplePay\Core\Settings\Subsection_Collection $subsections Subsections collection. * @return void */ function register_settings_subsection( $subsections ) { diff --git a/src/Admin/Education/PluginCustomersSettings.php b/src/Admin/Education/PluginCustomersSettings.php index e21cf91e..f3e9bf98 100644 --- a/src/Admin/Education/PluginCustomersSettings.php +++ b/src/Admin/Education/PluginCustomersSettings.php @@ -46,7 +46,7 @@ public function get_subscribed_events() { * * @since 4.4.0 * - * @param \SimplePay\Core\Settings\Section_Collection<\SimplePay\Core\Settings\Section> $sections Section collection. + * @param \SimplePay\Core\Settings\Section_Collection $sections Section collection. * @return void */ function register_section( $sections ) { @@ -70,7 +70,7 @@ function register_section( $sections ) { * * @since 4.4.0 * - * @param \SimplePay\Core\Settings\Subsection_Collection<\SimplePay\Core\Settings\Subsection> $subsections Subsections collection. + * @param \SimplePay\Core\Settings\Subsection_Collection $subsections Subsections collection. * @return void */ function register_subsections( $subsections ) { diff --git a/src/Admin/Education/PluginTaxesSettings.php b/src/Admin/Education/PluginTaxesSettings.php index 38bc10a0..bef948bd 100644 --- a/src/Admin/Education/PluginTaxesSettings.php +++ b/src/Admin/Education/PluginTaxesSettings.php @@ -40,7 +40,7 @@ public function get_subscribed_events() { * * @since 4.4.0 * - * @param \SimplePay\Core\Settings\Subsection_Collection<\SimplePay\Core\Settings\Subsection> $subsections Subsections collection. + * @param \SimplePay\Core\Settings\Subsection_Collection $subsections Subsections collection. * @return void */ function register_subsections( $subsections ) { diff --git a/src/Admin/FormBuilder/LicenseCheck.php b/src/Admin/FormBuilder/LicenseCheck.php index ec06ba88..b8311c6d 100644 --- a/src/Admin/FormBuilder/LicenseCheck.php +++ b/src/Admin/FormBuilder/LicenseCheck.php @@ -82,11 +82,16 @@ public function maybe_show_license_notice() { ) ); - $renew_url = simpay_ga_url( - 'https://wpsimplepay.com/my-account/licenses/', - 'payment-form-license-renewal' + $renew_url = add_query_arg( + array( + 'edd_license_key' => $this->license->get_key(), + 'discount' => 'SAVE50', + ), + sprintf( '%s/checkout', untrailingslashit( SIMPLE_PAY_STORE_URL ) ) // @phpstan-ignore-line ); + $renew_url = simpay_ga_url( $renew_url, 'payment-form-license-renewal' ); + $learn_more_url = simpay_ga_url( 'https://wpsimplepay.com/lite-vs-pro/', 'payment-form-license-renewal' diff --git a/src/Admin/SetupWizard/SetupWizardLaunch.php b/src/Admin/SetupWizard/SetupWizardLaunch.php index da6d183c..1abe716a 100644 --- a/src/Admin/SetupWizard/SetupWizardLaunch.php +++ b/src/Admin/SetupWizard/SetupWizardLaunch.php @@ -100,7 +100,7 @@ public function maybe_set_launched() { * * @since 4.4.2 * - * @param \SimplePay\Core\Settings\Setting_Collection<\SimplePay\Core\Settings\Setting> $settings Settings collection. + * @param \SimplePay\Core\Settings\Setting_Collection $settings Settings collection. * @return void */ public function add_settings_launch( $settings ) { diff --git a/src/Admin/SiteHealth/SiteHealthDebugInformation.php b/src/Admin/SiteHealth/SiteHealthDebugInformation.php index e14fb94a..3b6df15f 100644 --- a/src/Admin/SiteHealth/SiteHealthDebugInformation.php +++ b/src/Admin/SiteHealth/SiteHealthDebugInformation.php @@ -139,6 +139,19 @@ private function stripe_tls_check() { } } + /** + * Returns the Stripe Account ID, or - if not using Connect. + * + * @since 4.7.10 + * + * @return string + */ + private function get_stripe_account_id() { + $account_id = simpay_get_account_id(); + + return ! empty( $account_id ) ? $account_id : '-'; + } + /** * Returns "Test Mode" or "Live Mode" depending on the mode set in WPSP Stripe settings * @@ -444,6 +457,10 @@ public function debug_information( $debug_info ) { 'label' => __( 'Stripe TLS', 'stripe' ), 'value' => $this->stripe_tls_check(), ), + 'stripe_account_id' => array( + 'label' => __( 'Stripe Account ID', 'stripe' ), + 'value' => $this->get_stripe_account_id(), + ), 'mode' => array( 'label' => __( 'Global Payment Mode', 'stripe' ), 'value' => $this->get_test_or_live_mode(), @@ -472,14 +489,18 @@ public function debug_information( $debug_info ) { 'label' => __( 'Webhook Secret', 'stripe' ), 'value' => $this->get_webhook_secret(), ), - 'upe' => array( - 'label' => __( 'Using UPE', 'stripe' ), - 'value' => $this->get_upe_yes_or_upe_no(), - ), 'opinionated_styles' => array( 'label' => __( 'Opinionated Styles', 'stripe' ), 'value' => $this->get_opinionated_styles_enabled(), ), + 'db_tables' => array( + 'label' => __( 'Database Tables', 'stripe' ), + 'value' => $this->get_custom_database_tables(), + ), + 'upe' => array( + 'label' => __( 'Using UPE', 'stripe' ), + 'value' => $this->get_upe_yes_or_upe_no(), + ), ), ); @@ -534,4 +555,42 @@ function( $plugin_data ) { ); } + /** + * Returns the custom database tables, and their versions. + * + * @since 4.7.10 + * + * @return string + */ + private function get_custom_database_tables() { + global $wpdb; + + $tables = array( + 'wpsp_coupons', + 'wpsp_notifications', + 'wpsp_transactions', + 'wpsp_webhooks', + ); + + $ret = ''; + + foreach ( $tables as $table ) { + $table_name = $wpdb->prefix . $table; + + $found = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) + ); + + if ( empty( $found ) ) { + $ret .= '⚠️ ' . $table . ' table not found'; + } + } + + if ( '' === $ret ) { + $ret = 'All tables found'; + } + + return $ret; + } + } diff --git a/src/Admin/UpeNotification.php b/src/Admin/UpeNotification.php new file mode 100644 index 00000000..306ff682 --- /dev/null +++ b/src/Admin/UpeNotification.php @@ -0,0 +1,139 @@ +license->is_lite() ) { + return $subscribers; + } + + // Alert via Notification Inbox if available. + if ( $this->notifications instanceof NotificationRepository ) { + if ( ! simpay_is_upe() ) { + $subscribers['admin_init'][] = array( 'add_notification' ); + } + + $subscribers['pre_update_option_simpay_settings'] = array( 'dismiss_notification' ); + } + + return $subscribers; + } + + /** + * Adds a notification to leave a review. + * + * @since 4.7.10 + * + * @return void + */ + public function add_notification() { + $notification = $this->notifications->get_by( 'slug', 'upe-4710' ); + + if ( $notification instanceof Notification && $notification->dismissed ) { + return; + } + + $settings_url = Settings\get_url( + array( + 'section' => 'general', + 'subsection' => 'advanced', + 'setting' => 'is_upe', + ) + ); + + $this->notifications->restore( + array( + 'type' => 'success', + 'source' => 'internal', + 'title' => __( + 'A New Payment Experience is Available', + 'stripe' + ), + 'slug' => 'upe-4710', + 'content' => 'Join the other WP Simple Pay users who have already embraced the new payment experience to offer Stripe Link and other powerful payment form features. With the new smarter payment forms, you get access to: + +🔗  Stripe Link support for 9x faster payments
    +💳  Access to more payment methods
    +📍  Streamlined fields with automatic address suggestions
    +🤖  Additional anti-spam functionality
    +💯  + new features available each update
    + +Don\'t wait any longer to harness the power of WP Simple Pay\'s latest and greatest features.', + 'actions' => array( + array( + 'type' => 'primary', + 'text' => __( 'Enable Now', 'stripe' ), + 'url' => $settings_url, + ), + array( + 'type' => 'secondary', + 'text' => __( 'Learn More', 'stripe' ), + 'url' => simpay_docs_link( + '', + 'how-to-enable-the-new-payment-experience', + 'notification-inbox', + true + ), + ), + ), + 'conditions' => array(), + 'start' => date( 'Y-m-d H:i:s', time() ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + 'end' => date( 'Y-m-d H:i:s', time() + YEAR_IN_SECONDS * 10 ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + ) + ); + } + + /** + * Dismisses all UPE notifications when the UPE is enabled. + * + * @since 4.7.10 + * + * @param array $settings Settings. + * @return array + */ + public function dismiss_notification( $settings ) { + if ( ! current_user_can( 'manage_options' ) ) { + return $settings; + } + + if ( isset( $settings['is_upe'] ) && 'yes' === $settings['is_upe'] ) { + $this->notifications->dismiss( 'upe-4710' ); + } + + return $settings; + } + +} diff --git a/src/AdminNotice/LicenseExpiredNotice.php b/src/AdminNotice/LicenseExpiredNotice.php index 7e280e2f..bfb8d282 100644 --- a/src/AdminNotice/LicenseExpiredNotice.php +++ b/src/AdminNotice/LicenseExpiredNotice.php @@ -11,17 +11,12 @@ namespace SimplePay\Core\AdminNotice; -use SimplePay\Core\License\LicenseAwareInterface; -use SimplePay\Core\License\LicenseAwareTrait; - /** * LicenseExpiredNotice class. * * @since 4.4.6 */ -class LicenseExpiredNotice extends AbstractAdminNotice implements LicenseAwareInterface { - - use LicenseAwareTrait; +class LicenseExpiredNotice extends AbstractAdminNotice { /** * {@inheritdoc} @@ -67,14 +62,21 @@ public function should_display() { * {@inheritdoc} */ public function get_notice_data() { - $renew_url = simpay_ga_url( - 'https://wpsimplepay.com/my-account/billing', - 'admin-notice-expired-license' + $renew_url = add_query_arg( + array( + 'edd_license_key' => $this->license->get_key(), + 'discount' => 'SAVE50', + ), + sprintf( '%s/checkout', untrailingslashit( SIMPLE_PAY_STORE_URL ) ) // @phpstan-ignore-line ); - $learn_more_url = simpay_ga_url( - 'https://wpsimplepay.com/lite-vs-pro/', - 'admin-notice-expired-license' + $renew_url = simpay_ga_url( $renew_url, 'admin-notice-expired-license' ); + + $learn_more_url = simpay_docs_link( + 'Learn More', + 'what-happens-if-my-license-expires', + 'admin-notice-expired-license', + true ); /** @var string $expiration */ @@ -87,7 +89,7 @@ public function get_notice_data() { 'renew_url' => $renew_url, 'learn_more_url' => $learn_more_url, 'is_in_grace_period' => $this->license->is_in_grace_period(), - 'grace_period_ends' => date( + 'grace_period_ends' => date( // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $format, strtotime( $expiration ) + ( DAY_IN_SECONDS * 14 ) ), diff --git a/src/AdminNotice/LicenseMissingNotice.php b/src/AdminNotice/LicenseMissingNotice.php index 09507235..7f1032ec 100644 --- a/src/AdminNotice/LicenseMissingNotice.php +++ b/src/AdminNotice/LicenseMissingNotice.php @@ -11,8 +11,6 @@ namespace SimplePay\Core\AdminNotice; -use SimplePay\Core\License\LicenseAwareInterface; -use SimplePay\Core\License\LicenseAwareTrait; use SimplePay\Core\Settings; /** @@ -20,9 +18,7 @@ * * @since 4.4.1 */ -class LicenseMissingNotice extends AbstractAdminNotice implements LicenseAwareInterface { - - use LicenseAwareTrait; +class LicenseMissingNotice extends AbstractAdminNotice { /** * {@inheritdoc} diff --git a/src/CustomerSuccess/CustomerSuccessServiceProvider.php b/src/CustomerSuccess/CustomerSuccessServiceProvider.php index 5e4e65a5..98231fc1 100644 --- a/src/CustomerSuccess/CustomerSuccessServiceProvider.php +++ b/src/CustomerSuccess/CustomerSuccessServiceProvider.php @@ -34,11 +34,15 @@ public function get_services() { */ public function get_subscribers() { return array( + // Achievements. 'customer-success-achievement-first-form', 'customer-success-achievement-first-form-embed', 'customer-success-achievement-first-test-payment', 'customer-success-achievement-go-live', 'customer-success-achievement-first-live-payment', + + // Telemetry. + 'customer-success-telemetry', ); } @@ -100,6 +104,13 @@ public function register() { ) ->withArgument( $achievements ); } + + // Telemetry. + $container->share( + 'customer-success-telemetry', + TelemetrySubscriber::class + ) + ->withArgument( $container->get( 'scheduler' ) ); } } diff --git a/src/CustomerSuccess/TelemetryData/AbstractTelemetryData.php b/src/CustomerSuccess/TelemetryData/AbstractTelemetryData.php new file mode 100644 index 00000000..33ba6538 --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/AbstractTelemetryData.php @@ -0,0 +1,30 @@ + + */ + abstract public function get(); + +} diff --git a/src/CustomerSuccess/TelemetryData/AbstractTransactionTelemetryData.php b/src/CustomerSuccess/TelemetryData/AbstractTransactionTelemetryData.php new file mode 100644 index 00000000..87d4fa92 --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/AbstractTransactionTelemetryData.php @@ -0,0 +1,81 @@ +> Payment method types and their transaction amounts. + */ + protected function get_transaction_amount_by_pm_by_date_range( $range = 'all' ) { + global $wpdb; + + /** @var string $currency */ + $currency = simpay_get_setting( 'currency', 'usd' ); + $currency = strtolower( $currency ); + + $select_value = simpay_is_zero_decimal( $currency ) + ? 'SUM(amount_total) as total' + : 'SUM(amount_total / 100) as total'; + + $date_condition = '1=1'; + + if ( '7days' === $range ) { + $date_condition = 'date_created >= DATE_SUB(NOW(), INTERVAL 7 DAY)'; + } elseif ( '30days' === $range ) { + $date_condition = 'date_created >= DATE_SUB(NOW(), INTERVAL 30 DAY)'; + } + + $transactions = $wpdb->get_results( + // @phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->prepare( + "SELECT payment_method_type, {$select_value}, + COUNT(*) AS count + FROM {$wpdb->prefix}wpsp_transactions + WHERE status = 'succeeded' + AND currency = %s + AND {$date_condition} + GROUP BY payment_method_type;", + $currency + ), + // @phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ARRAY_A + ); + + $result = array(); + + foreach ( $transactions as $transaction ) { + if ( null === $transaction['payment_method_type'] ) { + continue; + } + + $result[ $transaction['payment_method_type'] ] = array( + 'total' => (int) round( $transaction['total'] ), + 'count' => (int) $transaction['count'], + ); + } + + return $result; + } + +} diff --git a/src/CustomerSuccess/TelemetryData/CustomerJourneyTelemetryData.php b/src/CustomerSuccess/TelemetryData/CustomerJourneyTelemetryData.php new file mode 100644 index 00000000..ed372f1d --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/CustomerJourneyTelemetryData.php @@ -0,0 +1,55 @@ + $achievements */ + $achievements = get_option( CustomerAchievements::OPTION_NAME, array() ); + + $journey = array( + 'start' => gmdate( 'Y-m-d H:i:s', (int) $start ), + ); + + // Determine how long it took for the achievement to be earned after + // the customer journey started. Create a human readable version of the difference. + foreach ( $achievements as $achievement_id => $achievement_time ) { + /** @var int $achievement_time */ + $journey[ $achievement_id ] = gmdate( 'Y-m-d H:i:s', $achievement_time ); + } + + return $journey; + } + +} diff --git a/src/CustomerSuccess/TelemetryData/EnvironmentTelemetryData.php b/src/CustomerSuccess/TelemetryData/EnvironmentTelemetryData.php new file mode 100644 index 00000000..854a3b5e --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/EnvironmentTelemetryData.php @@ -0,0 +1,112 @@ + phpversion(), + 'wp_version' => $this->get_wp_version(), + 'sql_version' => $this->get_sql_version(), + ), + $this->parse_server(), + array( + 'is_ssl' => (bool) is_ssl(), + 'locale' => get_locale(), + 'active_theme' => $this->get_active_theme(), + 'multisite' => (bool) is_multisite(), + ) + ); + } + + /** + * Adds the server data to the array of data. + * + * @since 4.7.10 + * + * @return array + */ + private function parse_server() { + $server = ( isset( $_SERVER['SERVER_SOFTWARE'] ) + ? $_SERVER['SERVER_SOFTWARE'] + : 'unknown' ); + + $server = explode( '/', $server ); + + $data = array( + 'server' => $server[0], + ); + + if ( isset( $server[1] ) ) { + $data['server_version'] = $server[1]; + } + + return $data; + } + + /** + * Gets the WordPress version. + * + * @since 4.7.10 + * + * @return string + */ + private function get_wp_version() { + $version = get_bloginfo( 'version' ); + $version = explode( '-', $version ); + + return reset( $version ); + } + + /** + * Returns a semi-normalized version of the SQL version. + * + * @since 4.7.10 + * + * @return string + */ + private function get_sql_version() { + global $wpdb; + + $type = $wpdb->db_server_info(); + $version = $wpdb->get_var( 'SELECT VERSION()' ); + + if ( stristr( $type, 'mariadb' ) ) { + $type = 'MariaDB'; + } else { + $type = 'MySQL'; + } + + return $type . ' ' . $version; + } + + /** + * Gets the active theme name. + * + * @since 4.7.10 + * + * @return string + */ + private function get_active_theme() { + return wp_get_theme()->name; + } +} diff --git a/src/CustomerSuccess/TelemetryData/IntegrationTelemetryData.php b/src/CustomerSuccess/TelemetryData/IntegrationTelemetryData.php new file mode 100644 index 00000000..e468698b --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/IntegrationTelemetryData.php @@ -0,0 +1,58 @@ +get_all_plugins() as $basename => $details ) { + if ( ! is_plugin_active( $basename ) ) { + continue; + } + + /** @var array{Name: string, Version: string} $details */ + + $data[] = array( + 'name' => $details['Name'], + 'version' => $details['Version'], + ); + } + + return $data; + } + + /** + * Gets all plugins on the site. + * + * @since 4.7.10 + * + * @return array + */ + private function get_all_plugins() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + return get_plugins(); + } + +} diff --git a/src/CustomerSuccess/TelemetryData/PaymentFormTelemetryData.php b/src/CustomerSuccess/TelemetryData/PaymentFormTelemetryData.php new file mode 100644 index 00000000..31f78d14 --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/PaymentFormTelemetryData.php @@ -0,0 +1,161 @@ +get_form_types(), + $this->get_tax_types(), + array( + 'payment_pages' => $this->get_payment_pages(), + 'inventory' => $this->get_inventory(), + 'scheduling' => $this->get_scheduling(), + ) + ); + } + + /** + * Returns the number of times a particular payment form type has been used across all forms. + * + * @since 4.7.10 + * + * @return array + */ + private function get_form_types() { + global $wpdb; + + $display_types = $wpdb->get_col( + $wpdb->prepare( + "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = %s", + '_form_display_type' + ) + ); + + $counts = array(); + + foreach ( $display_types as $display_type ) { + switch ( $display_type ) { + case 'embedded': + $display_type = 'type_on_site'; + break; + default: + $display_type = 'type_on_site'; + break; + } + + if ( ! isset( $counts[ $display_type ] ) ) { + $counts[ $display_type ] = 0; + } + + $counts[ $display_type ]++; + } + + return $counts; + } + + /** + * Returns the number of times inventory is used across all forms. + * + * @since 4.7.10 + * + * @return int + */ + private function get_inventory() { + global $wpdb; + + $enabled = $wpdb->get_var( + "SELECT COUNT(*) FROM $wpdb->postmeta WHERE meta_key = '_inventory' AND meta_value = 'yes'" + ); + + return $enabled ? (int) $enabled : 0; + } + + /** + * Returns the number of times scheudling is used across all forms. + * + * @since 4.7.10 + * + * @return int + */ + private function get_scheduling() { + global $wpdb; + + $enabled = $wpdb->get_var( + "SELECT COUNT(*) FROM $wpdb->postmeta WHERE meta_key IN('_schedule_start', '_schedule_end') AND meta_value = 'yes'" + ); + + return $enabled ? (int) $enabled : 0; + } + + /** + * Returns the number of times tax collection types have been enabled across all forms. + * + * @since 4.7.10 + * + * @return array + */ + private function get_tax_types() { + global $wpdb; + + $tax_meta = $wpdb->get_col( + "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_tax_status'" + ); + + $taxes = array(); + + foreach ( $tax_meta as $tax_type ) { + switch ( $tax_type ) { + case 'none': + case 'automatic': + $tax_type = 'tax_' . $tax_type; + break; + default: + $tax_type = 'tax_global'; + } + + if ( ! isset( $taxes[ $tax_type ] ) ) { + $taxes[ $tax_type ] = 0; + } + + $taxes[ $tax_type ]++; + } + + return $taxes; + } + + /** + * Returns the number of times payment pages across all forms. + * + * @since 4.7.10 + * + * @return int + */ + private function get_payment_pages() { + global $wpdb; + + $enabled = $wpdb->get_var( + "SELECT COUNT(*) FROM $wpdb->postmeta WHERE meta_key = '_enable_payment_page' AND meta_value = 'yes'" + ); + + return $enabled ? (int) $enabled : 0; + } +} diff --git a/src/CustomerSuccess/TelemetryData/PluginTelemetryData.php b/src/CustomerSuccess/TelemetryData/PluginTelemetryData.php new file mode 100644 index 00000000..72b04392 --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/PluginTelemetryData.php @@ -0,0 +1,165 @@ +license = $license; + } + + /** + * {@inheritdoc} + */ + public function get() { + /** @var string $stripe_account_id */ + $stripe_account_id = get_option( 'simpay_stripe_connect_account_id', '' ); + + /** @var string $country */ + $country = simpay_get_setting( 'account_country', 'US' ); + + /** @var string $currency */ + $currency = simpay_get_setting( 'currency', 'usd' ); + $currency = strtolower( $currency ); + + $data = array( + 'version' => SIMPLE_PAY_VERSION, // @phpstan-ignore-line + 'license_level' => ucfirst( $this->license->get_level() ), + 'license_status' => $this->license->is_lite() + ? 'valid' + : $this->license->get_status(), + 'stripe_livemode' => simpay_is_livemode(), + 'stripe_connect' => '' !== $stripe_account_id, + 'stripe_country' => strtolower( $country ), + 'stripe_currency' => $currency, + 'stripe_webhook' => $this->is_webhook_working(), + 'stripe_upe' => simpay_is_upe(), + 'fraud_captcha' => $this->get_antispam_captcha(), + ); + + if ( $this->license->is_lite() ) { + return $data; + } + + /** @var string $email_verification */ + $email_verification = simpay_get_setting( 'fraud_email_verification', 'yes' ); + $data['fraud_email_verification'] = ( 'yes' === $email_verification ); + + /** @var int $fraud_email_verification_threshold */ + $fraud_email_verification_threshold = simpay_get_setting( 'fraud_email_verification_threshold', 3 ); + $data['fraud_email_verification_threshold'] = $fraud_email_verification_threshold . ' times'; + + /** @var int $fraud_email_verification_timeframe */ + $fraud_email_verification_timeframe = simpay_get_setting( 'fraud_email_verification_timeframe', 6 ); + $data['fraud_email_verification_timeframe'] = $fraud_email_verification_timeframe . ' hours'; + + /** @var string $fraud_require_authentication */ + $fraud_require_authentication = simpay_get_setting( 'fraud_require_authentication', 'no' ); + $data['fraud_require_authentication'] = ( 'yes' === $fraud_require_authentication ); + + $data['email_payment_notification'] = ( new PaymentNotificationEmail() )->is_enabled(); + $data['email_payment_confirmation'] = ( new PaymentConfirmationEmail() )->is_enabled(); + $data['email_upcoming_invoice'] = ( new UpcomingInvoiceEmail() )->is_enabled(); + $data['email_summary'] = ( new SummaryReportEmail() )->is_enabled(); + + /** @var string $subscription_management */ + $subscription_management = simpay_get_setting( 'subscription_management', 'none' ); + $data['subscription_management'] = implode( + ' ', + array_map( + 'ucfirst', + explode( '-', $subscription_management ) + ) + ); + + return $data; + } + + /** + * Looks for the most recent wpsp_transaction's date_created, and the most recent + * wpsp_webhook's date_created, and determines if the webhook event occured within + * 15 minutes of the transaction. + * + * This mimics the logic in src/Webhook/EndpointHealthCheck.php, without the dependencies. + * + * @return bool + */ + private function is_webhook_working() { + global $wpdb; + + if ( $this->license->is_lite() ) { + return true; + } + + $latest_transaction = $wpdb->get_var( + "SELECT date_created FROM {$wpdb->prefix}wpsp_transactions ORDER BY date_created DESC LIMIT 1" + ); + + // No transactions, so we don't know if the webhook is working. + if ( ! $latest_transaction ) { + return true; + } + + $latest_webhook = $wpdb->get_var( + "SELECT date_created FROM {$wpdb->prefix}wpsp_webhooks ORDER BY date_created DESC LIMIT 1" + ); + + if ( ! $latest_webhook ) { + return false; + } + + return ( strtotime( $latest_transaction ) - strtotime( $latest_webhook ) ) < 15 * MINUTE_IN_SECONDS; + } + + /** + * Returns the antispam type. + * + * @return string + */ + private function get_antispam_captcha() { + $existing_recaptcha = simpay_get_setting( 'recaptcha_site_key', '' ); + $default = ! empty( $existing_recaptcha ) + ? 'recaptcha-v3' + : ''; + + $type = simpay_get_setting( 'captcha_type', $default ); + + switch ( $type ) { + case 'hcaptcha': + return 'hCaptcha'; + case 'recaptcha-v3': + return 'reCAPTCHA v3'; + case 'cloudflare-turnstile': + return 'Cloudflare Turnstile'; + default: + return 'None'; + } + } +} diff --git a/src/CustomerSuccess/TelemetryData/StatTelemetryData.php b/src/CustomerSuccess/TelemetryData/StatTelemetryData.php new file mode 100644 index 00000000..6959fd05 --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/StatTelemetryData.php @@ -0,0 +1,221 @@ +get_transaction_amount_by_pm_by_date_range(); + + /** @var string $currency */ + $currency = simpay_get_setting( 'currency', 'usd' ); + $currency = strtolower( $currency ); + + return array( + sprintf( 'transaction_average_%s', $currency ) => $this->get_average_transaction_amount(), + sprintf( 'transaction_total_%s', $currency ) => array_reduce( + $lifetime, + function( $carry, $amount ) { + return (int) $carry + (int) $amount['amount']; + }, + 0 + ), + sprintf( 'transaction_count_%s', $currency ) => array_reduce( + $lifetime, + function( $carry, $amount ) { + return (int) $carry + (int) $amount['count']; + }, + 0 + ), + + 'form_count' => $this->get_form_count(), + 'form_price_options_average' => $this->get_price_options_average(), + 'form_custom_fields_average' => $this->get_custom_fields_average(), + 'form_payment_methods_average' => $this->get_payment_methods_average(), + ); + } + + /** + * Returns the number of `simple-pay` post_type posts that are not auto drafts. + * + * @since 4.7.10 + * + * @return int + */ + private function get_form_count() { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( ID ) FROM $wpdb->posts WHERE post_type = %s AND post_status != %s", + 'simple-pay', + 'auto-draft' + ) + ); + } + + /** + * Returns the average transaction amount for the site's default currency. + * + * @since 4.7.10 + * + * @return int + */ + private function get_average_transaction_amount() { + global $wpdb; + + /** @var string $currency */ + $currency = simpay_get_setting( 'currency', 'usd' ); + $currency = strtolower( $currency ); + + $select_value = simpay_is_zero_decimal( $currency ) + ? 'AVG(amount_total)' + : 'AVG(amount_total / 100)'; + + $average = $wpdb->get_var( + // @phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->prepare( + "SELECT {$select_value} AS average_amount + FROM {$wpdb->prefix}wpsp_transactions + WHERE status = 'succeeded' + AND currency = %s;", + $currency + ) + // @phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + + return (int) round( $average ); + } + + /** + * Returns the average number of price options on each payment form. + * + * @since 4.7.10 + * + * @return int + */ + private function get_price_options_average() { + global $wpdb; + + $payment_mode = simpay_is_test_mode() ? 'test' : 'live'; + + $price_option_meta = $wpdb->get_col( + $wpdb->prepare( + "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = %s", + sprintf( '_simpay_prices_%s', $payment_mode ) + ) + ); + + $average = 0; + + foreach ( $price_option_meta as $price_option ) { + /** @var array $price_option_data */ + $price_option_data = maybe_unserialize( $price_option ); + + if ( ! is_array( $price_option_data ) ) { + $average += 0; + } + + $average += count( $price_option_data ); + } + + return (int) floor( $average / count( $price_option_meta ) ); + } + + /** + * Returns the average number of payment methods on each payment form. + * + * @since 4.7.10 + * + * @return int + */ + private function get_payment_methods_average() { + global $wpdb; + + $payment_methods_meta = $wpdb->get_col( + $wpdb->prepare( + "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = %s", + '_payment_methods' + ) + ); + + $payment_form_payment_methods = array_map( + function( $payment_methods ) { + /** @var array $payment_methods */ + $payment_methods = maybe_unserialize( $payment_methods ); + /** @var array> $payment_methods */ + $payment_methods = current( $payment_methods ); + + return array_map( + function( $payment_method ) { + return $payment_method['id']; + }, + $payment_methods + ); + }, + $payment_methods_meta + ); + + $average = 0; + + foreach ( $payment_form_payment_methods as $payment_form_payment_method ) { + $average += count( $payment_form_payment_method ); + } + + return (int) floor( $average / count( $payment_methods_meta ) ); + } + + /** + * Returns the average number of custom fields per form. + * + * @since 4.7.10 + * + * @return int + */ + private function get_custom_fields_average() { + global $wpdb; + + $custom_fields_meta = $wpdb->get_col( + $wpdb->prepare( + "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = %s", + '_custom_fields' + ) + ); + + foreach ( $custom_fields_meta as $key => $custom_field ) { + $custom_fields_meta[ $key ] = maybe_unserialize( $custom_field ); + } + + if ( ! is_array( $custom_fields_meta ) ) { + return 0; + } + + $average = 0; + + foreach ( $custom_fields_meta as $custom_field_data ) { + foreach ( $custom_field_data as $custom_field_children ) { + $average += count( $custom_field_children ); + } + } + + return (int) floor( $average / count( $custom_fields_meta ) ); + } + +} diff --git a/src/CustomerSuccess/TelemetryData/TransactionTelemetryData.php b/src/CustomerSuccess/TelemetryData/TransactionTelemetryData.php new file mode 100644 index 00000000..8c1616e4 --- /dev/null +++ b/src/CustomerSuccess/TelemetryData/TransactionTelemetryData.php @@ -0,0 +1,52 @@ + $this->get_transaction_amount_by_pm_by_date_range(), + '30days' => $this->get_transaction_amount_by_pm_by_date_range( '30days' ), + '7days' => $this->get_transaction_amount_by_pm_by_date_range( '7days' ), + ); + + $transactions = array(); + + foreach ( $periods as $period => $types ) { + foreach ( $types as $type => $data ) { + $transactions[] = array( + 'period' => $period, + 'type' => $type, + 'currency' => $currency, + 'count' => $data['count'], + 'total' => $data['total'], + ); + } + } + + return $transactions; + } + +} diff --git a/src/CustomerSuccess/TelemetrySubscriber.php b/src/CustomerSuccess/TelemetrySubscriber.php new file mode 100644 index 00000000..ab9d6413 --- /dev/null +++ b/src/CustomerSuccess/TelemetrySubscriber.php @@ -0,0 +1,163 @@ +scheduler = $scheduler; + } + + /** + * {@inheritdoc} + */ + public function get_subscribed_events() { + // If Lite, only send if opted in. + if ( true === $this->license->is_lite() ) { + $opted_in = simpay_get_setting( 'usage_tracking_opt_in', 'no' ); + + if ( 'no' === $opted_in ) { + return array( + 'admin_init' => 'unschedule', + ); + } + } + + return array( + 'init' => 'schedule_send', + 'simpay_send_telemetry' => 'send', + ); + } + + /** + * Unschedules sending telemetry data once a week, if it's scheduled. + * + * @since 4.7.10 + * + * @return void + */ + public function unschedule() { + if ( ! $this->scheduler->has_next( 'simpay_send_telemetry' ) ) { + return; + } + + $this->scheduler->unschedule( 'simpay_send_telemetry' ); + } + + /** + * Schedules sending telemetry data once a week. + * + * @since 4.7.10 + * + * @return void + */ + public function schedule_send() { + if ( $this->scheduler->has_next( 'simpay_send_telemetry' ) ) { + return; + } + + $this->scheduler->schedule_recurring( + time() + rand( 0, WEEK_IN_SECONDS ), + WEEK_IN_SECONDS, + 'simpay_send_telemetry' + ); + } + + /** + * Sends telemetry data to the telemetry server. + * + * @since 4.7.10 + * + * @return void + */ + public function send() { + $data = array( + 'id' => $this->get_id(), + 'env' => ( new TelemetryData\EnvironmentTelemetryData() )->get(), + 'plugin' => ( new TelemetryData\PluginTelemetryData( $this->license ) )->get(), + 'journey' => ( new TelemetryData\CustomerJourneyTelemetryData() )->get(), + 'integrations' => ( new TelemetryData\IntegrationTelemetryData() )->get(), + 'stats' => ( new TelemetryData\StatTelemetryData() )->get(), + 'transactions' => ( new TelemetryData\TransactionTelemetryData() )->get(), + 'forms' => ( new TelemetryData\PaymentFormTelemetryData() )->get(), + ); + + wp_remote_post( + 'https://telemetry.wpsimplepay.com/v1/checkin/', + array( + 'method' => 'POST', + 'timeout' => 8, + 'redirection' => 5, + 'httpversion' => '1.1', + 'blocking' => false, + 'body' => $data, + 'user-agent' => 'WPSP/' . SIMPLE_PAY_VERSION . '; ' . $data['id'], // @phpstan-ignore-line + ) + ); + + } + + /** + * Gets the unique site ID. + * + * This is generated from the home URL and two random pieces of data + * to create a hashed site ID that anonymizes the site data. + * + * @since 4.7.10 + * + * @return string + */ + private function get_id() { + /** @var string $id */ + $id = get_option( 'simpay_telemetry_uuid', '' ); + + if ( '' !== $id ) { + return $id; + } + + $home_url = get_home_url(); + $uuid = wp_generate_uuid4(); + $today = gmdate( 'now' ); + $id = md5( $home_url . $uuid . $today ); + + update_option( 'simpay_telemetry_uuid', $id, false ); + + return $id; + } + +} diff --git a/src/License/LicenseNotificationSubscriber.php b/src/License/LicenseNotificationSubscriber.php index 6c167a80..40a07e2b 100644 --- a/src/License/LicenseNotificationSubscriber.php +++ b/src/License/LicenseNotificationSubscriber.php @@ -132,6 +132,7 @@ public function add_expired_license_notification() { $date_format = get_option( 'date_format', 'Y-m-d' ); $content = sprintf( + /* translators: License extension date. */ __( 'We have extended WP Simple Pay Pro functionality until %s, at which point functionality will become limited. Renew your license to continue receiving automatic updates, technical support, and access to WP Simple Pay Pro features and functionality.', 'stripe' @@ -148,6 +149,16 @@ public function add_expired_license_notification() { ); } + $renew_url = add_query_arg( + array( + 'edd_license_key' => $this->license->get_key(), + 'discount' => 'SAVE50', + ), + sprintf( '%s/checkout', untrailingslashit( SIMPLE_PAY_STORE_URL ) ) // @phpstan-ignore-line + ); + + $renew_url = simpay_ga_url( $renew_url, 'notification-inbox' ); + $this->notifications->restore( array( 'type' => 'error', @@ -161,13 +172,18 @@ public function add_expired_license_notification() { 'actions' => array( array( 'type' => 'primary', - 'text' => __( 'Renew License', 'stripe' ), - 'url' => 'https://wpsimplepay.com/my-account/licenses/', + 'text' => __( 'Renew License for 50% Off!', 'stripe' ), + 'url' => $renew_url, ), array( 'type' => 'secondary', 'text' => __( 'Learn More', 'stripe' ), - 'url' => 'https://wpsimplepay.com/doc/activate-wp-simple-pay-pro-license/', + 'url' => simpay_docs_link( + '', + 'what-happens-if-my-license-expires', + 'admin-notice-expired-license', + true + ), ), ), 'conditions' => array(), diff --git a/src/PaymentPage/PaymentPageOutput.php b/src/PaymentPage/PaymentPageOutput.php index 4022a954..638ec747 100644 --- a/src/PaymentPage/PaymentPageOutput.php +++ b/src/PaymentPage/PaymentPageOutput.php @@ -62,6 +62,7 @@ public function get_subscribed_events() { return array( 'parse_request' => 'parse_pretty_request', + 'init' => 'maybe_redirect_back', ); } @@ -74,11 +75,6 @@ public function get_subscribed_events() { * @return void */ public function parse_pretty_request( $wp ) { - // Do not take over the request if we are returning from a payment method redirect. - if ( isset( $_GET['payment_intent'] ) ) { - return; - } - if ( ! empty( $wp->query_vars['name'] ) ) { $request = $wp->query_vars['name']; } @@ -102,15 +98,6 @@ public function parse_pretty_request( $wp ) { return; } - // Set the "Success Page" redirect _before_ we call the form, but when - // we are pretty sure we are on a Payment Page. - $this->events->add_callback( - 'simpay_payment_success_page', - array( $this, 'set_self_redirect' ), - 10, - 2 - ); - $this->form = simpay_get_form( $payment_form_obj->ID ); if ( @@ -206,38 +193,58 @@ private function compat_hooks() { } /** - * Update the payment confirmation URL if receipt should show on the same page. + * Redirects back to the payment page if needed. * - * @since 4.5.0 + * @since 4.7.10 * - * @param string $url Success page URL. - * @param int $form_id Payment form ID. - * @return string + * @return void */ - public function set_self_redirect( $url, $form_id ) { + public function maybe_redirect_back() { + // Avoid redirect loops. + if ( isset( $_GET['redirected'] ) ) { + return; + } + + $payment_confirmation_data = Payment_Confirmation\get_confirmation_data(); + + if ( empty( $payment_confirmation_data ) ) { + return; + } + + $form = $payment_confirmation_data['form']; + // Return standard success URL if Payment Page is not enabled. - if ( false === $this->is_payment_page_enabled( $form_id ) ) { - return $url; + if ( false === $this->is_payment_page_enabled( $form->id ) ) { + return; } $self_confirmation = get_post_meta( - $form_id, + $form->id, '_payment_page_self_confirmation', true ); // Return standard success URL if self confirmation is not enabled. if ( 'no' === $self_confirmation ) { - return $url; + return; } - $redirect_url = get_permalink( $form_id ); + $redirect_url = add_query_arg( + array_merge( + array( + 'redirected' => true, + ), + array_map( 'sanitize_text_field', $_GET ) + ), + get_permalink( $form->id ) + ); if ( ! is_string( $redirect_url ) ) { - return $url; + return; } - return $redirect_url; + wp_redirect( esc_url_raw( $redirect_url ) ); + exit; } /** diff --git a/src/RestApi/Internal/Payment/Traits/PaymentIntentTrait.php b/src/RestApi/Internal/Payment/Traits/PaymentIntentTrait.php index b364e058..3f233c97 100644 --- a/src/RestApi/Internal/Payment/Traits/PaymentIntentTrait.php +++ b/src/RestApi/Internal/Payment/Traits/PaymentIntentTrait.php @@ -27,12 +27,10 @@ trait PaymentIntentTrait { /** - * Application fee. - * - * This is initialized in the constructor that includes this trait, but - * is added here for IDE support. + * Application fee handling. * * @since 4.7.0 + * * @var \SimplePay\Core\StripeConnect\ApplicationFee */ protected $application_fee; diff --git a/src/RestApi/Internal/Payment/Traits/SubscriptionTrait.php b/src/RestApi/Internal/Payment/Traits/SubscriptionTrait.php index 48fa3d13..0921bbff 100644 --- a/src/RestApi/Internal/Payment/Traits/SubscriptionTrait.php +++ b/src/RestApi/Internal/Payment/Traits/SubscriptionTrait.php @@ -24,17 +24,14 @@ trait SubscriptionTrait { /** - * Application fee. - * - * This is initialized in the constructor that includes this trait, but - * is added here for IDE support. + * Application fee handling. * * @since 4.7.0 + * * @var \SimplePay\Core\StripeConnect\ApplicationFee */ protected $application_fee; - /** * Creates a Subscription for the given request. * diff --git a/src/StripeConnect/ApplicationFee.php b/src/StripeConnect/ApplicationFee.php index 40c57a9b..13a81364 100644 --- a/src/StripeConnect/ApplicationFee.php +++ b/src/StripeConnect/ApplicationFee.php @@ -148,21 +148,34 @@ public function maybe_show_application_fee( $message ) { '' ); } elseif ( false === $this->license->is_valid() ) { - $renew_url = simpay_ga_url( - 'https://wpsimplepay.com/my-account/licenses/', + $renew_url = add_query_arg( + array( + 'edd_license_key' => $this->license->get_key(), + 'discount' => 'SAVE50', + ), + sprintf( '%s/checkout', untrailingslashit( SIMPLE_PAY_STORE_URL ) ) // @phpstan-ignore-line + ); + + $renew_url = simpay_ga_url( $renew_url, 'stripe-account-settings' ); + + $learn_more_url = simpay_docs_link( + 'Learn More', + 'what-happens-if-my-license-expires', 'stripe-account-settings', - 'Renew license' + true ); $message .= sprintf( /* translators: %1$s Opening strong tag, do not translate. %2$s Closing strong tag, do not translate. %3$s Opening anchor tag, do not translate. %4$s Closing anchor tag, do not translate. */ __( - '%1$sPay as you go pricing%2$s: 3%% fee per-transaction + Stripe fees. %3$sRenew your license%4$s to remove additional fees and unlock powerful features.', + '%1$sPay as you go pricing%2$s: 3%% fee per-transaction + Stripe fees. %3$sRenew your license (save 50%% off!)%4$s to remove additional fees and unlock powerful features. %5$sLearn more%6$s', 'stripe' ), '', '', '', + '', + '', '' ); } diff --git a/src/StripeConnect/ConnectionSubscriber.php b/src/StripeConnect/ConnectionSubscriber.php index 56dfa93f..22febeea 100644 --- a/src/StripeConnect/ConnectionSubscriber.php +++ b/src/StripeConnect/ConnectionSubscriber.php @@ -79,7 +79,7 @@ public function connect() { $customer_site_url = remove_query_arg( array( 'state', - 'wpsp_gateway_connect_completion' + 'wpsp_gateway_connect_completion', ), $current_url ); @@ -342,7 +342,7 @@ public function get_account_information_json() { 'stripe' ), $connect - ) + ), ) ); } @@ -391,6 +391,10 @@ public function get_account_information_json() { } $message = ( + sprintf( + '%s', + $account->id + ) . $display_name . $email . esc_html__( 'Administrator (Owner)', 'stripe' ) diff --git a/src/Webhook/EndpointHealthCheck.php b/src/Webhook/EndpointHealthCheck.php index 3dc5d1de..71c62ea7 100644 --- a/src/Webhook/EndpointHealthCheck.php +++ b/src/Webhook/EndpointHealthCheck.php @@ -83,7 +83,7 @@ public function maybe_update() { // Check the ID. /** @var string $endpoint_id */ $endpoint_id = simpay_get_setting( - "${prefix}_webhook_endpoint_id", + "{$prefix}_webhook_endpoint_id", '' ); @@ -96,13 +96,13 @@ public function maybe_update() { // Check the URLs. /** @var string $endpoint_url */ $endpoint_url = simpay_get_setting( - "${prefix}_webhook_endpoint_url", + "{$prefix}_webhook_endpoint_url", '' ); if ( empty( $endpoint_url ) || - $endpoint_url !== simpay_get_webhook_url() + simpay_get_webhook_url() !== $endpoint_url ) { $endpoint = $this->endpoint->get( $endpoint_id ); $this->endpoint->update( $endpoint ); @@ -113,7 +113,7 @@ public function maybe_update() { // Check the events. /** @var array $endpoint_events */ $endpoint_events = simpay_get_setting( - "${prefix}_webhook_endpoint_events", + "{$prefix}_webhook_endpoint_events", array() ); @@ -150,7 +150,7 @@ public function update_endpoint_url() { $prefix = simpay_is_test_mode() ? 'test' : 'live'; simpay_update_setting( - "${prefix}_webhook_endpoint_url", + "{$prefix}_webhook_endpoint_url", simpay_get_webhook_url() ); } diff --git a/vendor/autoload.php b/vendor/autoload.php index 517871b7..6f35b09c 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -2,24 +2,6 @@ // autoload.php @generated by Composer -if (PHP_VERSION_ID < 50600) { - if (!headers_sent()) { - header('HTTP/1.1 500 Internal Server Error'); - } - $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; - if (!ini_get('display_errors')) { - if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { - fwrite(STDERR, $err); - } elseif (!headers_sent()) { - echo $err; - } - } - trigger_error( - $err, - E_USER_ERROR - ); -} - require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInita908c4d6f2ea3893a558f06b1b8257d1::getLoader(); +return ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0::getLoader(); diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php index a72151c7..6d0c3f2d 100644 --- a/vendor/composer/ClassLoader.php +++ b/vendor/composer/ClassLoader.php @@ -42,79 +42,30 @@ */ class ClassLoader { - /** @var \Closure(string):void */ - private static $includeFile; - - /** @var ?string */ private $vendorDir; // PSR-4 - /** - * @var array[] - * @psalm-var array> - */ private $prefixLengthsPsr4 = array(); - /** - * @var array[] - * @psalm-var array> - */ private $prefixDirsPsr4 = array(); - /** - * @var array[] - * @psalm-var array - */ private $fallbackDirsPsr4 = array(); // PSR-0 - /** - * @var array[] - * @psalm-var array> - */ private $prefixesPsr0 = array(); - /** - * @var array[] - * @psalm-var array - */ private $fallbackDirsPsr0 = array(); - /** @var bool */ private $useIncludePath = false; - - /** - * @var string[] - * @psalm-var array - */ private $classMap = array(); - - /** @var bool */ private $classMapAuthoritative = false; - - /** - * @var bool[] - * @psalm-var array - */ private $missingClasses = array(); - - /** @var ?string */ private $apcuPrefix; - /** - * @var self[] - */ private static $registeredLoaders = array(); - /** - * @param ?string $vendorDir - */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; - self::initializeIncludeClosure(); } - /** - * @return string[] - */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { @@ -124,47 +75,28 @@ public function getPrefixes() return array(); } - /** - * @return array[] - * @psalm-return array> - */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } - /** - * @return array[] - * @psalm-return array - */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } - /** - * @return array[] - * @psalm-return array - */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } - /** - * @return string[] Array of classname => path - * @psalm-return array - */ public function getClassMap() { return $this->classMap; } /** - * @param string[] $classMap Class to filename map - * @psalm-param array $classMap - * - * @return void + * @param array $classMap Class to filename map */ public function addClassMap(array $classMap) { @@ -179,11 +111,9 @@ public function addClassMap(array $classMap) * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories - * - * @return void + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories */ public function add($prefix, $paths, $prepend = false) { @@ -226,13 +156,11 @@ public function add($prefix, $paths, $prepend = false) * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException - * - * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { @@ -276,10 +204,8 @@ public function addPsr4($prefix, $paths, $prepend = false) * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 base directories - * - * @return void + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories */ public function set($prefix, $paths) { @@ -294,12 +220,10 @@ public function set($prefix, $paths) * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException - * - * @return void */ public function setPsr4($prefix, $paths) { @@ -319,8 +243,6 @@ public function setPsr4($prefix, $paths) * Turns on searching the include path for class files. * * @param bool $useIncludePath - * - * @return void */ public function setUseIncludePath($useIncludePath) { @@ -343,8 +265,6 @@ public function getUseIncludePath() * that have not been registered with the class map. * * @param bool $classMapAuthoritative - * - * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { @@ -365,8 +285,6 @@ public function isClassMapAuthoritative() * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix - * - * @return void */ public function setApcuPrefix($apcuPrefix) { @@ -387,8 +305,6 @@ public function getApcuPrefix() * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not - * - * @return void */ public function register($prepend = false) { @@ -408,8 +324,6 @@ public function register($prepend = false) /** * Unregisters this instance as an autoloader. - * - * @return void */ public function unregister() { @@ -429,8 +343,7 @@ public function unregister() public function loadClass($class) { if ($file = $this->findFile($class)) { - $includeFile = self::$includeFile; - $includeFile($file); + includeFile($file); return true; } @@ -490,11 +403,6 @@ public static function getRegisteredLoaders() return self::$registeredLoaders; } - /** - * @param string $class - * @param string $ext - * @return string|false - */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup @@ -560,26 +468,14 @@ private function findFileWithExtension($class, $ext) return false; } +} - /** - * @return void - */ - private static function initializeIncludeClosure() - { - if (self::$includeFile !== null) { - return; - } - - /** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - * - * @param string $file - * @return void - */ - self::$includeFile = \Closure::bind(static function($file) { - include $file; - }, null, null); - } +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; } diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 51e734a7..b3a4e161 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -20,27 +20,12 @@ * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * - * To require its presence, you can require `composer-runtime-api ^2.0` - * - * @final + * To require it's presence, you can require `composer-runtime-api ^2.0` */ class InstalledVersions { - /** - * @var mixed[]|null - * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null - */ private static $installed; - - /** - * @var bool|null - */ private static $canGetVendors; - - /** - * @var array[] - * @psalm-var array}> - */ private static $installedByVendor = array(); /** @@ -98,7 +83,7 @@ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { - return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); } } @@ -119,7 +104,7 @@ public static function isInstalled($packageName, $includeDevRequirements = true) */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { - $constraint = $parser->parseConstraints((string) $constraint); + $constraint = $parser->parseConstraints($constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); @@ -243,7 +228,7 @@ public static function getInstallPath($packageName) /** * @return array - * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string} */ public static function getRootPackage() { @@ -257,7 +242,7 @@ public static function getRootPackage() * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] - * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array} */ public static function getRawData() { @@ -280,7 +265,7 @@ public static function getRawData() * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ public static function getAllRawData() { @@ -303,7 +288,7 @@ public static function getAllRawData() * @param array[] $data A vendor/composer/installed.php data set * @return void * - * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array} $data */ public static function reload($data) { @@ -313,7 +298,7 @@ public static function reload($data) /** * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ private static function getInstalled() { @@ -328,9 +313,7 @@ private static function getInstalled() if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { - /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ - $required = require $vendorDir.'/composer/installed.php'; - $installed[] = self::$installedByVendor[$vendorDir] = $required; + $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } @@ -342,17 +325,12 @@ private static function getInstalled() // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { - /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ - $required = require __DIR__ . '/installed.php'; - self::$installed = $required; + self::$installed = require __DIR__ . '/installed.php'; } else { self::$installed = array(); } } - - if (self::$installed !== array()) { - $installed[] = self::$installed; - } + $installed[] = self::$installed; return $installed; } diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 0fb0a2c1..b26f1b13 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -2,7 +2,7 @@ // autoload_classmap.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php index 15a2ff3a..b7fc0125 100644 --- a/vendor/composer/autoload_namespaces.php +++ b/vendor/composer/autoload_namespaces.php @@ -2,7 +2,7 @@ // autoload_namespaces.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 44ef7e3d..9d82e6d7 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -2,7 +2,7 @@ // autoload_psr4.php @generated by Composer -$vendorDir = dirname(__DIR__); +$vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 804bd7aa..926cfae7 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInita908c4d6f2ea3893a558f06b1b8257d1 +class ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0 { private static $loader; @@ -22,12 +22,31 @@ public static function getLoader() return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInita908c4d6f2ea3893a558f06b1b8257d1', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); - spl_autoload_unregister(array('ComposerAutoloaderInita908c4d6f2ea3893a558f06b1b8257d1', 'loadClassLoader')); - - require __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInita908c4d6f2ea3893a558f06b1b8257d1::getInitializer($loader)); + spl_autoload_register(array('ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); + spl_autoload_unregister(array('ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0', 'loadClassLoader')); + + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } $loader->register(true); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index d6d1c735..b4f6a5cb 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -4,7 +4,7 @@ namespace Composer\Autoload; -class ComposerStaticInita908c4d6f2ea3893a558f06b1b8257d1 +class ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0 { public static $prefixLengthsPsr4 = array ( 'S' => @@ -37,9 +37,9 @@ class ComposerStaticInita908c4d6f2ea3893a558f06b1b8257d1 public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInita908c4d6f2ea3893a558f06b1b8257d1::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInita908c4d6f2ea3893a558f06b1b8257d1::$prefixDirsPsr4; - $loader->classMap = ComposerStaticInita908c4d6f2ea3893a558f06b1b8257d1::$classMap; + $loader->prefixLengthsPsr4 = ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::$classMap; }, null, ClassLoader::class); } diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 57582d37..7894993d 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -396,17 +396,17 @@ }, { "name": "woocommerce/action-scheduler", - "version": "3.4.0", - "version_normalized": "3.4.0.0", + "version": "3.6.2", + "version_normalized": "3.6.2.0", "source": { "type": "git", "url": "https://github.com/woocommerce/action-scheduler.git", - "reference": "3218a33ff14b968f8cb05de9656c2efa1eeb1330" + "reference": "4eb2fa9737a53e4d284dafcf3e0bf428b5f941bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/3218a33ff14b968f8cb05de9656c2efa1eeb1330", - "reference": "3218a33ff14b968f8cb05de9656c2efa1eeb1330", + "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/4eb2fa9737a53e4d284dafcf3e0bf428b5f941bc", + "reference": "4eb2fa9737a53e4d284dafcf3e0bf428b5f941bc", "shasum": "" }, "require-dev": { @@ -415,7 +415,7 @@ "wp-cli/wp-cli": "~2.5.0", "yoast/phpunit-polyfills": "^1.0" }, - "time": "2021-10-28T17:09:12+00:00", + "time": "2023-08-09T19:43:41+00:00", "type": "wordpress-plugin", "extra": { "scripts-description": { @@ -433,7 +433,7 @@ "homepage": "https://actionscheduler.org/", "support": { "issues": "https://github.com/woocommerce/action-scheduler/issues", - "source": "https://github.com/woocommerce/action-scheduler/tree/3.4.0" + "source": "https://github.com/woocommerce/action-scheduler/tree/3.6.2" }, "install-path": "../woocommerce/action-scheduler" } diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 8947ad5a..5f0a63ba 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -1,31 +1,31 @@ array( - 'name' => 'wpsimplepay/wp-simple-pay-pro-3', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '0d938862c6f1a2eb1a6bb9870334029a49934144', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), + 'reference' => '9f7c567ea7adf8d98d9e5f340a175cadd7e701ea', + 'name' => 'wpsimplepay/wp-simple-pay-pro-3', 'dev' => false, ), 'versions' => array( 'berlindb/core' => array( 'pretty_version' => '2.0.1', 'version' => '2.0.1.0', - 'reference' => '7dcddaddcffb69c58800d2fb3f6f169791cab1f7', 'type' => 'library', 'install_path' => __DIR__ . '/../berlindb/core', 'aliases' => array(), + 'reference' => '7dcddaddcffb69c58800d2fb3f6f169791cab1f7', 'dev_requirement' => false, ), 'container-interop/container-interop' => array( 'pretty_version' => '1.2.0', 'version' => '1.2.0.0', - 'reference' => '79cbf1341c22ec75643d841642dd5d6acd83bdb8', 'type' => 'library', 'install_path' => __DIR__ . '/../container-interop/container-interop', 'aliases' => array(), + 'reference' => '79cbf1341c22ec75643d841642dd5d6acd83bdb8', 'dev_requirement' => false, ), 'container-interop/container-interop-implementation' => array( @@ -37,10 +37,10 @@ 'league/container' => array( 'pretty_version' => '2.5.0', 'version' => '2.5.0.0', - 'reference' => '8438dc47a0674e3378bcce893a0a04d79a2c22b3', 'type' => 'library', 'install_path' => __DIR__ . '/../league/container', 'aliases' => array(), + 'reference' => '8438dc47a0674e3378bcce893a0a04d79a2c22b3', 'dev_requirement' => false, ), 'orno/di' => array( @@ -52,10 +52,10 @@ 'psr/container' => array( 'pretty_version' => '1.0.0', 'version' => '1.0.0.0', - 'reference' => 'b7ce3b176482dbbc1245ebf52b181af44c2cf55f', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/container', 'aliases' => array(), + 'reference' => 'b7ce3b176482dbbc1245ebf52b181af44c2cf55f', 'dev_requirement' => false, ), 'psr/container-implementation' => array( @@ -67,46 +67,46 @@ 'stripe/stripe-php' => array( 'pretty_version' => 'v10.6.0-beta.1', 'version' => '10.6.0.0-beta1', - 'reference' => '40505396844a9c3b7c16c1c3f3b9c1ee84af3fa6', 'type' => 'library', 'install_path' => __DIR__ . '/../stripe/stripe-php', 'aliases' => array(), + 'reference' => '40505396844a9c3b7c16c1c3f3b9c1ee84af3fa6', 'dev_requirement' => false, ), 'symfony/css-selector' => array( 'pretty_version' => 'v3.4.47', 'version' => '3.4.47.0', - 'reference' => 'da3d9da2ce0026771f5fe64cb332158f1bd2bc33', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/css-selector', 'aliases' => array(), + 'reference' => 'da3d9da2ce0026771f5fe64cb332158f1bd2bc33', 'dev_requirement' => false, ), 'tijsverkoyen/css-to-inline-styles' => array( 'pretty_version' => '2.2.0', 'version' => '2.2.0.0', - 'reference' => 'ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b', 'type' => 'library', 'install_path' => __DIR__ . '/../tijsverkoyen/css-to-inline-styles', 'aliases' => array(), + 'reference' => 'ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b', 'dev_requirement' => false, ), 'woocommerce/action-scheduler' => array( - 'pretty_version' => '3.4.0', - 'version' => '3.4.0.0', - 'reference' => '3218a33ff14b968f8cb05de9656c2efa1eeb1330', + 'pretty_version' => '3.6.2', + 'version' => '3.6.2.0', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../woocommerce/action-scheduler', 'aliases' => array(), + 'reference' => '4eb2fa9737a53e4d284dafcf3e0bf428b5f941bc', 'dev_requirement' => false, ), 'wpsimplepay/wp-simple-pay-pro-3' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '0d938862c6f1a2eb1a6bb9870334029a49934144', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), + 'reference' => '9f7c567ea7adf8d98d9e5f340a175cadd7e701ea', 'dev_requirement' => false, ), ), diff --git a/views/admin-notice-expired-license.php b/views/admin-notice-expired-license.php index 8b1044db..b3179ba6 100644 --- a/views/admin-notice-expired-license.php +++ b/views/admin-notice-expired-license.php @@ -52,7 +52,19 @@

    - + ', + '' + ), + array( + 'strong' => array(), + ) + ); + ?> diff --git a/views/admin-payment-forms-license-expired.php b/views/admin-payment-forms-license-expired.php index 5a305300..19486cc7 100644 --- a/views/admin-payment-forms-license-expired.php +++ b/views/admin-payment-forms-license-expired.php @@ -12,6 +12,7 @@ * @var string $learn_more_url "Learn More" URL. * @var string $action Payment form action. */ + ?> @@ -56,7 +57,19 @@

    - + ', + '' + ), + array( + 'strong' => array(), + ) + ); + ?>
    From f0ce8ceafe62c12c1548e9e10b8ba65725ec9e52 Mon Sep 17 00:00:00 2001 From: Spencer Finnell Date: Fri, 6 Oct 2023 11:26:43 +0200 Subject: [PATCH 2/6] Version bump --- package.json | 2 +- readme.txt | 2 +- stripe-checkout.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e96144c6..a6a414a8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "wp-simple-pay-lite", "title": "WP Simple Pay Lite for Stripe", "description": "Add high conversion Stripe Checkout forms to your WordPress site and start accepting payments in minutes. **Lite Version**", - "version": "4.7.9", + "version": "4.7.10-beta-1", "license": "GPL-2.0-or-later", "homepage": "https://wpsimplepay.com/", "repository": { diff --git a/readme.txt b/readme.txt index f219eb12..e4b56aaa 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: wpsimplepay, pderksen, spencerfinnell, adamjlea, mordauk, cklosows Tags: stripe, stripe checkout, stripe payments, credit card payments, stripe gateway Requires at least: 5.2 Tested up to: 6.3 -Stable tag: 4.7.9 +Stable tag: 4.7.10 Requires PHP: 5.6 License: GPLv2 or later diff --git a/stripe-checkout.php b/stripe-checkout.php index 0393746a..b345a1a1 100644 --- a/stripe-checkout.php +++ b/stripe-checkout.php @@ -5,7 +5,7 @@ * Description: Add high conversion Stripe payment forms to your WordPress site in minutes. * Author: WP Simple Pay * Author URI: https://wpsimplepay.com - * Version: 4.7.9 + * Version: 4.7.10-beta-1 * Text Domain: stripe * Domain Path: /languages */ @@ -54,7 +54,7 @@ // // Lite/Pro-specific. // - define( 'SIMPLE_PAY_VERSION', '4.7.9' ); + define( 'SIMPLE_PAY_VERSION', '4.7.10-beta-1' ); if ( ! defined( 'SIMPLE_PAY_PLUGIN_NAME' ) ) { define( 'SIMPLE_PAY_PLUGIN_NAME', 'WP Simple Pay Lite' ); From e291ab56369980512ae2d5aae781c31fe9bdb7fb Mon Sep 17 00:00:00 2001 From: Spencer Finnell Date: Fri, 6 Oct 2023 11:26:45 +0200 Subject: [PATCH 3/6] Update readme.txt --- readme.txt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/readme.txt b/readme.txt index e4b56aaa..bdc9e2b5 100644 --- a/readme.txt +++ b/readme.txt @@ -283,6 +283,15 @@ No. WP Simple Pay is a standalone Stripe payments plugin and does not integrate == Changelog == += Stripe Payment Forms v4.7.10 - October 11, 2023 = + +* New: Help improve WP Simple Pay with opt-in anonymous telemetry reporting. +* Fix: Anti-spam - only add notification in Live Mode. +* Dev: System Report - show missing database tables. +* Dev: Show connected Stripe account ID in connection status. +* Dev: Action Scheduler - update from `3.4.0` to `3.6.2`. +* Dev: PHP `8.x` compatibility fixes. + = Stripe Payment Forms v4.7.9 - August 22, 2023 = * New: Form Builder - add per-form confirmation and email messages. @@ -297,7 +306,3 @@ No. WP Simple Pay is a standalone Stripe payments plugin and does not integrate * New: Stripe Tax - update for general availability. * Fix: Lite Connect - improve connection process. - -= Stripe Payment Forms v4.7.7 - June 28, 2023 = - -* New: Add support for adding custom fields to Stripe Checkout. From 94e6bca47b15d79bae6e5d547886c74800175335 Mon Sep 17 00:00:00 2001 From: Spencer Finnell Date: Thu, 12 Oct 2023 09:51:10 +0200 Subject: [PATCH 4/6] Sync from Pro --- src/StripeConnect/ApplicationFee.php | 8 ++++++++ vendor/autoload.php | 2 +- vendor/composer/autoload_real.php | 8 ++++---- vendor/composer/autoload_static.php | 8 ++++---- vendor/composer/installed.php | 4 ++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/StripeConnect/ApplicationFee.php b/src/StripeConnect/ApplicationFee.php index 13a81364..6b58fb99 100644 --- a/src/StripeConnect/ApplicationFee.php +++ b/src/StripeConnect/ApplicationFee.php @@ -495,6 +495,14 @@ private function is_new_lite_connection() { '' ); + if ( + ! empty( $connect_account_type ) && + 'pro' === $connect_account_type && + $this->license->is_lite() + ) { + return true; + } + // Lite has not been reconnected yet, do not add a fee. return ( ! empty( $connect_account_type ) && diff --git a/vendor/autoload.php b/vendor/autoload.php index 6f35b09c..8146e40d 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -4,4 +4,4 @@ require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0::getLoader(); +return ComposerAutoloaderInit9c2f9632f4187c6edf98e26eed5af82e::getLoader(); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 926cfae7..3c6f23c9 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0 +class ComposerAutoloaderInit9c2f9632f4187c6edf98e26eed5af82e { private static $loader; @@ -22,15 +22,15 @@ public static function getLoader() return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInit9c2f9632f4187c6edf98e26eed5af82e', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); - spl_autoload_unregister(array('ComposerAutoloaderInit16d2c978dbf4663ece40bbaba35b8bf0', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInit9c2f9632f4187c6edf98e26eed5af82e', 'loadClassLoader')); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require __DIR__ . '/autoload_static.php'; - call_user_func(\Composer\Autoload\ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::getInitializer($loader)); + call_user_func(\Composer\Autoload\ComposerStaticInit9c2f9632f4187c6edf98e26eed5af82e::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index b4f6a5cb..870cba2c 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -4,7 +4,7 @@ namespace Composer\Autoload; -class ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0 +class ComposerStaticInit9c2f9632f4187c6edf98e26eed5af82e { public static $prefixLengthsPsr4 = array ( 'S' => @@ -37,9 +37,9 @@ class ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0 public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::$prefixDirsPsr4; - $loader->classMap = ComposerStaticInit16d2c978dbf4663ece40bbaba35b8bf0::$classMap; + $loader->prefixLengthsPsr4 = ComposerStaticInit9c2f9632f4187c6edf98e26eed5af82e::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit9c2f9632f4187c6edf98e26eed5af82e::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit9c2f9632f4187c6edf98e26eed5af82e::$classMap; }, null, ClassLoader::class); } diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 5f0a63ba..ae9a97aa 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '9f7c567ea7adf8d98d9e5f340a175cadd7e701ea', + 'reference' => '66292ae3c5f95fe2687c0aee1b84265375cbc1ae', 'name' => 'wpsimplepay/wp-simple-pay-pro-3', 'dev' => false, ), @@ -106,7 +106,7 @@ 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '9f7c567ea7adf8d98d9e5f340a175cadd7e701ea', + 'reference' => '66292ae3c5f95fe2687c0aee1b84265375cbc1ae', 'dev_requirement' => false, ), ), From 94a78dde8f419fd6b709ab67e8b5a5861fbe94e4 Mon Sep 17 00:00:00 2001 From: Spencer Finnell Date: Thu, 12 Oct 2023 09:52:32 +0200 Subject: [PATCH 5/6] Update readme.txt --- readme.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.txt b/readme.txt index bdc9e2b5..d8072a45 100644 --- a/readme.txt +++ b/readme.txt @@ -283,9 +283,9 @@ No. WP Simple Pay is a standalone Stripe payments plugin and does not integrate == Changelog == -= Stripe Payment Forms v4.7.10 - October 11, 2023 = += Stripe Payment Forms v4.7.10 - October 12, 2023 = -* New: Help improve WP Simple Pay with opt-in anonymous telemetry reporting. +* New: Help improve WP Simple Pay with *opt-in* anonymous telemetry reporting https://wpsimplepay.com/doc/what-information-does-wp-simple-pay-collect/ * Fix: Anti-spam - only add notification in Live Mode. * Dev: System Report - show missing database tables. * Dev: Show connected Stripe account ID in connection status. From 2c8e45199f8b8ac8919aa8554b83fba708b5c2a2 Mon Sep 17 00:00:00 2001 From: Spencer Finnell Date: Thu, 12 Oct 2023 09:52:39 +0200 Subject: [PATCH 6/6] Version bump --- package.json | 2 +- stripe-checkout.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a6a414a8..4ecaa1df 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "wp-simple-pay-lite", "title": "WP Simple Pay Lite for Stripe", "description": "Add high conversion Stripe Checkout forms to your WordPress site and start accepting payments in minutes. **Lite Version**", - "version": "4.7.10-beta-1", + "version": "4.7.10", "license": "GPL-2.0-or-later", "homepage": "https://wpsimplepay.com/", "repository": { diff --git a/stripe-checkout.php b/stripe-checkout.php index b345a1a1..5f9017bc 100644 --- a/stripe-checkout.php +++ b/stripe-checkout.php @@ -5,7 +5,7 @@ * Description: Add high conversion Stripe payment forms to your WordPress site in minutes. * Author: WP Simple Pay * Author URI: https://wpsimplepay.com - * Version: 4.7.10-beta-1 + * Version: 4.7.10 * Text Domain: stripe * Domain Path: /languages */ @@ -54,7 +54,7 @@ // // Lite/Pro-specific. // - define( 'SIMPLE_PAY_VERSION', '4.7.10-beta-1' ); + define( 'SIMPLE_PAY_VERSION', '4.7.10' ); if ( ! defined( 'SIMPLE_PAY_PLUGIN_NAME' ) ) { define( 'SIMPLE_PAY_PLUGIN_NAME', 'WP Simple Pay Lite' );