cacheName = 'user_content_' . get_current_user_id(); parent::__construct(); if (JVB_TESTING) { $this->cache->flush(); } $this->cache->connect('post', true); $this->cache->connect('term', true); add_action('init', [$this, 'registerContentExecutors'], 5); } /** * Register content operation types with the queue's TypeRegistry */ public function registerContentExecutors(): void { $registry = JVB()->queue()->registry(); $executor = new ContentExecutor(); // Content updates - chunked at 10 posts $registry->register('content_update', new TypeConfig( executor: $executor, chunkKey: 'posts', chunkSize: 10 )); // Batch creation (from uploads) TODO: I believe this is all handled by UploadExecutor // $registry->register('batch_creation', new TypeConfig( // executor: $executor // )); } /** * Registers content routes * @return void */ public function registerRoutes(): void { Route::for('content') ->get([$this, 'getContent']) ->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']])) ->args([ 'content' => 'string|required', 'status' => 'string|default:all', 'page' => 'integer|default:1|min:1', 'per_page' => 'integer|default:30|min:1|max:100', 'orderby' => 'string|enum:date,alphabetical|default:date', 'order' => 'string|enum:asc,desc|default:desc', 'search' => 'string', 'date-filter' => 'string', 'dateFrom' => 'string', 'dateTo' => 'string', ]) ->rateLimit(20) ->post([$this, 'postContent']) ->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']])) ->rateLimit(30) ->args([ 'user' => 'int|required', 'posts' => 'required', 'content' => 'string', ]) ->register(); } protected function initTimelineFields(string $content): void { $content = jvbNoBase($content); $config = Registrar::getInstance($content); if (!$config || !$config->hasFeature('is_timeline')) { return; } $this->fields = $config->getFields(); $this->timelineSharedFields = $this->getTimelineSharedFields($content); array_unshift($this->timelineSharedFields, 'post_thumbnail'); array_unshift($this->timelineSharedFields, 'post_title'); array_unshift($this->timelineSharedFields, 'post_status'); $this->timelineUniqueFields = $this->getTimelineUniqueFields($content); } public function getTimelineUniqueFields(string $content): array { $content = jvbNoBase($content); $registrar = Registrar::getInstance($content); if (!$registrar || !$registrar->hasFeature('is_timeline')) { return []; } $allFields = $registrar->getFields(); return array_keys(array_filter($allFields, function ($field) { if (array_key_exists('for_all', $field) && $field['for_all'] === true) { return true; } return false; })); } public function getTimelineSharedFields(string $content): array { $content = jvbNoBase($content); $registrar = Registrar::getInstance($content); if (!$registrar || !$registrar->hasFeature('is_timeline')) { return []; } $allFields = $registrar->getFields()??[]; return array_keys(array_filter($allFields, function ($field) { if (!array_key_exists('for_all', $field) || $field['for_all'] === false) { return true; } return false; })); } /** * Handle content update/creation * @param WP_REST_Request $request * * @return WP_REST_Response */ public function postContent(WP_REST_Request $request): WP_REST_Response { $data = $request->get_params(); $user_id = $data['user']; if (!array_key_exists('posts', $data) || !is_array($data['posts'])) { return Response::success(['message'=>'No posts found in request']); } $count = count($data['posts']); $operationId = $data['id']; unset($data['user']); unset($data['id']); error_log('[CONTENT]:'.print_r($data, true)); $queue = JVB()->queue(); $queue->queueOperation( 'content_update', $user_id, $data, [ 'operation_id' => $operationId ] ); return Response::queued($operationId); } /** * Handle request * @param WP_REST_Request $request * * @return WP_REST_Response */ public function getContent(WP_REST_Request $request): WP_REST_Response { $params = $request->get_params(); error_log('getContent::params '.print_r($params, true)); $registrar = Registrar::getInstance($params['content']); switch ($registrar->getType()) { case 'term': return $this->getTerms($request, $params, $registrar); case 'user': //TODO maybe do something? break; case 'post': return $this->getPosts($request, $params, $registrar); } return $this->error('Something went wrong, this does not appear to have a proper content type'); } public function getPosts(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response { $user_id = $params['user']; $post_status = $params['status']; if ($post_status === 'all') { $post_status = ['publish', 'draft']; } else { $post_status = explode(',', $post_status); } $post_type = str_replace('-', '_', jvbCheckBase($params['content'])); // Build query args $args = [ 'post_type' => $post_type, 'posts_per_page' => $params['per_page'] ?? 30, 'paged' => $params['page'], 'orderby' => 'date', 'order' => 'DESC', 'author' => $user_id, 'post_status' => $post_status ]; //Only top level posts for timeline types if ($registrar?->hasFeature('is_timeline')) { $args['post_parent'] = 0; } //Calendar filters if ($registrar?->hasFeature('is_calendar')) { $args = $this->applyCalendarFilters($args, $params); } $taxonomies = array_filter($params, function ($param) { return str_starts_with($param, 'tax_'); }, ARRAY_FILTER_USE_KEY); if (!empty($taxonomies)) { $params['taxonomies'] = []; foreach ($taxonomies as $taxonomy => $terms) { $taxonomy = str_replace('tax_', '', $taxonomy); $params['taxonomies'][$taxonomy] = $terms; } } if (array_key_exists('taxonomies', $params)) { $args = $this->applyTaxonomyFilters($args, $params); } if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) { $args = $this->applyDateFilters($args, $params); } if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) { $args = $this->applyOrderFilters($args, $params); } if (array_key_exists('search', $params)) { $args['s'] = sanitize_text_field($params['search']); } $key = $this->cache->generateKey($args); $cached = $this->checkCache($key, $request); if ($cached) { return $cached; } $this->post_type = jvbCheckBase($params['content'] ?? $params['type']); if (array_key_exists('s', $args)) { $args = $this->applySearchFilters($args, $params); } // Run query $query = new WP_Query($args); $registrar = Registrar::getInstance($this->post_type); $this->fields = $registrar->getFields()??[]; $this->taxonomies = $this->getTaxonomies($this->post_type); $posts = array_map([$this, 'preparePost'], $query->posts); $data = [ 'items' => $posts, 'total' => $query->found_posts, 'total_pages' => $query->max_num_pages, 'has_more' => $args['paged']??1 < $query->max_num_pages, ]; $this->cache->set($key, $data); $response = Response::success($data); return $this->addCacheHeaders($response); } public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response { // Build query args $args = [ 'taxonomy' => jvbCheckBase($params['content']), 'number' => $params['per_page'] ?? 30, 'orderby' => 'name', 'order' => 'DESC', 'hide_empty' => false, ]; $paged = $params['page']??1; $args['page'] = $paged; if ($paged > 1) { $args['offset'] = ($paged-1) * $args['number']; } //TODO // if (array_key_exists('taxonomies', $params)) { // $args = $this->applyTaxonomyFilters($args, $params); // } // if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) { // $args = $this->applyDateFilters($args, $params); // } if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) { $args = $this->applyOrderFilters($args, $params); } if (array_key_exists('search', $params)) { $args['s'] = sanitize_text_field($params['search']); } $key = $this->cache->generateKey($args); // Check HTTP cache headers with the specific content type $cache_check = $this->checkHeaders($request, $key); if ($cache_check) { return $cache_check; } $cache = $this->cache->get($key); if ($cache) { $response = Response::success($cache); return $this->addCacheHeaders($response); } $this->post_type = jvbCheckBase($params['content'] ?? $params['type']); // Only expand search to taxonomies if we're actually going to query if (array_key_exists('s', $args)) { $args = $this->applySearchFilters($args, $params); } // Run query $query = new WP_Term_Query($args); $terms = $query->get_terms(); $data = [ 'total' => 0, 'total_pages' => 0, 'has_more' => false ]; if (!is_wp_error($terms) && !empty($terms)) { $total = get_terms([ 'taxonomy' => $args['taxonomy'], 'hide_empty' => false, 'fields' => 'count' ]); $data['total'] = $total; $data['total_pages'] = max($total/$args['number'], 1); $data['has_more'] = ($args['page'] * $args['number']) < $total; } else { $terms = []; } $this->fields = $registrar->getFields()??[]; $this->taxonomies = []; $data['items'] =array_map([$this, 'prepareTerm'], $terms); $this->cache->set($key, $data); $response = Response::success($data); return $this->addCacheHeaders($response); } protected function applySearchFilters(array $args, array $params): array { $search_term = sanitize_text_field($params['search']); // Search term is already in $args['s'] from earlier // Get all taxonomies registered to this post type $taxonomies = get_object_taxonomies($this->post_type, 'names'); if (empty($taxonomies)) { return $args; } // Cache the taxonomy term lookup per search term + post type $term_cache_key = 'search_terms_' . md5($search_term . $this->post_type); $matching_term_ids = $this->cache->get($term_cache_key); if ($matching_term_ids === false) { $matching_term_ids = []; foreach ($taxonomies as $taxonomy) { $terms = get_terms([ 'taxonomy' => $taxonomy, 'search' => $search_term, 'hide_empty' => false, 'fields' => 'ids' ]); if (!is_wp_error($terms) && !empty($terms)) { $matching_term_ids = array_merge($matching_term_ids, $terms); } } // Cache term IDs for 1 hour $this->cache->set($term_cache_key, $matching_term_ids, 3600); } if (empty($matching_term_ids)) { return $args; } // Build tax_query for matching terms $term_queries = []; foreach ($taxonomies as $taxonomy) { $taxonomy_term_ids = array_filter($matching_term_ids, function ($term_id) use ($taxonomy) { $term = get_term($term_id); return !is_wp_error($term) && $term->taxonomy === $taxonomy; }); if (!empty($taxonomy_term_ids)) { $term_queries[] = [ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => array_values($taxonomy_term_ids), 'operator' => 'IN' ]; } } if (!empty($term_queries)) { if (isset($args['tax_query'])) { $args['tax_query'] = [ 'relation' => 'OR', $args['tax_query'], [ 'relation' => 'OR', ...$term_queries ] ]; } else { $args['tax_query'] = [ 'relation' => 'OR', ...$term_queries ]; } } return $args; } /** * Gets allowed taxonomies for a particular content * @param string $content * * @return array */ protected function getTaxonomies(string $content): array { $registrar = Registrar::getInstance($content); if (!$registrar || $registrar->getType()!== 'post') { return []; } $out = []; foreach ($registrar->registrar->taxonomies as $tax) { $taxReg = Registrar::getInstance($tax); $out[jvbCheckBase($tax)] = [ 'label' => $taxReg->getPlural(), 'icon' => $taxReg->getIcon()??jvbDefaultIcon() ]; } return $out; } /** * Generates a post title, based on content type * @param string $content the post type * * @return string */ protected function generatePostTitle(string $content): string { $username = get_user_meta($this->user_id, 'first_name', true); $link = get_user_meta($this->user_id, BASE . 'link', true); $city = jvbArtistCity($link); return ucfirst($content) . ' by ' . $city . ' artist ' . $username; } /** * @param WP_Post $post the post object * * @return array */ protected function preparePost(WP_Post $post, bool $skip = false, bool $fields = true): array { $registrar = Registrar::getInstance($post->post_type); if (!$skip && $registrar && $registrar->hasFeature('is_timeline')) { $this->initTimelineFields($post->post_type); return $this->formatTimeline($post); } $this->meta = Meta::forPost($post->ID); $fields = ($fields) ? $this->meta->getAll() : []; $data = [ 'id' => $post->ID, 'title' => $post->post_title, 'status' => $post->post_status, 'date' => $post->post_date, 'modified' => $post->post_modified, 'thumbnail' => get_the_post_thumbnail_url($post->ID), 'alt' => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true), 'icon' => $this->post_type, 'taxonomies' => [], 'fields' => $fields, 'images' => [], ]; $images = $this->extractImages($fields, $this->meta); if (!empty($images)) { $data['images'] = $images; } $taxonomies = $this->extractTerms($fields, $this->meta); if (!empty($taxonomies)) { $data['taxonomies'] = $taxonomies; } return $data; } /** * @param WP_Term $post the post object * * @return array */ protected function prepareTerm(WP_Term $post, bool $fields = true): array { $registrar = Registrar::getInstance($post->taxonomy); $this->meta = Meta::forTerm($post->term_id); $fields = ($fields) ? $this->meta->getAll() : []; $data = [ 'id' => $post->term_id, 'title' => $post->name, 'date' => $fields['created_date']??'', 'modified' => $fields['modified_date']??'', 'thumbnail' => '', 'icon' => $registrar->getIcon(), 'taxonomies' => [], 'fields' => $fields, 'images' => [], ]; $images = $this->extractImages($fields, $this->meta); if (!empty($images)) { $data['images'] = $images; } $taxonomies = $this->extractTerms($fields, $this->meta); if (!empty($taxonomies)) { $data['taxonomies'] = $taxonomies; } error_log('Term data: '.print_r($data, true)); return $data; } public function formatTimeline(WP_Post $post): array { $item = $this->preparePost($post, true, false); //Step 1: Get the fields that apply to all posts $mainMeta = Meta::forPost($post->ID); $item['fields'] = $mainMeta->getAll($this->timelineSharedFields); //Step 2: Get the fields for each individual posts $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields' => 'ids']); array_unshift($children, $post->ID); $subFields = []; $images = []; foreach ($children as $child) { $meta = Meta::forPost($child); $f = $meta->getAll($this->timelineUniqueFields); $f = ['id' => $child] + $f; $subFields[] = $f; $images[$f['post_thumbnail']] = jvbImageData((int)$f['post_thumbnail']); } $item['fields']['timeline'] = $subFields; $item['images'] = $item['images'] + $images; $item['number'] = $mainMeta->get('number'); return $item; } }