Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions alerts/class-alert-type-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class Alert_Type_Email extends Alert_Type {
*/
public $name = 'Email';

/**
* Meta key that stores the last time an alert sent an email.
*/
const LAST_SENT_META_KEY = '_wp_stream_email_last_sent';

/**
* Alert type slug
*
Expand Down Expand Up @@ -62,6 +67,10 @@ public function __construct( $plugin ) {
* @return void
*/
public function alert( $record_id, $recordarr, $alert ) {
if ( ! $this->is_send_allowed( $alert ) ) {
return;
}

$options = wp_parse_args(
$alert->alert_meta,
array(
Expand Down Expand Up @@ -120,9 +129,61 @@ public function alert( $record_id, $recordarr, $alert ) {
$edit_alert_link = admin_url( 'edit.php?post_type=wp_stream_alerts#post-' . $alert->ID );
$message .= __( 'Edit Alert', 'stream' ) . "\n<$edit_alert_link>";

$this->update_last_sent( $alert );

wp_mail( $options['email_recipient'], $options['email_subject'], $message );
}

/**
* Checks if the throttle allows this alert to send another email.
*
* @param Alert $alert Alert object.
* @return bool
*/
public function is_send_allowed( $alert ) {
$interval = $this->get_throttle_interval();

if ( $interval <= 0 ) {
return true;
}

$last_sent = (int) get_post_meta( $alert->ID, self::LAST_SENT_META_KEY, true );

return ( time() - $last_sent ) >= $interval;
}

/**
* Stores the time of the last email sent by this alert.
*
* @param Alert $alert Alert object.
* @return void
*/
public function update_last_sent( $alert ) {
if ( empty( $alert->ID ) ) {
return;
}

update_post_meta( $alert->ID, self::LAST_SENT_META_KEY, time() );
}

/**
* Returns the minimum number of seconds between two emails sent by one alert.
*
* @return int
*/
public function get_throttle_interval() {
/**
* Filters the minimum number of seconds between two emails sent by the same alert.
*
* Set it to 0 to send an email for every matching record.
*
* @param int $interval Minimum number of seconds between two emails. Default 300.
*/
$interval = (int) apply_filters( 'wp_stream_alert_email_throttle', 300 );

return $interval;
}

/**
* Displays a settings form for the alert type
*
Expand Down
139 changes: 139 additions & 0 deletions tests/phpunit/test-class-alert-type-email.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
namespace WP_Stream;

/**
* Class Test_Alert_Type_Email
*
* @package WP_Stream
* @group alerts
*/
class Test_Alert_Type_Email extends WP_StreamTestCase {

/**
* Holds the alert type object under test.
*
* @var Alert_Type_Email
*/
protected $alert_type;

/**
* Holds an alert post ID used by the tests.
*
* @var int
*/
protected static $alert_post_id;

public function setUp(): void {
parent::setUp();

$this->alert_type = new Alert_Type_Email( $this->plugin );

if ( empty( self::$alert_post_id ) || ! get_post( self::$alert_post_id ) ) {
self::$alert_post_id = self::factory()->post->create(
array(
'post_type' => Alerts::POST_TYPE,
'post_status' => Alerts::STATUS_ENABLED,
)
);
}
}

public function tearDown(): void {
remove_all_filters( 'wp_stream_alert_email_throttle' );
delete_post_meta( self::$alert_post_id, Alert_Type_Email::LAST_SENT_META_KEY );

parent::tearDown();
}

public function test_send_allowed_when_no_email_was_sent_before() {
$alert = $this->plugin->alerts->get_alert( self::$alert_post_id );

$this->assertTrue( $this->alert_type->is_send_allowed( $alert ) );
}

public function test_send_blocked_within_throttle_interval() {
$alert = $this->plugin->alerts->get_alert( self::$alert_post_id );

$this->alert_type->update_last_sent( $alert );

$this->assertFalse( $this->alert_type->is_send_allowed( $alert ) );
}

public function test_send_allowed_after_throttle_interval() {
$alert = $this->plugin->alerts->get_alert( self::$alert_post_id );

update_post_meta( self::$alert_post_id, Alert_Type_Email::LAST_SENT_META_KEY, time() - 301 );

$this->assertTrue( $this->alert_type->is_send_allowed( $alert ) );
}

public function test_send_allowed_with_zero_throttle() {
add_filter( 'wp_stream_alert_email_throttle', '__return_zero' );

$alert = $this->plugin->alerts->get_alert( self::$alert_post_id );

$this->alert_type->update_last_sent( $alert );

$this->assertTrue( $this->alert_type->is_send_allowed( $alert ) );
}

public function test_alert_sends_one_email_and_throttles_the_second() {
reset_phpmailer_instance();
$mailer = tests_retrieve_phpmailer_instance();

$alert = $this->plugin->alerts->get_alert( self::$alert_post_id );
$alert->alert_meta = array_merge(
(array) $alert->alert_meta,
array(
'email_recipient' => 'admin@example.org',
'email_subject' => 'Stream alert',
)
);

$this->alert_type->alert( 0, $this->dummy_record_data(), $alert );

$this->assertCount( 1, $mailer->mock_sent );

// A second trigger inside the throttle window sends no further email.
$this->alert_type->alert( 0, $this->dummy_record_data(), $alert );

$this->assertCount( 1, $mailer->mock_sent );
}

public function test_alert_sends_every_email_with_zero_throttle() {
add_filter( 'wp_stream_alert_email_throttle', '__return_zero' );

reset_phpmailer_instance();
$mailer = tests_retrieve_phpmailer_instance();

$alert = $this->plugin->alerts->get_alert( self::$alert_post_id );
$alert->alert_meta = array_merge(
(array) $alert->alert_meta,
array(
'email_recipient' => 'admin@example.org',
'email_subject' => 'Stream alert',
)
);

$this->alert_type->alert( 0, $this->dummy_record_data(), $alert );
$this->alert_type->alert( 0, $this->dummy_record_data(), $alert );

$this->assertCount( 2, $mailer->mock_sent );
}

private function dummy_record_data() {
return array(
'object_id' => null,
'site_id' => '1',
'blog_id' => get_current_blog_id(),
'user_id' => '1',
'user_role' => 'administrator',
'created' => gmdate( 'Y-m-d H:i:s' ),
'summary' => '"Hello Dave" plugin activated',
'ip' => '192.168.0.1',
'connector' => 'installer',
'context' => 'plugins',
'action' => 'activated',
);
}
}
127 changes: 127 additions & 0 deletions uninstall.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php
/**
* Uninstall cleanup for WP Stream.
*
* Removes alert configuration, plugin options, user meta, and scheduled jobs.
* The activity log tables wp_stream and wp_streammeta are kept, log data is
* not deleted because the removal is irreversible.
*
* @package WP_Stream
*/

if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
die;
}

if ( ! function_exists( 'wp_stream_uninstall_delete_site_data' ) ) {

/**
* Removes all Stream data stored for the current site.
*
* Runs inside the blog switch loop on multisite, so the data below is
* read and removed per site, and caches are invalidated on the site that
* owns them.
*
* @return void
*/
function wp_stream_uninstall_delete_site_data() {
global $wpdb;

// Remove all alert posts of every status. Core API keeps hooks,
// post caches, and object caches in sync.
$alert_ids = $wpdb->get_col(
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", 'wp_stream_alerts' )
);
foreach ( $alert_ids as $alert_id ) {
wp_delete_post( (int) $alert_id, true );
}

// Remove all Stream options, including transients and legacy keys.
// delete_option() invalidates the alloptions cache on every site.
$option_names = $wpdb->get_col(
$wpdb->prepare(
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
$wpdb->esc_like( 'wp_stream' ) . '%',
$wpdb->esc_like( '_transient_wp_stream' ) . '%'
)
);
foreach ( $option_names as $option_name ) {
delete_option( $option_name );
}

// Remove the legacy WP-Cron purge event for this site.
wp_clear_scheduled_hook( 'wp_stream_auto_purge' );

// Remove the recurring purge / reset jobs. WP-Cron events live in the
// per-site cron option. Action Scheduler tables are per-site, and the
// AS API is only loaded when another plugin or Stream itself provides
// it, so the tables are cleaned directly as a fallback.
$stream_scheduled_hooks = array(
'stream_auto_purge_action',
'stream_auto_purge_batch_action',
'stream_auto_purge_reaper_action',
'stream_erase_large_records_action',
);
if ( function_exists( 'as_unschedule_all_actions' ) ) {
foreach ( $stream_scheduled_hooks as $stream_scheduled_hook ) {
as_unschedule_all_actions( $stream_scheduled_hook );
}
} else {
foreach ( $stream_scheduled_hooks as $stream_scheduled_hook ) {
wp_clear_scheduled_hook( $stream_scheduled_hook );
}
}
}
}

if ( is_multisite() ) {
$stream_site_ids = get_sites(
array(
'fields' => 'ids',
'number' => 0,
)
);
foreach ( $stream_site_ids as $stream_site_id ) {
switch_to_blog( (int) $stream_site_id );
wp_stream_uninstall_delete_site_data();
restore_current_blog();
}

// Network options live outside the per-site loop. delete_site_option()
// covers wp_stream, wp_stream_network, wp_stream_db*, and any leftover
// Stream keys that were written via update_site_option().
$stream_network_option_names = $wpdb->get_col(
$wpdb->prepare(
"SELECT meta_key FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s OR meta_key LIKE %s",
$wpdb->esc_like( 'wp_stream' ) . '%',
$wpdb->esc_like( '_site_transient_wp_stream' ) . '%'
)
);
foreach ( $stream_network_option_names as $stream_network_option_name ) {
delete_site_option( $stream_network_option_name );
}
} else {
wp_stream_uninstall_delete_site_data();
// delete_site_option() works on single site too.
delete_site_option( 'wp_stream_network' );
// Network options that use update_site_option outside the settings class.
delete_site_option( 'wp_stream_db' );
delete_site_option( 'wp_stream_db_connectors' );
delete_site_option( 'wp_stream_db_registered_connectors' );
}

// User meta is global, so it is cleaned once. delete_user_meta() keeps the
// user meta cache in sync, unlike a direct query against the table.
global $wpdb;
$stream_user_meta_keys = $wpdb->get_results(
$wpdb->prepare(
"SELECT user_id, meta_key FROM {$wpdb->usermeta} WHERE meta_key LIKE %s OR meta_key LIKE %s OR meta_key IN ( 'stream_live_update_records', 'stream_last_read', 'stream_unread_count', 'stream_user_feed_key' )",
'%' . $wpdb->esc_like( 'wp_stream' ) . '%',
'%' . $wpdb->esc_like( 'edit_stream_per_page' )
)
);
if ( $stream_user_meta_keys ) {
foreach ( $stream_user_meta_keys as $stream_user_meta_key ) {
delete_user_meta( (int) $stream_user_meta_key->user_id, $stream_user_meta_key->meta_key );
}
}