├── .github └── workflows │ └── release.yml ├── .gitignore ├── README.md ├── assets ├── css │ └── admin.css ├── data │ ├── option_scrublist.txt │ └── plugin_denylist.txt └── js │ └── safety-net-admin.js ├── includes ├── admin.php ├── bootstrap.php ├── classes │ ├── class-actionscheduler-custom-dbstore.php │ └── cli │ │ └── class-safetynet-cli.php ├── common.php ├── deactivate-plugins.php ├── delete-transients.php ├── delete.php ├── disable-webhooks.php ├── scrub-options.php └── utilities.php └── safety-net.php /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Zip Folder 16 | run: zip -r ${{ github.event.repository.name }}.zip . -x ".git/*" ".github/*" ".gitignore" 17 | 18 | - name: Release 19 | uses: softprops/action-gh-release@v1 20 | if: startsWith(github.ref, 'refs/tags/') 21 | with: 22 | files: ${{ github.event.repository.name }}.zip 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | vendor/ 3 | .DS_Store 4 | node_modules/ 5 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | | :exclamation: This is a public repository | 2 | |--------------------------------------------| 3 | 4 | # Safety Net 5 | 6 | **for Team51 Development Sites** 7 | 8 | **[Download the latest release](https://github.com/a8cteam51/safety-net/releases/latest/download/safety-net.zip)** 9 | 10 | ## What's this? 11 | This is a WordPress plugin developed by WordPress.com Special Projects (Team 51) that secures sensitive data on development, staging, and local sites. It deletes users and WooCommerce orders and subscriptions, as well as prevents sites from acting on user data (e.g. sending emails, processing renewals, etc.) 12 | 13 | ## Disclaimer 14 | This public plugin is provided as an example of how such a plugin could be implemented, and is provided without any support or guarantees. Please use at your own discretion. Incorrect usage could result in data deletion. 15 | 16 | ## Existing Features 17 | - **Stop Emails**: When Safety Net is activated, WordPress will be blocked from sending emails. (Caution: may not block SMTP or other plugins from doing so). 18 | - **Pause Renewal Actions**: When Safety Net is activated, Action Scheduler will not claim renewal actions or payment retry actions from WooCommerce Subscriptions, effectively pausing them. Other scheduled actions will continue to run. This is toggleable in wp-admin. 19 | - **Discourage Search Engines**: Sets the "Discourage search engines" option and disallows all user agents in the `robots.txt` file. Also disables Jetpack 'publicize' option. 20 | - **Scrub Options**: Clears specific denylisted options, such as API keys, which could cause problems on a development site. 21 | - **Deactivate Plugins**: Deactivates denylisted plugins. Also, runs through installed Woo payment gateways and deactivates them as well (deactivates the actual plugin, not from the checkout settings). 22 | - **Delete**: Deletes all non-admin users, WooCommerce orders and subscriptions. 23 | 24 | #### Advanced features 25 | - **CLI commands**: CLI equivalents of the above features: `wp safety-net scrub-options`, `wp safety-net deactivate-plugins`, and `wp safety-net delete` 26 | 27 | ## Planned Features 28 | - Multi-site (WordPress network) compatibility 29 | - Do you have a suggestion for the next great feature to add? Please create an issue or submit a PR! 30 | 31 | ## How to use? 32 | Download the plugin code directly from this repo. 33 | 34 | Activating the plugin on a non-production site will: 35 | 36 | 1. Scrub denylisted options.* 37 | 2. Deactivate denylisted plugins.* 38 | 3. Delete users, orders, and subscriptions.* 39 | 4. Stop emails. You can still test and view emails by activating the [WP Mail Logging plugin](https://wordpress.org/plugins/wp-mail-logging/). 40 | 5. Pause Renewal Actions. 41 | 6. Discourage search engines. 42 | 43 | *Only runs automatically if `wp_get_environment_type` returns `staging`, `development`, or `local`. If you have access to WP-CLI, you can SSH in and run `wp config set WP_ENVIRONMENT_TYPE staging --type=constant` 44 | 45 | ## How to add plugins or options to the denylists 46 | These denylists are `txt` files that live in the `assets/data/` folder. Each plugin or option is on its own line. 47 | 48 | You may also: 49 | - Create a new issue or dev request to have a plugin or option added to the denylists, or 50 | - Submit a PR to add something yourself, and let us know so we can merge it 51 | 52 | ## Blocking Use in Production 53 | Safety Net will not run on production sites. It will check the `WP_ENVIRONMENT_TYPE` global system variable, or a constant of the same name. If it is set to `production`, the plugin will not run. You can manually trigger this using the `safety_net_show_production_notice` filter (just pass back false to disable safety net). 54 | 55 | ```php 56 | add_filter( 'safety_net_show_production_notice', '__return_false' ); 57 | ``` 58 | 59 | ## Troubleshooting 60 | 61 | ### Plugin not running 62 | For Safety Net to run - and to access the tools page - the environment type needs to be set as `staging`, `development`, or `local`. The type can be set via the `WP_ENVIRONMENT_TYPE` global system variable, or a constant of the same name. 63 | 64 | One way to do that is to edit your `wp-config.php` file, and add `define('WP_ENVIRONMENT_TYPE', 'development');` 65 | 66 | Or, if you have access to WP-CLI, you can SSH in and run `wp config set WP_ENVIRONMENT_TYPE staging --type=constant` 67 | 68 | If your site is on Pressable, you can also achieve this by [setting the site as a Staging Site](https://pressable.com/knowledgebase/how-sites-staging-websites-and-website-clones-work-at-pressable/#staging-websites). 69 | 70 | ### Plugin won't activate 71 | It's possible that there is another copy of the plugin active on the site. Check in the `mu-plugins` folder. 72 | 73 | ### I don't want the functions to automatically run on my non-production site 74 | You'll need to go into the `includes/bootstrap.php` file and comment out whichever of these 3 functions you don't want to run: 75 | ```php 76 | add_action( 'safety_net_loaded', __NAMESPACE__ . '\maybe_scrub_options' ); 77 | add_action( 'safety_net_loaded', __NAMESPACE__ . '\maybe_deactivate_plugins' ); 78 | add_action( 'safety_net_loaded', __NAMESPACE__ . '\maybe_delete_data' ) 79 | ``` 80 | -------------------------------------------------------------------------------- /assets/css/admin.css: -------------------------------------------------------------------------------- 1 | .loading-overlay { 2 | display: none; 3 | position: fixed; 4 | z-index: 1000; 5 | top: 0; 6 | left: 0; 7 | height: 100%; 8 | width: 100%; 9 | background: rgba( 255, 255, 255, .8 ) 10 | url('/wp-includes/images/spinner-2x.gif') 11 | 50% 50% 12 | no-repeat; 13 | } 14 | 15 | body.loading .loading-overlay { 16 | overflow: hidden; 17 | } 18 | 19 | body.loading .loading-overlay { 20 | display: block; 21 | } 22 | 23 | table { 24 | border: 1px solid #c3c4c7; 25 | box-shadow: 0 1px 1px rgba(0,0,0,.04); 26 | background: #fff; 27 | } 28 | 29 | .form-table td, 30 | .form-table th { 31 | padding: 2em; 32 | color: #3c434a; 33 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; 34 | font-size: 13px; 35 | line-height: 1.4em; 36 | min-height:252px; 37 | } 38 | 39 | .form-table td p.description { 40 | font-size: 13px; 41 | margin: 2px 0 5px; 42 | color: #646970; 43 | max-width: 650px; 44 | } 45 | 46 | .form-table tr:nth-child(2n) th, 47 | .form-table tr:nth-child(2n) td { 48 | background: #fcfcfc; 49 | } 50 | 51 | .button.button-large { 52 | float:right; 53 | } 54 | 55 | p { 56 | max-width: 650px; 57 | color: #3c434a; 58 | } 59 | 60 | p.info { 61 | padding: 25px; 62 | border: 1px solid #ccc; 63 | box-shadow: 0 1px 1px rgba(0,0,0,.04); 64 | background: white; 65 | } 66 | 67 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle { 68 | -webkit-appearance: none; 69 | -moz-appearance: none; 70 | appearance: none; 71 | -webkit-tap-highlight-color: transparent; 72 | width: auto; 73 | height: auto; 74 | vertical-align: middle; 75 | position: relative; 76 | border: 0; 77 | outline: 0; 78 | cursor: pointer; 79 | margin: 0 4px; 80 | background: none; 81 | box-shadow: none; 82 | } 83 | 84 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:focus { 85 | box-shadow: none; 86 | } 87 | 88 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:after { 89 | content: ''; 90 | font-size: 8px; 91 | font-weight: 400; 92 | line-height: 18px; 93 | text-indent: -14px; 94 | color: #ffffff; 95 | width: 56px; 96 | height: 18px; 97 | display: inline-block; 98 | background-color: #a7aaad; 99 | border-radius: 72px; 100 | box-shadow: 0 0 12px rgb(0 0 0 / 15%) inset; 101 | } 102 | 103 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:before { 104 | content: ''; 105 | width: 14px; 106 | height: 14px; 107 | display: block; 108 | position: absolute; 109 | top: 2px; 110 | left: 2px; 111 | margin: 0; 112 | border-radius: 50%; 113 | background-color: #ffffff; 114 | } 115 | 116 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:checked:before { 117 | left: 40px; 118 | margin: 0; 119 | background-color: #ffffff; 120 | } 121 | 122 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle, 123 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:before, 124 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:after, 125 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:checked:before, 126 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:checked:after { 127 | transition: ease .15s; 128 | } 129 | 130 | input[type="checkbox"].safety-net-pause-renewal-actions-toggle:checked:after { 131 | content: 'PAUSED'; 132 | background-color: #2271b1; 133 | } 134 | 135 | button.safety-net-save { 136 | margin-top: 10px; 137 | } -------------------------------------------------------------------------------- /assets/data/option_scrublist.txt: -------------------------------------------------------------------------------- 1 | jetpack_active_modules 2 | jetpack_private_options 3 | jetpack_secrets 4 | klaviyo_api_key 5 | klaviyo_edd_license_key 6 | klaviyo_settings 7 | leadin_access_token 8 | mailchimp-woocommerce 9 | mailchimp-woocommerce-cached-api-account-name 10 | mailster_options 11 | mc4wp 12 | nelio-content_settings 13 | northbeam_api_key 14 | northbeam_client_id 15 | _transient_timeout_nelio_content_news 16 | _transient_nelio_content_news 17 | novos_klaviyo_option_name 18 | shareasale_wc_tracker_options 19 | woocommerce-ppcp-settings 20 | woocommerce_afterpay_settings 21 | woocommerce_braintree_credit_card_settings 22 | woocommerce_braintree_paypal_settings 23 | woocommerce_paypal_settings 24 | woocommerce_ppcp-gateway_settings 25 | woocommerce_referralcandy_settings 26 | woocommerce_shipstation_auth_key 27 | woocommerce_stripe_account_settings 28 | woocommerce_stripe_api_settings 29 | woocommerce_stripe_settings 30 | woocommerce_woocommerce_payments_settings 31 | wpmandrill 32 | wprus 33 | yotpo_settings 34 | zmail_access_token 35 | zmail_auth_code 36 | zmail_integ_client_secret 37 | zmail_refresh_token 38 | passport_api_key 39 | passport_api_secret -------------------------------------------------------------------------------- /assets/data/plugin_denylist.txt: -------------------------------------------------------------------------------- 1 | affirm 2 | afterpay 3 | algolia 4 | automatewoo 5 | automator 6 | avatax 7 | console 8 | distributor 9 | hubspot 10 | in-stock-mailer-for-wc 11 | klaviyo 12 | leadin 13 | mail-bank 14 | mailchimp 15 | mailchimp-for-woocommerce 16 | mailgun 17 | mailpoet 18 | mailster 19 | metorik 20 | microsoft-start 21 | nelio-content 22 | paypal 23 | postmark 24 | postmatic 25 | publish-to-apple-news 26 | referralcandy 27 | routeapp 28 | sendgrid 29 | sendinblue 30 | shareasale 31 | shipstation 32 | smtp 33 | socketlabs 34 | sparkpost 35 | stripe 36 | webgility 37 | woocommerce-zapier 38 | wp-remote-users-sync 39 | wp-ses 40 | wp-to-buffer-pro 41 | wpmandrill 42 | yotpo 43 | zapier 44 | zoho-mail 45 | -------------------------------------------------------------------------------- /assets/js/safety-net-admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function (window, document, $) { 4 | const scrubOptionsButton = document.getElementById( 'safety-net-scrub-options' ); 5 | const deactivatePluginsButton = document.getElementById( 'safety-net-deactivate-plugins' ); 6 | const deleteUsersButton = document.getElementById( 'safety-net-delete-users' ); 7 | const settingsTitle = document.getElementById( 'safety-net-settings-title' ); 8 | const deleteTransientsButton = document.getElementById( 'safety-net-delete-transients' ); 9 | const disableWebhooksButton = document.getElementById( 'safety-net-disable-webhooks' ); 10 | 11 | function deleteTransients() { 12 | if ( ! confirm( 'Are you sure you want to delete transients? This cannot be undone!') ) { 13 | return; 14 | } 15 | 16 | ajax({ 17 | action: 'safety_net_delete_transients', 18 | nonce: deleteTransientsButton.dataset.nonce, 19 | }); 20 | } 21 | 22 | function scrubOptions() { 23 | if ( ! confirm( 'Are you sure you want to scrub options? This cannot be undone!') ) { 24 | return; 25 | } 26 | 27 | ajax({ 28 | action: 'safety_net_scrub_options', 29 | nonce: scrubOptionsButton.dataset.nonce, 30 | }); 31 | } 32 | 33 | function deactivatePlugins() { 34 | if ( ! confirm( 'Are you sure you want to deactivate plugins? Make sure you scrub options first!') ) { 35 | return; 36 | } 37 | 38 | ajax({ 39 | action: 'safety_net_deactivate_plugins', 40 | nonce: deactivatePluginsButton.dataset.nonce, 41 | }); 42 | } 43 | 44 | 45 | function deleteUsers() { 46 | if ( ! confirm( 'Are you sure you want to delete all users? This cannot be undone!') ) { 47 | return; 48 | } 49 | 50 | ajax({ 51 | action: 'safety_net_delete_users', 52 | nonce: deleteUsersButton.dataset.nonce, 53 | }); 54 | } 55 | 56 | function disableWebhooks() { 57 | if ( ! confirm( 'Are you sure you want to disable webhooks? This cannot be undone!') ) { 58 | return; 59 | } 60 | 61 | ajax({ 62 | action: 'safety_net_disable_webhooks', 63 | nonce: disableWebhooksButton.dataset.nonce, 64 | }); 65 | } 66 | 67 | function ajax(data) { 68 | $.ajax( 69 | { 70 | type : 'POST', 71 | url : window.safety_net_params.ajax_url, 72 | data : data, 73 | dataType: 'json', 74 | beforeSend: function() { 75 | hideAdminNotice(); 76 | 77 | // If the scrub options button exists, disable it. 78 | if (scrubOptionsButton) { 79 | scrubOptionsButton.disabled = true; 80 | } 81 | 82 | // If the deactivate plugins button exists, disable it. 83 | if (deactivatePluginsButton) { 84 | deactivatePluginsButton.disabled = true; 85 | } 86 | 87 | // If the delete users button exists, disable it. 88 | if (deleteUsersButton) { 89 | deleteUsersButton.disabled = true; 90 | } 91 | 92 | toggleLoadingOverlay(); 93 | }, 94 | error : function(request, status, error) { 95 | showAdminNotice({ 96 | type : 'error', 97 | message : status + ': ' + error, 98 | }); 99 | }, 100 | success: function(response) { 101 | 102 | // If the scrub options button exists, enable it. 103 | if (scrubOptionsButton) { 104 | scrubOptionsButton.disabled = false; 105 | } 106 | 107 | // If the deactivate plugins button exists, enable it. 108 | if (deactivatePluginsButton) { 109 | deactivatePluginsButton.disabled = false; 110 | } 111 | 112 | // If the delete users button exists, enable it. 113 | if (deleteUsersButton) { 114 | deleteUsersButton.disabled = false; 115 | } 116 | 117 | toggleLoadingOverlay(); 118 | 119 | if ( true === response.success ) { 120 | showAdminNotice({ 121 | type : 'success', 122 | message : response.message, 123 | }); 124 | } else { 125 | showAdminNotice({ 126 | type : 'error', 127 | message : response.message, 128 | }); 129 | } 130 | } 131 | } 132 | ) 133 | } 134 | 135 | function toggleLoadingOverlay() { 136 | $('body').toggleClass('loading'); 137 | } 138 | 139 | function showAdminNotice(options) { 140 | let classes = 'notice settings-error is-dismissible'; 141 | 142 | if (undefined !== options.type) { 143 | classes += ' notice-' + options.type; 144 | } 145 | 146 | $(settingsTitle).after('

' + options.message + '

'); 147 | 148 | $('#safety-net-dismiss-notice').on('click', hideAdminNotice); 149 | } 150 | 151 | function hideAdminNotice() { 152 | const $notice = $('.notice.settings-error'); 153 | 154 | $notice.fadeTo(100, 0, function() { 155 | $notice.slideUp(100, function() { 156 | $notice.remove(); 157 | }); 158 | }); 159 | } 160 | 161 | // If the scrub options button exists, add a click event listener. 162 | if (scrubOptionsButton) { 163 | scrubOptionsButton.addEventListener('click', scrubOptions); 164 | } 165 | 166 | // If the deactivate plugins button exists, add a click event listener. 167 | if (deactivatePluginsButton) { 168 | deactivatePluginsButton.addEventListener('click', deactivatePlugins); 169 | } 170 | 171 | // If the delete users button exists, add a click event listener. 172 | if (deleteUsersButton) { 173 | deleteUsersButton.addEventListener('click', deleteUsers); 174 | } 175 | 176 | // If the delete transients button exists, add a click event listener. 177 | if (deleteTransientsButton) { 178 | deleteTransientsButton.addEventListener('click', deleteTransients); 179 | } 180 | 181 | // If the disable webhooks button exists, add a click event listener. 182 | if (disableWebhooksButton) { 183 | disableWebhooksButton.addEventListener('click', disableWebhooks); 184 | } 185 | })(window, document, jQuery); 186 | -------------------------------------------------------------------------------- /includes/admin.php: -------------------------------------------------------------------------------- 1 | admin_url( 'admin-ajax.php' ), 56 | ) 57 | ); 58 | 59 | wp_enqueue_style( 'safety-net-admin-style', SAFETY_NET_URL . 'assets/css/admin.css', array(), '0.0' ); 60 | } 61 | 62 | /** 63 | * Adds the options page under Tools > Safety Net. 64 | * 65 | * @return void 66 | */ 67 | function create_options_menu() { 68 | add_submenu_page( 69 | 'tools.php', 70 | esc_html__( 'Safety Net', 'safety-net' ), 71 | esc_html__( 'Safety Net', 'safety-net' ), 72 | 'manage_options', 73 | 'safety_net_options', 74 | __NAMESPACE__ . '\render_options_html' 75 | ); 76 | } 77 | 78 | /** 79 | * Registers the fields on the Tools page. 80 | * 81 | * @return void 82 | */ 83 | function settings_init() { 84 | // Register settings for Safety Net 85 | register_setting( 'safety-net', 'safety_net_' ); 86 | register_setting( 'safety-net', 'safety_net_dismiss_bar' ); 87 | register_setting( 'safety-net', 'safety_net_display_content' ); 88 | register_setting( 'safety-net', 'safety_net_pause_renewal_actions_toggle' ); 89 | 90 | // Register section for the settings 91 | add_settings_section( 92 | 'safety_net_option', 93 | '', 94 | null, 95 | 'safety_net_options' 96 | ); 97 | 98 | add_settings_field( 99 | 'safety_net_scrub_options', 100 | esc_html__( 'Scrub Options', 'safety-net' ), 101 | __NAMESPACE__ . '\render_field', 102 | 'safety_net_options', 103 | 'safety_net_option', 104 | array( 105 | 'type' => 'button', 106 | 'id' => 'safety-net-scrub-options', 107 | 'button_text' => esc_html__( 'Scrub Options', 'safety-net' ), 108 | 'description' => esc_html__( 'Clears specific denylisted options, such as API keys, which could cause problems on a development site.', 'safety-net' ), 109 | ) 110 | ); 111 | 112 | add_settings_field( 113 | 'safety_net_deactivate_plugins', 114 | esc_html__( 'Deactivate Plugins', 'safety-net' ), 115 | __NAMESPACE__ . '\render_field', 116 | 'safety_net_options', 117 | 'safety_net_option', 118 | array( 119 | 'type' => 'button', 120 | 'id' => 'safety-net-deactivate-plugins', 121 | 'button_text' => esc_html__( 'Deactivate Plugins', 'safety-net' ), 122 | 'description' => esc_html__( 'Deactivates a handful of denylisted plugins. Also, runs through installed Woo payment gateways and deactivates them (deactivates the actual plugin, not from the checkout settings).', 'safety-net' ), 123 | ) 124 | ); 125 | 126 | add_settings_field( 127 | 'safety_net_disable_webhooks', 128 | esc_html__( 'Disable Webhooks', 'safety-net' ), 129 | __NAMESPACE__ . '\render_field', 130 | 'safety_net_options', 131 | 'safety_net_option', 132 | array( 133 | 'type' => 'button', 134 | 'id' => 'safety-net-disable-webhooks', 135 | 'button_text' => esc_html__( 'Disable Webhooks', 'safety-net' ), 136 | 'description' => esc_html__( 'Disables all WooCommerce webhooks.', 'safety-net' ), 137 | ) 138 | ); 139 | 140 | add_settings_field( 141 | 'safety_net_delete_users', 142 | esc_html__( 'Delete Users, Orders, and Subscriptions', 'safety-net' ), 143 | __NAMESPACE__ . '\render_field', 144 | 'safety_net_options', 145 | 'safety_net_option', 146 | array( 147 | 'type' => 'button', 148 | 'id' => 'safety-net-delete-users', 149 | 'button_text' => esc_html__( 'Delete', 'safety-net' ), 150 | 'description' => esc_html__( 'Deletes all non-admin users, as well as WooCommerce orders and subscriptions.', 'safety-net' ), 151 | ) 152 | ); 153 | 154 | add_settings_field( 155 | 'safety_net_delete_transients', 156 | esc_html__( 'Delete Transients', 'safety-net' ), 157 | __NAMESPACE__ . '\render_field', 158 | 'safety_net_options', 159 | 'safety_net_option', 160 | array( 161 | 'type' => 'button', 162 | 'id' => 'safety-net-delete-transients', 163 | 'button_text' => esc_html__( 'Delete', 'safety-net' ), 164 | 'description' => esc_html__( 'Deletes all transients.', 'safety-net' ), 165 | ) 166 | ); 167 | 168 | add_settings_field( 169 | 'safety_net_pause_renewal_actions_toggle', 170 | esc_html__( 'Pause renewal actions', 'safety-net' ), 171 | __NAMESPACE__ . '\render_field', 172 | 'safety_net_options', 173 | 'safety_net_option', 174 | array( 175 | 'type' => 'checkbox', 176 | 'name' => 'safety_net_pause_renewal_actions_toggle', 177 | 'class' => 'safety-net-pause-renewal-actions-toggle', 178 | 'label_for' => 'safety_net_pause_renewal_actions_toggle', 179 | ) 180 | ); 181 | } 182 | 183 | /** 184 | * Renders the HTML for the settings. 185 | * 186 | * @param array $args Arguments passed to the fields. 187 | * 188 | * @return void 189 | */ 190 | function render_field( array $args = array() ) { 191 | if ( ! isset( $args['type'] ) ) { 192 | return; 193 | } 194 | 195 | if ( 'checkbox' === $args['type'] ) { 196 | printf( 197 | '', 198 | esc_attr( $args['name'] ), 199 | esc_attr( $args['class'] ), 200 | esc_attr( $args['name'] ), 201 | checked( 'on', get_option( $args['name'] ), false ) 202 | ); 203 | } 204 | 205 | if ( 'button' === $args['type'] ) { 206 | printf( 207 | '', 208 | esc_attr( $args['id'] ), 209 | esc_attr( wp_create_nonce( $args['id'] ) ), 210 | esc_html( $args['button_text'] ) 211 | ); 212 | } 213 | 214 | if ( isset( $args['description'] ) ) { 215 | printf( 216 | '

%s

', 217 | esc_html( $args['description'] ) 218 | ); 219 | } 220 | } 221 | 222 | /** 223 | * Renders the HTML for the options page. 224 | * 225 | * @return void 226 | */ 227 | function render_options_html() { 228 | if ( ! current_user_can( 'manage_options' ) ) { 229 | return; 230 | } 231 | ?> 232 |
233 |

234 |

Read about Safety Net or create issues/suggestions in the 235 | Safety Net repository.

236 |
237 | 238 |

239 | It appears that you are are viewing this page on a production site.
240 | For Safety Net to run - and to access the tools on this page - the environment type needs to be set as staging, development, or local. 241 | More info in the README. 242 |

243 | 244 |

Tools

245 |
246 | 250 |

251 |
252 |
253 |
254 | true, 271 | 'message' => esc_html__( 'You can not run these tools on a production site. Please set the environment type correctly.' ), 272 | ) 273 | ); 274 | die(); 275 | } 276 | 277 | // Permissions and security checks. 278 | check_the_permissions(); 279 | check_the_nonce( $_POST['nonce'], 'safety-net-scrub-options' ); // phpcs:ignore WordPress.Security.NonceVerification 280 | 281 | // Checks passed. Scrub the options. 282 | scrub_options(); 283 | 284 | // Send the AJAX response. 285 | echo wp_json_encode( 286 | array( 287 | 'success' => true, 288 | 'message' => esc_html__( 'Options have been scrubbed.' ), 289 | ) 290 | ); 291 | 292 | die(); 293 | } 294 | 295 | /** 296 | * Handles the AJAX request for deactivating plugins. 297 | * 298 | * @return void 299 | */ 300 | function handle_ajax_deactivate_plugins() { 301 | 302 | // If we're not on staging, development, or a local environment, die with a warning. 303 | if ( is_production() ) { 304 | // Send an AJAX warning. 305 | echo wp_json_encode( 306 | array( 307 | 'warning' => true, 308 | 'message' => esc_html__( 'You can not run these tools on a production site. Please set the environment type correctly.' ), 309 | ) 310 | ); 311 | die(); 312 | } 313 | 314 | // Permissions and security checks. 315 | check_the_permissions(); 316 | check_the_nonce( $_POST['nonce'], 'safety-net-deactivate-plugins' ); // phpcs:ignore WordPress.Security.NonceVerification 317 | 318 | // Checks passed. Scrub the options. 319 | deactivate_plugins(); 320 | 321 | // Send the AJAX response. 322 | echo wp_json_encode( 323 | array( 324 | 'success' => true, 325 | 'message' => esc_html__( 'Plugins have been deactivated.' ), 326 | ) 327 | ); 328 | 329 | die(); 330 | } 331 | 332 | /** 333 | * Handles the AJAX request for deleting all users. 334 | * 335 | * @return void 336 | */ 337 | function handle_ajax_delete_users() { 338 | 339 | // If we're not on staging, development, or a local environment, die with a warning. 340 | if ( is_production() ) { 341 | // Send an AJAX warning. 342 | echo wp_json_encode( 343 | array( 344 | 'warning' => true, 345 | 'message' => esc_html__( 'You can not run these tools on a production site. Please set the environment type correctly.' ), 346 | ) 347 | ); 348 | die(); 349 | } 350 | 351 | // Permissions and security checks. 352 | check_the_permissions(); 353 | check_the_nonce( $_POST['nonce'], 'safety-net-delete-users' ); // phpcs:ignore WordPress.Security.NonceVerification 354 | 355 | // Checks passed. Delete the users. 356 | delete_users_and_orders(); 357 | 358 | echo wp_json_encode( 359 | array( 360 | 'success' => true, 361 | 'message' => esc_html__( 'Users, orders, and subscriptions have been successfully deleted!' ), 362 | ) 363 | ); 364 | 365 | die(); 366 | } 367 | 368 | /** 369 | * Handles the AJAX request for deleting transients. 370 | * 371 | * @return void 372 | */ 373 | function handle_ajax_delete_transients() { 374 | 375 | // Permissions and security checks. 376 | check_the_permissions(); 377 | check_the_nonce( $_POST['nonce'], 'safety-net-delete-transients' ); // phpcs:ignore WordPress.Security.NonceVerification 378 | 379 | // Checks passed. Delete the transients. 380 | delete_transients(); 381 | 382 | echo wp_json_encode( 383 | array( 384 | 'success' => true, 385 | 'message' => esc_html__( 'Transients have been deleted.' ), 386 | ) 387 | ); 388 | 389 | die(); 390 | } 391 | 392 | /** 393 | * Handles the AJAX request for disabling webhooks. 394 | * 395 | * @return void 396 | */ 397 | function handle_ajax_disable_webhooks() { 398 | 399 | // Permissions and security checks. 400 | check_the_permissions(); 401 | check_the_nonce( $_POST['nonce'], 'safety-net-disable-webhooks' ); // phpcs:ignore WordPress.Security.NonceVerification 402 | 403 | // Checks passed. Disable the webhooks. 404 | disable_webhooks(); 405 | 406 | echo wp_json_encode( 407 | array( 408 | 'success' => true, 409 | 'message' => esc_html__( 'Webhooks have been disabled.' ), 410 | ) 411 | ); 412 | 413 | die(); 414 | } 415 | 416 | /** 417 | * Checks if the user has the correct permissions, and sends the AJAX response if they don't. 418 | * 419 | * @return void 420 | */ 421 | function check_the_permissions() { 422 | if ( ! current_user_can( 'manage_options' ) ) { 423 | echo wp_json_encode( 424 | array( 425 | 'success' => false, 426 | 'message' => esc_html__( 'You do not have permission to do that.' ), 427 | ) 428 | ); 429 | 430 | die(); 431 | } 432 | } 433 | 434 | /** 435 | * Checks if the nonce passed is correct, and sends the AJAX response if it doesn't. 436 | * 437 | * @param string $nonce The nonce to check. 438 | * @param string $action The action the nonce was created from. 439 | * 440 | * @return void 441 | */ 442 | function check_the_nonce( string $nonce, $action ) { 443 | if ( ! wp_verify_nonce( $nonce, $action ) ) { 444 | echo wp_json_encode( 445 | array( 446 | 'success' => false, 447 | 'message' => esc_html__( 'Security check failed. Refresh the page and try again.' ), 448 | ) 449 | ); 450 | 451 | die(); 452 | } 453 | } 454 | 455 | /** 456 | * Adds the action link on plugins page 457 | * 458 | * @return array 459 | */ 460 | 461 | function add_action_links( $actions ) { 462 | $links = array( 463 | 'Tools', 464 | ); 465 | 466 | return array_merge( $actions, $links ); 467 | } 468 | 469 | /** 470 | * Pause WooCommerce Subscriptions renewal and failed payment retry scheduled actions 471 | * 472 | */ 473 | function pause_renewal_actions() { 474 | if ( 'on' === get_option( 'safety_net_pause_renewal_actions_toggle' ) ) { 475 | require_once __DIR__ . '/classes/class-actionscheduler-custom-dbstore.php'; 476 | add_filter( 477 | 'action_scheduler_store_class', 478 | function ( $class ) { 479 | return 'SafetyNet\ActionScheduler_Custom_DBStore'; 480 | }, 481 | 101, 482 | 1 483 | ); 484 | } 485 | } 486 | 487 | /** 488 | * Display Warning that Safety Net is activated. 489 | * 490 | */ 491 | function show_warning() { 492 | // If we're not on staging, development, or a local environment, return. 493 | if ( is_production() ) { 494 | return; 495 | } 496 | echo "\n

"; 497 | echo ''; 498 | esc_html_e( 'Safety Net Activated', 'safety-net' ); 499 | echo ': '; 500 | echo ''; 501 | esc_html_e( 'The Safety Net plugin is currently active', 'safety-net' ); 502 | echo '
'; 503 | if ( 'on' === get_option( 'safety_net_pause_renewal_actions_toggle' ) ) { 504 | esc_html_e( 'WooCommerce Subscriptions scheduled actions are currently paused.', 'safety-net' ); 505 | echo '
'; 506 | } 507 | echo 'This site\'s environment type is set to "' . esc_html( wp_get_environment_type() ) . '".'; 508 | echo '

'; 509 | } 510 | 511 | /** 512 | * Stop all emails except password resets 513 | * 514 | */ 515 | function stop_emails( $return, $args ) { 516 | if ( ! strstr( $args['subject'], 'Password Reset' ) ) { 517 | error_log( "Email blocked: " . $args['subject'] ); // phpcs:ignore -- Logging is okay here. 518 | // returning false says "short-circuit the wp_mail() function and indicate we did not send the email" 519 | $return = false; 520 | } else { 521 | error_log( "Email sent: " . $args['subject'] ); // phpcs:ignore -- Logging is okay here. 522 | // returning null says "don't short circuit the wp_mail function" 523 | $return = null; 524 | } 525 | 526 | return $return; 527 | } 528 | -------------------------------------------------------------------------------- /includes/bootstrap.php: -------------------------------------------------------------------------------- 1 | update() because of the <= condition. 19 | $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; 20 | $params = array( 21 | $claim_id, 22 | $now->format( 'Y-m-d H:i:s' ), 23 | current_time( 'mysql' ), 24 | ); 25 | 26 | $where = "WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s AND hook NOT IN ( 'woocommerce_scheduled_subscription_payment', 'woocommerce_scheduled_subscription_payment_retry', 'woocommerce_scheduled_subscription_end_of_prepaid_term' )"; 27 | $params[] = $date->format( 'Y-m-d H:i:s' ); 28 | $params[] = self::STATUS_PENDING; 29 | 30 | if ( ! empty( $hooks ) ) { 31 | $remove_these = array( 'woocommerce_scheduled_subscription_payment', 'woocommerce_scheduled_subscription_payment_retry', 'woocommerce_scheduled_subscription_end_of_prepaid_term' ); 32 | $hooks = array_diff( $hooks, $remove_these ); 33 | 34 | $placeholders = array_fill( 0, count( $hooks ), '%s' ); 35 | $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')'; 36 | $params = array_merge( $params, array_values( $hooks ) ); 37 | } 38 | 39 | if ( ! empty( $group ) ) { 40 | 41 | $group_id = $this->get_group_id( $group, false ); 42 | 43 | // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour. 44 | if ( empty( $group_id ) ) { 45 | /* translators: %s: group name */ 46 | throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'woocommerce' ), $group ) ); 47 | } 48 | 49 | $where .= ' AND group_id = %d'; 50 | $params[] = $group_id; 51 | } 52 | 53 | /** 54 | * Sets the order-by clause used in the action claim query. 55 | * 56 | * @since 3.4.0 57 | * 58 | * @param string $order_by_sql 59 | */ 60 | $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); 61 | $params[] = $limit; 62 | 63 | $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders 64 | $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 65 | if ( false === $rows_affected ) { 66 | throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); 67 | } 68 | 69 | return (int) $rows_affected; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /includes/classes/cli/class-safetynet-cli.php: -------------------------------------------------------------------------------- 1 | false, 18 | 'message' => esc_html__( 'Safety Net Error: options need to be scrubbed first.' ), 19 | ) 20 | ); 21 | 22 | die(); 23 | } 24 | 25 | if ( ! function_exists( 'get_plugins' ) ) { 26 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 27 | } 28 | 29 | $all_installed_plugins = array_keys( get_plugins() ); 30 | 31 | $denylisted_plugins = apply_filters( 'safety_net_denylisted_plugins', get_denylist_array( 'plugins' ) ); 32 | 33 | // let's tack on all the Woo payment methods, in case we can deactivate any of those too 34 | if ( class_exists( 'woocommerce' ) ) { 35 | $installed_payment_methods = array_keys( WC()->payment_gateways->payment_gateways() ); 36 | foreach ( $installed_payment_methods as $key => $installed_payment_method ) { 37 | $installed_payment_method = str_replace( '_', '-', $installed_payment_method ); 38 | $denylisted_plugins[] = $installed_payment_method; 39 | } 40 | } 41 | 42 | foreach ( $all_installed_plugins as $key => $installed_plugin ) { 43 | 44 | if ( stristr( $installed_plugin, 'safety-net' ) ) { 45 | continue; 46 | } 47 | 48 | foreach ( $denylisted_plugins as $denylisted_plugin ) { 49 | 50 | // denylist can be partial matches, i.e. 'paypal' will match with any plugin that has 'paypal' in the slug 51 | if ( stristr( $installed_plugin, $denylisted_plugin ) ) { 52 | 53 | // remove plugin silently from active plugins list without triggering hooks 54 | $current = get_option( 'active_plugins', array() ); 55 | // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict 56 | $key = array_search( $installed_plugin, $current ); 57 | if ( false !== $key ) { 58 | array_splice( $current, $key, 1 ); 59 | } 60 | update_option( 'active_plugins', $current ); 61 | break; // break out of nested loop once plugin has been deactivated 62 | 63 | } 64 | } 65 | } 66 | 67 | update_option( 'safety_net_plugins_deactivated', true ); 68 | } -------------------------------------------------------------------------------- /includes/delete-transients.php: -------------------------------------------------------------------------------- 1 | query( "DELETE FROM $wpdb->options WHERE option_name LIKE '_transient_%'" ); 18 | 19 | // Set option so this function doesn't run again. 20 | update_option( 'safety_net_transients_deleted', true ); 21 | 22 | wp_cache_flush(); 23 | } 24 | -------------------------------------------------------------------------------- /includes/delete.php: -------------------------------------------------------------------------------- 1 | false, 21 | 'message' => esc_html__( 'Safety Net Error: plugins need to be deactivated first.' ), 22 | ) 23 | ); 24 | 25 | die(); 26 | } 27 | 28 | global $wpdb; 29 | 30 | // Delete orders and subscriptions 31 | $table_name = $wpdb->prefix . 'woocommerce_order_itemmeta'; 32 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 33 | $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta" ); 34 | } 35 | $table_name = $wpdb->prefix . 'woocommerce_order_items'; 36 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 37 | $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_order_items" ); 38 | } 39 | $wpdb->query( "DELETE FROM $wpdb->comments WHERE comment_type = 'order_note'" ); 40 | $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE post_id IN ( SELECT ID FROM {$wpdb->posts} WHERE post_type = 'shop_order' OR post_type = 'shop_subscription' )" ); 41 | $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_type = 'shop_order'" ); 42 | $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_type = 'shop_subscription'" ); 43 | 44 | // Delete Woo memberships 45 | $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE post_id IN ( SELECT ID FROM {$wpdb->posts} WHERE post_type = 'wc_user_membership' )" ); 46 | $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_type = 'wc_user_membership'" ); 47 | 48 | // Delete data from the High Performance Order Tables 49 | $table_name = $wpdb->prefix . 'wc_orders'; 50 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 51 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_orders" ); 52 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_addresses" ); 53 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_operational_data" ); 54 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_orders_meta" ); 55 | } 56 | 57 | // Delete Woo API keys 58 | $table_name = $wpdb->prefix . 'woocommerce_api_keys'; 59 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 60 | $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_api_keys" ); 61 | } 62 | 63 | // Delete Woo webhooks 64 | $table_name = $wpdb->prefix . 'wc_webhooks'; 65 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 66 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_webhooks" ); 67 | } 68 | 69 | // Delete Woo payment tokens 70 | $table_name = $wpdb->prefix . 'woocommerce_payment_tokens'; 71 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 72 | $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_payment_tokens" ); 73 | $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_payment_tokenmeta" ); 74 | } 75 | 76 | // Delete Woo customers and analytics 77 | $table_name = $wpdb->prefix . 'wc_customer_lookup'; 78 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 79 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_customer_lookup" ); 80 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_product_lookup" ); 81 | $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_log" ); 82 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_order_stats" ); 83 | } 84 | 85 | // Delete renewal scheduled actions 86 | $table_name = $wpdb->prefix . 'actionscheduler_logs'; // check if table exists before purging 87 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 88 | $wpdb->query( "DELETE lg FROM {$wpdb->prefix}actionscheduler_logs lg LEFT JOIN {$wpdb->prefix}actionscheduler_actions aa ON aa.action_id = lg.action_id WHERE aa.hook IN ( 'woocommerce_scheduled_subscription_payment', 'woocommerce_scheduled_subscription_payment_retry', 'woocommerce_scheduled_subscription_end_of_prepaid_term' )" ); 89 | } 90 | $table_name = $wpdb->prefix . 'actionscheduler_actions'; // check if table exists before purging 91 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 92 | $wpdb->query( "DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE hook IN ( 'woocommerce_scheduled_subscription_payment', 'woocommerce_scheduled_subscription_payment_retry', 'woocommerce_scheduled_subscription_end_of_prepaid_term' )" ); 93 | } 94 | 95 | // Delete WP Mail Logging logs 96 | $table_name = $wpdb->prefix . 'wpml_mails'; // check if table exists before purging 97 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 98 | $wpdb->query( "DELETE FROM {$wpdb->prefix}wpml_mails" ); 99 | } 100 | 101 | // Delete Newsletter plugin subscribers 102 | $table_name = $wpdb->prefix . 'newsletter'; 103 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 104 | $wpdb->query( "DELETE FROM {$wpdb->prefix}newsletter" ); 105 | } 106 | 107 | // Delete Give plugin data 108 | $table_name = $wpdb->prefix . 'give_donors'; 109 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 110 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_donors" ); 111 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_donormeta" ); 112 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_donationmeta" ); 113 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_comments" ); 114 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_commentmeta" ); 115 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_sessions" ); 116 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_subscriptions" ); 117 | $wpdb->query( "DELETE FROM {$wpdb->prefix}give_subscriptionmeta" ); 118 | } 119 | 120 | // Delete Give payment and donation posts 121 | $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE post_id IN ( SELECT ID FROM {$wpdb->posts} WHERE post_type = 'give_forms' )" ); 122 | $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_type = 'give_forms'" ); 123 | $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE post_id IN ( SELECT ID FROM {$wpdb->posts} WHERE post_type = 'give_payment' )" ); 124 | $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_type = 'give_payment'" ); 125 | 126 | // Reassigning all posts to the first admin user 127 | reassign_all_posts(); 128 | 129 | $admins = get_admin_user_ids(); // returns an array of ids 130 | 131 | // Delete all non-admin users and their user meta 132 | $placeholders = implode( ',', array_fill( 0, count( $admins ), '%d' ) ); 133 | $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE user_id NOT IN ($placeholders)", ...$admins ) ); // phpcs:ignore 134 | $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->users WHERE ID NOT IN ($placeholders)", ...$admins ) ); // phpcs:ignore 135 | 136 | // Set option so this function doesn't run again. 137 | update_option( 'safety_net_data_deleted', true ); 138 | 139 | wp_cache_flush(); 140 | } 141 | 142 | /** 143 | * Reassigns all posts to an admin. 144 | * 145 | * @return void 146 | */ 147 | function reassign_all_posts() { 148 | global $wpdb; 149 | 150 | $wpdb->get_results( $wpdb->prepare( "UPDATE $wpdb->posts SET post_author = %d", get_admin_id() ) ); 151 | 152 | wp_cache_flush(); 153 | } 154 | 155 | /** 156 | * Returns an admin ID that posts can be reassigned to. 157 | * 158 | * @return mixed 159 | */ 160 | function get_admin_id() { 161 | $admin = get_users( 162 | array( 163 | 'role__in' => array( 164 | 'administrator', 165 | ), 166 | 'fields' => 'ids', 167 | 'number' => 1, 168 | ) 169 | ); 170 | 171 | return $admin[0]; 172 | } 173 | -------------------------------------------------------------------------------- /includes/disable-webhooks.php: -------------------------------------------------------------------------------- 1 | query( "UPDATE {$wpdb->prefix}wc_webhooks SET status = 'disabled'" ); 17 | 18 | // Set option so this function doesn't run again. 19 | update_option( 'safety_net_webhooks_disabled', true ); 20 | 21 | wp_cache_flush(); 22 | } -------------------------------------------------------------------------------- /includes/scrub-options.php: -------------------------------------------------------------------------------- 1 | array( 68 | 'aes_key', 69 | 'hmac_key', 70 | ), 71 | ); 72 | $option_array = $option_value; 73 | foreach ( $keys_to_scrub as $index => $keys ) { 74 | if ( array_key_exists( $index, $option_array ) ) { 75 | foreach ( $keys as $key ) { 76 | if ( array_key_exists( $key, $option_array[ $index ] ) ) { 77 | $option_array[ $index ][ $key ] = ''; 78 | } 79 | } 80 | } 81 | } 82 | safety_net_update_option_direct( $option, $option_array ); 83 | } else { 84 | // Some plugins don't like it when options are deleted, so we will save their value as either an empty string or array, depending on which it already is. 85 | if ( is_array( get_option( $option ) ) ) { 86 | $empty_array = array(); 87 | safety_net_update_option_direct( $option, $empty_array ); 88 | } else { 89 | safety_net_update_option_direct( $option, '' ); 90 | } 91 | } 92 | } 93 | } 94 | 95 | // Disable all Woo Webhooks 96 | if ( class_exists( 'WooCommerce' ) ) { 97 | $data_store = WC_Data_Store::load( 'webhook' ); 98 | $webhooks = $data_store->search_webhooks(); 99 | 100 | if ( ! empty( $webhooks ) ) { 101 | foreach ( $webhooks as $webhook_id ) { 102 | $webhook = new WC_Webhook( $webhook_id ); 103 | $webhook->set_status( 'disabled' ); 104 | $webhook->save(); 105 | } 106 | } 107 | } 108 | 109 | // Disable AutomateWoo workflows, clear the queue, and set scheduled actions to "done" 110 | $wpdb->query( "UPDATE $wpdb->posts SET post_status = 'aw-disabled' WHERE post_type = 'aw_workflow' AND post_status = 'publish'" ); 111 | 112 | $table_name = $wpdb->prefix . 'automatewoo_queue'; 113 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 114 | $wpdb->query( "DELETE FROM {$wpdb->prefix}automatewoo_queue" ); 115 | $wpdb->query( "DELETE FROM {$wpdb->prefix}automatewoo_queue_meta" ); 116 | } 117 | 118 | $table_name = $wpdb->prefix . 'actionscheduler_actions'; 119 | if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name ) { 120 | $wpdb->query( "UPDATE {$wpdb->prefix}actionscheduler_actions SET status = 'done' WHERE status = 'pending' AND hook LIKE '%automatewoo%'" ); 121 | } 122 | 123 | update_option( 'safety_net_options_scrubbed', true ); 124 | 125 | // Clear object cache since the updates happen directly in the database. 126 | wp_cache_flush(); 127 | } 128 | 129 | /** 130 | * Updates options directly in the database to prevent notifications from being sent. 131 | * 132 | * @param string $option_name The name of the option to update. 133 | * @param mixed $option_value The value to set the option to. 134 | * 135 | * @return void 136 | */ 137 | function safety_net_update_option_direct( $option_name, $option_value ) { 138 | global $wpdb; 139 | 140 | if ( is_array( $option_value ) ) { 141 | $option_value = serialize( $option_value ); 142 | } 143 | 144 | $wpdb->update( 145 | $wpdb->options, 146 | array( 'option_value' => $option_value ), 147 | array( 'option_name' => $option_name ), 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /includes/utilities.php: -------------------------------------------------------------------------------- 1 | get_col( "SELECT u.ID FROM $wpdb->users u INNER JOIN $wpdb->usermeta m ON m.user_id = u.ID WHERE m.meta_key = '{$wpdb->prefix}capabilities' AND m.meta_value LIKE '%administrator%' ORDER BY u.user_registered" ); 14 | } 15 | 16 | /** 17 | * Returns true if plugin is running on production 18 | * 19 | * @return boolean 20 | */ 21 | function is_production() { 22 | // If we're not on staging, development, or a local environment, return true. 23 | if ( ! in_array( wp_get_environment_type(), array( 'staging', 'development', 'local' ), true ) ) { 24 | return true; 25 | } 26 | } 27 | 28 | /** 29 | * Reads the plugin or options denylist txt files, and returns an array for use 30 | * 31 | * @param string $denylist_type Type of denylist. Accepts 'options' or 'plugins'. 32 | * 33 | * @return array 34 | */ 35 | function get_denylist_array( $denylist_type ): array { 36 | global $wp_filesystem; 37 | 38 | if ( ! $wp_filesystem ) { 39 | require_once ABSPATH . 'wp-admin/includes/file.php'; 40 | WP_Filesystem(); 41 | } 42 | 43 | $denylist_array = array(); 44 | $filename = 'options' === $denylist_type ? 'option_scrublist.txt' : 'plugin_denylist.txt'; 45 | $file_path = SAFETY_NET_PATH . '/assets/data/' . $filename; 46 | 47 | if ( ! $wp_filesystem->exists( $file_path ) ) { 48 | return $denylist_array; 49 | } 50 | 51 | $file_contents = $wp_filesystem->get_contents( $file_path ); 52 | if ( false === $file_contents ) { 53 | return $denylist_array; 54 | } 55 | 56 | $rows = explode( "\n", $file_contents ); 57 | 58 | foreach ( $rows as $row ) { 59 | $data = str_getcsv( $row ); 60 | foreach ( $data as $item ) { 61 | $denylist_array[] = trim( $item ); 62 | } 63 | } 64 | 65 | return array_filter( $denylist_array ); 66 | } 67 | 68 | /** 69 | * Renders an admin notice, if the plugin is running on production 70 | * 71 | * @filter safety_net_show_production_notice 72 | * 73 | * @return void 74 | */ 75 | function show_production_notice() { 76 | // If not production, return. 77 | if ( ! is_production() ) { 78 | return; 79 | } 80 | 81 | // Check the if the user has the capability to manage options. 82 | $allowed = current_user_can( 'manage_options' ); 83 | // Filter for third-party plugins to add their own capability check. 84 | $allowed = apply_filters( 'safety_net_show_production_notice', $allowed ); 85 | 86 | if ( ! $allowed ) { 87 | return; 88 | } 89 | 90 | // Check if the constant starts as an mu plugin. 91 | $is_mu = defined( 'WPMU_PLUGIN_DIR' ) && \str_starts_with( SAFETY_NET_PATH, WPMU_PLUGIN_DIR ); 92 | ?> 93 |
94 |

95 | 104 |

105 |
106 |