<?php
|
|
namespace JVBase\rest\routes;
|
|
use JVBase\managers\queue\executors\ContentExecutor;
|
use JVBase\managers\queue\TypeConfig;
|
use JVBase\meta\Meta;
|
use JVBase\registrar\Registrar;
|
use JVBase\rest\PermissionHandler;
|
use JVBase\rest\Response;
|
use JVBase\rest\Rest;
|
use JVBase\rest\Route;
|
use WP_Post;
|
use WP_Query;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use WP_Term;
|
use WP_Term_Query;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
class ContentRoutes extends Rest
|
{
|
protected array $fields = [];
|
protected array $taxonomies = [];
|
protected string $post_type = '';
|
protected string $user_id = '';
|
|
//For Timeline-specific posts
|
protected array $timelineSharedFields = [];
|
protected array $timelineUniqueFields = [];
|
protected static ?string $action = 'dash-';
|
protected Meta $meta;
|
|
public function __construct()
|
{
|
$this->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;
|
}
|
}
|