diff --git a/aryo-activity-log.php b/aryo-activity-log.php
index 7f7b7ed..9dca7b2 100644
--- a/aryo-activity-log.php
+++ b/aryo-activity-log.php
@@ -1,6 +1,6 @@
{
@@ -53,3 +53,27 @@ export function buildExportUrl( filters ) {
return url.toString();
}
+
+export function fetchSettings() {
+ return apiFetch( {
+ path: 'activity-log/v1/settings',
+ headers: { 'X-AAL-Settings-Nonce': config.settingsNonce || '' },
+ } );
+}
+
+export function saveSettings( data ) {
+ return apiFetch( {
+ path: 'activity-log/v1/settings',
+ method: 'PUT',
+ data,
+ headers: { 'X-AAL-Settings-Nonce': config.settingsNonce || '' },
+ } );
+}
+
+export function eraseLogs() {
+ return apiFetch( {
+ path: 'activity-log/v1/logs/erase',
+ method: 'POST',
+ headers: { 'X-AAL-Settings-Nonce': config.settingsNonce || '' },
+ } );
+}
diff --git a/assets/js/admin/src/settings-index.js b/assets/js/admin/src/settings-index.js
new file mode 100644
index 0000000..511b469
--- /dev/null
+++ b/assets/js/admin/src/settings-index.js
@@ -0,0 +1,8 @@
+import { createRoot } from '@wordpress/element';
+import Settings from './settings';
+
+const container = document.getElementById( 'aal-settings-root' );
+if ( container ) {
+ const root = createRoot( container );
+ root.render( );
+}
diff --git a/assets/js/admin/src/settings.js b/assets/js/admin/src/settings.js
new file mode 100644
index 0000000..14cc2ef
--- /dev/null
+++ b/assets/js/admin/src/settings.js
@@ -0,0 +1,232 @@
+import { useState, useEffect, useCallback } from '@wordpress/element';
+import {
+ Button,
+ SelectControl,
+ Notice,
+ Spinner,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { fetchSettings, saveSettings, eraseLogs } from './api';
+
+export default function Settings() {
+ const [ schema, setSchema ] = useState( null );
+ const [ values, setValues ] = useState( {} );
+ const [ canEraseLogs, setCanEraseLogs ] = useState( false );
+ const [ loading, setLoading ] = useState( true );
+ const [ saving, setSaving ] = useState( false );
+ const [ erasing, setErasing ] = useState( false );
+ const [ notice, setNotice ] = useState( null );
+
+ const loadSettings = useCallback( () => {
+ setLoading( true );
+ fetchSettings()
+ .then( ( result ) => {
+ setSchema( result.fields );
+ setCanEraseLogs( result.canEraseLogs );
+
+ const initial = {};
+ Object.entries( result.fields ).forEach( ( [ key, field ] ) => {
+ initial[ key ] = field.value;
+ } );
+ setValues( initial );
+
+ setLoading( false );
+ } )
+ .catch( ( err ) => {
+ setNotice( { status: 'error', message: err.message || __( 'Failed to load settings.', 'aryo-activity-log' ) } );
+ setLoading( false );
+ } );
+ }, [] );
+
+ useEffect( () => {
+ loadSettings();
+ }, [ loadSettings ] );
+
+ const handleSave = () => {
+ setSaving( true );
+ setNotice( null );
+
+ saveSettings( values )
+ .then( () => {
+ setNotice( { status: 'success', message: __( 'Settings saved.', 'aryo-activity-log' ) } );
+ setSaving( false );
+ } )
+ .catch( ( err ) => {
+ setNotice( { status: 'error', message: err.message || __( 'Failed to save settings.', 'aryo-activity-log' ) } );
+ setSaving( false );
+ } );
+ };
+
+ const handleErase = () => {
+ if ( ! window.confirm( __( 'Attention: We are going to DELETE ALL ACTIVITIES from the database. Are you sure you want to do that?', 'aryo-activity-log' ) ) ) {
+ return;
+ }
+
+ setErasing( true );
+ setNotice( null );
+
+ eraseLogs()
+ .then( () => {
+ setNotice( { status: 'success', message: __( 'All activities have been successfully deleted.', 'aryo-activity-log' ) } );
+ setErasing( false );
+ } )
+ .catch( ( err ) => {
+ setNotice( { status: 'error', message: err.message || __( 'Failed to delete activities.', 'aryo-activity-log' ) } );
+ setErasing( false );
+ } );
+ };
+
+ const updateValue = ( key, val ) => {
+ setValues( ( prev ) => ( { ...prev, [ key ]: val } ) );
+ };
+
+ if ( loading ) {
+ return (
+
+
+
+ );
+ }
+
+ if ( ! schema ) {
+ return null;
+ }
+
+ return (
+
+ { notice && (
+
setNotice( null ) }
+ style={ { margin: '0 0 16px' } }
+ >
+ { notice.message }
+
+ ) }
+
+
+
+ { schema.logs_lifespan && (
+
+ |
+
+ |
+
+ updateValue( 'logs_lifespan', e.target.value ) }
+ />
+ { ' ' }
+ { schema.logs_lifespan.suffix }
+ { schema.logs_lifespan.description && (
+ { schema.logs_lifespan.description }
+ ) }
+ |
+
+ ) }
+
+ { schema.logs_failed_login && (
+
+ |
+
+ |
+
+ updateValue( 'logs_failed_login', v ) }
+ __nextHasNoMarginBottom
+ />
+ |
+
+ ) }
+
+ { schema.logs_email && (
+
+ |
+
+ |
+
+ updateValue( 'logs_email', v ) }
+ __nextHasNoMarginBottom
+ />
+ |
+
+ ) }
+
+ { schema.log_visitor_ip_source && (
+
+ |
+
+ |
+
+ updateValue( 'log_visitor_ip_source', v ) }
+ __nextHasNoMarginBottom
+ />
+ { schema.log_visitor_ip_source.description && (
+ { schema.log_visitor_ip_source.description }
+ ) }
+ |
+
+ ) }
+
+ { canEraseLogs && (
+
+ |
+ { __( 'Delete Log Activities', 'aryo-activity-log' ) }
+ |
+
+
+
+ { __( 'Warning: Clicking this will delete all activities from the database.', 'aryo-activity-log' ) }
+
+ |
+
+ ) }
+
+
+
+
+
+
+
+ );
+}
diff --git a/changelog.txt b/changelog.txt
index f582b95..149c2de 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,8 @@
== Changelog ==
+= 2.15.0 - 2026-09-07 =
+* New: Settings page rebuilt with React UI and REST API
+
= 2.14.1 - 2026-09-02 =
* Tweak: Moved Application Password badge from Source column to User column
diff --git a/classes/class-aal-rest.php b/classes/class-aal-rest.php
index 31c3af3..643b69b 100644
--- a/classes/class-aal-rest.php
+++ b/classes/class-aal-rest.php
@@ -25,6 +25,25 @@ public function register_routes() {
'permission_callback' => array( $this, 'check_permissions' ),
) );
+ register_rest_route( self::NAMESPACE_V1, '/settings', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_settings' ),
+ 'permission_callback' => array( $this, 'check_settings_permissions' ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'update_settings' ),
+ 'permission_callback' => array( $this, 'check_settings_permissions' ),
+ ),
+ ) );
+
+ register_rest_route( self::NAMESPACE_V1, '/logs/erase', array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'erase_logs' ),
+ 'permission_callback' => array( $this, 'check_settings_permissions' ),
+ ) );
+
register_rest_route( self::NAMESPACE_V1, '/promotions/(?P[a-z_]+)/dismiss', array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'dismiss_promotion' ),
@@ -55,6 +74,173 @@ public function check_permissions() {
);
}
+ public function check_settings_permissions( WP_REST_Request $request ) {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return new WP_Error(
+ 'rest_forbidden',
+ __( 'You do not have permission to manage settings.', 'aryo-activity-log' ),
+ array( 'status' => \WP_Http::FORBIDDEN )
+ );
+ }
+
+ $nonce = $request->get_header( 'X-AAL-Settings-Nonce' );
+
+ if ( ! $nonce || ! wp_verify_nonce( $nonce, 'aal_settings' ) ) {
+ return new WP_Error(
+ 'rest_forbidden',
+ __( 'Invalid or missing settings nonce.', 'aryo-activity-log' ),
+ array( 'status' => \WP_Http::FORBIDDEN )
+ );
+ }
+
+ return true;
+ }
+
+ private static $allowed_ip_sources = array(
+ 'REMOTE_ADDR',
+ 'HTTP_CF_CONNECTING_IP',
+ 'HTTP_TRUE_CLIENT_IP',
+ 'HTTP_CLIENT_IP',
+ 'HTTP_X_FORWARDED_FOR',
+ 'HTTP_X_FORWARDED',
+ 'HTTP_X_CLUSTER_CLIENT_IP',
+ 'HTTP_FORWARDED_FOR',
+ 'HTTP_FORWARDED',
+ 'no-collect-ip',
+ );
+
+ public function get_settings( WP_REST_Request $request ) {
+ $options = AAL_Main::instance()->settings->get_options();
+
+ $fields = array(
+ 'logs_lifespan' => array(
+ 'value' => isset( $options['logs_lifespan'] ) ? $options['logs_lifespan'] : '',
+ 'label' => __( 'Keep logs for', 'aryo-activity-log' ),
+ 'type' => 'number',
+ 'description' => __( 'Maximum number of days to keep activity log. Leave blank to keep activity log forever (not recommended).', 'aryo-activity-log' ),
+ 'suffix' => __( 'days.', 'aryo-activity-log' ),
+ ),
+ 'logs_failed_login' => array(
+ 'value' => isset( $options['logs_failed_login'] ) ? $options['logs_failed_login'] : 'yes',
+ 'label' => __( 'Keep Failed Login Logs', 'aryo-activity-log' ),
+ 'type' => 'select',
+ 'options' => array(
+ array( 'value' => 'yes', 'label' => __( 'Keep', 'aryo-activity-log' ) ),
+ array( 'value' => 'no', 'label' => __( "Don't Keep (Not recommended)", 'aryo-activity-log' ) ),
+ ),
+ ),
+ 'logs_email' => array(
+ 'value' => isset( $options['logs_email'] ) ? $options['logs_email'] : 'yes',
+ 'label' => __( 'Keep Email Logs', 'aryo-activity-log' ),
+ 'type' => 'select',
+ 'options' => array(
+ array( 'value' => 'yes', 'label' => __( 'Keep', 'aryo-activity-log' ) ),
+ array( 'value' => 'no', 'label' => __( "Don't Keep", 'aryo-activity-log' ) ),
+ ),
+ ),
+ 'log_visitor_ip_source' => array(
+ 'value' => isset( $options['log_visitor_ip_source'] ) ? $options['log_visitor_ip_source'] : 'REMOTE_ADDR',
+ 'label' => __( 'Visitor IP Detected', 'aryo-activity-log' ),
+ 'type' => 'select',
+ 'description' => __( 'Select the source of the visitor IP address. For example, if you are using Cloudflare, select HTTP_CF_CONNECTING_IP. Please note: if you choose "Do not collect IP", the IP column will be hidden in the log.', 'aryo-activity-log' ),
+ 'options' => array(
+ array( 'value' => 'REMOTE_ADDR', 'label' => 'REMOTE_ADDR' ),
+ array( 'value' => 'HTTP_CF_CONNECTING_IP', 'label' => 'HTTP_CF_CONNECTING_IP' ),
+ array( 'value' => 'HTTP_TRUE_CLIENT_IP', 'label' => 'HTTP_TRUE_CLIENT_IP' ),
+ array( 'value' => 'HTTP_CLIENT_IP', 'label' => 'HTTP_CLIENT_IP' ),
+ array( 'value' => 'HTTP_X_FORWARDED_FOR', 'label' => 'HTTP_X_FORWARDED_FOR' ),
+ array( 'value' => 'HTTP_X_FORWARDED', 'label' => 'HTTP_X_FORWARDED' ),
+ array( 'value' => 'HTTP_X_CLUSTER_CLIENT_IP', 'label' => 'HTTP_X_CLUSTER_CLIENT_IP' ),
+ array( 'value' => 'HTTP_FORWARDED_FOR', 'label' => 'HTTP_FORWARDED_FOR' ),
+ array( 'value' => 'HTTP_FORWARDED', 'label' => 'HTTP_FORWARDED' ),
+ array( 'value' => 'no-collect-ip', 'label' => __( 'Do not collect IP', 'aryo-activity-log' ) ),
+ ),
+ ),
+ );
+
+ return rest_ensure_response( array(
+ 'fields' => $fields,
+ 'canEraseLogs' => (bool) apply_filters( 'aal_allow_option_erase_logs', true ),
+ ) );
+ }
+
+ public function update_settings( WP_REST_Request $request ) {
+ $current = AAL_Main::instance()->settings->get_options();
+ $body = $request->get_json_params();
+
+ $allowed_keys = array( 'logs_lifespan', 'logs_failed_login', 'logs_email', 'log_visitor_ip_source' );
+ $sanitized = array();
+
+ foreach ( $allowed_keys as $key ) {
+ if ( ! array_key_exists( $key, $body ) ) {
+ continue;
+ }
+
+ $value = $body[ $key ];
+
+ switch ( $key ) {
+ case 'logs_lifespan':
+ if ( '' === $value || null === $value ) {
+ $sanitized[ $key ] = '';
+ } elseif ( ! is_numeric( $value ) || (int) $value < 0 ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'logs_lifespan must be a non-negative number or empty.', 'aryo-activity-log' ),
+ array( 'status' => \WP_Http::BAD_REQUEST )
+ );
+ } else {
+ $sanitized[ $key ] = (string) absint( $value );
+ }
+ break;
+
+ case 'logs_failed_login':
+ case 'logs_email':
+ if ( ! in_array( $value, array( 'yes', 'no' ), true ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ /* translators: %s: option key name */
+ sprintf( __( '%s must be "yes" or "no".', 'aryo-activity-log' ), $key ),
+ array( 'status' => \WP_Http::BAD_REQUEST )
+ );
+ }
+ $sanitized[ $key ] = $value;
+ break;
+
+ case 'log_visitor_ip_source':
+ if ( ! in_array( $value, self::$allowed_ip_sources, true ) ) {
+ return new WP_Error(
+ 'rest_invalid_param',
+ __( 'Invalid IP source.', 'aryo-activity-log' ),
+ array( 'status' => \WP_Http::BAD_REQUEST )
+ );
+ }
+ $sanitized[ $key ] = $value;
+ break;
+ }
+ }
+
+ $output = array_merge( $current, $sanitized );
+ $output = apply_filters( 'aal_validate_options', $output, $current );
+
+ update_option( 'activity-log-settings', $output );
+
+ return rest_ensure_response( array( 'success' => true ) );
+ }
+
+ public function erase_logs( WP_REST_Request $request ) {
+ if ( ! apply_filters( 'aal_allow_option_erase_logs', true ) ) {
+ return new WP_Error(
+ 'rest_forbidden',
+ __( 'Log erasure is disabled.', 'aryo-activity-log' ),
+ array( 'status' => \WP_Http::FORBIDDEN )
+ );
+ }
+
+ AAL_Main::instance()->api->erase_all_items();
+
+ return rest_ensure_response( array( 'success' => true ) );
+ }
+
private function get_logs_args() {
return array(
'page' => array(
diff --git a/classes/class-aal-settings.php b/classes/class-aal-settings.php
index ed72167..cbb225f 100644
--- a/classes/class-aal-settings.php
+++ b/classes/class-aal-settings.php
@@ -10,11 +10,7 @@ public function __construct() {
add_action( 'init', array( &$this, 'init' ) );
add_action( 'admin_menu', array( &$this, 'action_admin_menu' ), 30 );
add_action( 'admin_init', array( &$this, 'register_settings' ) );
- add_action( 'admin_notices', array( &$this, 'admin_notices' ) );
- add_action( 'admin_footer', array( &$this, 'admin_footer' ) );
add_filter( 'plugin_action_links_' . ACTIVITY_LOG_BASE, array( &$this, 'plugin_action_links' ) );
-
- add_action( 'wp_ajax_aal_reset_items', array( &$this, 'ajax_aal_reset_items' ) );
}
public function init() {
@@ -39,191 +35,74 @@ public function plugin_action_links( $links ) {
public function action_admin_menu() {
$this->hook = add_submenu_page(
'activity-log-page',
- __( 'Activity Log Settings', 'aryo-activity-log' ), // tag
- __( 'Settings', 'aryo-activity-log' ), // menu label
- 'manage_options', // required cap to view this page
- $this->slug, // page slug
- array( &$this, 'display_settings_page' ) // callback
+ __( 'Activity Log Settings', 'aryo-activity-log' ),
+ __( 'Settings', 'aryo-activity-log' ),
+ 'manage_options',
+ $this->slug,
+ array( &$this, 'display_settings_page' )
);
- // register scripts & styles, specific for the settings page
- add_action( "admin_print_scripts-{$this->hook}", array( &$this, 'scripts_n_styles' ) );
- // this callback will initialize the settings for AAL
- // add_action( "load-$this->hook", array( $this, 'register_settings' ) );
- }
-
- /**
- * Register scripts & styles
- *
- * @since 1.0
- */
- public function scripts_n_styles() {
- wp_enqueue_style( 'aal-settings', plugins_url( 'assets/css/settings.css', ACTIVITY_LOG__FILE__ ) );
+ add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_settings_scripts' ) );
}
- public function register_settings() {
- // If no options exist, create them.
- if ( ! get_option( $this->slug ) ) {
- update_option( $this->slug, apply_filters( 'aal_default_options', array(
- 'logs_lifespan' => '30',
- 'logs_failed_login' => 'yes',
- 'logs_email' => 'yes',
- ) ) );
+ public function enqueue_settings_scripts( $hook ) {
+ if ( ! isset( $this->hook ) || $hook !== $this->hook ) {
+ return;
}
- register_setting( 'aal-options', $this->slug, array( $this, 'validate_options' ) );
- $section = $this->get_setup_section();
-
- switch ( $section ) {
- case 'general':
- // First, we register a section. This is necessary since all future options must belong to a
- add_settings_section(
- 'general_settings_section', // ID used to identify this section and with which to register options
- __( 'Display Options', 'aryo-activity-log' ), // Title to be displayed on the administration page
- array( 'AAL_Settings_Fields', 'general_settings_section_header' ), // Callback used to render the description of the section
- $this->slug // Page on which to add this section of options
- );
-
- add_settings_field(
- 'logs_lifespan',
- __( 'Keep logs for', 'aryo-activity-log' ),
- array( 'AAL_Settings_Fields', 'number_field' ),
- $this->slug,
- 'general_settings_section',
- array(
- 'id' => 'logs_lifespan',
- 'page' => $this->slug,
- 'classes' => array( 'small-text' ),
- 'type' => 'number',
- 'sub_desc' => __( 'days.', 'aryo-activity-log' ),
- 'desc' => __( 'Maximum number of days to keep activity log. Leave blank to keep activity log forever (not recommended).', 'aryo-activity-log' ),
- )
- );
-
- add_settings_field(
- 'logs_failed_login',
- __( 'Keep Failed Login Logs', 'aryo-activity-log' ),
- array( 'AAL_Settings_Fields', 'select_field' ),
- $this->slug,
- 'general_settings_section',
- array(
- 'id' => 'logs_failed_login',
- 'page' => $this->slug,
- 'type' => 'select',
- 'options' => array(
- 'yes' => __( 'Keep', 'aryo-activity-log' ),
- 'no' => __( "Don't Keep (Not recommended)", 'aryo-activity-log' ),
- ),
- )
- );
-
- add_settings_field(
- 'logs_email',
- __( 'Keep Email Logs', 'aryo-activity-log' ),
- array( 'AAL_Settings_Fields', 'select_field' ),
- $this->slug,
- 'general_settings_section',
- array(
- 'id' => 'logs_email',
- 'page' => $this->slug,
- 'type' => 'select',
- 'options' => array(
- 'yes' => __( 'Keep', 'aryo-activity-log' ),
- 'no' => __( "Don't Keep", 'aryo-activity-log' ),
- ),
- )
- );
-
- add_settings_field(
- 'log_visitor_ip_source',
- __( 'Visitor IP Detected', 'aryo-activity-log' ),
- array( 'AAL_Settings_Fields', 'select_field' ),
- $this->slug,
- 'general_settings_section',
- array(
- 'id' => 'log_visitor_ip_source',
- 'page' => $this->slug,
- 'type' => 'select',
- 'options' => array(
- 'REMOTE_ADDR' => 'REMOTE_ADDR',
- 'HTTP_CF_CONNECTING_IP' => 'HTTP_CF_CONNECTING_IP',
- 'HTTP_TRUE_CLIENT_IP' => 'HTTP_TRUE_CLIENT_IP',
- 'HTTP_CLIENT_IP' => 'HTTP_CLIENT_IP',
- 'HTTP_X_FORWARDED_FOR' => 'HTTP_X_FORWARDED_FOR',
- 'HTTP_X_FORWARDED' => 'HTTP_X_FORWARDED',
- 'HTTP_X_CLUSTER_CLIENT_IP' => 'HTTP_X_CLUSTER_CLIENT_IP',
- 'HTTP_FORWARDED_FOR' => 'HTTP_FORWARDED_FOR',
- 'HTTP_FORWARDED' => 'HTTP_FORWARDED',
- 'no-collect-ip' => __( 'Do not collect IP', 'aryo-activity-log' ),
- ),
- 'desc' => __( 'Select the source of the visitor IP address. For example, if you are using Cloudflare, select HTTP_CF_CONNECTING_IP.', 'aryo-activity-log' )
- . '
'
- . __( 'Please note: if you choose "Do not collect IP", the IP column will be hidden in the log.', 'aryo-activity-log' ),
- )
- );
+ $build_dir = plugin_dir_path( ACTIVITY_LOG__FILE__ ) . 'assets/js/admin/build/';
+ $asset_file = $build_dir . 'settings-index.asset.php';
- if ( apply_filters( 'aal_allow_option_erase_logs', true ) ) {
- add_settings_field(
- 'raw_delete_log_activities',
- __( 'Delete Log Activities', 'aryo-activity-log' ),
- array( 'AAL_Settings_Fields', 'raw_html' ),
- $this->slug,
- 'general_settings_section',
- array(
- 'html' => sprintf( __( 'Reset Database', 'aryo-activity-log' ), add_query_arg( array(
- 'action' => 'aal_reset_items',
- '_nonce' => wp_create_nonce( 'aal_reset_items' ),
- ), admin_url( 'admin-ajax.php' ) ), 'aal-delete-log-activities' ),
- 'desc' => __( 'Warning: Clicking this will delete all activities from the database.', 'aryo-activity-log' ),
- )
- );
- }
- break;
+ if ( ! file_exists( $asset_file ) || ! file_exists( $build_dir . 'settings-index.js' ) ) {
+ wp_admin_notice(
+ esc_html__( 'Activity Log: admin assets are not built. Run `npm run build` in the plugin directory.', 'aryo-activity-log' ),
+ array( 'type' => 'error' )
+ );
+ return;
}
- }
- /**
- * Returns the current section within AAL's setting pages
- *
- * @return string
- */
- public function get_setup_section() {
- if ( isset( $_REQUEST['aal_section'] ) )
- return strtolower( $_REQUEST['aal_section'] );
+ $asset = include $asset_file;
- return 'general';
- }
+ wp_enqueue_script(
+ 'aal-settings-app',
+ plugins_url( 'assets/js/admin/build/settings-index.js', ACTIVITY_LOG__FILE__ ),
+ $asset['dependencies'],
+ $asset['version'],
+ true
+ );
- /**
- * Prints section tabs within the settings area
- */
- private function menu_print_tabs() {
- $current_section = $this->get_setup_section();
- $sections = array(
- 'general' => __( 'General', 'aryo-activity-log' ),
+ wp_enqueue_style( 'wp-components' );
+
+ $bootstrap = array(
+ 'restBase' => rest_url( AAL_REST::NAMESPACE_V1 ),
+ 'settingsNonce' => wp_create_nonce( 'aal_settings' ),
);
- $sections = apply_filters( 'aal_setup_sections', $sections );
+ wp_add_inline_script(
+ 'aal-settings-app',
+ 'window.aalAdmin = ' . wp_json_encode( $bootstrap ) . ';',
+ 'before'
+ );
- if ( 1 >= count( $sections ) ) {
- return;
- }
+ wp_set_script_translations( 'aal-settings-app', 'aryo-activity-log' );
+ }
- foreach ( $sections as $section_key => $section_caption ) {
- $active = $current_section === $section_key ? 'nav-tab-active' : '';
- $url = add_query_arg( 'aal_section', $section_key );
- echo '' . esc_html( $section_caption ) . '';
+ public function register_settings() {
+ if ( ! get_option( $this->slug ) ) {
+ update_option( $this->slug, apply_filters( 'aal_default_options', array(
+ 'logs_lifespan' => '30',
+ 'logs_failed_login' => 'yes',
+ 'logs_email' => 'yes',
+ ) ) );
}
}
public function validate_options( $input ) {
- $options = $this->options; // CTX,L1504
+ $options = $this->options;
- // @todo some data validation/sanitization should go here
$output = apply_filters( 'aal_validate_options', $input, $options );
- // merge with current settings
$output = array_merge( $options, $output );
return $output;
@@ -231,62 +110,13 @@ public function validate_options( $input ) {
public function display_settings_page() {
?>
-
-
-
- menu_print_tabs(); ?>
-
-
-
-
+
+
%s
', esc_html__( 'All activities have been successfully deleted.', 'aryo-activity-log' ) );
- break;
- }
- }
-
- public function admin_footer() {
- // TODO: move to a separate file.
- ?>
-
- api->erase_all_items();
-
- wp_redirect( add_query_arg( array(
- 'page' => 'activity-log-settings',
- 'message' => 'data_erased',
- ), admin_url( 'admin.php' ) ) );
- die();
- }
-
public function get_option( $key = '' ) {
$settings = $this->get_options();
return ! empty( $settings[ $key ] ) ? $settings[ $key ] : false;
@@ -299,7 +129,6 @@ public function get_option( $key = '' ) {
* @return array
*/
public function get_options() {
- // Allow other plugins to get AAL's options.
if ( isset( $this->options ) && is_array( $this->options ) && ! empty( $this->options ) )
return $this->options;
@@ -310,127 +139,3 @@ public function slug() {
return $this->slug;
}
}
-
-// TODO: Need rewrite this class to useful tool.
-final class AAL_Settings_Fields {
-
- public static function general_settings_section_header() {
- ?>
-
-
-
- array(),
- ) );
- if ( empty( $args['id'] ) || empty( $args['page'] ) )
- return;
-
- ?>
-
-
-
- array(),
- 'rows' => 5,
- 'cols' => 50,
- ) );
-
- if ( empty( $args['id'] ) || empty( $args['page'] ) )
- return;
-
- ?>
-
-
-
-
- array(),
- 'min' => '1',
- 'step' => '1',
- 'desc' => '',
- ) );
- if ( empty( $args['id'] ) || empty( $args['page'] ) )
- return;
-
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- settings->get_option( $args['id'] );
- }
- }
-}
diff --git a/package.json b/package.json
index 06314fb..26abbfe 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"description": "Activity Log",
"version": "2.14.1",
"scripts": {
- "build": "wp-scripts build assets/js/admin/src/index.js --output-path=assets/js/admin/build",
- "start": "wp-scripts start assets/js/admin/src/index.js --output-path=assets/js/admin/build",
+ "build": "wp-scripts build assets/js/admin/src/index.js assets/js/admin/src/settings-index.js --output-path=assets/js/admin/build",
+ "start": "wp-scripts start assets/js/admin/src/index.js assets/js/admin/src/settings-index.js --output-path=assets/js/admin/build",
"clean": "rimraf $npm_package_name $npm_package_name.*.zip",
"package": "npm run clean && npm run build && rsync -av --exclude-from=.build-rsync-exclude . $npm_package_name",
"package:zip": "npm run package && zip -r $npm_package_name.$npm_package_version.zip ./$npm_package_name/*",
diff --git a/readme.txt b/readme.txt
index cf7fec6..87f8818 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
-=== Activity Log - Monitor & Record User Changes ===
+=== Activity Log – Monitor User and Agent Changes ===
Contributors: elemntor, KingYes, ariel.k
Tags: Activity Log, User Log, Audit Log, Security, Email Log,
Requires at least: 6.2
@@ -143,6 +143,9 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro
== Changelog ==
+= 2.15.0 - 2026-09-07 =
+* New: Settings page rebuilt with React UI and REST API
+
= 2.14.1 - 2026-09-02 =
* Tweak: Moved Application Password badge from Source column to User column
diff --git a/tests/phpunit/test-settings-rest.php b/tests/phpunit/test-settings-rest.php
new file mode 100644
index 0000000..4ea6b54
--- /dev/null
+++ b/tests/phpunit/test-settings-rest.php
@@ -0,0 +1,210 @@
+admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ $this->editor = self::factory()->user->create( array( 'role' => 'editor' ) );
+
+ wp_set_current_user( $this->admin );
+ $this->nonce = wp_create_nonce( 'aal_settings' );
+
+ update_option( 'activity-log-settings', array(
+ 'logs_lifespan' => '30',
+ 'logs_failed_login' => 'yes',
+ 'logs_email' => 'yes',
+ 'log_visitor_ip_source' => 'REMOTE_ADDR',
+ ) );
+
+ $this->reset_settings_cache();
+ }
+
+ private function reset_settings_cache() {
+ $settings = AAL_Main::instance()->settings;
+ $ref = new ReflectionProperty( $settings, 'options' );
+ $ref->setAccessible( true );
+ $ref->setValue( $settings, null );
+ }
+
+ private function do_request( $method, $route, $body = array(), $user = null, $nonce = null ) {
+ if ( null !== $user ) {
+ wp_set_current_user( $user );
+ }
+
+ $request = new WP_REST_Request( $method, '/activity-log/v1/' . $route );
+ $request->set_header( 'Content-Type', 'application/json' );
+ $request->set_header( 'X-AAL-Settings-Nonce', null !== $nonce ? $nonce : $this->nonce );
+
+ if ( ! empty( $body ) ) {
+ $request->set_body( wp_json_encode( $body ) );
+ }
+
+ return rest_get_server()->dispatch( $request );
+ }
+
+ // --- Permission tests ---
+
+ public function test_editor_cannot_get_settings() {
+ wp_set_current_user( $this->editor );
+ $nonce = wp_create_nonce( 'aal_settings' );
+
+ $response = $this->do_request( 'GET', 'settings', array(), $this->editor, $nonce );
+ $this->assertSame( 403, $response->get_status() );
+ }
+
+ public function test_editor_cannot_update_settings() {
+ wp_set_current_user( $this->editor );
+ $nonce = wp_create_nonce( 'aal_settings' );
+
+ $response = $this->do_request( 'PUT', 'settings', array( 'logs_lifespan' => '60' ), $this->editor, $nonce );
+ $this->assertSame( 403, $response->get_status() );
+ }
+
+ public function test_editor_cannot_erase_logs() {
+ wp_set_current_user( $this->editor );
+ $nonce = wp_create_nonce( 'aal_settings' );
+
+ $response = $this->do_request( 'POST', 'logs/erase', array(), $this->editor, $nonce );
+ $this->assertSame( 403, $response->get_status() );
+ }
+
+ // --- Nonce tests ---
+
+ public function test_missing_nonce_forbidden() {
+ $response = $this->do_request( 'GET', 'settings', array(), $this->admin, '' );
+ $this->assertSame( 403, $response->get_status() );
+ }
+
+ public function test_invalid_nonce_forbidden() {
+ $response = $this->do_request( 'GET', 'settings', array(), $this->admin, 'bad-nonce' );
+ $this->assertSame( 403, $response->get_status() );
+ }
+
+ // --- GET settings ---
+
+ public function test_get_settings_returns_fields_and_erase() {
+ $response = $this->do_request( 'GET', 'settings' );
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data();
+ $this->assertArrayHasKey( 'fields', $data );
+ $this->assertArrayHasKey( 'canEraseLogs', $data );
+ $this->assertArrayHasKey( 'logs_lifespan', $data['fields'] );
+ $this->assertArrayHasKey( 'logs_failed_login', $data['fields'] );
+ $this->assertArrayHasKey( 'logs_email', $data['fields'] );
+ $this->assertArrayHasKey( 'log_visitor_ip_source', $data['fields'] );
+ }
+
+ // --- PUT settings (sanitization) ---
+
+ public function test_update_valid_settings() {
+ $response = $this->do_request( 'PUT', 'settings', array(
+ 'logs_lifespan' => '60',
+ 'logs_failed_login' => 'no',
+ 'logs_email' => 'no',
+ 'log_visitor_ip_source' => 'HTTP_CF_CONNECTING_IP',
+ ) );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $this->reset_settings_cache();
+ $saved = get_option( 'activity-log-settings' );
+ $this->assertSame( '60', $saved['logs_lifespan'] );
+ $this->assertSame( 'no', $saved['logs_failed_login'] );
+ $this->assertSame( 'no', $saved['logs_email'] );
+ $this->assertSame( 'HTTP_CF_CONNECTING_IP', $saved['log_visitor_ip_source'] );
+ }
+
+ public function test_update_empty_lifespan_keeps_forever() {
+ $response = $this->do_request( 'PUT', 'settings', array(
+ 'logs_lifespan' => '',
+ ) );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $this->reset_settings_cache();
+ $saved = get_option( 'activity-log-settings' );
+ $this->assertSame( '', $saved['logs_lifespan'] );
+ }
+
+ public function test_update_invalid_ip_source_rejected() {
+ $response = $this->do_request( 'PUT', 'settings', array(
+ 'log_visitor_ip_source' => 'INVALID_HEADER',
+ ) );
+
+ $this->assertSame( 400, $response->get_status() );
+
+ $this->reset_settings_cache();
+ $saved = get_option( 'activity-log-settings' );
+ $this->assertSame( 'REMOTE_ADDR', $saved['log_visitor_ip_source'] );
+ }
+
+ public function test_update_invalid_yesno_rejected() {
+ $response = $this->do_request( 'PUT', 'settings', array(
+ 'logs_failed_login' => 'maybe',
+ ) );
+
+ $this->assertSame( 400, $response->get_status() );
+ }
+
+ public function test_update_ignores_unknown_keys() {
+ $response = $this->do_request( 'PUT', 'settings', array(
+ 'logs_lifespan' => '45',
+ 'evil_key' => 'injected',
+ ) );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $this->reset_settings_cache();
+ $saved = get_option( 'activity-log-settings' );
+ $this->assertSame( '45', $saved['logs_lifespan'] );
+ $this->assertArrayNotHasKey( 'evil_key', $saved );
+ }
+
+ public function test_update_merges_with_existing() {
+ $response = $this->do_request( 'PUT', 'settings', array(
+ 'logs_lifespan' => '90',
+ ) );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ $this->reset_settings_cache();
+ $saved = get_option( 'activity-log-settings' );
+ $this->assertSame( '90', $saved['logs_lifespan'] );
+ $this->assertSame( 'yes', $saved['logs_failed_login'] );
+ $this->assertSame( 'REMOTE_ADDR', $saved['log_visitor_ip_source'] );
+ }
+
+ public function test_update_negative_lifespan_rejected() {
+ $response = $this->do_request( 'PUT', 'settings', array(
+ 'logs_lifespan' => '-5',
+ ) );
+
+ $this->assertSame( 400, $response->get_status() );
+ }
+
+ // --- Erase ---
+
+ public function test_erase_logs_success() {
+ $response = $this->do_request( 'POST', 'logs/erase' );
+ $this->assertSame( 200, $response->get_status() );
+
+ $data = $response->get_data();
+ $this->assertTrue( $data['success'] );
+ }
+
+ public function test_erase_blocked_by_filter() {
+ add_filter( 'aal_allow_option_erase_logs', '__return_false' );
+
+ $response = $this->do_request( 'POST', 'logs/erase' );
+ $this->assertSame( 403, $response->get_status() );
+
+ remove_filter( 'aal_allow_option_erase_logs', '__return_false' );
+ }
+}