diff --git a/classes/class-aal-export.php b/classes/class-aal-export.php index 697ed00..97bc34f 100644 --- a/classes/class-aal-export.php +++ b/classes/class-aal-export.php @@ -55,6 +55,7 @@ public function admin_capture_action( $list_table ) { $list_table->prepare_items(); $items = $list_table->items; $columns = $list_table->get_columns(); + $columns = $this->add_export_ip_column( $columns ); $op = array(); foreach ( $items as $item ) { @@ -184,6 +185,30 @@ public function increase_throughput( $records_per_page ) { return PHP_INT_MAX; } + private function add_export_ip_column( array $columns ): array { + if ( 'no-collect-ip' === AAL_Main::instance()->settings->get_option( 'log_visitor_ip_source' ) ) { + return $columns; + } + + $ip_label = __( 'IP', 'aryo-activity-log' ); + + $result = array(); + $inserted = false; + foreach ( $columns as $key => $label ) { + $result[ $key ] = $label; + if ( 'source' === $key ) { + $result['ip'] = $ip_label; + $inserted = true; + } + } + + if ( ! $inserted ) { + $result['ip'] = $ip_label; + } + + return $result; + } + private static function format_source_label( $raw ) { $parsed = AAL_API::parse_request_source( $raw ); $parts = array(); diff --git a/readme.txt b/readme.txt index 0851c13..7b3f349 100644 --- a/readme.txt +++ b/readme.txt @@ -143,6 +143,9 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == += 2.13.1 - 2026-08-26 = +* Fix: CSV export file missing IP column ([Topic](https://wordpress.org/support/topic/missing-source-ip-in-csv-export/)) + = 2.13.0 - 2026-08-24 = * Removed: Email notifications feature (hidden since 2.5 for sites that never enabled it) has been fully removed diff --git a/tests/phpunit/test-export.php b/tests/phpunit/test-export.php new file mode 100644 index 0000000..2a6fe74 --- /dev/null +++ b/tests/phpunit/test-export.php @@ -0,0 +1,143 @@ +setAccessible( true ); + + return $method->invoke( $export, $columns ); + } + + private function invoke_prep_row( AAL_Export $export, $item, array $columns ): array { + $method = new ReflectionMethod( AAL_Export::class, 'prep_row' ); + $method->setAccessible( true ); + + $list_table = $this->getMockBuilder( AAL_Activity_Log_List_Table::class ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'get_action_label' ] ) + ->getMock(); + + $list_table->method( 'get_action_label' ) + ->willReturnArgument( 0 ); + + return $method->invoke( $export, $item, $columns, $list_table ); + } + + private function set_ip_source_option( string $value ): void { + update_option( 'activity-log-settings', [ 'log_visitor_ip_source' => $value ] ); + + $settings = AAL_Main::instance()->settings; + $ref = new ReflectionProperty( $settings, 'options' ); + $ref->setAccessible( true ); + $ref->setValue( $settings, null ); + } + + public function test_ip_column_inserted_after_source() { + $this->set_ip_source_option( 'REMOTE_ADDR' ); + $export = $this->get_export_instance(); + + $columns = [ + 'date' => 'Date', + 'author' => 'User', + 'source' => 'Source', + 'type' => 'Topic', + ]; + + $result = $this->invoke_add_export_ip_column( $export, $columns ); + + $keys = array_keys( $result ); + $this->assertSame( [ 'date', 'author', 'source', 'ip', 'type' ], $keys ); + $this->assertSame( 'IP', $result['ip'] ); + } + + public function test_ip_column_omitted_when_no_collect() { + $this->set_ip_source_option( 'no-collect-ip' ); + $export = $this->get_export_instance(); + + $columns = [ + 'date' => 'Date', + 'author' => 'User', + 'source' => 'Source', + 'type' => 'Topic', + ]; + + $result = $this->invoke_add_export_ip_column( $export, $columns ); + + $this->assertArrayNotHasKey( 'ip', $result ); + $this->assertSame( [ 'date', 'author', 'source', 'type' ], array_keys( $result ) ); + } + + public function test_ip_column_appended_when_source_column_missing() { + $this->set_ip_source_option( 'REMOTE_ADDR' ); + $export = $this->get_export_instance(); + + $columns = [ + 'date' => 'Date', + 'author' => 'User', + 'type' => 'Topic', + ]; + + $result = $this->invoke_add_export_ip_column( $export, $columns ); + + $keys = array_keys( $result ); + $this->assertSame( [ 'date', 'author', 'type', 'ip' ], $keys ); + $this->assertSame( 'IP', $result['ip'] ); + } + + public function test_prep_row_populates_source_and_ip() { + $this->set_ip_source_option( 'REMOTE_ADDR' ); + $export = $this->get_export_instance(); + + $columns = [ + 'source' => 'Source', + 'ip' => 'IP', + ]; + + $item = (object) [ + 'hist_ip' => '10.0.0.1', + 'request_source' => 'rest', + 'hist_time' => time(), + 'user_id' => 0, + 'object_type' => 'Posts', + 'object_subtype' => 'post', + 'object_name' => 'hello', + 'action' => 'updated', + ]; + + $row = $this->invoke_prep_row( $export, $item, $columns ); + + $this->assertSame( '10.0.0.1', $row['ip'] ); + $this->assertNotEmpty( $row['source'] ); + } + + public function test_prep_row_ip_empty_source_when_no_request_source() { + $this->set_ip_source_option( 'REMOTE_ADDR' ); + $export = $this->get_export_instance(); + + $columns = [ + 'source' => 'Source', + 'ip' => 'IP', + ]; + + $item = (object) [ + 'hist_ip' => '192.168.1.1', + 'request_source' => '', + 'hist_time' => time(), + 'user_id' => 0, + 'object_type' => 'Posts', + 'object_subtype' => 'post', + 'object_name' => 'test', + 'action' => 'created', + ]; + + $row = $this->invoke_prep_row( $export, $item, $columns ); + + $this->assertSame( '192.168.1.1', $row['ip'] ); + $this->assertSame( '', $row['source'] ); + } +}