Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- 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