├── index.php ├── wpml-config.xml ├── .eslintrc.js ├── .editorconfig ├── phpcs.xml ├── src ├── admin-notice-install-deps.php ├── functions.php ├── Admin.php └── Bar.php ├── assets └── src │ ├── js │ ├── cookies.js │ ├── admin.js │ ├── loader.js │ └── script.js │ └── css │ └── bar.css ├── .github └── workflows │ └── check-php-syntax.yml ├── webpack.config.js ├── README.md ├── mailchimp-top-bar.php ├── create-version.sh ├── composer.lock ├── CHANGELOG.md ├── readme.txt ├── views └── settings-page.php └── LICENSE /index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly' 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2018 16 | }, 17 | rules: { 18 | 'no-prototype-builtins': 'off' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.{yaml,yml}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.js] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | 28 | [{*.txt,wp-config-sample.php}] 29 | end_of_line = crlf 30 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rules 4 | 5 | src/ 6 | mailchimp-top-bar.php 7 | *\.(html|css|js) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/admin-notice-install-deps.php: -------------------------------------------------------------------------------- 1 | 13 |
14 |

%s in order to use %s.', 'mailchimp-top-bar'), $url, 'MailChimp for WordPress', 'MailChimp Top Bar'); ?>

15 |
16 | { 40 | return postcss([cssnano]) 41 | .process(content, { 42 | from: path 43 | }) 44 | .then((result) => { 45 | return result.css 46 | }) 47 | } 48 | } 49 | ] 50 | }) 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /assets/src/js/admin.js: -------------------------------------------------------------------------------- 1 | const elSelectList = document.getElementById('select-mailchimp-list') 2 | const msgRequiresFields = document.getElementById('message-list-requires-fields') 3 | 4 | /* 5 | * Functions 6 | */ 7 | function maybeShowRequiredFieldsNotice () { 8 | msgRequiresFields.style.display = 'none' 9 | const listId = elSelectList.value 10 | const xhr = new XMLHttpRequest() 11 | xhr.open('GET', window.ajaxurl + '?action=mc4wp_get_list_details&ids=' + listId, true) 12 | xhr.onload = function () { 13 | if (this.status >= 400) { 14 | console.error('Error retrieving list details') 15 | return 16 | } 17 | const lists = JSON.parse(this.responseText) 18 | // iterate over selected lists 19 | for (let i = 0; i < lists.length; i++) { 20 | const list = lists[i] 21 | 22 | // iterate over list fields 23 | for (let j = 0; j < list.merge_fields.length; j++) { 24 | const field = list.merge_fields[j] 25 | 26 | // if field other than EMAIL is required, show notice and stop loop 27 | if (field.tag !== 'EMAIL' && field.required) { 28 | msgRequiresFields.style.display = '' 29 | return 30 | } 31 | } 32 | } 33 | } 34 | xhr.send(null) 35 | } 36 | 37 | // init colorpickers 38 | window.jQuery('.mc4wp-color').wpColorPicker() 39 | 40 | // if a list changes, check which fields are required 41 | elSelectList.addEventListener('change', maybeShowRequiredFieldsNotice) 42 | 43 | // check right away 44 | maybeShowRequiredFieldsNotice() 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MC4WP: Mailchimp Top Bar 2 | ============== 3 | 4 | Adds a beautiful and customizable Mailchimp opt-in bar to the top of your WordPress site. 5 | 6 | Requirements 7 | ------------ 8 | 9 | - WordPress version 4.9 or later 10 | - PHP version 7.3 or later 11 | 12 | 13 | Installation 14 | ------------ 15 | 16 | If you just want to install this plugin on your WordPress site, please download and install the latest version from WordPress.org: [Mailchimp Top Bar plugin on WordPress.org](https://wordpress.org/plugins/mailchimp-top-bar/installation/). 17 | 18 | To install the development version, take the following steps: 19 | 20 | 1. Clone the GitHub repository: 21 | 22 | ``` 23 | git clone https://github.com/ibericode/mailchimp-top-bar.git 24 | ``` 25 | 26 | 2. Install NPM dependencies: 27 | 28 | ``` 29 | npm install 30 | ``` 31 | 32 | 3. Generate plugin asset files: 33 | 34 | ``` 35 | npm run build 36 | ``` 37 | 38 | 5. Activate the plugin in your WordPress admin. 39 | 40 | Bugs 41 | ---- 42 | If you think you've found a bug, [please open an issue here](https://github.com/ibericode/mailchimp-top-bar/issues?state=open)! 43 | 44 | Support 45 | ------- 46 | This is a developer's portal for the Mailchimp Top Bar plugin and should not be used for support. Please visit the 47 | [Mailchimp Top Bar support forum on WordPress.org](https://wordpress.org/support/plugin/mailchimp-top-bar). 48 | 49 | If you need priority support, please purchase a [Mailchimp for WordPress Premium](https://www.mc4wp.com/) plugin license. 50 | 51 | License 52 | ------- 53 | GPLv3 or later 54 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | '', 12 | 'enabled' => 1, 13 | 'show_to_administrators' => 1, 14 | 'cookie_length' => 365, 15 | 'color_bar' => '#ffcc00', 16 | 'color_text' => '#222222', 17 | 'color_button' => '#222222', 18 | 'color_button_text' => '#ffffff', 19 | 'disable_after_use' => 1, 20 | 'size' => 'medium', 21 | 'sticky' => 1, 22 | 'text_email_placeholder' => __('Your email address..', 'mailchimp-top-bar'), 23 | 'text_bar' => __('Sign-up now - don\'t miss the fun!', 'mailchimp-top-bar'), 24 | 'text_button' => __('Subscribe', 'mailchimp-top-bar'), 25 | 'redirect' => '', 26 | 'position' => 'top', 27 | 'double_optin' => 1, 28 | 'send_welcome' => 0, 29 | 'update_existing' => 0, 30 | 'text_subscribed' => __("Thanks, you're in! Please check your email inbox for a confirmation.", 'mailchimp-top-bar'), 31 | 'text_error' => __("Oops. Something went wrong.", 'mailchimp-top-bar'), 32 | 'text_invalid_email' => __('That email seems to be invalid.', 'mailchimp-top-bar'), 33 | 'text_already_subscribed' => __("You are already subscribed. Thank you!", 'mailchimp-top-bar'), 34 | 'disable_on_pages' => '', 35 | ]; 36 | 37 | $options = (array) get_option('mailchimp_top_bar', []); 38 | $options = array_merge($defaults, $options); 39 | 40 | // for BC with MailChimp Top Bar v1.2.3, always fill text option keys 41 | $text_keys = [ 42 | 'text_subscribed', 43 | 'text_error', 44 | 'text_invalid_email', 45 | 'text_already_subscribed' 46 | ]; 47 | 48 | foreach ($text_keys as $text_key) { 49 | if (empty($options[ $text_key ]) && ! empty($defaults[ $text_key ])) { 50 | $options[ $text_key ] = $defaults[ $text_key ]; 51 | } 52 | } 53 | 54 | return $options; 55 | } 56 | -------------------------------------------------------------------------------- /mailchimp-top-bar.php: -------------------------------------------------------------------------------- 1 | . 30 | */ 31 | 32 | 33 | defined('ABSPATH') or exit; 34 | 35 | add_action('plugins_loaded', function () { 36 | // check for PHP 7.3 or higher 37 | if (PHP_VERSION_ID < 70300) { 38 | return; 39 | } 40 | 41 | // check for MailChimp for WordPress (version 3.0 or higher) 42 | if (!defined('MC4WP_VERSION') || version_compare(MC4WP_VERSION, '3.0', '<')) { 43 | require __DIR__ . '/src/admin-notice-install-deps.php'; 44 | return; 45 | } 46 | 47 | 48 | define('MAILCHIMP_TOP_BAR_FILE', __FILE__); 49 | define('MAILCHIMP_TOP_BAR_DIR', __DIR__); 50 | define('MAILCHIMP_TOP_BAR_VERSION', '1.7.3'); 51 | 52 | require __DIR__ . '/src/functions.php'; 53 | 54 | if (is_admin()) { 55 | require __DIR__ . '/src/Admin.php'; 56 | $admin = new Mailchimp\TopBar\Admin(); 57 | $admin->add_hooks(); 58 | } else { 59 | require __DIR__ . '/src/Bar.php'; 60 | $bar = new MailChimp\TopBar\Bar(); 61 | add_action('wp', [$bar, 'init']); 62 | } 63 | }, 30); 64 | -------------------------------------------------------------------------------- /assets/src/js/loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {HTMLElement} button 3 | */ 4 | function getButtonText (button) { 5 | return button.innerHTML ? button.innerHTML : button.value 6 | } 7 | 8 | /** 9 | * @param {HTMLElement} button 10 | * @param {string} text 11 | */ 12 | function setButtonText (button, text) { 13 | button.innerHTML ? button.innerHTML = text : button.value = text 14 | } 15 | 16 | /** 17 | * @param {HTMLFormElement} formElement 18 | */ 19 | function Loader (formElement) { 20 | this.form = formElement 21 | this.button = formElement.querySelector('input[type="submit"],button[type="submit"]') 22 | this.char = '\u00B7' 23 | 24 | if (this.button) { 25 | this.originalButton = this.button.cloneNode(true) 26 | } 27 | 28 | this.start() 29 | } 30 | 31 | /** 32 | * @param {string} c 33 | */ 34 | Loader.prototype.setCharacter = function (c) { 35 | this.char = c 36 | } 37 | 38 | /** 39 | * Start the loading indicator 40 | */ 41 | Loader.prototype.start = function () { 42 | if (this.button) { 43 | // loading text 44 | const loadingText = this.button.getAttribute('data-loading-text') 45 | if (loadingText) { 46 | setButtonText(this.button, loadingText) 47 | return 48 | } 49 | 50 | // Show AJAX loader 51 | const styles = window.getComputedStyle(this.button) 52 | this.button.style.width = styles.width 53 | setButtonText(this.button, this.char) 54 | this.loadingInterval = window.setInterval(this.tick.bind(this), 500) 55 | } else { 56 | this.form.style.opacity = '0.5' 57 | } 58 | } 59 | 60 | /** 61 | * Single step in the loading indicator's "animation" 62 | */ 63 | Loader.prototype.tick = function () { 64 | // count chars, start over at 5 65 | const text = getButtonText(this.button) 66 | const loadingChar = this.char 67 | setButtonText(this.button, text.length >= 5 ? loadingChar : text + ' ' + loadingChar) 68 | } 69 | 70 | /** 71 | * Stops the loading indicator 72 | */ 73 | Loader.prototype.stop = function () { 74 | if (this.button) { 75 | this.button.style.width = this.originalButton.style.width 76 | const text = getButtonText(this.originalButton) 77 | setButtonText(this.button, text) 78 | window.clearInterval(this.loadingInterval) 79 | } else { 80 | this.form.style.opacity = '' 81 | } 82 | } 83 | 84 | export default Loader 85 | -------------------------------------------------------------------------------- /create-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Check if VERSION argument was supplied 6 | if [ "$#" -lt 1 ]; then 7 | echo "1 parameters expected, $# found" 8 | echo "Usage: package.sh " 9 | exit 1 10 | fi 11 | 12 | PLUGIN_SLUG=$(basename "$PWD") 13 | PLUGIN_FILE="$PLUGIN_SLUG.php" 14 | VERSION=$1 15 | PACKAGE_FILE="$PWD/../$PLUGIN_SLUG-$VERSION.zip" 16 | 17 | # Check if we're inside plugin directory 18 | if [ ! -e "$PLUGIN_FILE" ]; then 19 | echo "Plugin entry file not found. Please run this command from inside the $PLUGIN_SLUG directory." 20 | exit 1 21 | fi 22 | 23 | # Check if there are uncommitted changes 24 | if [ -n "$(git status --porcelain)" ]; then 25 | echo "There are uncommitted changes. Please commit those changes before initiating a release." 26 | exit 1 27 | fi 28 | 29 | # Check if there is an existing file for this release already 30 | rm -f "$PACKAGE_FILE" 31 | 32 | # Build (optimized) client-side assets 33 | npm run build 34 | 35 | # Update version numbers in code 36 | sed -i "s/^Version: .*$/Version: $VERSION/g" "$PLUGIN_FILE" 37 | sed -i "s/define('\(.*_VERSION\)', '.*');/define('\1', '$VERSION');/g" "$PLUGIN_FILE" 38 | sed -i "s/^Stable tag: .*$/Stable tag: $VERSION/g" "readme.txt" 39 | 40 | # Copy over changelog from CHANGELOG.md to readme.txt 41 | # Ref: https://git.sr.ht/~dvko/dotfiles/tree/master/item/bin/wp-update-changelog 42 | wp-update-changelog 43 | 44 | # Move up one directory level because we need plugin directory in ZIP file 45 | cd .. 46 | 47 | # Create archive (excl. development files) 48 | zip -r "$PACKAGE_FILE" "$PLUGIN_SLUG" \ 49 | -x "$PLUGIN_SLUG/.*" \ 50 | -x "$PLUGIN_SLUG/vendor/*" \ 51 | -x "$PLUGIN_SLUG/node_modules/*" \ 52 | -x "$PLUGIN_SLUG/tests/*" \ 53 | -x "$PLUGIN_SLUG/webpack.config*.js" \ 54 | -x "$PLUGIN_SLUG/*.json" \ 55 | -x "$PLUGIN_SLUG/*.lock" \ 56 | -x "$PLUGIN_SLUG/phpcs.xml" \ 57 | -x "$PLUGIN_SLUG/phpunit.xml.dist" \ 58 | -x "$PLUGIN_SLUG/*.sh" \ 59 | -x "$PLUGIN_SLUG/assets/src/*" \ 60 | -x "$PLUGIN_SLUG/sample-code-snippets/*" 61 | 62 | cd "$PLUGIN_SLUG" 63 | 64 | SIZE=$(ls -lh "$PACKAGE_FILE" | cut -d' ' -f5) 65 | echo "$(basename "$PACKAGE_FILE") created ($SIZE)" 66 | 67 | # # Create tag in Git and push to remote 68 | git add . -A 69 | git commit -m "v$VERSION" 70 | git tag "$VERSION" 71 | git push origin main 72 | git push origin "tags/$VERSION" 73 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "460ae9ad90b58faf96aef91fa960724f", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "squizlabs/php_codesniffer", 12 | "version": "3.11.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", 16 | "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", 21 | "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "ext-simplexml": "*", 26 | "ext-tokenizer": "*", 27 | "ext-xmlwriter": "*", 28 | "php": ">=5.4.0" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" 32 | }, 33 | "bin": [ 34 | "bin/phpcbf", 35 | "bin/phpcs" 36 | ], 37 | "type": "library", 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "3.x-dev" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "BSD-3-Clause" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Greg Sherwood", 50 | "role": "Former lead" 51 | }, 52 | { 53 | "name": "Juliette Reinders Folmer", 54 | "role": "Current lead" 55 | }, 56 | { 57 | "name": "Contributors", 58 | "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" 59 | } 60 | ], 61 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 62 | "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", 63 | "keywords": [ 64 | "phpcs", 65 | "standards", 66 | "static analysis" 67 | ], 68 | "support": { 69 | "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", 70 | "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", 71 | "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", 72 | "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" 73 | }, 74 | "funding": [ 75 | { 76 | "url": "https://github.com/PHPCSStandards", 77 | "type": "github" 78 | }, 79 | { 80 | "url": "https://github.com/jrfnl", 81 | "type": "github" 82 | }, 83 | { 84 | "url": "https://opencollective.com/php_codesniffer", 85 | "type": "open_collective" 86 | } 87 | ], 88 | "time": "2024-12-11T16:04:26+00:00" 89 | } 90 | ], 91 | "aliases": [], 92 | "minimum-stability": "stable", 93 | "stability-flags": {}, 94 | "prefer-stable": false, 95 | "prefer-lowest": false, 96 | "platform": { 97 | "php": ">=7.2" 98 | }, 99 | "platform-dev": {}, 100 | "plugin-api-version": "2.6.0" 101 | } 102 | -------------------------------------------------------------------------------- /assets/src/css/bar.css: -------------------------------------------------------------------------------- 1 | #mailchimp-top-bar form, 2 | #mailchimp-top-bar input, 3 | #mailchimp-top-bar label { 4 | vertical-align: middle; 5 | margin: 0; 6 | padding: 0; 7 | box-shadow: none; 8 | text-shadow: none; 9 | font-size: 100%; 10 | outline: 0; 11 | height: auto; 12 | line-height: initial; 13 | float: none; 14 | box-sizing: border-box; 15 | } 16 | 17 | #mailchimp-top-bar input, 18 | #mailchimp-top-bar label { 19 | display: inline-block !important; 20 | vertical-align: middle !important; 21 | width: auto; 22 | } 23 | 24 | #mailchimp-top-bar form { 25 | text-align: center; 26 | margin: 0 !important; 27 | padding: 0 !important; 28 | } 29 | 30 | #mailchimp-top-bar label { 31 | margin: 0 6px 0 0; 32 | } 33 | 34 | #mailchimp-top-bar input, 35 | #mailchimp-top-bar input[type="text"], 36 | #mailchimp-top-bar input[type="email"], 37 | #mailchimp-top-bar .mctb-email, 38 | #mailchimp-top-bar .mctb-button { 39 | margin: 0 0 0 6px; 40 | border: 1px solid white; 41 | background: white; 42 | height: auto; 43 | } 44 | 45 | #mailchimp-top-bar .mctb-email { 46 | width: 100%; 47 | /* this is important because otherwise the field will stack horizontally */ 48 | max-width: 240px !important; 49 | } 50 | 51 | #mailchimp-top-bar .mctb-button { 52 | margin-left: 0; 53 | cursor: pointer; 54 | } 55 | 56 | #mailchimp-top-bar .mctb-email-confirm { 57 | display: none !important; 58 | } 59 | 60 | #mailchimp-top-bar.mctb-small { 61 | font-size: 10px; 62 | } 63 | 64 | #mailchimp-top-bar.mctb-small .mctb-bar { 65 | padding: 5px 6px; 66 | } 67 | 68 | #mailchimp-top-bar.mctb-small .mctb-button { 69 | padding: 4px 12px; 70 | } 71 | 72 | #mailchimp-top-bar.mctb-small input, 73 | #mailchimp-top-bar.mctb-small .mctb-email, 74 | #mailchimp-top-bar.mctb-small .mctb-label { 75 | padding: 4px 6px !important; 76 | } 77 | 78 | #mailchimp-top-bar.mctb-small label, 79 | #mailchimp-top-bar.mctb-small input { 80 | font-size: 12px; 81 | } 82 | 83 | #mailchimp-top-bar.mctb-small .mctb-close { 84 | padding: 4px 12px; 85 | font-size: 16px; 86 | } 87 | 88 | #mailchimp-top-bar.mctb-medium { 89 | font-size: 12.5px; 90 | } 91 | 92 | #mailchimp-top-bar.mctb-medium .mctb-bar { 93 | padding: 6.25px 7.5px; 94 | } 95 | 96 | #mailchimp-top-bar.mctb-medium .mctb-button { 97 | padding: 5px 15px; 98 | } 99 | 100 | #mailchimp-top-bar.mctb-medium input, 101 | #mailchimp-top-bar.mctb-medium .mctb-email, 102 | #mailchimp-top-bar.mctb-medium .mctb-label { 103 | padding: 5px 7.5px !important; 104 | } 105 | 106 | #mailchimp-top-bar.mctb-medium label, 107 | #mailchimp-top-bar.mctb-medium input { 108 | font-size: 15px; 109 | } 110 | 111 | #mailchimp-top-bar.mctb-medium .mctb-close { 112 | padding: 5px 15px; 113 | font-size: 20px; 114 | } 115 | 116 | #mailchimp-top-bar.mctb-big { 117 | font-size: 15px; 118 | } 119 | 120 | #mailchimp-top-bar.mctb-big .mctb-bar { 121 | padding: 7.5px 9px; 122 | } 123 | 124 | #mailchimp-top-bar.mctb-big .mctb-button { 125 | padding: 6px 18px; 126 | } 127 | 128 | #mailchimp-top-bar.mctb-big input, 129 | #mailchimp-top-bar.mctb-big .mctb-email, 130 | #mailchimp-top-bar.mctb-big .mctb-label { 131 | padding: 6px 9px !important; 132 | } 133 | 134 | #mailchimp-top-bar.mctb-big label, 135 | #mailchimp-top-bar.mctb-big input { 136 | font-size: 18px; 137 | } 138 | 139 | #mailchimp-top-bar.mctb-big .mctb-close { 140 | padding: 6px 18px; 141 | font-size: 24px; 142 | } 143 | 144 | @media (max-width: 580px) { 145 | #mailchimp-top-bar input, 146 | #mailchimp-top-bar label, 147 | #mailchimp-top-bar .mctb-email, 148 | #mailchimp-top-bar .mctb-label { 149 | width: 100%; 150 | max-width: 100% !important; 151 | } 152 | 153 | #mailchimp-top-bar input, 154 | #mailchimp-top-bar .mctb-email { 155 | margin: 6px 0 0 !important; 156 | } 157 | } 158 | 159 | @media (max-width: 860px) { 160 | #mailchimp-top-bar.multiple-input-fields .mctb-label { 161 | display: block !important; 162 | margin: 0 0 6px; 163 | } 164 | } 165 | 166 | /* make sure z-index is slightly below that of admin bar (for dropdown items) */ 167 | .admin-bar .mctb { 168 | z-index: 99998; 169 | } 170 | 171 | .admin-bar .mctb-position-top { 172 | top: 32px; 173 | } 174 | 175 | @media screen and (max-width: 782px) { 176 | .admin-bar .mctb-position-top { 177 | top: 46px; 178 | } 179 | } 180 | 181 | /* admin bar is non-sticky on small screens */ 182 | @media screen and (max-width: 600px) { 183 | .admin-bar .mctb-position-top.mctb-sticky { 184 | top: 0; 185 | } 186 | } 187 | 188 | .mctb { 189 | position: absolute; 190 | top: 0; 191 | left: 0; 192 | right: 0; 193 | width: 100%; 194 | margin: 0; 195 | background: transparent; 196 | text-align: center; 197 | z-index: 100000; 198 | } 199 | 200 | .mctb-bar { 201 | overflow: hidden; 202 | position: relative; 203 | width: 100%; 204 | } 205 | 206 | .mctb-sticky { 207 | position: fixed; 208 | } 209 | 210 | .mctb-position-bottom { 211 | position: fixed; 212 | bottom: 0; 213 | top: auto; 214 | } 215 | 216 | .mctb-position-bottom .mctb-bar { 217 | clear: both; 218 | } 219 | 220 | .mctb-response { 221 | position: absolute; 222 | z-index: 100; 223 | top: 0; 224 | left: 0; 225 | width: 100%; 226 | transition-duration: 800ms; 227 | } 228 | 229 | .mctb-close { 230 | display: inline-block; 231 | float: right; 232 | margin-right: 12px; 233 | cursor: pointer; 234 | clear: both; 235 | z-index: 10; 236 | line-height: initial; 237 | } 238 | 239 | .mctb-icon-inside-bar.mctb-position-bottom .mctb-bar { 240 | position: absolute; 241 | bottom: 0; 242 | } 243 | 244 | .mctb-icon-inside-bar .mctb-close { 245 | float: none; 246 | position: absolute; 247 | top: 0; 248 | right: 0; 249 | } 250 | -------------------------------------------------------------------------------- /src/Admin.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace MailChimp\TopBar; 21 | 22 | class Admin 23 | { 24 | /** 25 | * Add plugin hooks 26 | */ 27 | public function add_hooks() 28 | { 29 | add_action('admin_init', [ $this, 'init' ], 10, 0); 30 | add_action('admin_footer_text', [ $this, 'footer_text' ], 11, 1); 31 | add_filter('mc4wp_admin_menu_items', [ $this, 'add_menu_item' ], 10, 1); 32 | add_action('mc4wp_admin_enqueue_assets', [ $this, 'load_assets' ], 10, 2); 33 | } 34 | 35 | /** 36 | * Runs on `admin_init` 37 | */ 38 | public function init() 39 | { 40 | // only run for administrators 41 | // TODO: Use mc4wp capability here 42 | if (! current_user_can('manage_options')) { 43 | return; 44 | } 45 | 46 | // register settings 47 | register_setting('mailchimp_top_bar', 'mailchimp_top_bar', [ $this, 'sanitize_settings' ]); 48 | 49 | // add link to settings page from plugins page 50 | add_filter('plugin_action_links_' . plugin_basename(MAILCHIMP_TOP_BAR_FILE), [ $this, 'add_plugin_settings_link' ]); 51 | add_filter('plugin_row_meta', [ $this, 'add_plugin_meta_links'], 10, 2); 52 | } 53 | 54 | /** 55 | * Register menu pages 56 | * 57 | * @param array $items 58 | * 59 | * @return array 60 | */ 61 | public function add_menu_item(array $items) 62 | { 63 | $item = [ 64 | 'title' => esc_html__('Mailchimp Top Bar', 'mailchimp-top-bar'), 65 | 'text' => esc_html__('Top Bar', 'mailchimp-top-bar'), 66 | 'slug' => 'top-bar', 67 | 'callback' => [$this, 'show_settings_page'] 68 | ]; 69 | 70 | // insert item before the last menu item 71 | \array_splice($items, \count($items) - 1, 0, [ $item ]); 72 | return $items; 73 | } 74 | 75 | /** 76 | * Add the settings link to the Plugins overview 77 | * 78 | * @param array $links 79 | * @return array 80 | */ 81 | public function add_plugin_settings_link(array $links) 82 | { 83 | $link_href = esc_attr(admin_url('admin.php?page=mailchimp-for-wp-top-bar')); 84 | $link_title = esc_html__('Settings', 'mailchimp-for-wp'); 85 | $settings_link = "{$link_title}"; 86 | \array_unshift($links, $settings_link); 87 | return $links; 88 | } 89 | 90 | /** 91 | * Adds meta links to the plugin in the WP Admin > Plugins screen 92 | * 93 | * @param array $links 94 | * @param string $file 95 | * @return array 96 | */ 97 | public function add_plugin_meta_links(array $links, $file) 98 | { 99 | if ($file !== plugin_basename(MAILCHIMP_TOP_BAR_FILE)) { 100 | return $links; 101 | } 102 | 103 | $links[] = \sprintf(esc_html__('An add-on for %s', 'mailchimp-top-bar'), 'Mailchimp for WordPress'); 104 | return $links; 105 | } 106 | 107 | /** 108 | * Load assets if we're on the settings page of this plugin 109 | * 110 | * @param string $suffix 111 | * @param string $page 112 | * @return void 113 | */ 114 | public function load_assets($suffix, $page) 115 | { 116 | if ($page !== 'top-bar') { 117 | return; 118 | } 119 | 120 | wp_enqueue_style('wp-color-picker'); 121 | wp_enqueue_script('mailchimp-top-bar-admin', $this->asset_url("/admin.js"), [ 'jquery', 'wp-color-picker' ], MAILCHIMP_TOP_BAR_VERSION, true); 122 | } 123 | 124 | /** 125 | * Outputs the settings page 126 | */ 127 | public function show_settings_page() 128 | { 129 | $current_tab = isset($_GET['tab']) ? $_GET['tab'] : 'settings'; 130 | $options = mctb_get_options(); 131 | $mailchimp = new \MC4WP_MailChimp(); 132 | $lists = $mailchimp->get_lists(); 133 | 134 | require MAILCHIMP_TOP_BAR_DIR . '/views/settings-page.php'; 135 | } 136 | 137 | /** 138 | * @param string $url 139 | * @return string 140 | */ 141 | protected function asset_url($url) 142 | { 143 | return plugins_url('/assets' . $url, MAILCHIMP_TOP_BAR_FILE); 144 | } 145 | 146 | /** 147 | * @param string $option_name 148 | * @return string 149 | */ 150 | protected function name_attr($option_name) 151 | { 152 | return "mailchimp_top_bar[{$option_name}]"; 153 | } 154 | 155 | /** 156 | * @param array $dirty 157 | * @return array $clean 158 | */ 159 | public function sanitize_settings(array $dirty) 160 | { 161 | $unfiltered_html = current_user_can('unfiltered_html'); 162 | $clean = $dirty; 163 | $safe_attributes = [ 164 | 'class' => [], 165 | 'id' => [], 166 | 'title' => [], 167 | 'tabindex' => [], 168 | ]; 169 | $unsafe_attributes = \array_merge($safe_attributes, ['href' => []]); 170 | $allowed_html = [ 171 | 'strong' => $safe_attributes, 172 | 'b' => $safe_attributes, 173 | 'em' => $safe_attributes, 174 | 'i' => $safe_attributes, 175 | 'u' => $safe_attributes, 176 | // only allow href attribute on elements if user has unfiltered_html capability 177 | 'a' => $unfiltered_html ? $unsafe_attributes : $safe_attributes, 178 | 'span' => $safe_attributes, 179 | ]; 180 | 181 | foreach ($clean as $key => $value) { 182 | // make sure colors start with `#` 183 | if (\strpos($key, 'color_') === 0) { 184 | $value = \strip_tags($value); 185 | if ('' !== $value && $value[0] !== '#') { 186 | $clean[$key] = '#' . $value; 187 | } 188 | } 189 | 190 | // only allow certain HTML elements inside all text settings 191 | if (\strpos($key, 'text_') === 0) { 192 | $clean[$key] = wp_kses(\strip_tags($value, ''), $allowed_html); 193 | } 194 | } 195 | 196 | 197 | // make sure size is either `small`, `medium` or `big` 198 | if (! in_array($dirty['size'], ['small', 'medium', 'big'])) { 199 | $clean['size'] = 'medium'; 200 | } 201 | 202 | if (! in_array($dirty['position'], ['top', 'bottom'])) { 203 | $clean['position'] = 'top'; 204 | } 205 | 206 | // button & email placeholders can have no HTML at all 207 | $clean['text_button'] = \strip_tags($dirty['text_button']); 208 | $clean['text_email_placeholder'] = \strip_tags($dirty['text_email_placeholder']); 209 | 210 | return $clean; 211 | } 212 | 213 | /** 214 | * Ask for a plugin review in the WP Admin footer, if this is one of the plugin pages. 215 | * 216 | * @param string $text 217 | * @return string 218 | */ 219 | public function footer_text($text) 220 | { 221 | if (( isset($_GET['page']) && strpos($_GET['page'], 'mailchimp-for-wp-top-bar') === 0 )) { 222 | $text = 'If you enjoy using Mailchimp Top Bar, please leave us a ★★★★★ rating. A huge thank you in advance!'; 223 | } 224 | 225 | return $text; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /assets/src/js/script.js: -------------------------------------------------------------------------------- 1 | import cookies from './cookies.js' 2 | import Loader from './loader.js' 3 | const COOKIE_NAME = 'mctb_bar_hidden' 4 | 5 | /** 6 | * @param {function} fn callback 7 | * @param {int} delay in ms 8 | */ 9 | function debounce (fn, delay) { 10 | let timeout 11 | return function () { 12 | clearTimeout(timeout) 13 | timeout = setTimeout(fn, delay) 14 | } 15 | } 16 | 17 | function Bar () { 18 | const wrapperEl = document.getElementById('mailchimp-top-bar') 19 | const config = window.mctb 20 | const barEl = wrapperEl.querySelector('.mctb-bar') 21 | const iconEl = document.createElement('span') 22 | const formEl = barEl.querySelector('form') 23 | let barHeight 24 | let barPadding 25 | let responseEl = wrapperEl.querySelector('.mctb-response') 26 | let visible = !cookies.exists(COOKIE_NAME) 27 | let originalBodyPadding = 0 28 | let bodyPadding = 0 29 | const isBottomBar = (config.position === 'bottom') 30 | const state = config.state 31 | 32 | // remove "no_js" field (which is used to detect bots and prevent spam) 33 | const noJsField = barEl.querySelector('input[name="_mctb_no_js"]') 34 | noJsField.parentElement.removeChild(noJsField) 35 | 36 | formEl.addEventListener('submit', submitForm) 37 | 38 | // save original bodyPadding 39 | if (isBottomBar) { 40 | wrapperEl.insertBefore(iconEl, barEl) 41 | originalBodyPadding = (parseInt(document.body.style.paddingBottom) || 0) 42 | } else { 43 | wrapperEl.insertBefore(iconEl, barEl.nextElementSibling) 44 | originalBodyPadding = (parseInt(document.body.style.paddingTop) || 0) 45 | } 46 | 47 | // configure icon 48 | iconEl.className = 'mctb-close' 49 | iconEl.innerHTML = visible ? config.icons.hide : config.icons.show 50 | iconEl.addEventListener('click', toggle) 51 | 52 | // count input fields (3 because of hidden input honeypot) 53 | if (barEl.querySelectorAll('input:not([type="hidden"])').length > 3) { 54 | wrapperEl.className += ' multiple-input-fields' 55 | } 56 | 57 | // calculate bar size whenever layout shifts or window is resized 58 | window.requestAnimationFrame(calculateDimensions) 59 | window.addEventListener('load', calculateDimensions) 60 | window.addEventListener('resize', debounce(calculateDimensions, 100)) 61 | 62 | // fade response 4 seconds after showing bar 63 | if (responseEl) { 64 | window.setTimeout(fadeResponse, 4000) 65 | } 66 | 67 | function submitForm (evt) { 68 | evt.preventDefault() 69 | 70 | const loader = new Loader(formEl) 71 | const data = new FormData(formEl) 72 | const request = new XMLHttpRequest() 73 | request.onload = function () { 74 | // remove loading indicator 75 | loader.stop() 76 | 77 | // parse json response 78 | let response 79 | if (this.status >= 200 && this.status < 400) { 80 | try { 81 | response = JSON.parse(this.responseText) 82 | } catch (error) { 83 | console.log('MailChimp Top Bar: failed to parse AJAX response.\n\nError: "' + error + '"') 84 | return 85 | } 86 | 87 | state.success = !!response.success 88 | state.submitted = true 89 | 90 | // maybe redirect to url from settings 91 | if (response.success && response.redirect_url) { 92 | window.location.href = response.redirect_url 93 | return 94 | } 95 | 96 | showResponseMessage(response.message) 97 | 98 | // clear form 99 | if (state.success) { 100 | formEl.reset() 101 | } 102 | } else { 103 | // Server error :( 104 | console.log(this.responseText) 105 | } 106 | } 107 | request.open('POST', window.location.href, true) 108 | request.setRequestHeader('X-Requested-With', 'XMLHttpRequest') 109 | request.send(data) 110 | } 111 | 112 | function showResponseMessage (msg) { 113 | if (responseEl && responseEl.parentElement) { 114 | responseEl.parentElement.removeChild(responseEl) 115 | } 116 | 117 | responseEl = document.createElement('div') 118 | responseEl.className = 'mctb-response' 119 | 120 | const labelEl = document.createElement('label') 121 | labelEl.className = 'mctb-response-label' 122 | labelEl.innerText = msg 123 | responseEl.appendChild(labelEl) 124 | formEl.parentElement.insertBefore(responseEl, formEl.nextElementSibling) 125 | 126 | calculateDimensions() 127 | window.setTimeout(fadeResponse, 4000) 128 | } 129 | 130 | function iconFitsInsideBar () { 131 | // would the close icon fit inside the bar? 132 | let elementsWidth = 0 133 | for (let i = 0; i < barEl.firstElementChild.children.length; i++) { 134 | elementsWidth += barEl.firstElementChild.children[i].clientWidth 135 | } 136 | 137 | return (elementsWidth + iconEl.clientWidth + 200) < barEl.clientWidth 138 | } 139 | 140 | function calculateDimensions () { 141 | // make sure bar is visible 142 | if (!visible) { 143 | barEl.style.visibility = 'hidden' 144 | } 145 | barEl.style.display = '' 146 | barEl.style.height = '' 147 | barEl.style.paddingTop = '' 148 | barEl.style.paddingBottom = '' 149 | 150 | // measure bar padding and height 151 | // we use this as our animation target values 152 | const styles = window.getComputedStyle(barEl) 153 | barHeight = styles.height 154 | barPadding = styles.paddingTop 155 | 156 | // calculate & set new body padding if bar is currently visible 157 | bodyPadding = (originalBodyPadding + barEl.clientHeight) + 'px' 158 | if (visible) { 159 | document.body.style[isBottomBar ? 'paddingBottom' : 'paddingTop'] = bodyPadding 160 | } 161 | 162 | wrapperEl.className = wrapperEl.className.replace('mctb-icon-inside-bar', '') 163 | if (iconFitsInsideBar()) { 164 | wrapperEl.className += ' mctb-icon-inside-bar' 165 | 166 | // since icon is now absolutely positioned, we need to set a min height 167 | if (isBottomBar) { 168 | wrapperEl.style.minHeight = iconEl.clientHeight + 'px' 169 | } 170 | } 171 | 172 | // fix response height 173 | if (responseEl) { 174 | responseEl.style.height = barEl.clientHeight + 'px' 175 | responseEl.style.lineHeight = barEl.clientHeight + 'px' 176 | } 177 | 178 | // reset bar again, we're done measuring 179 | barEl.style.visibility = '' 180 | barEl.style.height = visible ? barHeight : 0 181 | barEl.style.paddingTop = visible ? barPadding : 0 182 | barEl.style.paddingBottom = visible ? barPadding : 0 183 | } 184 | 185 | /** 186 | * @param {Event} evt 187 | */ 188 | function removeTransition (evt) { 189 | evt.target.style.transition = '' 190 | evt.target.removeEventListener('transitionend', removeTransition) 191 | } 192 | /** 193 | * @param {HTMLElement} el 194 | * @param {object} styles 195 | */ 196 | function animate (el, styles) { 197 | el.style.transition = 'all 0.6s ease' 198 | el.addEventListener('transitionend', removeTransition) 199 | window.requestAnimationFrame(() => css(el, styles)) 200 | } 201 | 202 | /** 203 | * @param {HTMLElement} el 204 | * @param {object} styles 205 | */ 206 | function css (el, styles) { 207 | for (const prop in styles) { 208 | el.style[prop] = styles[prop] 209 | } 210 | } 211 | 212 | /** 213 | * Show the bar 214 | * @param {boolean} manual 215 | * @returns {boolean} 216 | */ 217 | function show (manual) { 218 | if (visible) { 219 | return false 220 | } 221 | 222 | const barStyles = { 223 | height: barHeight, 224 | paddingTop: barPadding, 225 | paddingBottom: barPadding 226 | } 227 | const bodyStyles = {} 228 | bodyStyles[isBottomBar ? 'paddingBottom' : 'paddingTop'] = bodyPadding 229 | if (manual) { 230 | animate(barEl, barStyles) 231 | animate(document.body, bodyStyles) 232 | cookies.erase(COOKIE_NAME) 233 | } else { 234 | css(barEl, barStyles) 235 | css(document.body, bodyStyles) 236 | } 237 | 238 | iconEl.innerHTML = config.icons.hide 239 | visible = true 240 | return true 241 | } 242 | 243 | /** 244 | * Hide the bar 245 | * 246 | * @returns {boolean} 247 | */ 248 | function hide (manual) { 249 | if (!visible) { 250 | return false 251 | } 252 | 253 | const barStyles = { 254 | height: 0, 255 | paddingBottom: 0, 256 | paddingTop: 0 257 | } 258 | const bodyStyles = {} 259 | bodyStyles[isBottomBar ? 'paddingBottom' : 'paddingTop'] = originalBodyPadding + 'px' 260 | if (manual) { 261 | animate(barEl, barStyles) 262 | animate(document.body, bodyStyles) 263 | cookies.create(COOKIE_NAME, state.success ? 'used' : 'hidden', config.cookieLength) 264 | } else { 265 | css(barEl, barStyles) 266 | css(document.body, bodyStyles) 267 | } 268 | 269 | visible = false 270 | iconEl.innerHTML = config.icons.show 271 | return true 272 | } 273 | 274 | /** 275 | * Fade out the response message 276 | */ 277 | function fadeResponse () { 278 | if (!responseEl) { 279 | return 280 | } 281 | 282 | responseEl.style.opacity = '0' 283 | window.setTimeout(() => { 284 | // remove response element so form is usable again 285 | responseEl.parentElement.removeChild(responseEl) 286 | 287 | // hide bar if sign-up was successful 288 | if (state.submitted && state.success) { 289 | hide(true) 290 | } 291 | }, 1000) 292 | } 293 | 294 | /** 295 | * Toggle visibility of the bar 296 | * 297 | * @returns {boolean} 298 | */ 299 | function toggle () { 300 | return visible ? hide(true) : show(true) 301 | } 302 | 303 | // Return values 304 | return { 305 | element: wrapperEl, 306 | toggle, 307 | show, 308 | hide 309 | } 310 | } 311 | 312 | document.addEventListener('DOMContentLoaded', function () { 313 | window.MailChimpTopBar = new Bar() 314 | }) 315 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========== 3 | 4 | ### 1.7.3 - Oct 1, 2025 5 | 6 | - Minor performance or memory usage related improvements. 7 | - Compatibility check with latest WordPress version. 8 | 9 | 10 | ### 1.7.1 - Jul 2, 2025 11 | 12 | - Update dependencies and WordPress compatibility. 13 | - Decrease timestamp check to one second ago. 14 | 15 | 16 | ### 1.7.0 - Jan 27, 2025 17 | 18 | - Bump required PHP version to 7.3 or higher. 19 | - Bump required WordPress version to 4.9 or higher. 20 | - Remove compatibility code for Mailchimp for WordPress versions before 3.0. 21 | - Add visitor IP to sign-ups through Top Bar. 22 | - Fix response not showing up after first trying with an invalid email address. 23 | - Minor performance improvements troughout the code by explicitly specifying the global namespace on core PHP functions. 24 | 25 | 26 | ### 1.6.2 - Oct 3, 2024 27 | 28 | - Fix button text setting not updating after saving settings. 29 | 30 | 31 | ### 1.6.1 - Oct 1, 2024 32 | 33 | - Escape return value of `add_query_arg` before outputting, fixing a potential XSS issue. Thanks to vgo0 for the responsible disclosure. 34 | - Escape or kses return values of all gettext calls. 35 | - Improved sanitization of all plugin settings. 36 | - Minor server side performance improvements by getting rid of some unneccessary string copies or sprintf calls. 37 | 38 | 39 | ### 1.6.0 - Jan 4, 2023 40 | 41 | - JS file now has `defer` attribute so it is not render blocking. 42 | - Stylesheet is now inserted through JS, so it is not render blocking. 43 | - Animations now entirely handled using CSS. 44 | - JS file is now 20% smaller because of the above (2.6 kB gzipped). 45 | 46 | 47 | ### 1.5.6 - Dec 1, 2022 48 | 49 | - Minor JS improvements to shrink ~500 bytes off script file. 50 | - Prepare admin tab navigation for upcoming [Mailchimp for WordPress](https://wordpress.org/plugins/mailchimp-for-wp/) release. 51 | 52 | 53 | #### 1.5.5 - May 14, 2021 54 | 55 | - Always use minified asset file, regardless of `SCRIPT_DEBUG` setting. 56 | - Add nonce to all URL's using `_mc4wp_action` parameter. 57 | 58 | 59 | #### 1.5.4 - May 7, 2021 60 | 61 | - Update classnames to work with MailChimp for WordPress version 4.8.4 (and up). 62 | - Minor JS optimizations. 63 | 64 | 65 | #### 1.5.3 - Mar 30, 2021 66 | 67 | - Fix typo in help text. 68 | - Show bar server-side to speed-up height calculation. 69 | 70 | 71 | #### 1.5.2 - Mar 9, 2020 72 | 73 | - Add setting to disable bar (stop loading it altogether) after it is used. 74 | - Increase default cookie lifetime to 1 year. 75 | 76 | 77 | #### 1.5.1 - Jan 21, 2020 78 | 79 | - Fade response element using CSS animations for better performance. 80 | - Various minor performance improvements. 81 | 82 | 83 | #### 1.5.0 - Oct 7, 2019 84 | 85 | Compatibility with [Mailchimp for WordPress](https://wordpress.org/plugins/mailchimp-for-wp/) version 4.6. 86 | 87 | 88 | #### 1.4.1 - Sep 11, 2019 89 | 90 | **Changes** 91 | 92 | - Change name to MC4WP: Mailchimp Top Bar. 93 | 94 | 95 | #### 1.4.0 - Sep 4, 2019 96 | 97 | **Improvements** 98 | 99 | - Add (advanced) setting to quickly disable the top bar on certain pages. 100 | 101 | 102 | #### 1.3.2 - Aug 8, 2018 103 | 104 | **Fixes** 105 | 106 | - Required fields notice on selected list was not showing because of invalid list property. 107 | 108 | **Improvements** 109 | 110 | - Prefix internal CSS classes for improved compatibility with other themes or plugins applying global admin styles. 111 | 112 | 113 | #### 1.3.1 - May 29, 2018 114 | 115 | **Improvements** 116 | 117 | - 30% reduction in script file size because of removed JS dependency. 118 | - Stop setting unused cookie when Top Bar form is used to subscribe. 119 | - Add mctb_after_submit_button action hook. 120 | - Improve animation performance. 121 | 122 | 123 | #### 1.3 - November 1, 2017 124 | 125 | **Improvements** 126 | 127 | - Form now submits over AJAX, no longer reloading the entire page. 128 | - Added `for` attribute to label elements, thanks [gabriel-kaam](https://github.com/gabriel-kaam). 129 | - Added `mctb_replace_interests` filter hook. 130 | 131 | #### 1.2.16 - January 19, 2017 132 | 133 | Various minor code improvements. 134 | 135 | 136 | #### 1.2.15 - September 8, 2016 137 | 138 | **Improvements** 139 | 140 | - Improved responsiveness when bar has additional input fields. 141 | - Add `required` attribute to email input. 142 | 143 | 144 | #### 1.2.14 - August 29, 2016 145 | 146 | **Fixes** 147 | 148 | - Top padding for small screens with admin bar. 149 | 150 | **Improvements** 151 | 152 | - Better bar responsiveness when window dimensions change on the fly (eg resizing a window or changing device orientation mode). (Thanks [tech4him1](https://github.com/tech4him1)!) 153 | 154 | 155 | #### 1.2.13 - August 2, 2016 156 | 157 | **Fixes** 158 | 159 | - Error in animating body padding back to its original value. 160 | 161 | 162 | #### 1.2.12 - July 21, 2016 163 | 164 | **Fixes** 165 | 166 | - Bar would crash when clicking toggle icon during bar animation. 167 | 168 | **Improvements** 169 | 170 | - Function scope generated JavaScript file to prevent Browserify clashes with other loaded scripts. 171 | - Make sure script works even though it's loaded in the head section. 172 | - Preparations for upcoming Mailchimp for WordPress v4.0 release. 173 | 174 | **Additions** 175 | 176 | - Added Spanish language files, thanks to [Ángel Guzmán Maeso](http://shakaran.net/) 177 | - Added `mctb_data` filter, to filter form data before it is processed. 178 | 179 | **Deprecated** 180 | 181 | - Deprecated `mctb_merge_vars` filter. 182 | 183 | 184 | #### 1.2.11 - July 8, 2016 185 | 186 | **Improvements** 187 | 188 | - Completely removed optional jQuery dependency. The plugin now uses JavaScript animations, resulting in a much smoother experience. 189 | 190 | #### 1.2.10 - April 12, 2016 191 | 192 | **Fixes** 193 | 194 | - Closed bar would still overlap underlying elements (like fixed top menu's). 195 | 196 | 197 | #### 1.2.9 - March 16, 2016 198 | 199 | **Fixes** 200 | 201 | Top Bar was invisible on some themes because of `z-index` being too low. 202 | 203 | 204 | #### 1.2.8 - March 15, 2016 205 | 206 | **Improvements** 207 | 208 | - Make sure top bar doesn't appear on top of WP admin bar. 209 | - Hardened CSS styles for improved theme compatability. 210 | 211 | 212 | #### 1.2.7 - January 26, 2016 213 | 214 | **Improvements** 215 | 216 | - Miscellaneous code improvements 217 | 218 | **Additions** 219 | 220 | - Add support for new [debug log](https://www.mc4wp.com/kb/how-to-enable-log-debugging/) in Mailchimp for WordPress 3.1 221 | 222 | 223 | #### 1.2.6 - January 4, 2016 224 | 225 | **Additions** 226 | 227 | - Option to "update existing subscribers" in Mailchimp, which is useful if you have added fields. 228 | 229 | **Improvements** 230 | 231 | - Toggle icon now has a background color, for increased visibility. 232 | - Toggle icon now stacks above or below bar on small screens. 233 | 234 | #### 1.2.5 - December 10, 2015 235 | 236 | The plugin now requires [Mailchimp for WordPress](https://wordpress.org/plugins/mailchimp-for-wp/) version 3.0 or higher. 237 | 238 | **Fixes** 239 | 240 | - Fixed column alignment in Appearance tab, thanks [Chantal Coolsma](https://github.com/chantalcoolsma)! 241 | 242 | **Improvements** 243 | 244 | - Improved admin notice when dependencies are not installed. 245 | 246 | 247 | #### 1.2.4 - November 22, 2015 248 | 249 | - Compatibility for [the upcoming Mailchimp for WordPress 3.0 release](https://www.mc4wp.com/blog/breaking-backwards-compatibility-in-version-3-0/) tomorrow. 250 | - Added `mctb_subscribed` filter. 251 | 252 | #### 1.2.3 - November 13, 2015 253 | 254 | **Improvements** 255 | 256 | - Minor refactoring in the way the plugin is bootstrapped. 257 | 258 | #### 1.2.2 - September 10, 2015 259 | 260 | **Fixes** 261 | 262 | - Honeypot field being auto-completed in some browsers. 263 | - Honeypot field was accessible by pressing "tab" key. 264 | - Hardened security for cookie that tracks sign-up attempts. 265 | 266 | #### 1.2.1 - September 8, 2015 267 | 268 | **Fixes** 269 | 270 | - Response message was not showing for some themes. 271 | 272 | **Improvements** 273 | 274 | - Better mobile responsiveness 275 | 276 | 277 | #### 1.2 - September 3, 2015 278 | 279 | **Improvements** 280 | 281 | - The bar will now auto-dismiss after every successful sign-up. 282 | - Placeholders will now work in Internet Explorer 7, 8 & 9 as well. 283 | 284 | **Additions** 285 | 286 | - Added options for double opt-in and sending Mailchimp's "welcome email". 287 | - Added `mctb_before_label` action allowing you to add HTML before the label-element. 288 | - Added `mctb_before_email_field` action allowing you to add HTML before the email field. 289 | - Added `mctb_before_submit_button` action allowing you to add HTML before the submit button. 290 | - Added `mctb_form_action` filter allowing you to set a custom form action. 291 | 292 | #### 1.1.3 - June 23, 2015 293 | 294 | **Fixes** 295 | 296 | - Fixes fatal error when visiting settings page on some servers 297 | 298 | #### 1.1.2 - June 18, 2015 299 | 300 | **Improvements** 301 | 302 | - Fixes height of response message 303 | - CSS improvements for compatibility with various popular themes 304 | 305 | #### 1.1.1 - June 12, 2015 306 | 307 | **Fixes** 308 | 309 | - Fixes unclickable admin bar (or fixed navigation menu's). 310 | 311 | **Improvements** 312 | 313 | - Various improvements to bar CSS so it can be easily overridden. 314 | - Fix vertical alignment of toggle icon. 315 | 316 | #### 1.1 - June 10, 2015 317 | 318 | **Improvements** 319 | 320 | - Bar no longer requires jQuery script, saving an additional HTTP request and 100kb 321 | 322 | **Additions** 323 | 324 | - Position option: top or bottom 325 | - New filter: `mctb_mailchimp_list` (set lists to subscribe to) 326 | - Lithuanian translation, thanks to [Aleksandr Charkov](https://github.com/dec0n) 327 | 328 | #### 1.0.8 - May 6, 2015 329 | 330 | **Fixes** 331 | 332 | - Compatibility with [Mailchimp for WordPress Lite v2.3](https://wordpress.org/plugins/mailchimp-for-wp/) and [Mailchimp for WordPress Pro v2.7](https://www.mc4wp.com/). 333 | 334 | #### 1.0.7 - April 15, 2015 335 | 336 | **Fixes** 337 | 338 | - `mctb_show_bar` filter was not functioning properly with some themes. 339 | - Form always errored when using WPML with String Translations. 340 | 341 | **Improvements** 342 | 343 | - Toggle icon is no longer shown for users without JavaScript. 344 | 345 | #### 1.0.6 - March 17, 2015 346 | 347 | **Fixes** 348 | 349 | - Compatibility issues with latest version of Enfold theme 350 | - Conflict with other plugins shipping _very old_ versions of Composer 351 | 352 | **Improvements** 353 | 354 | - Allow simple inline tags in the bar text 355 | 356 | 357 | #### 1.0.5 - February 25, 2015 358 | 359 | **Fixes** 360 | 361 | - Bar not loading in some themes after latest update 362 | - Colors not working because of missing leading `#` value. Color settings are now validated before saving them. 363 | 364 | #### 1.0.4 - February 23, 2015 365 | 366 | **Fixes** 367 | 368 | - Styling issues with Enfold theme. 369 | 370 | **Additions** 371 | 372 | - Settings page now uses a tabbed interface. 373 | - You can now set a "redirect url" in the bar settings 374 | - All form response messages can now be customised for the bar form 375 | 376 | #### 1.0.3 - February 17, 2015 377 | 378 | **Improvements** 379 | 380 | - Bar will now show "already subscribed" message from Mailchimp for WordPress when a person is already on the selected list. 381 | - Response message will now show and fadeout after 3 seconds. 382 | - Various usability improvements for the settings screen. 383 | - Improved spam detection. 384 | - Major JS performance improvements. 385 | 386 | **Additions** 387 | 388 | - Multiple new anti-spam measures 389 | - WPML compatibility 390 | 391 | 392 | #### 1.0.2 - February 12, 2015 393 | 394 | **Improvements** 395 | 396 | - Better CSS reset for elements inside the bar 397 | - Other minor CSS improvements 398 | 399 | **Additions** 400 | 401 | - Top Bar sign-ups are now shown in the log for [Mailchimp for WordPress Pro](https://www.mc4wp.com/). 402 | 403 | #### 1.0.1 - February 4, 2015 404 | 405 | **Fixes** 406 | 407 | - The plugin will no longer overlap header menu's or other elements 408 | 409 | **Additions** 410 | 411 | - You can now set the bar as "sticky", meaning it will stick to the op your window, even when scrolling. 412 | - You can now choose the size of the bar, small/medium/big. 413 | - Added Dutch translation files. 414 | 415 | **Improvements** 416 | 417 | - The menu item will now show above the item asking you to upgrade to Mailchimp for WordPress Pro. 418 | 419 | Please update the [Mailchimp for WordPress plugin](https://wordpress.org/plugins/mailchimp-for-wp/) before updating to this version. 420 | 421 | #### 1.0 - January 28, 2015 422 | 423 | Initial release 424 | -------------------------------------------------------------------------------- /src/Bar.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace MailChimp\TopBar; 22 | 23 | use Exception; 24 | use MC4WP_MailChimp; 25 | use MC4WP_Debug_Log; 26 | use MC4WP_MailChimp_Subscriber; 27 | use MC4WP_List_Data_Mapper; 28 | 29 | class Bar 30 | { 31 | /** 32 | * @var bool 33 | */ 34 | private $success = false; 35 | 36 | /** 37 | * @var string 38 | */ 39 | private $error_type = ''; 40 | 41 | /** 42 | * @var bool 43 | */ 44 | private $submitted = false; 45 | 46 | /** 47 | * 48 | */ 49 | public function init() 50 | { 51 | if (! $this->should_show_bar()) { 52 | return; 53 | } 54 | 55 | add_action('wp_enqueue_scripts', [ $this, 'load_assets' ]); 56 | add_action('wp_head', [ $this, 'output_css' ], 90); 57 | add_action('wp_footer', [ $this, 'output_html' ], 1); 58 | 59 | $this->listen(); 60 | } 61 | 62 | /** 63 | * Should the bar be shown? 64 | * 65 | * @return bool 66 | */ 67 | public function should_show_bar() 68 | { 69 | $options = mctb_get_options(); 70 | 71 | // don't show if bar is disabled 72 | if (! $options['enabled']) { 73 | return false; 74 | } 75 | 76 | $show_bar = true; 77 | 78 | if (! empty($options['disable_on_pages'])) { 79 | $disable_on_pages = \explode(',', $options['disable_on_pages']); 80 | $disable_on_pages = \array_map('trim', $disable_on_pages); 81 | $show_bar = ! is_page($disable_on_pages); 82 | } 83 | 84 | if ($options['disable_after_use'] && isset($_COOKIE['mctb_bar_hidden']) && $_COOKIE['mctb_bar_hidden'] === 'used') { 85 | $show_bar = false; 86 | } 87 | 88 | /** 89 | * @deprecated 1.1 90 | * @use `mctb_show_bar` 91 | */ 92 | $show_bar = apply_filters('mctp_show_bar', $show_bar); 93 | 94 | 95 | /** 96 | * @filter `mctb_show_bar` 97 | * @expects boolean 98 | * 99 | * Set to true if the bar should be loaded for this request, false if not. 100 | */ 101 | return apply_filters('mctb_show_bar', $show_bar); 102 | } 103 | 104 | /** 105 | * Listens for actions to take 106 | */ 107 | public function listen() 108 | { 109 | 110 | if (! isset($_POST['_mctb']) || $_POST['_mctb'] != 1) { 111 | return; 112 | } 113 | 114 | $options = mctb_get_options(); 115 | $this->success = $this->process(); 116 | 117 | if (! empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { 118 | $data = [ 119 | 'message' => $this->get_response_message(), 120 | 'success' => $this->success, 121 | 'redirect_url' => $this->success ? $options['redirect'] : '', 122 | ]; 123 | 124 | wp_send_json($data); 125 | exit; 126 | } 127 | 128 | if ($this->success) { 129 | // should we redirect 130 | $redirect_url = $options['redirect']; 131 | if (! empty($redirect_url)) { 132 | wp_redirect($redirect_url); 133 | exit; 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Process a form submission 140 | * @return boolean 141 | */ 142 | private function process() 143 | { 144 | $options = mctb_get_options(); 145 | $this->submitted = true; 146 | $log = $this->get_log(); 147 | 148 | /** @var MC4WP_MailChimp_Subscriber $subscriber_data */ 149 | $subscriber = null; 150 | $result = false; 151 | 152 | if (! $this->validate()) { 153 | if ($log) { 154 | $log->info(sprintf('Top Bar > Submitted with errors: %s', $this->error_type)); 155 | } 156 | 157 | return false; 158 | } 159 | 160 | /** 161 | * Filters the list to which Mailchimp Top Bar subscribes. 162 | * 163 | * @param string $list_id 164 | */ 165 | $mailchimp_list_id = apply_filters('mctb_mailchimp_list', $options['list']); 166 | 167 | // check if a Mailchimp list was given 168 | if (empty($mailchimp_list_id)) { 169 | $this->error_type = 'error'; 170 | 171 | if ($log) { 172 | $log->warning('Top Bar > No Mailchimp lists were selected'); 173 | } 174 | 175 | return false; 176 | } 177 | 178 | $email_address = sanitize_text_field($_POST['email']); 179 | $data = [ 180 | 'EMAIL' => $email_address, 181 | ]; 182 | 183 | /** 184 | * Filters the data received by Mailchimp Top Bar, before it is further processed. 185 | * 186 | * @param $data 187 | */ 188 | $data = apply_filters('mctb_data', $data); 189 | 190 | /** @ignore */ 191 | $data = apply_filters('mctb_merge_vars', $data); 192 | $email_type = apply_filters('mctb_email_type', 'html'); 193 | 194 | $replace_interests = true; 195 | 196 | /** 197 | * Filters whether interests should be replaced or appended to. 198 | * 199 | * @param bool $replace_interests 200 | */ 201 | $replace_interests = apply_filters('mctb_replace_interests', $replace_interests); 202 | $mailchimp = new MC4WP_MailChimp(); 203 | 204 | $mapper = new MC4WP_List_Data_Mapper($data, [ $mailchimp_list_id ]); 205 | $map = $mapper->map(); 206 | 207 | foreach ($map as $list_id => $subscriber) { 208 | $subscriber->email_type = $email_type; 209 | $subscriber->status = $options['double_optin'] ? 'pending' : 'subscribed'; 210 | $subscriber->ip_signup = mc4wp_get_request_ip_address(); 211 | 212 | /** @ignore (documented elsewhere) */ 213 | $subscriber = apply_filters('mc4wp_subscriber_data', $subscriber); 214 | 215 | /** 216 | * Filter subscriber data before it is sent to Mailchimp. Runs only for Mailchimp Top Bar requests. 217 | * 218 | * @param MC4WP_MailChimp_Subscriber 219 | */ 220 | $subscriber = apply_filters('mctb_subscriber_data', $subscriber); 221 | 222 | $result = $mailchimp->list_subscribe($mailchimp_list_id, $subscriber->email_address, $subscriber->to_array(), $options['update_existing'], $replace_interests); 223 | $result = is_object($result) && ! empty($result->id); 224 | } 225 | 226 | 227 | // return true if success.. 228 | if ($result) { 229 | 230 | /** 231 | * Fires for every successful sign-up using Top Bar. 232 | * 233 | * @param string $mailchimp_list_id 234 | * @param string $email 235 | * @param array $data 236 | */ 237 | do_action('mctb_subscribed', $mailchimp_list_id, $email_address, $data); 238 | 239 | // log sign-up attempt 240 | if ($log) { 241 | $log->info(sprintf('Top Bar > Successfully subscribed %s', $email_address)); 242 | } 243 | 244 | return true; 245 | } 246 | 247 | // An API error occured... Oh noes! 248 | if ($mailchimp->get_error_code() === 214) { 249 | $this->error_type = 'already_subscribed'; 250 | 251 | if ($log) { 252 | $log->warning(sprintf('Top Bar > %s is already subscribed to the selected list(s)', $email_address)); 253 | } 254 | } else { 255 | $this->error_type = 'error'; 256 | 257 | if ($log) { 258 | $log->error(sprintf('Top Bar > Mailchimp API error: %s', $mailchimp->get_error_message())); 259 | } 260 | } 261 | 262 | return false; 263 | } 264 | 265 | /** 266 | * Validate the form submission 267 | * @return boolean 268 | */ 269 | private function validate() 270 | { 271 | 272 | // make sure `email_confirm` field is given but not filled (honeypot) 273 | if (! isset($_POST['email_confirm']) || '' !== $_POST['email_confirm']) { 274 | $this->error_type = 'spam'; 275 | return false; 276 | } 277 | 278 | // make sure `_mctb_timestamp` is at least 1 seconds ago 279 | if (empty($_POST['_mctb_timestamp']) || time() < ( intval($_POST['_mctb_timestamp']) + 1 )) { 280 | $this->error_type = 'spam'; 281 | return false; 282 | } 283 | 284 | // don't work for users without JavaScript (since bar is hidden anyway, must be a bot) 285 | if (isset($_POST['_mctb_no_js'])) { 286 | $this->error_type = 'spam'; 287 | return false; 288 | } 289 | 290 | // simple user agent check 291 | $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; 292 | if (strlen($user_agent) < 2) { 293 | $this->error_type = 'spam'; 294 | return false; 295 | } 296 | 297 | // check if email is given and valid 298 | if (empty($_POST['email']) || ! is_string($_POST['email']) || ! is_email($_POST['email'])) { 299 | $this->error_type = 'invalid_email'; 300 | return false; 301 | } 302 | 303 | return apply_filters('mctb_validate', true); 304 | } 305 | 306 | /** 307 | * Loads the required scripts & styles 308 | */ 309 | public function load_assets() 310 | { 311 | $options = mctb_get_options(); 312 | wp_enqueue_script('mailchimp-top-bar', $this->asset_url("/script.js"), [], MAILCHIMP_TOP_BAR_VERSION, true); 313 | add_filter('script_loader_tag', [ $this, 'add_defer_attribute' ], 10, 2); 314 | $bottom = $options['position'] === 'bottom'; 315 | 316 | $data = [ 317 | 'cookieLength' => $options['cookie_length'], 318 | 'icons' => [ 319 | 'hide' => ( $bottom ) ? '▼' : '▲', 320 | 'show' => ( $bottom ) ? '▲' : '▼' 321 | ], 322 | 'position' => $options['position'], 323 | 'state' => [ 324 | 'submitted' => $this->submitted, 325 | 'success' => $this->success, 326 | ], 327 | ]; 328 | 329 | /** 330 | * @filter `mctb_bar_config` 331 | * @expects array 332 | * 333 | * Can be used to filter the following values 334 | * - cookieLength: The length of the cookie 335 | * - icons: Array with `hide` and `show` keys. Holds the hide/show icon strings. 336 | */ 337 | $data = apply_filters('mctb_bar_config', $data); 338 | 339 | wp_localize_script('mailchimp-top-bar', 'mctb', $data); 340 | } 341 | 342 | /** 343 | * Adds defer attribute to our