├── .distignore ├── .editorconfig ├── .eslintignore ├── .github ├── dependabot.yml └── workflows │ ├── linting.yml │ ├── phpunit.yml │ ├── wordpress-org-asset-update.yml │ └── wordpress-org-deploy.yml ├── .gitignore ├── .npmrc ├── .stylelintignore ├── .wordpress-org ├── banner-1544x500.png ├── banner-772x250.png ├── blueprints │ └── blueprint.json ├── icon.svg ├── screenshot-1.png └── screenshot-2.png ├── CHANGELOG.md ├── bin └── install-wp-tests.sh ├── composer.json ├── css ├── digest.css └── src │ └── digest.css ├── digest.php ├── inc ├── Cron.php ├── Event │ └── Registry.php ├── Message │ ├── CommentModeration.php │ ├── CommentNotification.php │ ├── CoreUpdate.php │ ├── MessageInterface.php │ ├── PasswordChangeNotification.php │ ├── Section.php │ └── UserNotification.php ├── Plugin.php ├── Queue.php ├── Setting │ ├── FrequencySetting.php │ └── SettingInterface.php └── namespace.php ├── js ├── digest.js └── src │ └── digest.js ├── license.txt ├── package.json ├── phpcs.xml.dist ├── phpunit.xml ├── postcss.config.js ├── readme.md ├── template-tags ├── functions.php └── pluggable.php ├── tests └── phpunit │ ├── bootstrap.php │ └── tests │ ├── CommentModeration.php │ ├── CommentNotification.php │ ├── CoreUpdate.php │ ├── Cron.php │ ├── Digest.php │ ├── PasswordChangedNotification.php │ ├── Plugin.php │ ├── Queue.php │ ├── Registry.php │ ├── Settings.php │ └── UserNotification.php ├── uninstall.php └── webpack.config.js /.distignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .wordpress-org 4 | .distignore 5 | .editorconfig 6 | .eslintignore 7 | .gitignore 8 | .npmrc 9 | .stylelintignore 10 | composer.json 11 | composer.lock 12 | package.json 13 | package-lock.json 14 | yarn.lock 15 | phpunit.xml 16 | phpcs.xml.dist 17 | postcss.config.js 18 | webpack.config.js 19 | 20 | bin 21 | node_modules 22 | tests 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [{*.json,*.yml}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /**/node_modules/* 2 | /vendor 3 | js/* 4 | !js/src 5 | !.*.js 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | linters: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out Git repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: "7.4" 21 | coverage: none 22 | tools: composer 23 | 24 | - name: Install PHP dependencies 25 | uses: ramsey/composer-install@v2 26 | 27 | - name: Make Composer packages available globally 28 | run: echo "${PWD}/vendor/bin" >> $GITHUB_PATH 29 | 30 | - name: Run linters 31 | uses: wearerequired/lint-action@v2 32 | with: 33 | php_codesniffer: true 34 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | phpunit: 11 | name: PHPUnit (PHP ${{ matrix.php }} with WordPress ${{ matrix.wordpress }}) 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | mysql: 16 | image: mysql:5.7 17 | ports: 18 | - 3306/tcp 19 | env: 20 | MYSQL_ROOT_PASSWORD: password 21 | # Set health checks to wait until mysql has started 22 | options: >- 23 | --health-cmd "mysqladmin ping" 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 3 27 | 28 | continue-on-error: ${{ matrix.experimental }} 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | php: ['8.0','7.4'] 33 | wordpress: ['latest'] 34 | experimental: [false] 35 | include: 36 | - php: '8.1' 37 | wordpress: 'trunk' 38 | experimental: true 39 | 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Setup PHP 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: ${{ matrix.php }} 48 | coverage: none 49 | extensions: mysqli 50 | tools: composer, cs2pr 51 | 52 | - name: Install WordPress test setup 53 | env: 54 | WP_VERSION: ${{ matrix.wordpress }} 55 | MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} 56 | run: bash bin/install-wp-tests.sh wordpress_test root password "127.0.0.1:$MYSQL_PORT" "$WP_VERSION" 57 | 58 | - name: Install PHP dependencies 59 | uses: ramsey/composer-install@v2 60 | 61 | - name: Setup problem matchers for PHPUnit 62 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 63 | 64 | - name: Run tests 65 | run: composer run test 66 | -------------------------------------------------------------------------------- /.github/workflows/wordpress-org-asset-update.yml: -------------------------------------------------------------------------------- 1 | name: Plugin asset/readme update 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'readme.md' 9 | - '.wordpress-org/**' 10 | 11 | jobs: 12 | update: 13 | name: Update 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@v3 19 | 20 | - name: WordPress.org plugin asset/readme update 21 | uses: 10up/action-wordpress-plugin-asset-update@stable 22 | env: 23 | README_NAME: 'readme.md' 24 | IGNORE_OTHER_FILES: 'true' 25 | SVN_USERNAME: ${{ secrets.WORDPRESS_ORG_SVN_USERNAME }} 26 | SVN_PASSWORD: ${{ secrets.WORDPRESS_ORG_SVN_PASSWORD }} 27 | -------------------------------------------------------------------------------- /.github/workflows/wordpress-org-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress.org 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | new-release: 9 | name: New release 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out Git repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: "7.4" 20 | coverage: none 21 | tools: composer 22 | 23 | - name: Install dependencies 24 | run: | 25 | composer install --no-progress --no-dev --no-ansi --prefer-dist --no-interaction --no-scripts --optimize-autoloader 26 | 27 | - name: WordPress Plugin Deploy 28 | uses: 10up/action-wordpress-plugin-deploy@stable 29 | env: 30 | SVN_USERNAME: ${{ secrets.WORDPRESS_ORG_SVN_USERNAME }} 31 | SVN_PASSWORD: ${{ secrets.WORDPRESS_ORG_SVN_PASSWORD }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | /node_modules 3 | /package-lock.json 4 | 5 | # Vendor (e.g. Composer) 6 | vendor/* 7 | !vendor/.gitkeep 8 | /composer.lock 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /**/node_modules/* 2 | /vendor/* 3 | *.* 4 | LICENSE 5 | !css/src/*.css 6 | -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wearerequired/digest/375cde0fb4fa9977baae2e55864e5f51df5dc1f8/.wordpress-org/banner-1544x500.png -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wearerequired/digest/375cde0fb4fa9977baae2e55864e5f51df5dc1f8/.wordpress-org/banner-772x250.png -------------------------------------------------------------------------------- /.wordpress-org/blueprints/blueprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/options-general.php#digest", 4 | "preferredVersions": { 5 | "php": "8.1", 6 | "wp": "latest" 7 | }, 8 | "features": { 9 | "networking": true 10 | }, 11 | "steps": [ 12 | { 13 | "step": "login" 14 | }, 15 | { 16 | "step": "installPlugin", 17 | "pluginData": { 18 | "resource": "wordpress.org/plugins", 19 | "slug": "digest" 20 | }, 21 | "options": { 22 | "activate": true 23 | } 24 | }, 25 | { 26 | "step": "setSiteOptions", 27 | "options": { 28 | "blogname": "Digest Notifications Demo" 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.wordpress-org/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.wordpress-org/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wearerequired/digest/375cde0fb4fa9977baae2e55864e5f51df5dc1f8/.wordpress-org/screenshot-1.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wearerequired/digest/375cde0fb4fa9977baae2e55864e5f51df5dc1f8/.wordpress-org/screenshot-2.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 3.0.0 - 2023-03-27 ### 2 | * Added: Support for sending digest on first day of a month 3 | * Changed: Bump minimum requirements to WordPress 6.0 and PHP 7.4 4 | 5 | ### 2.0.0 - 2021-06-02 ### 6 | * Changed: Plugin rewrite 7 | * Changed: Tested with WordPress 5.7 8 | * Changed: Bump minimum requirements to WordPress 4.7 and PHP 5.6 9 | 10 | ### 1.2.1 - 2015-10-06 ### 11 | * Fixed: A small issue with password notifications emails in WordPress 4.3.1. The behaviour changed slightly in that release. 12 | 13 | ### 1.2.0 - 2015-09-02 ### 14 | * Fixed: Supports the new password notification emails introduced in WordPress 4.3 15 | * Enhancement: Began writing user tests to ensure compatibility. 16 | 17 | ### 1.1.0 - 2015-06-30 ### 18 | * Enhancement: Don’t include already processed items in the email. 19 | 20 | ### 1.0.0 - 2015-06-25 ### 21 | * First release 22 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-nightly 66 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 67 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 68 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 111 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 112 | fi 113 | 114 | if [ ! -f wp-tests-config.php ]; then 115 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 116 | # remove all forward slashes in the end 117 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 118 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 119 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 123 | fi 124 | 125 | } 126 | 127 | install_db() { 128 | 129 | if [ ${SKIP_DB_CREATE} = "true" ]; then 130 | return 0 131 | fi 132 | 133 | # parse DB_HOST for port or socket references 134 | local PARTS=(${DB_HOST//\:/ }) 135 | local DB_HOSTNAME=${PARTS[0]}; 136 | local DB_SOCK_OR_PORT=${PARTS[1]}; 137 | local EXTRA="" 138 | 139 | if ! [ -z $DB_HOSTNAME ] ; then 140 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 141 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 142 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 143 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 144 | elif ! [ -z $DB_HOSTNAME ] ; then 145 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 146 | fi 147 | fi 148 | 149 | # create database 150 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 151 | } 152 | 153 | install_wp 154 | install_test_suite 155 | install_db 156 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wearerequired/digest", 3 | "description": "Get a daily/weekly/monthly digest of what's happening on your site instead of receiving a single email each time.", 4 | "license": "GPL-2.0-or-later", 5 | "type": "wordpress-plugin", 6 | "authors": [ 7 | { 8 | "name": "required", 9 | "email": "info@required.ch", 10 | "homepage": "https://required.com", 11 | "role": "Company" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.4", 16 | "wearerequired/wp-requirements-check": "^1.0" 17 | }, 18 | "require-dev": { 19 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", 20 | "wearerequired/coding-standards": "^5.0", 21 | "yoast/phpunit-polyfills": "^1.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Required\\Digest\\": "inc" 26 | } 27 | }, 28 | "config": { 29 | "allow-plugins": { 30 | "dealerdirect/phpcodesniffer-composer-installer": true 31 | }, 32 | "platform-check": false, 33 | "sort-packages": true 34 | }, 35 | "scripts": { 36 | "format": "vendor/bin/phpcbf --report-summary --report-source .", 37 | "lint": "vendor/bin/phpcs --report-summary --report-source .", 38 | "test": "@php ./vendor/bin/phpunit" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /css/digest.css: -------------------------------------------------------------------------------- 1 | #digest-frequency-day-wrapper{opacity:1;transition:opacity .3s ease-in-out}#digest-frequency-day-wrapper.digest-hidden{opacity:0} -------------------------------------------------------------------------------- /css/src/digest.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Digest Notifications 3 | * https://github.com/wearerequired/digest/ 4 | * 5 | * Copyright (c) 2015-2017 required 6 | * Licensed under the GPLv2+ license. 7 | */ 8 | 9 | #digest-frequency-day-wrapper { 10 | opacity: 1; 11 | transition: opacity .3s ease-in-out; 12 | 13 | &.digest-hidden { 14 | opacity: 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /digest.php: -------------------------------------------------------------------------------- 1 | __( 'Digest Notifications', 'digest' ), 56 | 'php' => '7.4', 57 | 'wp' => '6.0', 58 | 'file' => __FILE__, 59 | ) 60 | ); 61 | 62 | if ( $requirements_check->passes() ) { 63 | require_once dirname( __FILE__ ) . '/template-tags/pluggable.php'; 64 | require_once dirname( __FILE__ ) . '/template-tags/functions.php'; 65 | require_once dirname( __FILE__ ) . '/inc/namespace.php'; 66 | 67 | add_action( 'plugins_loaded', 'Required\\Digest\\bootstrap' ); 68 | } 69 | 70 | unset( $requirements_check ); 71 | -------------------------------------------------------------------------------- /inc/Cron.php: -------------------------------------------------------------------------------- 1 | 'weekly', 35 | 'hour' => 18, 36 | 'day' => absint( get_option( 'start_of_week' ) ), 37 | ] 38 | ); 39 | 40 | self::ready() && self::run(); 41 | } 42 | 43 | /** 44 | * Checks if it's already time to send the emails. 45 | * 46 | * @since 1.0.0 47 | * 48 | * @return bool True if the queue can be processed, false otherwise. 49 | */ 50 | protected static function ready() { 51 | // Return early if the hour is wrong. 52 | if ( absint( self::$options['hour'] ) !== absint( wp_date( 'G' ) ) ) { 53 | return false; 54 | } 55 | 56 | // Return early if the day is wrong. 57 | if ( 58 | 'weekly' === self::$options['period'] && 59 | absint( self::$options['day'] ) !== absint( wp_date( 'w' ) ) 60 | ) { 61 | return false; 62 | } 63 | 64 | // Return early if it's not the first day of the month. 65 | if ( 66 | 'monthly' === self::$options['period'] && 67 | wp_date( 'Y-m-d' ) !== wp_date( 'Y-m-01' ) 68 | ) { 69 | return false; 70 | } 71 | 72 | return true; 73 | } 74 | 75 | /** 76 | * Processes the queue and sends the emails. 77 | * 78 | * Run Boy Run. 79 | * 80 | * @since 1.0.0 81 | */ 82 | protected static function run() { 83 | $queue = Queue::get(); 84 | 85 | if ( empty( $queue ) ) { 86 | return; 87 | } 88 | 89 | // Set up the correct subject. 90 | // translators: %s: Site name. 91 | $subject = __( 'Past Week on %s', 'digest' ); 92 | if ( 'daily' === self::$options['period'] ) { 93 | // translators: %s: Site name. 94 | $subject = __( 'Today on %s', 'digest' ); 95 | } elseif ( 'monthly' === self::$options['period'] ) { 96 | // translators: %s: Site name. 97 | $subject = __( 'Past Month on %s', 'digest' ); 98 | } 99 | 100 | /** 101 | * Filter the digest subject. 102 | * 103 | * @param string $subject The digest's subject line. 104 | * @return string The filtered subject. 105 | */ 106 | $subject = apply_filters( 'digest_cron_email_subject', sprintf( $subject, get_bloginfo( 'name' ) ) ); 107 | 108 | send_email( $subject ); 109 | 110 | // Clear queue. 111 | Queue::clear(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /inc/Event/Registry.php: -------------------------------------------------------------------------------- 1 | entries ) - \count( array_filter( $this->entries ) ); 27 | 28 | $message = '

' . __( 'Pending Comments', 'digest' ) . '

'; 29 | $message .= '

'; 30 | $message .= sprintf( 31 | // translators: %s: Number of comments. 32 | _n( 33 | 'There is %s new comment waiting for approval.', 34 | 'There are %s new comments waiting for approval.', 35 | \count( $this->entries ), 36 | 'digest' 37 | ), 38 | number_format_i18n( \count( $this->entries ) ) 39 | ); 40 | if ( $processed_count > 0 ) { 41 | $message .= ' '; 42 | $message .= sprintf( 43 | // translators: %s: Number of comments. 44 | _n( 45 | '%s comment was already moderated.', 46 | '%s comments were already moderated.', 47 | $processed_count, 48 | 'digest' 49 | ), 50 | number_format_i18n( $processed_count ) 51 | ); 52 | } 53 | $message .= '

'; 54 | 55 | $message .= implode( '', $this->entries ); 56 | $message .= sprintf( 57 | // translators: %s: URL for moderation page. 58 | '

' . __( 'Please visit the moderation panel.', 'digest' ) . '

', 59 | admin_url( 'edit-comments.php?comment_status=moderated' ) 60 | ); 61 | 62 | return $message; 63 | } 64 | 65 | /** 66 | * Returns the comment moderation message. 67 | * 68 | * @since 2.0.0 69 | * 70 | * @param int $comment The comment ID. 71 | * @param int $time The timestamp when the comment was written. 72 | * @return string The comment moderation message. 73 | */ 74 | protected function get_single_message( $comment, $time ) { 75 | /* @var WP_Comment $comment */ 76 | $comment = get_comment( $comment ); 77 | 78 | if ( null === $comment || '0' !== $comment->comment_approved ) { 79 | return ''; 80 | } 81 | 82 | $message = $this->get_single_comment_content( $comment, $time ); 83 | 84 | $actions = [ 85 | 'view' => __( 'Permalink', 'digest' ), 86 | ]; 87 | 88 | if ( $this->user_can_edit_comment( $comment->comment_ID ) ) { 89 | $actions['approve'] = __( 'Approve', 'digest' ); 90 | 91 | if ( \defined( 'EMPTY_TRASH_DAYS' ) && EMPTY_TRASH_DAYS ) { 92 | $actions['trash'] = _x( 'Trash', 'verb', 'digest' ); 93 | } else { 94 | $actions['delete'] = __( 'Delete', 'digest' ); 95 | } 96 | $actions['spam'] = _x( 'Spam', 'verb', 'digest' ); 97 | } 98 | 99 | if ( ! empty( $actions ) ) { 100 | $message .= '

' . $this->get_comment_action_links( $actions, $comment ) . '

'; 101 | } 102 | 103 | return $message; 104 | } 105 | 106 | /** 107 | * Returns the message for a single comment. 108 | * 109 | * @since 2.0.0 110 | * 111 | * @param \WP_Comment $comment The comment object. 112 | * @param int $time The timestamp when the comment was written. 113 | * @return string The comment message. 114 | */ 115 | protected function get_single_comment_content( WP_Comment $comment, $time ) { 116 | $post_link = '' . get_the_title( $comment->comment_post_ID ) . ''; 117 | 118 | $message = ''; 119 | 120 | switch ( $comment->comment_type ) { 121 | case 'trackback': 122 | case 'pingback': 123 | if ( 'pingback' === $comment->comment_type ) { 124 | // translators: 1: Post name, 2: Humman time diff. 125 | $message .= sprintf( __( 'Pingback on %1$s %2$s ago:', 'digest' ), $post_link, human_time_diff( $time, time() ) ) . '
'; 126 | } else { 127 | // translators: 1: Post name, 2: Humman time diff. 128 | $message .= sprintf( __( 'Trackback on %1$s %2$s ago:', 'digest' ), $post_link, human_time_diff( $time, time() ) ) . '
'; 129 | } 130 | // translators: %s: Website link. 131 | $message .= sprintf( __( 'Website: %s', 'digest' ), '' . esc_html( $comment->comment_author ) . '' ) . '
'; 132 | // translators: %s: Comment text. 133 | $message .= sprintf( __( 'Excerpt: %s', 'digest' ), '
' . $this->get_comment_text( $comment->comment_ID ) ); 134 | break; 135 | default: // Comments. 136 | // translators: %s: Comment author. 137 | $author = sprintf( __( 'Author: %s', 'digest' ), esc_html( $comment->comment_author ) ); 138 | if ( $comment->comment_author_url ) { 139 | // translators: %s: Comment author. 140 | $author = sprintf( __( 'Author: %s', 'digest' ), '' . esc_html( $comment->comment_author ) . '' ); 141 | } 142 | // translators: 1: Post name, 2: Humman time diff. 143 | $message = sprintf( __( 'Comment on %1$s %2$s ago:', 'digest' ), $post_link, human_time_diff( $time, time() ) ) . '
'; 144 | $message .= $author . '
'; 145 | if ( $comment->comment_author_email ) { 146 | // translators: %s: Comment author email address. 147 | $message .= sprintf( __( 'Email: %s', 'digest' ), '' . esc_html( $comment->comment_author_email ) . '' ) . '
'; 148 | } 149 | // translators: %s: Comment. 150 | $message .= sprintf( __( 'Comment: %s', 'digest' ), '
' . $this->get_comment_text( $comment->comment_ID ) ); 151 | break; 152 | } 153 | 154 | return $message; 155 | } 156 | 157 | /** 158 | * Returns the comment text. 159 | * 160 | * It is already filtered by WordPress. 161 | * 162 | * @since 2.0.0 163 | * 164 | * @param int $comment_id The comment ID. 165 | * @return string The filtered comment text 166 | */ 167 | protected function get_comment_text( $comment_id ) { 168 | $comment = get_comment( $comment_id ); 169 | 170 | $comment_text = get_comment_text( $comment ); 171 | 172 | /** This filter is documented in wp-includes/comment-template.php */ 173 | return apply_filters( 'comment_text', $comment_text, $comment, [] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 174 | } 175 | 176 | /** 177 | * Adds action links to the message. 178 | * 179 | * @since 2.0.0 180 | * 181 | * @param array $actions Actions for that comment. 182 | * @param \WP_Comment $comment The comment object. 183 | * @return string The comment action links. 184 | */ 185 | protected function get_comment_action_links( array $actions, WP_Comment $comment ) { 186 | $links = []; 187 | 188 | foreach ( $actions as $action => $label ) { 189 | $url = admin_url( sprintf( 'comment.php?action=%s&c=%d', $action, $comment->comment_ID ) ); 190 | 191 | if ( 'view' === $action ) { 192 | $url = get_comment_link( $comment ); 193 | } 194 | 195 | $links[] = sprintf( 196 | '%2$s', 197 | esc_url( $url ), 198 | esc_html( $label ) 199 | ); 200 | } 201 | 202 | return implode( ' | ', $links ); 203 | } 204 | 205 | /** 206 | * Whether the current user can edit a given comment or not. 207 | * 208 | * @since 2.0.0 209 | * 210 | * @param int $comment_id Comment ID. 211 | * @return bool True if the user can edit the comment, false otherwise. 212 | */ 213 | protected function user_can_edit_comment( $comment_id ) { 214 | return $this->user && ( user_can( $this->user, 'edit_comment', $comment_id ) || get_option( 'admin_email' ) === $this->user->user_email ); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /inc/Message/CommentNotification.php: -------------------------------------------------------------------------------- 1 | entries ) - \count( array_filter( $this->entries ) ); 21 | 22 | $message = '

' . __( 'New Comments', 'digest' ) . '

'; 23 | $message .= '

'; 24 | $message .= sprintf( 25 | // translators: %s: Number of comments. 26 | _n( 27 | 'There was %s new comment.', 28 | 'There were %s new comments.', 29 | \count( $this->entries ), 30 | 'digest' 31 | ), 32 | number_format_i18n( \count( $this->entries ) ) 33 | ); 34 | if ( 0 < $processed_count ) { 35 | $message .= ' '; 36 | $message .= sprintf( 37 | // translators: %s: Number of comments. 38 | _n( 39 | '%s comment was already moderated.', 40 | '%s comments were already moderated.', 41 | $processed_count, 42 | 'digest' 43 | ), 44 | number_format_i18n( $processed_count ) 45 | ); 46 | } 47 | $message .= '

'; 48 | $message .= implode( '', $this->entries ); 49 | $message .= sprintf( 50 | // translators: %s: URL for moderation page. 51 | '

' . __( 'Please visit the moderation panel.', 'digest' ) . '

', 52 | admin_url( 'edit-comments.php?comment_status=moderated' ) 53 | ); 54 | 55 | return $message; 56 | } 57 | 58 | /** 59 | * Get the comment moderation message. 60 | * 61 | * @param int $comment The comment ID. 62 | * @param int $time The timestamp when the comment was written. 63 | * @return string The comment moderation message. 64 | */ 65 | protected function get_single_message( $comment, $time ) { 66 | /* @var WP_Comment $comment */ 67 | $comment = get_comment( $comment ); 68 | 69 | if ( null === $comment ) { 70 | return ''; 71 | } 72 | 73 | $message = $this->get_single_comment_content( $comment, $time ); 74 | 75 | $actions = [ 76 | 'view' => __( 'Permalink', 'digest' ), 77 | ]; 78 | 79 | if ( $this->user_can_edit_comment( $comment->comment_ID ) ) { 80 | if ( \defined( 'EMPTY_TRASH_DAYS' ) && EMPTY_TRASH_DAYS ) { 81 | $actions['trash'] = _x( 'Trash', 'verb', 'digest' ); 82 | } else { 83 | $actions['delete'] = __( 'Delete', 'digest' ); 84 | } 85 | $actions['spam'] = _x( 'Spam', 'verb', 'digest' ); 86 | } 87 | 88 | if ( ! empty( $actions ) ) { 89 | $message .= '

' . $this->get_comment_action_links( $actions, $comment ) . '

'; 90 | } 91 | 92 | return $message; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /inc/Message/CoreUpdate.php: -------------------------------------------------------------------------------- 1 | event = $event; 34 | 35 | parent::__construct( $entries, $user ); 36 | } 37 | 38 | /** 39 | * Get core update section message. 40 | * 41 | * @return string The section message. 42 | */ 43 | public function get_message() { 44 | return implode( '', $this->entries ); 45 | } 46 | 47 | /** 48 | * Get the single core update/failure message. 49 | * 50 | * @param string $version The version WordPress was updated to. 51 | * @param int $time The timestamp when the update happened. 52 | * @return string The core update message. 53 | */ 54 | protected function get_single_message( $version, $time ) { 55 | if ( 'core_update_success' === $this->event ) { 56 | return $this->get_core_update_success_message( $version, $time ); 57 | } 58 | 59 | return $this->get_core_update_fail_message( $version ); 60 | } 61 | 62 | /** 63 | * Get the message for a successful core update. 64 | * 65 | * @param string $version The version WordPress was updated to. 66 | * @param int $time The timestamp when the update happened. 67 | * @return string The core update message. 68 | */ 69 | protected function get_core_update_success_message( $version, $time ) { 70 | $message = sprintf( 71 | // translators: 1: Site name, 2: WP Version, 3: Humman time diff. 72 | '

' . __( 'Your site at %1$s has been updated automatically to WordPress %2$s %3$s ago.', 'digest' ) . '

', 73 | sprintf( 74 | '%2$s', 75 | esc_url( home_url() ), 76 | esc_html( str_replace( [ 'http://', 'https://' ], '', home_url() ) ) 77 | ), 78 | esc_html( $version ), 79 | human_time_diff( $time, time() ) 80 | ); 81 | 82 | // Can only reference the About screen if their update was successful. 83 | list( $about_version ) = explode( '-', $version, 2 ); 84 | 85 | $message .= sprintf( 86 | // translators: 1: WP Version, 2: Link to about page. 87 | '

' . __( 'For more on version %1$s, see the About WordPress screen.', 'digest' ) . '

', 88 | esc_html( $about_version ), 89 | esc_url( admin_url( 'about.php' ) ) 90 | ); 91 | 92 | return $message; 93 | } 94 | 95 | /** 96 | * Get the message for a failed core update. 97 | * 98 | * @param string $version The version WordPress was updated to. 99 | * @return string The core update message. 100 | */ 101 | protected function get_core_update_fail_message( $version ) { 102 | global $wp_version; 103 | 104 | // Check if WordPress hasn't already been updated. 105 | if ( version_compare( $wp_version, $version, '>=' ) ) { 106 | return ''; 107 | } 108 | 109 | $message = sprintf( 110 | // translators: 1: Site name, 2: WP Version. 111 | '

' . __( 'Please update your site at %1$s to WordPress %2$s. Updating is easy and only takes a few moments.', 'digest' ) . '

', 112 | sprintf( 113 | '%2$s', 114 | esc_url( home_url() ), 115 | esc_html( str_replace( [ 'http://', 'https://' ], '', home_url() ) ) 116 | ), 117 | esc_html( $version ) 118 | ); 119 | 120 | $message .= '

' . sprintf( '%s', network_admin_url( 'update-core.php' ), __( 'Update now', 'digest' ) ) . '

'; 121 | 122 | return $message; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /inc/Message/MessageInterface.php: -------------------------------------------------------------------------------- 1 | entries = array_filter( $this->entries ); 25 | 26 | if ( empty( $this->entries ) ) { 27 | return ''; 28 | } 29 | 30 | $message = '

' . __( 'Password Changes', 'digest' ) . '

'; 31 | if ( 1 === \count( $this->entries ) ) { 32 | $message .= '

' . __( 'The following user lost and changed his password:', 'digest' ) . '

'; 33 | } else { 34 | $message .= '

' . __( 'The following users lost and changed their passwords:', 'digest' ) . '

'; 35 | } 36 | $message .= '
    ' . implode( '', $this->entries ) . '
'; 37 | 38 | return $message; 39 | } 40 | 41 | /** 42 | * Returns the password change notification message. 43 | * 44 | * @since 2.0.0 45 | * 46 | * @param int $user_id The user ID. 47 | * @param int $time The timestamp when the user changed his password. 48 | * @return string The password change notification message. 49 | */ 50 | protected function get_single_message( $user_id, $time ) { 51 | $user = get_user_by( 'ID', $user_id ); 52 | 53 | if ( ! $user ) { 54 | return ''; 55 | } 56 | 57 | return sprintf( 58 | /* translators: 1: user display name, 2: user ID, 3: human time dif */ 59 | '
  • ' . __( '%1$s (ID: %2$d) %3$s ago', 'digest' ) . '
  • ', 60 | esc_html( $user->display_name ), 61 | absint( $user->ID ), 62 | human_time_diff( $time, time() ) 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /inc/Message/Section.php: -------------------------------------------------------------------------------- 1 | user = $user; 66 | 67 | foreach ( $entries as $entry => $time ) { 68 | $this->entries[] = $this->get_single_message( $entry, $time ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /inc/Message/UserNotification.php: -------------------------------------------------------------------------------- 1 | entries ); 25 | } 26 | 27 | /** 28 | * Returns the new user notification message. 29 | * 30 | * @since 2.0.0 31 | * 32 | * @param int $user_id The user ID. 33 | * @param int $time The timestamp when the user signed up. 34 | * @return string The new user notification message. 35 | */ 36 | protected function get_single_message( $user_id, $time ) { 37 | $user = get_user_by( 'ID', $user_id ); 38 | 39 | if ( ! $user ) { 40 | return ''; 41 | } 42 | 43 | return sprintf( 44 | /* translators: 1: user display name, 2: user ID, 3: human time dif */ 45 | '
  • ' . __( '%1$s (ID: %2$d) %3$s ago', 'digest' ) . '
  • ', 46 | $user->display_name, 47 | $user->ID, 48 | human_time_diff( $time, time() ) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /inc/Plugin.php: -------------------------------------------------------------------------------- 1 | user = $user; 40 | } 41 | 42 | $this->events = $this->process_event_items( $items ); 43 | } 44 | 45 | /** 46 | * Process all queue items and generate the according messages. 47 | * 48 | * @param array $items The queue items. 49 | * @return array The processed event messages. 50 | */ 51 | protected function process_event_items( $items ) { 52 | $events = []; 53 | 54 | foreach ( $items as $item ) { 55 | $events[ $item[1] ][ $item[2] ] = $item[0]; 56 | } 57 | 58 | return $events; 59 | } 60 | 61 | /** 62 | * Process the queue for a single recipient. 63 | * 64 | * @return string The generated message. 65 | */ 66 | public function get_message() { 67 | $message = ''; 68 | 69 | // Loop through the processed events. 70 | foreach ( digest_get_registered_events() as $event ) { 71 | if ( ! isset( $this->events[ $event ] ) ) { 72 | continue; 73 | } 74 | 75 | /** 76 | * Filter the message section 77 | * 78 | * @param string $message The message. 79 | * @param array $entries The event items. 80 | * @param \WP_User $user The current user. 81 | * @param string $event The current event. 82 | */ 83 | $message .= apply_filters( 'digest_message_section_' . $event, '', $this->events[ $event ], $this->user, $event ); 84 | } 85 | 86 | if ( '' === $message ) { 87 | return ''; 88 | } 89 | 90 | // translators: %s: Display Name. 91 | $salutation = $this->user ? sprintf( __( 'Hi %s', 'digest' ), $this->user->display_name ) : __( 'Hi there', 'digest' ); 92 | $valediction = '

    ' . __( "That's it, have a nice day!", 'digest' ) . '

    '; 93 | $salutation = '

    ' . $salutation . '

    ' . __( "See what's happening on your site:", 'digest' ) . '

    '; 94 | 95 | return $salutation . $message . $valediction; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /inc/Queue.php: -------------------------------------------------------------------------------- 1 | General. 45 | * 46 | * @since 2.0.0 47 | */ 48 | public function add_settings_fields() { 49 | add_settings_section( 50 | 'digest_notifications', 51 | __( 'Email Notifications', 'digest' ), 52 | function () { 53 | esc_html_e( "You get a daily, weekly, or monthly digest of what's happening on your site. Here you can configure its frequency.", 'digest' ); 54 | }, 55 | 'general' 56 | ); 57 | 58 | add_settings_field( 59 | 'digest_frequency', 60 | sprintf( '', __( 'Frequency', 'digest' ) ), 61 | [ $this, 'settings_field_frequency' ], 62 | 'general', 63 | 'digest_notifications' 64 | ); 65 | } 66 | 67 | /** 68 | * Settings field callback that prints the actual input fields. 69 | * 70 | * @since 2.0.0 71 | */ 72 | public function settings_field_frequency() { 73 | $options = get_option( 74 | 'digest_frequency', 75 | [ 76 | 'period' => 'weekly', 77 | 'hour' => 18, 78 | 'day' => absint( get_option( 'start_of_week' ) ), 79 | ] 80 | ); 81 | $time_format = get_option( 'time_format' ); 82 | ?> 83 |

    84 | 85 | 96 | 97 | 98 | 105 | 106 | 107 | > 108 | 113 | 120 | 121 |

    122 | [ 150 | 'default' => 18, 151 | 'min_range' => 0, 152 | 'max_range' => 23, 153 | ], 154 | ] 155 | ); 156 | 157 | $new_value['day'] = filter_var( 158 | $new_value['day'], 159 | FILTER_VALIDATE_INT, 160 | [ 161 | 'options' => [ 162 | 'default' => get_option( 'start_of_week', 0 ), 163 | 'min_range' => 0, 164 | 'max_range' => 6, 165 | ], 166 | ] 167 | ); 168 | 169 | return $new_value; 170 | } 171 | 172 | /** 173 | * Enqueue scripts and styles. 174 | * 175 | * @since 2.0.0 176 | * 177 | * @param string $hook_suffix The current admin page. 178 | */ 179 | public function admin_enqueue_scripts( $hook_suffix ) { 180 | if ( 'options-general.php' === $hook_suffix ) { 181 | wp_enqueue_script( 'digest', plugin_dir_url( PLUGIN_FILE ) . '/js/digest.js', [], VERSION, true ); 182 | wp_enqueue_style( 'digest', plugin_dir_url( PLUGIN_FILE ) . '/css/digest.css', [], VERSION ); 183 | } 184 | } 185 | 186 | /** 187 | * Add settings action link to the plugins page. 188 | * 189 | * @since 2.0.0 190 | * 191 | * @param array $links Plugin action links. 192 | * @return array The modified plugin action links 193 | */ 194 | public function plugin_action_links( array $links ) { 195 | return array_merge( 196 | [ 197 | 'settings' => sprintf( 198 | '%s', 199 | esc_url( admin_url( 'options-general.php#digest' ) ), 200 | __( 'Settings', 'digest' ) 201 | ), 202 | ], 203 | $links 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /inc/Setting/SettingInterface.php: -------------------------------------------------------------------------------- 1 | comment_post_ID ); 69 | $author = get_userdata( $post->post_author ); 70 | 71 | /** This filters is documented in wp-includes/pluggable.php */ 72 | $notify_author = apply_filters( 'comment_notification_notify_author', false, $comment_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 73 | 74 | $skipped = []; 75 | 76 | if ( $author && ! $notify_author ) { 77 | // The comment was left by the author. 78 | if ( $comment->user_id === $post->post_author ) { 79 | $skipped[] = $author->user_email; 80 | } 81 | 82 | // The author moderated a comment on their own post. 83 | if ( get_current_user_id() === (int) $post->post_author ) { 84 | $skipped[] = $author->user_email; 85 | } 86 | 87 | // The post author is no longer a member of the blog. 88 | if ( ! user_can( $post->post_author, 'read_post', $post->ID ) ) { 89 | $skipped[] = $author->user_email; 90 | } 91 | } 92 | 93 | foreach ( $emails as $recipient ) { 94 | if ( ! \in_array( $recipient, $skipped, true ) ) { 95 | Queue::add( $recipient, 'comment_notification', $comment_id ); 96 | } 97 | } 98 | 99 | return []; 100 | } 101 | 102 | /** 103 | * Hook into the comment moderation notification to add the comment to the queue. 104 | * 105 | * @param string[] $emails An array of email addresses to receive a comment notification. 106 | * @param int $comment_id The comment ID. 107 | * @return array An empty array to prevent sending an email directly. 108 | */ 109 | function comment_moderation_recipients( $emails, $comment_id ) { 110 | foreach ( $emails as $recipient ) { 111 | Queue::add( $recipient, 'comment_moderation', $comment_id ); 112 | } 113 | 114 | return []; 115 | } 116 | 117 | /** 118 | * Add core update notifications to our queue. 119 | * 120 | * This is only done when the update failed or was successful. 121 | * If there was a critical error, WordPress should still send the email immediately. 122 | * 123 | * @see WP_Automatic_Updater::send_email() 124 | * 125 | * @param array $email { 126 | * Array of email arguments that will be passed to wp_mail(). 127 | * 128 | * @type string $to The email recipient. An array of emails 129 | * can be returned, as handled by wp_mail(). 130 | * @type string $subject The email's subject. 131 | * @type string $body The email message body. 132 | * @type string $headers Any email headers, defaults to no headers. 133 | * } 134 | * @param string $type The type of email being sent. Can be one of 135 | * 'success', 'fail', 'manual', 'critical'. 136 | * @param \stdClass $core_update The update offer that was attempted. 137 | * @return array The modified $email array without a recipient. 138 | */ 139 | function auto_core_update_email( array $email, $type, $core_update ) { 140 | $next_core_update = get_preferred_from_update_core(); 141 | 142 | // If the update transient is empty, use the update we just performed. 143 | if ( ! $next_core_update ) { 144 | $next_core_update = $core_update; 145 | } 146 | 147 | // If the auto update is not to the latest version, say that the current version of WP is available instead. 148 | $version = 'success' === $type ? $core_update->current : $next_core_update->current; 149 | 150 | if ( \in_array( $type, [ 'success', 'fail', 'manual' ], true ) ) { 151 | Queue::add( get_site_option( 'admin_email' ), 'core_update_' . $type, $version ); 152 | $email['to'] = []; 153 | } 154 | 155 | return $email; 156 | } 157 | 158 | /** 159 | * Registers all the default events. 160 | * 161 | * @return void 162 | */ 163 | function register_default_events() { 164 | // Register default events. 165 | Registry::register_event( 166 | 'core_update_success', 167 | function ( $content, $entries, $user, $event ) { 168 | $message = new CoreUpdate( $entries, $user, $event ); 169 | 170 | if ( '' === $content ) { 171 | $content = '

    ' . __( 'Core Updates', 'digest' ) . '

    '; 172 | } 173 | 174 | return $content . $message->get_message(); 175 | } 176 | ); 177 | 178 | Registry::register_event( 179 | 'core_update_failure', 180 | function ( $content, $entries, $user, $event ) { 181 | $message = new CoreUpdate( $entries, $user, $event ); 182 | 183 | if ( '' === $content ) { 184 | $content = '

    ' . __( 'Core Updates', 'digest' ) . '

    '; 185 | } 186 | 187 | return $content . $message->get_message(); 188 | } 189 | ); 190 | 191 | Registry::register_event( 192 | 'comment_moderation', 193 | function ( $content, $entries, $user ) { 194 | $message = new CommentModeration( $entries, $user ); 195 | 196 | return $content . $message->get_message(); 197 | } 198 | ); 199 | 200 | Registry::register_event( 201 | 'comment_notification', 202 | function ( $content, $entries, $user ) { 203 | $message = new CommentNotification( $entries, $user ); 204 | 205 | return $content . $message->get_message(); 206 | } 207 | ); 208 | 209 | if ( \in_array( 'new_user_notification', get_option( 'digest_hooks' ), true ) ) { 210 | Registry::register_event( 211 | 'new_user_notification', 212 | function ( $content, $entries, $user ) { 213 | $message = new UserNotification( $entries, $user ); 214 | 215 | return $content . $message->get_message(); 216 | } 217 | ); 218 | } 219 | 220 | if ( \in_array( 'password_change_notification', get_option( 'digest_hooks' ), true ) ) { 221 | Registry::register_event( 222 | 'password_change_notification', 223 | function ( $content, $entries, $user ) { 224 | $message = new PasswordChangeNotification( $entries, $user ); 225 | 226 | return $content . $message->get_message(); 227 | } 228 | ); 229 | } 230 | 231 | /** 232 | * Fires after registering the default events. 233 | * 234 | * @since 2.0.0 235 | */ 236 | do_action( 'digest_register_events' ); 237 | } 238 | 239 | /** 240 | * Sends the scheduled email to all the recipients in the digest queue. 241 | * 242 | * @param string $subject Email subject. 243 | */ 244 | function send_email( $subject ) { 245 | $queue = Queue::get(); 246 | 247 | if ( empty( $queue ) ) { 248 | return; 249 | } 250 | 251 | // Loop through the queue. 252 | foreach ( $queue as $recipient => $items ) { 253 | $digest = new Plugin( $recipient, $items ); 254 | 255 | /** 256 | * Filter the digest message. 257 | * 258 | * @since 1.0.0 259 | * 260 | * @param string $digest The message to be sent. 261 | * @param string $recipient The recipient's email address. 262 | */ 263 | $digest = apply_filters( 'digest_cron_email_message', $digest->get_message(), $recipient ); 264 | 265 | // Send digest. 266 | wp_mail( $recipient, $subject, $digest, [ 'Content-Type: text/html; charset=UTF-8' ] ); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /js/digest.js: -------------------------------------------------------------------------------- 1 | !function(){const e=document.getElementById("digest_frequency_period"),n=document.getElementById("digest-frequency-day-wrapper");function t(){"weekly"===(this.value||e.value)?n.className="":n.className="digest-hidden"}e.onchange=t,t()}(); -------------------------------------------------------------------------------- /js/src/digest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Digest Notifications 3 | * 4 | * Copyright (c) 2015-2021 required 5 | * Licensed under the GPLv2+ license. 6 | */ 7 | ( function () { 8 | const frequencyPeriod = document.getElementById( 'digest_frequency_period' ); 9 | const frequencyDayWrapper = document.getElementById( 'digest-frequency-day-wrapper' ); 10 | 11 | function hideAndSeek() { 12 | if ( 'weekly' === ( this.value || frequencyPeriod.value ) ) { 13 | frequencyDayWrapper.className = ''; 14 | } else { 15 | frequencyDayWrapper.className = 'digest-hidden'; 16 | } 17 | } 18 | 19 | frequencyPeriod.onchange = hideAndSeek; 20 | 21 | hideAndSeek(); 22 | } )(); 23 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digest", 3 | "title": "Digest Notifications", 4 | "description": "Get a daily/weekly/monthly digest of what's happening on your site instead of receiving a single email each time.", 5 | "version": "0.0.0", 6 | "private": true, 7 | "homepage": "https://github.com/wearerequired/digest", 8 | "license": "GPL-2.0-or-later", 9 | "author": { 10 | "name": "required", 11 | "email": "support@required.ch", 12 | "url": "https://required.com" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/wearerequired/digest" 17 | }, 18 | "scripts": { 19 | "build:css": "NODE_ENV=production postcss css/src/*.css -d css", 20 | "build:js": "NODE_ENV=production webpack", 21 | "build": "concurrently \"npm:build:*\"", 22 | "watch:css": "NODE_ENV=development postcss --watch css/src/*.css -d css", 23 | "watch:js": "webpack --watch", 24 | "watch": "concurrently \"npm:watch:*\"", 25 | "lint-js": "eslint .", 26 | "lint-js:fix": "npm run lint-js -- --fix", 27 | "lint-css": "stylelint .", 28 | "lint-css:fix": "npm run lint-css -- --fix" 29 | }, 30 | "stylelint": { 31 | "extends": [ 32 | "@wearerequired/stylelint-config" 33 | ] 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "@wearerequired/eslint-config" 38 | ], 39 | "rules": { 40 | "@wordpress/i18n-text-domain": [ 41 | "error", 42 | { 43 | "allowedTextDomain": [ 44 | "digest" 45 | ] 46 | } 47 | ], 48 | "@wordpress/no-global-event-listener": "off" 49 | } 50 | }, 51 | "browserslist": [ 52 | "extends @wearerequired/browserslist-config/modern" 53 | ], 54 | "babel": { 55 | "presets": [ 56 | [ 57 | "@babel/preset-env", 58 | { 59 | "modules": false, 60 | "useBuiltIns": "usage", 61 | "corejs": 3 62 | } 63 | ] 64 | ] 65 | }, 66 | "devDependencies": { 67 | "@babel/cli": "7.21.0", 68 | "@babel/core": "7.21.3", 69 | "@babel/preset-env": "7.20.2", 70 | "@wearerequired/browserslist-config": "0.3.0", 71 | "@wearerequired/eslint-config": "5.0.0", 72 | "@wearerequired/postcss-config": "1.1.0", 73 | "@wearerequired/stylelint-config": "5.0.0", 74 | "babel-loader": "9.1.2", 75 | "concurrently": "7.6.0", 76 | "eslint": "8.36.0", 77 | "postcss-cli": "10.1.0", 78 | "prettier": "npm:wp-prettier@2.8.5", 79 | "terser-webpack-plugin": "5.3.7", 80 | "webpack-cli": "5.0.1" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coding Standard for the "Digest" plugin. 4 | 5 | . 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | /assets/* 36 | /tests/* 37 | 38 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/phpunit/tests 12 | 13 | 14 | 15 | 16 | ./classes/ 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require( '@wearerequired/postcss-config' ); 2 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Digest Notifications # 2 | Contributors: wearerequired, swissspidy, grapplerulrich, ocean90 3 | Tags: admin, emails, comments, notification, updates 4 | Tested up to: 6.7 5 | Stable tag: 3.0.0 6 | License: GPLv2 or later 7 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 8 | 9 | Get a daily, weekly, or monthly digest of what's happening on your site instead of receiving a single email each time. 10 | 11 | ## Description ## 12 | 13 | When you have lots of new user sign-ups or comments every day, it’s very distracting to receive a single email for each new event. 14 | 15 | With this plugin you get a daily, weekly, or monthly digest of your website’s activity. The digest includes the following events: 16 | 17 | * New Core Updates 18 | * New comments that need to be moderated (depending on your settings under 'Settings' -> 'Discussion') 19 | * New user sign-ups 20 | * Password resets by users 21 | 22 | ## Installation ## 23 | 24 | ### Manual Installation ### 25 | 26 | 1. Upload the entire `/digest` directory to the `/wp-content/plugins/` directory. 27 | 2. Activate Digest Notifications through the 'Plugins' menu in WordPress. 28 | 3. Head over to 'Settings' -> 'General' to configure the digest schedule. 29 | 30 | ## Frequently Asked Questions ## 31 | 32 | ### What’s the default schedule? ### 33 | 34 | By default, the digest is sent at the beginning of the week at 18:00. 35 | 36 | ### I still get single notification emails for event X! ### 37 | 38 | This plugin relies on specific hooks and filters in WordPress and also overrides two pluggable functions for user sign-ups and password reset notifications. If another plugin already overrides these, we can’t include these events in the digest. 39 | 40 | ### How can I add something to the digest? ### 41 | 42 | The plugin is quite extensible. There are many well documented hooks developers can use to add something to the digest queue and modify the complete email message. 43 | 44 | ## Screenshots ## 45 | 46 | 1. The plugin settings under 'Settings' -> 'General'. 47 | 2. An example digest sent by the plugin. 48 | 49 | ## Contribute ## 50 | 51 | If you would like to contribute to this plugin, report an issue or anything like that, please note that we develop this plugin [on GitHub](https://github.com/wearerequired/digest). Please submit pull requests to the develop branch. 52 | 53 | Developed by [required](https://required.com/). 54 | 55 | ## Changelog ## 56 | 57 | ### 3.0.0 - 2023-03-27 ### 58 | * Added: Support for sending digest on first day of a month 59 | * Changed: Bump minimum requirements to WordPress 6.0 and PHP 7.4 60 | 61 | ### 2.0.0 - 2021-06-02 ### 62 | * Changed: Plugin rewrite 63 | * Changed: Tested with WordPress 5.7 64 | * Changed: Bump minimum requirements to WordPress 4.7 and PHP 5.6 65 | 66 | For previous updates see [CHANGELOG.md](https://github.com/wearerequired/digest/blob/master/CHANGELOG.md). 67 | -------------------------------------------------------------------------------- /template-tags/functions.php: -------------------------------------------------------------------------------- 1 | user_login ) . "\r\n\r\n"; 63 | /* translators: %s: User email address. */ 64 | $message .= sprintf( __( 'Email: %s' ), $user->user_email ) . "\r\n"; 65 | 66 | $wp_new_user_notification_email_admin = array( 67 | 'to' => get_option( 'admin_email' ), 68 | /* translators: New user registration notification email subject. %s: Site title. */ 69 | 'subject' => __( '[%s] New User Registration' ), 70 | 'message' => $message, 71 | 'headers' => '', 72 | ); 73 | 74 | /** 75 | * Filters the contents of the new user notification email sent to the site admin. 76 | * 77 | * @since 4.9.0 78 | * 79 | * @param array $wp_new_user_notification_email_admin { 80 | * Used to build wp_mail(). 81 | * 82 | * @type string $to The intended recipient - site admin email address. 83 | * @type string $subject The subject of the email. 84 | * @type string $message The body of the email. 85 | * @type string $headers The headers of the email. 86 | * } 87 | * @param WP_User $user User object for new user. 88 | * @param string $blogname The site title. 89 | */ 90 | $wp_new_user_notification_email_admin = apply_filters( 'wp_new_user_notification_email_admin', $wp_new_user_notification_email_admin, $user, $blogname ); 91 | 92 | wp_mail( 93 | $wp_new_user_notification_email_admin['to'], 94 | wp_specialchars_decode( sprintf( $wp_new_user_notification_email_admin['subject'], $blogname ) ), 95 | $wp_new_user_notification_email_admin['message'], 96 | $wp_new_user_notification_email_admin['headers'] 97 | ); 98 | 99 | if ( $switched_locale ) { 100 | restore_previous_locale(); 101 | } 102 | } 103 | 104 | // `$deprecated` was pre-4.3 `$plaintext_pass`. An empty `$plaintext_pass` didn't sent a user notification. 105 | if ( 'admin' === $notify || ( empty( $deprecated ) && empty( $notify ) ) ) { 106 | return; 107 | } 108 | 109 | $key = get_password_reset_key( $user ); 110 | if ( is_wp_error( $key ) ) { 111 | return; 112 | } 113 | 114 | $switched_locale = switch_to_locale( get_user_locale( $user ) ); 115 | 116 | /* translators: %s: User login. */ 117 | $message = sprintf( __( 'Username: %s' ), $user->user_login ) . "\r\n\r\n"; 118 | $message .= __( 'To set your password, visit the following address:' ) . "\r\n\r\n"; 119 | $message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user->user_login ), 'login' ) . "\r\n\r\n"; 120 | 121 | $message .= wp_login_url() . "\r\n"; 122 | 123 | $wp_new_user_notification_email = array( 124 | 'to' => $user->user_email, 125 | /* translators: Login details notification email subject. %s: Site title. */ 126 | 'subject' => __( '[%s] Login Details' ), 127 | 'message' => $message, 128 | 'headers' => '', 129 | ); 130 | 131 | /** 132 | * Filters the contents of the new user notification email sent to the new user. 133 | * 134 | * @since 4.9.0 135 | * 136 | * @param array $wp_new_user_notification_email { 137 | * Used to build wp_mail(). 138 | * 139 | * @type string $to The intended recipient - New user email address. 140 | * @type string $subject The subject of the email. 141 | * @type string $message The body of the email. 142 | * @type string $headers The headers of the email. 143 | * } 144 | * @param WP_User $user User object for new user. 145 | * @param string $blogname The site title. 146 | */ 147 | $wp_new_user_notification_email = apply_filters( 'wp_new_user_notification_email', $wp_new_user_notification_email, $user, $blogname ); 148 | 149 | wp_mail( 150 | $wp_new_user_notification_email['to'], 151 | wp_specialchars_decode( sprintf( $wp_new_user_notification_email['subject'], $blogname ) ), 152 | $wp_new_user_notification_email['message'], 153 | $wp_new_user_notification_email['headers'] 154 | ); 155 | 156 | if ( $switched_locale ) { 157 | restore_previous_locale(); 158 | } 159 | } 160 | } 161 | 162 | if ( ! function_exists( 'wp_password_change_notification' ) ) { 163 | $digest_enabled_hooks[] = 'password_change_notification'; 164 | 165 | /** 166 | * Notify the blog admin of a user changing password, normally via email. 167 | * 168 | * @since 2.7.0 169 | * 170 | * @param WP_User $user User object. 171 | */ 172 | function wp_password_change_notification( $user ) { 173 | Queue::add( get_option( 'admin_email' ), 'password_change_notification', $user->ID ); 174 | } 175 | } 176 | 177 | update_option( 'digest_hooks', $digest_enabled_hooks, false ); 178 | -------------------------------------------------------------------------------- /tests/phpunit/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertStringContainsString( 'There are 0 new comments waiting for approval', $message->get_message() ); 13 | $this->assertStringNotContainsString( 'already moderated.', $message->get_message() ); 14 | } 15 | 16 | public function test_invalid_entry() { 17 | $message = new CommentModerationMessage( [ 18 | '123' => time(), 19 | ] ); 20 | 21 | $this->assertStringContainsString( 'There is 1 new comment waiting for approval', $message->get_message() ); 22 | } 23 | 24 | public function test_already_processed_entry() { 25 | $comment_id = self::factory()->comment->create(); 26 | 27 | $message = new CommentModerationMessage( [ 28 | $comment_id => time(), 29 | ] ); 30 | 31 | $this->assertStringContainsString( 'There is 1 new comment waiting for approval', $message->get_message() ); 32 | $this->assertStringContainsString( '1 comment was already moderated.', $message->get_message() ); 33 | } 34 | 35 | public function test_comment_action_links() { 36 | $comment_id = self::factory()->comment->create( [ 37 | 'comment_approved' => 0, 38 | ] ); 39 | 40 | $user = self::factory()->user->create_and_get( [ 41 | 'role' => 'administrator', 42 | 'user_email' => 'foo@example.com', 43 | ] ); 44 | 45 | $message = new CommentModerationMessage( [ 46 | $comment_id => time(), 47 | ], $user ); 48 | 49 | $this->assertStringContainsString( 'Approve', $message->get_message() ); 50 | $this->assertStringContainsString( 'Trash', $message->get_message() ); 51 | $this->assertStringContainsString( 'Spam', $message->get_message() ); 52 | } 53 | 54 | public function test_comment_action_links_no_capabilities() { 55 | $comment_id = self::factory()->comment->create( [ 56 | 'comment_approved' => 0, 57 | ] ); 58 | 59 | $user = self::factory()->user->create_and_get( [ 60 | 'role' => 'subscriber', 61 | 'user_email' => 'foo@example.com', 62 | ] ); 63 | 64 | $message = new CommentModerationMessage( [ 65 | $comment_id => time(), 66 | ], $user ); 67 | 68 | $this->assertStringNotContainsString( 'Approve', $message->get_message() ); 69 | $this->assertStringNotContainsString( 'Trash', $message->get_message() ); 70 | $this->assertStringNotContainsString( 'Spam', $message->get_message() ); 71 | } 72 | 73 | public function test_pingback() { 74 | $comment_id = self::factory()->comment->create( [ 75 | 'comment_approved' => 0, 76 | 'comment_type' => 'pingback', 77 | ] ); 78 | 79 | $message = new CommentModerationMessage( [ 80 | $comment_id => time(), 81 | ] ); 82 | 83 | $this->assertStringContainsString( 'Pingback on ', $message->get_message() ); 84 | } 85 | 86 | public function test_trackback() { 87 | $comment_id = self::factory()->comment->create( [ 88 | 'comment_approved' => 0, 89 | 'comment_type' => 'trackback', 90 | ] ); 91 | 92 | $message = new CommentModerationMessage( [ 93 | $comment_id => time(), 94 | ] ); 95 | 96 | $this->assertStringContainsString( 'Trackback on ', $message->get_message() ); 97 | } 98 | 99 | public function test_comment_author_url() { 100 | $comment_id = self::factory()->comment->create( [ 101 | 'comment_approved' => 0, 102 | 'comment_author_url' => 'http://example.com' 103 | ] ); 104 | 105 | $message = new CommentModerationMessage( [ 106 | $comment_id => time(), 107 | ] ); 108 | 109 | $this->assertStringContainsString( 'http://example.com', $message->get_message() ); 110 | } 111 | 112 | public function test_comment_author_email() { 113 | $comment_id = self::factory()->comment->create( [ 114 | 'comment_approved' => 0, 115 | 'comment_author_email' => 'foo@example.com' 116 | ] ); 117 | 118 | $message = new CommentModerationMessage( [ 119 | $comment_id => time(), 120 | ] ); 121 | 122 | $this->assertStringContainsString( 'foo@example.com', $message->get_message() ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/phpunit/tests/CommentNotification.php: -------------------------------------------------------------------------------- 1 | assertStringContainsString( 'There were 0 new comments', $message->get_message() ); 13 | $this->assertStringNotContainsString( 'already moderated.', $message->get_message() ); 14 | } 15 | 16 | public function test_invalid_entry() { 17 | $message = new CommentNotificationMessage( [ 18 | '123' => time(), 19 | ] ); 20 | 21 | $this->assertStringContainsString( 'There was 1 new comment', $message->get_message() ); 22 | $this->assertStringContainsString( '1 comment was already moderated.', $message->get_message() ); 23 | } 24 | 25 | public function test_already_processed_entry() { 26 | $comment_id = self::factory()->comment->create(); 27 | 28 | $message = new CommentNotificationMessage( [ 29 | $comment_id => time(), 30 | ] ); 31 | 32 | $this->assertStringContainsString( 'There was 1 new comment', $message->get_message() ); 33 | } 34 | 35 | public function test_comment_action_links() { 36 | $comment_id = self::factory()->comment->create( [ 37 | 'comment_approved' => 0, 38 | ] ); 39 | 40 | $user = self::factory()->user->create_and_get( [ 41 | 'role' => 'administrator', 42 | 'user_email' => 'foo@example.com', 43 | ] ); 44 | 45 | $message = new CommentNotificationMessage( [ 46 | $comment_id => time(), 47 | ], $user ); 48 | 49 | $this->assertStringNotContainsString( 'Approve', $message->get_message() ); 50 | $this->assertStringContainsString( 'Trash', $message->get_message() ); 51 | $this->assertStringContainsString( 'Spam', $message->get_message() ); 52 | } 53 | 54 | public function test_comment_action_links_no_capabilities() { 55 | $comment_id = self::factory()->comment->create( [ 56 | 'comment_approved' => 0, 57 | ] ); 58 | 59 | $user = self::factory()->user->create_and_get( [ 60 | 'role' => 'subscriber', 61 | 'user_email' => 'foo@example.com', 62 | ] ); 63 | 64 | $message = new CommentNotificationMessage( [ 65 | $comment_id => time(), 66 | ], $user ); 67 | 68 | $this->assertStringNotContainsString( 'Trash', $message->get_message() ); 69 | $this->assertStringNotContainsString( 'Spam', $message->get_message() ); 70 | } 71 | 72 | public function test_pingback() { 73 | $comment_id = self::factory()->comment->create( [ 74 | 'comment_approved' => 0, 75 | 'comment_type' => 'pingback', 76 | ] ); 77 | 78 | $message = new CommentNotificationMessage( [ 79 | $comment_id => time(), 80 | ] ); 81 | 82 | $this->assertStringContainsString( 'Pingback on ', $message->get_message() ); 83 | } 84 | 85 | public function test_trackback() { 86 | $comment_id = self::factory()->comment->create( [ 87 | 'comment_approved' => 0, 88 | 'comment_type' => 'trackback', 89 | ] ); 90 | 91 | $message = new CommentNotificationMessage( [ 92 | $comment_id => time(), 93 | ] ); 94 | 95 | $this->assertStringContainsString( 'Trackback on ', $message->get_message() ); 96 | } 97 | 98 | public function test_comment_author_url() { 99 | $comment_id = self::factory()->comment->create( [ 100 | 'comment_approved' => 0, 101 | 'comment_author_url' => 'http://example.com' 102 | ] ); 103 | 104 | $message = new CommentNotificationMessage( [ 105 | $comment_id => time(), 106 | ] ); 107 | 108 | $this->assertStringContainsString( 'http://example.com', $message->get_message() ); 109 | } 110 | 111 | public function test_comment_author_email() { 112 | $comment_id = self::factory()->comment->create( [ 113 | 'comment_approved' => 0, 114 | 'comment_author_email' => 'foo@example.com' 115 | ] ); 116 | 117 | $message = new CommentNotificationMessage( [ 118 | $comment_id => time(), 119 | ] ); 120 | 121 | $this->assertStringContainsString( 'foo@example.com', $message->get_message() ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/phpunit/tests/CoreUpdate.php: -------------------------------------------------------------------------------- 1 | assertSame( '', $message->get_message() ); 13 | } 14 | 15 | public function test_no_entries_failure() { 16 | $message = new CoreUpdateMessage( [], null, 'core_update_failure' ); 17 | 18 | $this->assertSame( '', $message->get_message() ); 19 | } 20 | 21 | public function test_single_entry_success() { 22 | $message = new CoreUpdateMessage( 23 | [ 24 | '100.1.0' => time(), 25 | ], 26 | null, 27 | 'core_update_success' 28 | ); 29 | 30 | $this->assertStringContainsString( 'has been updated automatically to WordPress 100.1.0', $message->get_message() ); 31 | } 32 | 33 | public function test_single_entry_failure() { 34 | $message = new CoreUpdateMessage( 35 | [ 36 | '100.1.0' => time(), 37 | ], 38 | null, 39 | 'core_update_failure' 40 | ); 41 | 42 | $this->assertStringContainsString( 'Please update your site', $message->get_message() ); 43 | $this->assertStringContainsString( 'WordPress 100.1.0', $message->get_message() ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/phpunit/tests/Cron.php: -------------------------------------------------------------------------------- 1 | email_subject = $subject; 27 | 28 | Queue::clear(); 29 | 30 | return $subject; 31 | } 32 | 33 | public function test_run_cron() { 34 | update_option( 'digest_frequency', [ 35 | 'period' => 'daily', 36 | 'hour' => date( 'G' ), 37 | 'day' => date( 'w' ), 38 | ] ); 39 | 40 | Queue::add( 'foo@example.com', 'foo', 'bar' ); 41 | 42 | add_filter( 'digest_cron_email_subject', [ $this, 'filter_digest_cron_email_subject' ] ); 43 | Digest_Cron::init(); 44 | remove_filter( 'digest_cron_email_subject', [ $this, 'filter_digest_cron_email_subject' ] ); 45 | 46 | $this->assertSame( 47 | sprintf( __( 'Today on %s', 'digest' ), get_bloginfo( 'name' ) ), 48 | $this->email_subject 49 | ); 50 | } 51 | 52 | public function test_run_cron_weekly() { 53 | update_option( 'digest_frequency', [ 54 | 'period' => 'weekly', 55 | 'hour' => date( 'G' ), 56 | 'day' => date( 'w' ), 57 | ] ); 58 | 59 | Queue::add( 'foo@example.com', 'foo', 'bar' ); 60 | 61 | add_filter( 'digest_cron_email_subject', [ $this, 'filter_digest_cron_email_subject' ] ); 62 | Digest_Cron::init(); 63 | remove_filter( 'digest_cron_email_subject', [ $this, 'filter_digest_cron_email_subject' ] ); 64 | 65 | $this->assertSame( 66 | sprintf( __( 'Past Week on %s', 'digest' ), get_bloginfo( 'name' ) ), 67 | $this->email_subject 68 | ); 69 | } 70 | 71 | public function test_run_cron_monthly() { 72 | update_option( 'digest_frequency', [ 73 | 'period' => 'monthly', 74 | 'hour' => date( 'G' ), 75 | ] ); 76 | 77 | Queue::add( 'foo@example.com', 'foo', 'bar' ); 78 | 79 | add_filter( 'wp_date', [ $this, 'filter_wp_date_to_return_first_day_of_month' ], 10, 2 ); 80 | add_filter( 'digest_cron_email_subject', [ $this, 'filter_digest_cron_email_subject' ] ); 81 | Digest_Cron::init(); 82 | remove_filter( 'digest_cron_email_subject', [ $this, 'filter_digest_cron_email_subject' ] ); 83 | remove_filter( 'wp_date', [ $this, 'filter_wp_date_to_return_first_day_of_month' ] ); 84 | 85 | $this->assertSame( 86 | sprintf( __( 'Past Month on %s', 'digest' ), get_bloginfo( 'name' ) ), 87 | $this->email_subject 88 | ); 89 | } 90 | 91 | public function filter_wp_date_to_return_first_day_of_month( $date, $format ) { 92 | if ( 'Y-m-d' !== $format || 'Y-m-01' === $format ) { 93 | return $date; 94 | } 95 | 96 | return gmdate( 'Y-m-01' ); 97 | } 98 | 99 | public function test_run_cron_monthly_not_first_day_of_month() { 100 | update_option( 'digest_frequency', [ 101 | 'period' => 'monthly', 102 | 'hour' => date( 'G' ), 103 | ] ); 104 | 105 | Queue::add( 'foo@example.com', 'foo', 'bar' ); 106 | 107 | $action = new MockAction(); 108 | 109 | add_filter( 'wp_date', [ $this, 'filter_wp_date_to_return_second_day_of_month' ], 10, 2 ); 110 | add_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 111 | Digest_Cron::init(); 112 | remove_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 113 | remove_filter( 'wp_date', [ $this, 'filter_wp_date_to_return_second_day_of_month' ] ); 114 | 115 | $this->assertSame( 0, $action->get_call_count() ); 116 | } 117 | 118 | public function filter_wp_date_to_return_second_day_of_month( $date, $format ) { 119 | if ( 'Y-m-d' !== $format ) { 120 | return $date; 121 | } 122 | 123 | return gmdate( 'Y-m-02' ); 124 | } 125 | 126 | public function test_run_cron_empty_queue() { 127 | update_option( 'digest_frequency', [ 128 | 'period' => 'daily', 129 | 'hour' => date( 'G' ), 130 | 'day' => date( 'w' ), 131 | ] ); 132 | 133 | $action = new MockAction(); 134 | 135 | add_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 136 | Digest_Cron::init(); 137 | remove_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 138 | 139 | $this->assertSame( 0, $action->get_call_count() ); 140 | } 141 | 142 | public function test_run_cron_wrong_hour() { 143 | update_option( 'digest_frequency', [ 144 | 'period' => 'daily', 145 | 'hour' => date( 'H' ) + 1, 146 | 'day' => date( 'w' ), 147 | ] ); 148 | 149 | $action = new MockAction(); 150 | 151 | add_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 152 | Digest_Cron::init(); 153 | remove_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 154 | 155 | $this->assertSame( 0, $action->get_call_count() ); 156 | } 157 | 158 | public function test_run_cron_wrong_day() { 159 | update_option( 'digest_frequency', [ 160 | 'period' => 'weekly', 161 | 'hour' => date( 'H' ), 162 | 'day' => date( 'w' ) + 1, 163 | ] ); 164 | 165 | $action = new MockAction(); 166 | 167 | add_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 168 | Digest_Cron::init(); 169 | remove_filter( 'digest_cron_email_subject', [ $action, 'filter' ] ); 170 | 171 | $this->assertSame( 0, $action->get_call_count() ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/phpunit/tests/Digest.php: -------------------------------------------------------------------------------- 1 | user->create( [ 16 | 'display_name' => 'John Doe', 17 | 'user_email' => 'foo@example.com', 18 | ] ); 19 | } 20 | 21 | public function test_empty_digest() { 22 | $digest = new Digest_Message( 'bar@example.com', [] ); 23 | 24 | $this->assertEmpty( $digest->get_message() ); 25 | } 26 | 27 | public function test_digest_with_unregistered_events() { 28 | $digest = new Digest_Message( 'foo@example.com', [ 29 | [ current_time( 'timestamp' ), 'foo', 'bar' ], 30 | [ current_time( 'timestamp' ), 'bar', 'baz' ], 31 | ] ); 32 | 33 | $this->assertEmpty( $digest->get_message() ); 34 | } 35 | 36 | public function test_digest_with_registered_events() { 37 | $comment_id = self::factory()->comment->create(); 38 | 39 | $digest = new Digest_Message( 'foo@example.com', [ 40 | [ current_time( 'timestamp' ), 'new_user_notification', self::$user_id ], 41 | [ current_time( 'timestamp' ), 'comment_notification', $comment_id ], 42 | ] ); 43 | 44 | $this->assertStringContainsString( 'Hi John Doe', $digest->get_message() ); 45 | $this->assertStringContainsString( 'That\'s it, have a nice day!', $digest->get_message() ); 46 | } 47 | 48 | public function test_digest_with_registered_events_no_user() { 49 | $comment_id = self::factory()->comment->create(); 50 | 51 | $digest = new Digest_Message( 'bar@example.com', [ 52 | [ current_time( 'timestamp' ), 'new_user_notification', self::$user_id ], 53 | [ current_time( 'timestamp' ), 'comment_notification', $comment_id ], 54 | ] ); 55 | 56 | $this->assertStringContainsString( 'Hi there', $digest->get_message() ); 57 | $this->assertStringContainsString( 'That\'s it, have a nice day!', $digest->get_message() ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/phpunit/tests/PasswordChangedNotification.php: -------------------------------------------------------------------------------- 1 | assertSame( '', $message->get_message() ); 13 | } 14 | 15 | public function test_invalid_entry() { 16 | $message = new PasswordMessage( [ 17 | '123' => time(), 18 | ] ); 19 | 20 | $this->assertSame( '', $message->get_message() ); 21 | } 22 | 23 | public function test_single_entry() { 24 | $user_1 = self::factory()->user->create( [ 25 | 'display_name' => 'John Doe', 26 | ] ); 27 | 28 | $message = new PasswordMessage( [ 29 | $user_1 => time(), 30 | ] ); 31 | 32 | $this->assertStringContainsString( 'The following user lost and changed his password', $message->get_message() ); 33 | $this->assertStringContainsString( 'John Doe', $message->get_message() ); 34 | $this->assertStringContainsString( "ID: $user_1", $message->get_message() ); 35 | } 36 | 37 | public function test_entries() { 38 | $user_1 = self::factory()->user->create( [ 39 | 'display_name' => 'John Doe', 40 | ] ); 41 | 42 | $user_2 = self::factory()->user->create( [ 43 | 'display_name' => 'Jane Doe', 44 | ] ); 45 | 46 | $message = new PasswordMessage( [ 47 | $user_1 => time(), 48 | $user_2 => time(), 49 | ] ); 50 | 51 | $this->assertStringContainsString( 'The following users lost and changed their passwords', $message->get_message() ); 52 | $this->assertStringContainsString( 'John Doe', $message->get_message() ); 53 | $this->assertStringContainsString( 'Jane Doe', $message->get_message() ); 54 | $this->assertStringContainsString( "ID: $user_1", $message->get_message() ); 55 | $this->assertStringContainsString( "ID: $user_2", $message->get_message() ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/phpunit/tests/Plugin.php: -------------------------------------------------------------------------------- 1 | assertTrue( class_exists( 'Required\\Digest\\Plugin' ) ); 18 | } 19 | 20 | public function test_cron_event_scheduled() { 21 | $this->assertFalse( wp_next_scheduled( 'digest_event' ) ); 22 | 23 | activate_plugin(); 24 | 25 | $this->assertIsInt( wp_next_scheduled( 'digest_event' ) ); 26 | } 27 | 28 | public function test_cron_event_unscheduled() { 29 | activate_plugin(); 30 | deactivate_plugin(); 31 | 32 | $this->assertFalse( wp_next_scheduled( 'digest_event' ) ); 33 | } 34 | 35 | public function test_send_email_empty_queue() { 36 | $action = new MockAction(); 37 | 38 | add_filter( 'digest_cron_email_message', [ $action, 'filter' ] ); 39 | send_email( 'Foo' ); 40 | remove_filter( 'digest_cron_email_message', [ $action, 'filter' ] ); 41 | 42 | $this->assertSame( 0, $action->get_call_count() ); 43 | } 44 | 45 | public function test_send_email_non_empty_queue() { 46 | Queue::add( 'foo@example.com', 'foo', 'bar' ); 47 | 48 | send_email( 'Foo' ); 49 | 50 | /** @var MockPHPMailer $mailer */ 51 | $mailer = tests_retrieve_phpmailer_instance(); 52 | 53 | $this->assertSame( 'Foo', $mailer->Subject ); 54 | } 55 | 56 | public function test_send_email_non_empty_queue_registered_events() { 57 | Queue::add( 'foo@example.com', 'core_update_success', '100.1.0' ); 58 | 59 | send_email( 'Foo' ); 60 | 61 | /** @var MockPHPMailer $mailer */ 62 | $mailer = tests_retrieve_phpmailer_instance(); 63 | 64 | $this->assertSame( 'Foo', $mailer->get_sent()->subject ); 65 | $this->assertStringContainsString( 'Hi there', $mailer->get_sent()->body ); 66 | } 67 | 68 | public function test_add_hooks() { 69 | $this->assertNotFalse( 70 | has_action( 'comment_notification_recipients', 'Required\Digest\comment_notification_recipients' ) 71 | ); 72 | $this->assertNotFalse( 73 | has_action( 'comment_moderation_recipients', 'Required\Digest\comment_moderation_recipients' ) 74 | ); 75 | $this->assertNotFalse( 76 | has_action( 'auto_core_update_email', 'Required\Digest\auto_core_update_email' ) 77 | ); 78 | 79 | $this->assertNotFalse( 80 | has_action( 'init', 'Required\Digest\register_default_events' ) 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/phpunit/tests/Queue.php: -------------------------------------------------------------------------------- 1 | assertEquals( $expected, $actual, '', 1 ); 29 | } 30 | 31 | public function test_empty_queue() { 32 | $this->assertEmpty( Digest_Queue::get() ); 33 | } 34 | 35 | public function test_queue_clearing() { 36 | Digest_Queue::add( 'johndoe@example.org', 'foo', 'bar' ); 37 | 38 | $queue_1 = Digest_Queue::get(); 39 | Digest_Queue::clear(); 40 | $queue_2 = Digest_Queue::get(); 41 | 42 | $this->assertEquals( 1, count( $queue_1 ) ); 43 | $this->assertEquals( 0, count( $queue_2 ) ); 44 | } 45 | 46 | public function test_queue_add() { 47 | $expected = [ 48 | 'foo@example.com' => [ 49 | [ 50 | current_time( 'timestamp' ), 51 | 'foo', 52 | 'bar', 53 | ], 54 | ], 55 | ]; 56 | 57 | Digest_Queue::add( 'foo@example.com', 'foo', 'bar' ); 58 | 59 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 60 | } 61 | 62 | public function test_queue_add_duplicate() { 63 | $expected = [ 64 | 'foo@example.com' => [ 65 | [ 66 | current_time( 'timestamp'), 67 | 'foo', 68 | 'bar', 69 | ], 70 | [ 71 | current_time( 'timestamp'), 72 | 'hello', 73 | 'world', 74 | ], 75 | ], 76 | ]; 77 | 78 | Digest_Queue::add( 'foo@example.com', 'foo', 'bar' ); 79 | Digest_Queue::add( 'foo@example.com', 'hello', 'world' ); 80 | 81 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 82 | } 83 | 84 | /** 85 | * Test adding many entries to the queue. 86 | * 87 | * @dataProvider data_queue_entries 88 | * 89 | * @param string $recipient The recipient. 90 | * @param string $event The type of the event. 91 | * @param string $data Data to store for this event, for example a comment ID. 92 | */ 93 | public function test_queue_add_many( $recipient, $event, $data ) { 94 | $expected = [ [ current_time( 'timestamp' ), $event, $data ] ]; 95 | 96 | Digest_Queue::add( $recipient, $event, $data ); 97 | 98 | $queue = Digest_Queue::get(); 99 | 100 | $this->assertArrayHasKey( $recipient, $queue ); 101 | $this->assertEquals( 1, count( $queue[ $recipient ] ) ); 102 | $this->assertEqualSetsWithDelta( $expected, $queue[ $recipient ] ); 103 | } 104 | 105 | public function data_queue_entries() { 106 | return [ 107 | [ 'johndoe@example.org', 'foo', 'bar' ], 108 | [ 'janedoe@example.org', '', '' ], 109 | [ 'foo@example.org', 0, 0 ], 110 | [ 'bar@example.org', 'foo', 'bar' ], 111 | ]; 112 | } 113 | 114 | public function test_comment_notification_recipients_returns_empty_aray() { 115 | $post_id = self::factory()->post->create(); 116 | $comment_id = self::factory()->comment->create( [ 117 | 'comment_post_ID' => $post_id, 118 | ] ); 119 | 120 | $actual = comment_notification_recipients( [ 121 | 'foo@example.com', 122 | 'bar@example.com', 123 | ], $comment_id ); 124 | 125 | $this->assertEmpty( $actual ); 126 | } 127 | 128 | public function test_comment_notification_recipients() { 129 | $post_id = self::factory()->post->create(); 130 | $comment_id = self::factory()->comment->create( [ 131 | 'comment_post_ID' => $post_id, 132 | ] ); 133 | 134 | comment_notification_recipients( [ 135 | 'foo@example.com', 136 | 'bar@example.com', 137 | ], $comment_id ); 138 | 139 | $expected = [ 140 | 'foo@example.com' => [ 141 | [ 142 | current_time( 'timestamp'), 143 | 'comment_notification', 144 | $comment_id, 145 | ], 146 | ], 147 | 'bar@example.com' => [ 148 | [ 149 | current_time( 'timestamp'), 150 | 'comment_notification', 151 | $comment_id, 152 | ], 153 | ], 154 | ]; 155 | 156 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 157 | } 158 | 159 | public function test_comment_notification_recipients_comment_by_author() { 160 | $user_id = self::factory()->user->create( [ 161 | 'user_email' => 'foo@example.com', 162 | ] ); 163 | $post_id = self::factory()->post->create( [ 164 | 'post_author' => $user_id, 165 | ] ); 166 | $comment_id = self::factory()->comment->create( [ 167 | 'comment_post_ID' => $post_id, 168 | 'user_id' => $user_id, 169 | ] ); 170 | 171 | comment_notification_recipients( [ 172 | 'foo@example.com', 173 | 'bar@example.com', 174 | ], $comment_id ); 175 | 176 | $expected = [ 177 | 'bar@example.com' => [ 178 | [ 179 | current_time( 'timestamp'), 180 | 'comment_notification', 181 | $comment_id, 182 | ], 183 | ] 184 | ]; 185 | 186 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 187 | } 188 | 189 | public function test_comment_notification_recipients_comment_by_current_user() { 190 | $user_id = self::factory()->user->create( [ 191 | 'user_email' => 'foo@example.com', 192 | ] ); 193 | $post_id = self::factory()->post->create( [ 194 | 'post_author' => $user_id, 195 | ] ); 196 | $comment_id = self::factory()->comment->create( [ 197 | 'comment_post_ID' => $post_id, 198 | ] ); 199 | 200 | wp_set_current_user( $user_id ); 201 | 202 | comment_notification_recipients( [ 203 | 'foo@example.com', 204 | 'bar@example.com', 205 | ], $comment_id ); 206 | 207 | $expected = [ 208 | 'bar@example.com' => [ 209 | [ 210 | current_time( 'timestamp'), 211 | 'comment_notification', 212 | $comment_id, 213 | ], 214 | ] 215 | ]; 216 | 217 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 218 | } 219 | 220 | public function test_comment_notification_recipients_author_has_no_capabilities() { 221 | $user_id = self::factory()->user->create( [ 222 | 'user_email' => 'foo@example.com', 223 | ] ); 224 | $post_id = self::factory()->post->create( [ 225 | 'post_author' => $user_id, 226 | 'post_status' => 'private', 227 | 'post_type' => 'revision', 228 | ] ); 229 | $comment_id = self::factory()->comment->create( [ 230 | 'comment_post_ID' => $post_id, 231 | ] ); 232 | 233 | comment_notification_recipients( [ 234 | 'foo@example.com', 235 | 'bar@example.com', 236 | ], $comment_id ); 237 | 238 | $expected = [ 239 | 'bar@example.com' => [ 240 | [ 241 | current_time( 'timestamp' ), 242 | 'comment_notification', 243 | $comment_id, 244 | ], 245 | ], 246 | ]; 247 | 248 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 249 | } 250 | 251 | public function test_comment_moderation_recipients() { 252 | comment_moderation_recipients( [ 253 | 'foo@example.com', 254 | 'bar@example.com', 255 | ], 123 ); 256 | 257 | $expected = [ 258 | 'foo@example.com' => [ 259 | [ 260 | current_time( 'timestamp'), 261 | 'comment_moderation', 262 | 123, 263 | ], 264 | ], 265 | 'bar@example.com' => [ 266 | [ 267 | current_time( 'timestamp'), 268 | 'comment_moderation', 269 | 123, 270 | ], 271 | ], 272 | ]; 273 | 274 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 275 | } 276 | 277 | public function test_auto_core_update_email_returns_empty_array() { 278 | set_site_transient( 'update_core', new \stdClass() ); 279 | 280 | $actual = auto_core_update_email( [], 'success', (object) [ 'current' => '100.1.0' ] ); 281 | 282 | $this->assertSame( [ 'to' => [] ], $actual ); 283 | } 284 | 285 | public function test_auto_core_update_email() { 286 | set_site_transient( 'update_core', new \stdClass() ); 287 | 288 | auto_core_update_email( [], 'success', (object) [ 'current' => '100.1.0' ] ); 289 | 290 | $expected = [ 291 | get_bloginfo( 'admin_email' ) => [ 292 | [ 293 | current_time( 'timestamp' ), 294 | 'core_update_success', 295 | '100.1.0', 296 | ], 297 | ], 298 | ]; 299 | 300 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 301 | } 302 | 303 | public function test_auto_core_update_email_invalid_type() { 304 | set_site_transient( 'update_core', new \stdClass() ); 305 | 306 | auto_core_update_email( [], 'foo', (object) [ 'current' => '100.1.0' ] ); 307 | 308 | $this->assertEmpty( Digest_Queue::get() ); 309 | } 310 | 311 | public function test_auto_core_update_email_no_updates() { 312 | set_site_transient( 'update_core', (object) [ 313 | 'updates' => [], 314 | ] ); 315 | 316 | auto_core_update_email( [], 'success', (object) [ 'current' => '100.1.0' ] ); 317 | 318 | $expected = [ 319 | get_bloginfo( 'admin_email' ) => [ 320 | [ 321 | current_time( 'timestamp' ), 322 | 'core_update_success', 323 | '100.1.0', 324 | ], 325 | ], 326 | ]; 327 | 328 | $this->assertEqualSetsWithDelta( $expected, Digest_Queue::get() ); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /tests/phpunit/tests/Registry.php: -------------------------------------------------------------------------------- 1 | assertSame( [ 'foo' ], EventRegistry::get_registered_events() ); 28 | } 29 | 30 | public function test_register_event_twice() { 31 | EventRegistry::register_event( 'foo' ); 32 | EventRegistry::register_event( 'foo' ); 33 | 34 | $this->assertSame( [ 'foo' ], EventRegistry::get_registered_events() ); 35 | } 36 | 37 | public function test_register_event_adds_filter_with_callback() { 38 | EventRegistry::register_event( 'foo', 'bar' ); 39 | 40 | $this->assertNotFalse( has_filter( 'digest_message_section_foo', 'bar' ) ); 41 | } 42 | 43 | public function test_is_registered_event() { 44 | $before = EventRegistry::is_registered_event( 'foo' ); 45 | EventRegistry::register_event( 'foo' ); 46 | $after = EventRegistry::is_registered_event( 'foo' ); 47 | 48 | $this->assertFalse( $before ); 49 | $this->assertTrue( $after ); 50 | } 51 | 52 | public function get_registered_events_empty() { 53 | $this->assertEmpty( EventRegistry::get_registered_events() ); 54 | } 55 | 56 | public function test_register_default_events( ) { 57 | register_default_events(); 58 | 59 | $this->assertEqualSets( [ 60 | 'comment_moderation', 61 | 'comment_notification', 62 | 'core_update_failure', 63 | 'core_update_success', 64 | 'new_user_notification', 65 | 'password_change_notification' 66 | ], EventRegistry::get_registered_events() ); 67 | } 68 | 69 | public function test_register_default_events_without_pluggable( ) { 70 | update_option( 'digest_hooks', [], false ); 71 | 72 | register_default_events(); 73 | 74 | $this->assertEqualSets( [ 75 | 'comment_moderation', 76 | 'comment_notification', 77 | 'core_update_failure', 78 | 'core_update_success', 79 | ], EventRegistry::get_registered_events() ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/phpunit/tests/Settings.php: -------------------------------------------------------------------------------- 1 | register(); 20 | 21 | $this->assertNotFalse( 22 | has_action( 'admin_init', [ 23 | self::$frequency_setting, 24 | 'add_settings_fields', 25 | ] ) 26 | ); 27 | 28 | $this->assertNotFalse( 29 | has_action( 'admin_enqueue_scripts', [ 30 | self::$frequency_setting, 31 | 'admin_enqueue_scripts', 32 | ] ) 33 | ); 34 | } 35 | 36 | public function test_add_settings_fields() { 37 | global $wp_settings_sections, $wp_settings_fields; 38 | 39 | self::$frequency_setting->add_settings_fields(); 40 | 41 | $this->assertArrayHasKey( 'digest_notifications', $wp_settings_sections['general'] ); 42 | $this->assertArrayHasKey( 'digest_frequency', $wp_settings_fields['general']['digest_notifications'] ); 43 | } 44 | 45 | public function test_admin_enqueue_scripts_invalid_hook_suffix() { 46 | self::$frequency_setting->admin_enqueue_scripts( 'foo' ); 47 | 48 | $this->assertFalse( wp_script_is( 'digest' ) ); 49 | $this->assertFalse( wp_style_is( 'digest' ) ); 50 | } 51 | 52 | public function test_admin_enqueue_scripts() { 53 | self::$frequency_setting->admin_enqueue_scripts( 'options-general.php' ); 54 | 55 | $this->assertTrue( wp_script_is( 'digest' ) ); 56 | $this->assertTrue( wp_style_is( 'digest' ) ); 57 | } 58 | 59 | public function test_plugin_action_links() { 60 | $this->assertEqualSets( 61 | [ 62 | 'settings' => sprintf( 63 | '%s', 64 | esc_url( admin_url( 'options-general.php#digest' ) ), 65 | __( 'Settings', 'digest' ) 66 | ), 67 | ], 68 | self::$frequency_setting->plugin_action_links( [] ) 69 | ); 70 | } 71 | 72 | public function data_sanitize_frequency_option() { 73 | return [ 74 | // Period. 75 | [ 76 | [ 'period' => 'weekly', 'hour' => 1, 'day' => 1 ], 77 | [ 'period' => 'weekly', 'hour' => 1, 'day' => 1 ], 78 | ], 79 | [ 80 | [ 'period' => 'daily', 'hour' => 1, 'day' => 1 ], 81 | [ 'period' => 'daily', 'hour' => 1, 'day' => 1 ], 82 | ], 83 | [ 84 | [ 'period' => 'monthly', 'hour' => 1, 'day' => 1 ], 85 | [ 'period' => 'monthly', 'hour' => 1, 'day' => 1 ], 86 | ], 87 | [ 88 | [ 'period' => 'weekly', 'hour' => 1, 'day' => 1 ], 89 | [ 'period' => 'foo', 'hour' => 1, 'day' => 1 ], 90 | ], 91 | // Hour. 92 | [ 93 | [ 'period' => 'weekly', 'hour' => 18, 'day' => 1 ], 94 | [ 'period' => 'weekly', 'hour' => -1, 'day' => 1 ], 95 | ], 96 | [ 97 | [ 'period' => 'weekly', 'hour' => 0, 'day' => 1 ], 98 | [ 'period' => 'weekly', 'hour' => 0, 'day' => 1 ], 99 | ], 100 | [ 101 | [ 'period' => 'weekly', 'hour' => 18, 'day' => 1 ], 102 | [ 'period' => 'weekly', 'hour' => 24, 'day' => 1 ], 103 | ], 104 | [ 105 | [ 'period' => 'weekly', 'hour' => 23, 'day' => 1 ], 106 | [ 'period' => 'weekly', 'hour' => 23, 'day' => 1 ], 107 | ], 108 | [ 109 | [ 'period' => 'weekly', 'hour' => 18, 'day' => 1 ], 110 | [ 'period' => 'weekly', 'hour' => 'foo', 'day' => 1 ], 111 | ], 112 | // Day. 113 | [ 114 | [ 'period' => 'weekly', 'hour' => 1, 'day' => get_option( 'start_of_week' ) ], 115 | [ 'period' => 'foo', 'hour' => 1, 'day' => -1 ], 116 | ], 117 | [ 118 | [ 'period' => 'weekly', 'hour' => 1, 'day' => 0 ], 119 | [ 'period' => 'foo', 'hour' => 1, 'day' => 0 ], 120 | ], 121 | [ 122 | [ 'period' => 'weekly', 'hour' => 1, 'day' => get_option( 'start_of_week' ) ], 123 | [ 'period' => 'foo', 'hour' => 1, 'day' => 7 ], 124 | ], 125 | [ 126 | [ 'period' => 'weekly', 'hour' => 1, 'day' => 6 ], 127 | [ 'period' => 'foo', 'hour' => 1, 'day' => 6 ], 128 | ], 129 | [ 130 | [ 'period' => 'weekly', 'hour' => 1, 'day' => get_option( 'start_of_week' ) ], 131 | [ 'period' => 'foo', 'hour' => 1, 'day' => 'foo' ], 132 | ], 133 | ]; 134 | } 135 | 136 | /** 137 | * @dataProvider data_sanitize_frequency_option 138 | * 139 | * @param array $expected 140 | * @param array $actual 141 | */ 142 | public function test_sanitize_frequency_option( $expected, $actual ) { 143 | $this->assertEqualSetsWithIndex( $expected, self::$frequency_setting->sanitize_frequency_option( $actual ) ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/phpunit/tests/UserNotification.php: -------------------------------------------------------------------------------- 1 | assertSame( '', $message->get_message() ); 13 | } 14 | 15 | public function test_invalid_entry() { 16 | $message = new UserNotificationMessage( [ 17 | '123' => time(), 18 | ] ); 19 | 20 | $this->assertSame( '', $message->get_message() ); 21 | } 22 | 23 | public function test_entries() { 24 | $user_1 = self::factory()->user->create( [ 25 | 'display_name' => 'John Doe', 26 | ] ); 27 | 28 | $user_2 = self::factory()->user->create( [ 29 | 'display_name' => 'Jane Doe', 30 | ] ); 31 | 32 | $message = new UserNotificationMessage( [ 33 | $user_1 => time(), 34 | $user_2 => time(), 35 | ] ); 36 | 37 | $this->assertStringContainsString( 'John Doe', $message->get_message() ); 38 | $this->assertStringContainsString( 'Jane Doe', $message->get_message() ); 39 | $this->assertStringContainsString( "ID: $user_1", $message->get_message() ); 40 | $this->assertStringContainsString( "ID: $user_2", $message->get_message() ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 |