Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ jobs:
- name: Install Composer dependencies
run: composer install

- name: Refresh apt metadata
# `shivammathur/setup-php` adds the ondrej/php PPA, which recently
# changed its repository `Label`. `apt-get update` (invoked by
# Playwright's `--with-deps`) aborts with exit code 100 on such
# changes unless `--allow-releaseinfo-change` is passed. Refresh the
# metadata here so the subsequent Playwright dependency install works.
run: sudo apt-get update --allow-releaseinfo-change

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

Expand Down
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Stream Changelog

## 4.2.2 - July 6, 2026

### Security

- Harden authorization for the live update preference: enforce the Stream view capability and always target the current user in the `stream_enable_live_update` AJAX handler so a user can only change their own live update preference ([#1918](https://github.com/xwp/stream/pull/1918)).

## 4.2.1 - July 2, 2026

### Bug Fixes
Expand Down
3 changes: 0 additions & 3 deletions classes/class-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -1156,9 +1156,6 @@ public function screen_controls( $status, $args ) {
<div>
<input type="hidden" name="stream_live_update_nonce" id="stream_live_update_nonce" value="<?php echo esc_attr( $nonce ); ?>"/>
</div>
<div>
<input type="hidden" name="enable_live_update_user" id="enable_live_update_user" value="<?php echo absint( $user_id ); ?>"/>
</div>
<div class="metabox-prefs stream-live-update-checkbox">
<label for="enable_live_update">
<input type="checkbox" value="on" name="enable_live_update" id="enable_live_update" data-heartbeat="<?php echo esc_attr( $heartbeat ); ?>" <?php checked( $option, 'on' ); ?> />
Expand Down
18 changes: 5 additions & 13 deletions classes/class-live-update.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,15 @@ public function __construct( $plugin ) {
public function enable_live_update() {
check_ajax_referer( $this->user_meta_key . '_nonce', 'nonce' );

$input = array(
'checked' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'user' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'heartbeat' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
);

$input = filter_input_array( INPUT_POST, $input );

if ( false === $input ) {
wp_send_json_error( 'Error in live update checkbox' );
if ( ! current_user_can( $this->plugin->admin->view_cap ) ) {
wp_send_json_error( esc_html__( 'You do not have permission to do this.', 'stream' ) );
}

$checked = ( 'checked' === $input['checked'] ) ? 'on' : 'off';
$checked = ( 'checked' === wp_stream_filter_input( INPUT_POST, 'checked' ) ) ? 'on' : 'off';

$user = (int) $input['user'];
$user = get_current_user_id();

if ( 'false' === $input['heartbeat'] ) {
if ( 'false' === wp_stream_filter_input( INPUT_POST, 'heartbeat' ) ) {
update_user_meta( $user, $this->user_meta_key, 'off' );

wp_send_json_error( esc_html__( "Live updates could not be enabled because Heartbeat is not loaded.\n\nYour hosting provider or another plugin may have disabled it for performance reasons.", 'stream' ) );
Expand Down
2 changes: 1 addition & 1 deletion classes/class-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Plugin {
*
* @const string
*/
const VERSION = '4.2.1';
const VERSION = '4.2.2';

/**
* WP-CLI command
Expand Down
6 changes: 5 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Contributors: xwp
Tags: wp stream, stream, activity, logs, track
Requires at least: 4.6
Tested up to: 7.0
Stable tag: 4.2.1
Stable tag: 4.2.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -134,6 +134,10 @@ Use only `$_SERVER['REMOTE_ADDR']` as the client IP address for event logs witho

== Changelog ==

= 4.2.2 - July 6, 2026 =

See: [https://github.com/xwp/stream/blob/develop/changelog.md#422---july-6-2026](https://github.com/xwp/stream/blob/develop/changelog.md#422---july-6-2026)

= 4.2.1 - July 2, 2026 =

See: [https://github.com/xwp/stream/blob/develop/changelog.md#421---july-2-2026](https://github.com/xwp/stream/blob/develop/changelog.md#421---july-2-2026)
Expand Down
2 changes: 0 additions & 2 deletions src/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ $( document ).ready(
$( '#enable_live_update' ).click(
function() {
const nonce = $( '#stream_live_update_nonce' ).val();
const user = $( '#enable_live_update_user' ).val();
let checked = 'unchecked';
let heartbeat = 'true';

Expand All @@ -239,7 +238,6 @@ $( document ).ready(
data: {
action: 'stream_enable_live_update',
nonce,
user,
checked,
heartbeat,
},
Expand Down
2 changes: 1 addition & 1 deletion stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Stream
* Plugin URI: https://xwp.co/work/stream/
* Description: Stream tracks logged-in user activity so you can monitor every change made on your WordPress site in beautifully organized detail. All activity is organized by context, action and IP address for easy filtering. Developers can extend Stream with custom connectors to log any kind of action.
* Version: 4.2.1
* Version: 4.2.2
* Author: XWP
* Author URI: https://xwp.co
* License: GPLv2+
Expand Down
86 changes: 86 additions & 0 deletions tests/phpunit/test-class-live-update.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php
namespace WP_Stream;

/**
* Class Test_Live_Update
*
* @package WP_Stream
*/
class Test_Live_Update extends WP_StreamTestCase {
/**
* Live update instance under test.
*
* @var Live_Update
*/
protected $live_update;

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

$this->live_update = new Live_Update( $this->plugin );
}

public function tearDown(): void {
unset( $_POST['action'], $_POST['nonce'], $_POST['checked'], $_POST['heartbeat'], $_POST['user'] );

parent::tearDown();
}

public function test_enable_live_update_only_updates_current_user() {
$attacker_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
$victim_id = $this->factory->user->create( array( 'role' => 'administrator' ) );

$this->plugin->settings->options['general_role_access'] = array( 'subscriber' );
wp_set_current_user( $attacker_id );

$this->assertTrue( current_user_can( $this->plugin->admin->view_cap ) );

update_user_meta( $attacker_id, $this->live_update->user_meta_key, 'off' );
update_user_meta( $victim_id, $this->live_update->user_meta_key, 'off' );

$_POST['action'] = 'stream_enable_live_update';
$_POST['nonce'] = wp_create_nonce( $this->live_update->user_meta_key . '_nonce' );
$_POST['checked'] = 'checked';
$_POST['heartbeat'] = 'true';
$_POST['user'] = (string) $victim_id;

try {
$this->_handleAjax( 'stream_enable_live_update' );
} catch ( \WPAjaxDieContinueException $e ) {
// Expected: wp_send_json_success() terminates the AJAX request.
}

$response = json_decode( $this->_last_response );
$this->assertTrue( $response->success );

$this->assertSame( 'on', get_user_meta( $attacker_id, $this->live_update->user_meta_key, true ) );
$this->assertSame( 'off', get_user_meta( $victim_id, $this->live_update->user_meta_key, true ) );
}

public function test_enable_live_update_denies_users_without_view_cap() {
$user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );

$this->plugin->settings->options['general_role_access'] = array( 'administrator' );
wp_set_current_user( $user_id );

$this->assertFalse( current_user_can( $this->plugin->admin->view_cap ) );

update_user_meta( $user_id, $this->live_update->user_meta_key, 'off' );

$_POST['action'] = 'stream_enable_live_update';
$_POST['nonce'] = wp_create_nonce( $this->live_update->user_meta_key . '_nonce' );
$_POST['checked'] = 'checked';
$_POST['heartbeat'] = 'true';

try {
$this->_handleAjax( 'stream_enable_live_update' );
} catch ( \WPAjaxDieContinueException $e ) {
// Expected: wp_send_json_error() terminates the AJAX request.
}

$response = json_decode( $this->_last_response );
$this->assertFalse( $response->success );

$this->assertSame( 'off', get_user_meta( $user_id, $this->live_update->user_meta_key, true ) );
}
}
Loading