| | |
| | | <?php |
| | | namespace JVBase\meta; |
| | | |
| | | use DateTime; |
| | | use Exception; |
| | | use JVBase\utility\Features; |
| | | use WP_Post; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | |
| | | /** |
| | | * Core meta management class |
| | | */ |
| | | |
| | | /** |
| | | * @deprecated Use Meta() now |
| | | */ |
| | | class MetaManager |
| | | { |
| | | public MetaTypeManager $type_manager; |
| | | public MetaValidator $validator; |
| | | public MetaSanitizer $sanitizer; |
| | | public MetaRenderer $renderer; |
| | | public MetaForm $form; |
| | | public Validator $validator; |
| | | public Sanitizer $sanitizer; |
| | | public Render $renderer; |
| | | protected int|null $object_id; |
| | | public object|null $data; |
| | | protected array $fields =[]; |
| | |
| | | protected string|null $object_type; |
| | | protected int $max_file_size = 5242880; |
| | | protected ?string $content = null; |
| | | protected bool $isTimeline = false; |
| | | |
| | | protected ?string $baseKey = null; |
| | | protected \wpdb $wpdb; |
| | | protected array $postFields = [ |
| | | 'post_title', |
| | | 'post_excerpt', |
| | | 'post_content', |
| | | 'post_date', |
| | | 'post_status', |
| | | 'post_modified', |
| | | 'post_thumbnail' |
| | | |
| | | protected array $wpDefaults = [ |
| | | 'post' => [ |
| | | 'post_title', |
| | | 'post_excerpt', |
| | | 'post_content', |
| | | 'post_date', |
| | | 'post_status', |
| | | 'post_modified', |
| | | 'post_thumbnail', |
| | | 'menu_order' |
| | | ], |
| | | 'user' => [ |
| | | 'first_name', |
| | | 'last_name', |
| | | 'display_name', |
| | | 'description', |
| | | 'user_email', |
| | | ], |
| | | 'term' => [ |
| | | 'term_name', |
| | | 'description' |
| | | ] |
| | | ]; |
| | | |
| | | protected array $userFields = [ |
| | | 'first_name', |
| | | 'last_name', |
| | | 'description', |
| | | 'display_name', |
| | | 'user_email', |
| | | ]; |
| | | |
| | | protected array $termFields = [ |
| | | 'term_name', |
| | | 'description' |
| | | ]; |
| | | |
| | | public function __construct(?int $ID = null, ?string $type = null, ?string $content = null) |
| | | public function __construct(int|string|null $ID = null, ?string $type = null, ?string $content = null) |
| | | { |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->object_id = $ID; |
| | | |
| | | $this->object_id = is_int($ID) ? $ID : null; |
| | | $this->object_type = $type; |
| | | if ($ID) { |
| | | switch ($type) { |
| | | case 'post': |
| | | $this->data = get_post((string)$ID); |
| | | $this->content = jvbNoBase($this->data->post_type); |
| | | $this->isTimeline = Features::forContent($this->content)->has('is_timeline'); |
| | | break; |
| | | case 'term': |
| | | $this->data = get_term($ID); |
| | | $this->content = jvbNoBase($this->data->taxonomy); |
| | | break; |
| | | case 'user': |
| | | case 'integrations': |
| | | $this->data = get_user($ID); |
| | | $this->content = jvbUserRole($ID); |
| | | break; |
| | | case 'options': |
| | | $this->baseKey = $ID; |
| | | $this->data = null; |
| | | break; |
| | | default: |
| | | $this->data = null; |
| | | break; |
| | | } |
| | | } |
| | | $this->content = $content; |
| | | |
| | | $this->type_manager = new MetaTypeManager(); |
| | | $this->validator = new MetaValidator(); |
| | | $this->sanitizer = new MetaSanitizer(); |
| | | $this->renderer = new MetaRenderer(); |
| | | $this->form = new MetaForm(); |
| | | $this->validator = new Validator(); |
| | | $this->sanitizer = new Sanitizer(); |
| | | $this->renderer = new Render(); |
| | | } |
| | | |
| | | /** |
| | |
| | | public function getValue(string $name): mixed |
| | | { |
| | | //Get standard post fields first |
| | | switch ($name) { |
| | | case 'post_title': |
| | | return $this->data->post_title ?? ''; |
| | | case 'post_excerpt': |
| | | return $this->data->post_excerpt ?? ''; |
| | | case 'post_content': |
| | | return $this->data->post_content ?? ''; |
| | | case 'featured_image': |
| | | case 'post_thumbnail': |
| | | return get_post_thumbnail_id($this->object_id); |
| | | case 'display_name': |
| | | if (is_null($this->data) || !$this->data->display_name) { |
| | | $user = get_userdata((int)get_post_meta($this->object_id, BASE . 'link', true)); |
| | | return $user->display_name; |
| | | if (array_key_exists($this->object_type, $this->wpDefaults)) { |
| | | $defaults = $this->wpDefaults[$this->object_type]; |
| | | if (in_array($name, $defaults)) { |
| | | if (in_array($name, ['featured_image', 'post_thumbnail'])) { |
| | | return get_post_thumbnail_id($this->object_id); |
| | | } |
| | | return $this->data->display_name ?? ''; |
| | | case 'user_email': |
| | | if (is_null($this->data) || !$this->data->display_name) { |
| | | $user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true)); |
| | | return $user->user_email; |
| | | } |
| | | return $this->data->user_email ?? ''; |
| | | case 'term_name': |
| | | return htmlspecialchars_decode($this->data->name); |
| | | return match ($this->object_type) { |
| | | 'term' => $this->getTermField($name), |
| | | 'post' => $this->getPostField($name), |
| | | 'user' => $this->getUserField($name), |
| | | default => '' |
| | | }; |
| | | } |
| | | } |
| | | |
| | | |
| | | $meta_key = BASE . $name; |
| | | switch ($this->object_type) { |
| | | case 'post': |
| | |
| | | case 'integrations': |
| | | return get_user_meta($this->object_id, $meta_key, true); |
| | | case 'options': |
| | | return get_option($meta_key); |
| | | $key = $this->baseKey |
| | | ? BASE . $this->baseKey . '_' . $name |
| | | : BASE . $name; |
| | | return get_option($key); |
| | | default: |
| | | return ''; |
| | | } |
| | | } |
| | | |
| | | protected function getTermField(string $name): mixed |
| | | { |
| | | // WordPress handles entity decoding and filters |
| | | return match ($name) { |
| | | 'term_name' => get_term_field('name', $this->object_id), |
| | | 'description' => get_term_field('description', $this->object_id), |
| | | default => '' |
| | | }; |
| | | } |
| | | |
| | | protected function getPostField(string $name): mixed |
| | | { |
| | | return match ($name) { |
| | | 'post_title' => get_the_title($this->object_id), |
| | | 'post_excerpt' => get_the_excerpt($this->object_id), |
| | | 'post_content' => get_post_field('post_content', $this->object_id), |
| | | default => $this->data->$name ?? '' |
| | | }; |
| | | } |
| | | |
| | | protected function getUserField(string $name): mixed |
| | | { |
| | | return match ($name) { |
| | | 'display_name' => get_the_author_meta('display_name', $this->object_id), |
| | | 'user_email' => get_the_author_meta('user_email', $this->object_id), |
| | | 'first_name' => get_the_author_meta('first_name', $this->object_id), |
| | | 'last_name' => get_the_author_meta('last_name', $this->object_id), |
| | | default => $this->data->$name ?? '' |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * @param string $name |
| | | * |
| | |
| | | * |
| | | * @return bool |
| | | */ |
| | | public function updateValue(string $name, mixed $value): bool |
| | | public function updateValue(string $name, mixed $value, bool $updatePost = true): bool |
| | | { |
| | | try { |
| | | // Get field definition |
| | |
| | | $field_config['name'] = $name; |
| | | // Validate value |
| | | if (!$this->validator->validate($value, $field_config)) { |
| | | error_log('Validation unsuccessful'); |
| | | throw new Exception("Validation failed for {$name}"); |
| | | } |
| | | |
| | |
| | | return true; |
| | | } |
| | | |
| | | switch ($name) { |
| | | case 'post_title': |
| | | if (array_key_exists($this->object_type, $this->wpDefaults)) { |
| | | $check = $this->wpDefaults[$this->object_type]; |
| | | if (in_array($name, $check)) { |
| | | $ID = true; |
| | | if ($this->data->post_title !== $sanitized) { |
| | | $ID = wp_update_post([ |
| | | 'ID' => $this->object_id, |
| | | 'post_title' => $sanitized |
| | | ]); |
| | | if (in_array($name, ['featured_image', 'post_thumbnail'])) { |
| | | $old = get_post_thumbnail_id($this->object_id); |
| | | if ($old !== $sanitized) { |
| | | $ID = set_post_thumbnail($this->object_id, $sanitized); |
| | | } |
| | | return $ID !== false; |
| | | } |
| | | return ($ID !== 0); |
| | | case 'post_excerpt': |
| | | $ID = true; |
| | | if ($this->data->post_excerpt !== $sanitized) { |
| | | $ID = wp_update_post([ |
| | | 'ID' => $this->object_id, |
| | | 'post_excerpt' => $sanitized |
| | | ]); |
| | | } |
| | | return ($ID !== 0); |
| | | case 'post_content': |
| | | $ID = true; |
| | | if ($this->data->post_content !== $sanitized) { |
| | | $ID = wp_update_post([ |
| | | 'ID' => $this->object_id, |
| | | 'post_content' => $sanitized |
| | | ]); |
| | | } |
| | | return ($ID !== 0); |
| | | case 'featured_image': |
| | | case 'post_thumbnail': |
| | | $ID = true; |
| | | $old = get_post_thumbnail_id($this->object_id); |
| | | $old = $this->data->$name; |
| | | if ($old !== $sanitized) { |
| | | $ID = set_post_thumbnail($this->object_id, $sanitized); |
| | | } |
| | | return ($ID !== false); |
| | | case 'display_name': |
| | | $ID = true; |
| | | $object_id = $this->object_id; |
| | | $displayName = $this->data->display_name; |
| | | if (!$this->data->display_name) { |
| | | $user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true)); |
| | | $object_id = $user->ID; |
| | | $displayName = $user->display_name; |
| | | } |
| | | switch ($this->object_type) { |
| | | case 'post': |
| | | |
| | | if ($displayName !== $sanitized) { |
| | | $ID = wp_update_user([ |
| | | 'ID' => $object_id, |
| | | 'display_name' => $sanitized |
| | | ]); |
| | | $link = get_user_meta($object_id, BASE . 'link', true); |
| | | wp_update_post([ |
| | | 'ID' => $link, |
| | | 'post_title' => $sanitized, |
| | | ]); |
| | | } |
| | | return (!is_wp_error($ID)); |
| | | case 'user_email': |
| | | $ID = true; |
| | | $ID = jvb_update_post([ |
| | | 'ID' => $this->object_id, |
| | | $name => $sanitized |
| | | ]); |
| | | break; |
| | | case 'term': |
| | | $data = [$name => $sanitized]; |
| | | if ($name === 'term_name') { |
| | | $data['slug'] = sanitize_title($sanitized); |
| | | } |
| | | $ID = wp_update_term( |
| | | $this->data->term_id, |
| | | $this->data->taxonomy, |
| | | $data |
| | | ); |
| | | break; |
| | | case 'user': |
| | | $ID = wp_update_user([ |
| | | 'ID' => $this->object_id, |
| | | $name => $sanitized |
| | | ]); |
| | | if ($name === 'display_name') { |
| | | $link = get_user_meta($this->object_id, BASE.'link', true); |
| | | if ($link !== '') { |
| | | jvb_update_post([ |
| | | 'ID' => $link, |
| | | 'post_title' => $sanitized |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | $object_id = $this->object_id; |
| | | $email = $this->data->user_email; |
| | | if (!$this->data->display_name) { |
| | | $user = get_userdata(get_post_meta($this->object_id, BASE . 'link', true)); |
| | | $object_id = $user->ID; |
| | | $email = $user->user_email; |
| | | break; |
| | | } |
| | | } |
| | | if ($email !== $sanitized) { |
| | | $ID = wp_update_user([ |
| | | 'ID' => $object_id, |
| | | 'user_email' => $sanitized |
| | | ]); |
| | | } |
| | | return (!is_wp_error($ID)); |
| | | case 'term_name': |
| | | $ID = true; |
| | | $name = $this->data->name; |
| | | if ($name !== $sanitized) { |
| | | $ID = wp_update_term($this->data->term_id, $this->data->taxonomy, [ |
| | | 'name' => $sanitized, |
| | | 'slug' => sanitize_title($sanitized) |
| | | ]); |
| | | } |
| | | return $ID !== false; |
| | | } |
| | | } |
| | | |
| | | |
| | | if ($field_config['type'] == 'taxonomy' && (!array_key_exists('taxonomy_type', $field_config))) { |
| | | error_log('Attempting to set taxonomies: ' . print_r($this->object_id, true)); |
| | | error_log('Sanitized data: ' . print_r($sanitized, true)); |
| | | error_log('Taxonomy: ' . print_r($field_config['taxonomy'], true)); |
| | | $set = wp_set_post_terms($this->object_id, $sanitized, jvbCheckBase($field_config['taxonomy']), false); |
| | | error_log('Set post terms: ' . print_r($set, true)); |
| | | if (empty(trim($sanitized))) { |
| | | // Clear all terms when value is empty |
| | | wp_set_object_terms($this->object_id, [], jvbCheckBase($field_config['taxonomy']), false); |
| | | } else { |
| | | $term_ids = array_map('intval', array_filter(explode(',', $sanitized))); |
| | | wp_set_object_terms($this->object_id, $term_ids, jvbCheckBase($field_config['taxonomy']), false); |
| | | } |
| | | } |
| | | if ($field_config['type'] === 'location' && empty($sanitized)) { |
| | | $this->addMeta('has_map', false); |
| | |
| | | $result = update_user_meta($this->object_id, $meta_key, $sanitized); |
| | | break; |
| | | case 'options': |
| | | $result = update_option($meta_key, $sanitized); |
| | | $key = $this->baseKey |
| | | ? BASE . $this->baseKey . '_' . $name |
| | | : BASE . $name; |
| | | return update_option($key, $sanitized); |
| | | } |
| | | |
| | | if ($result === false) { |
| | | throw new Exception("Failed to update meta value for {$name}"); |
| | | } |
| | | |
| | | if ($updatePost && $this->object_type === 'post') { |
| | | //Flush the cache for this post. |
| | | jvb_update_post([ |
| | | 'ID' => $this->object_id, |
| | | ]); |
| | | } |
| | | return true; |
| | | |
| | | } catch (Exception $e) { |
| | |
| | | if (!empty($this->fields)) { |
| | | return $this->fields; |
| | | } |
| | | $type = false; |
| | | switch ($this->object_type) { |
| | | case 'post': |
| | | $type = get_post_type((int)$this->object_id); |
| | |
| | | case 'options': |
| | | return jvbGetFields('options'); |
| | | } |
| | | if (!$type) { |
| | | return []; |
| | | } |
| | | |
| | | return jvbGetFields($type, $this->object_type); |
| | | $this->fields = jvbGetFields($type, $this->object_type); |
| | | return $this->fields; |
| | | } |
| | | |
| | | protected function getObjectType(): string|false |
| | |
| | | |
| | | protected function getSections():array |
| | | { |
| | | $type = false; |
| | | switch ($this->object_type) { |
| | | case 'post': |
| | | $type = get_post_type((int)$this->object_id); |
| | |
| | | $out = ''; |
| | | switch ($type) { |
| | | case 'form': |
| | | $out = $this->form->render($name, $value, $config, $showHidden, true); |
| | | $out = Form::render($name, $value, $config); |
| | | $out = apply_filters('jvbRenderFormMeta', $out, $name, $config, $value, $this->getObjectType()); |
| | | break; |
| | | case 'render': |
| | | $out = $this->renderer->render($name, $value, $config, true); |
| | | $out = $this->renderer->render($name, $value, $config); |
| | | if (empty($out) && !$hideEmpty) { |
| | | $out = $this->getEmptyTemplate($config['type'], $name); |
| | | } |
| | |
| | | } |
| | | |
| | | |
| | | echo (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('save').'Save</button>' : ''; |
| | | echo (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('floppy-disk').'Save</button>' : ''; |
| | | echo '</form>'; |
| | | |
| | | $out = ob_get_clean(); |
| | |
| | | $out = '<p class="'.$name.'">'.jvbIcon('calendar').'<span></span></p>'; |
| | | break; |
| | | case 'time': |
| | | $out = '<p class="'.$name.'">'.jvbIcon('time').'<time></time></p>'; |
| | | $out = '<p class="'.$name.'">'.jvbIcon('clock').'<time></time></p>'; |
| | | break; |
| | | case 'true_false': |
| | | $out = '<p class="'.$name.'"></p>'; |
| | |
| | | return ''; |
| | | } |
| | | |
| | | $out = apply_filters('jvbMetaTypeTemplate', $out, $type); |
| | | |
| | | return $out; |
| | | return apply_filters('jvbMetaTypeTemplate', $out, $type); |
| | | } |
| | | |
| | | public function getDefaultValue(string $type):mixed { |
| | |
| | | return []; |
| | | } |
| | | |
| | | switch ($this->object_type) { |
| | | case 'user': |
| | | $check = $this->userFields; |
| | | break; |
| | | case 'term': |
| | | $check = $this->termFields; |
| | | break; |
| | | case 'post': |
| | | $check = $this->postFields; |
| | | break; |
| | | default: |
| | | $check = []; |
| | | } |
| | | $check = array_key_exists($this->object_type, $this->wpDefaults) ? $this->wpDefaults[$this->object_type] : []; |
| | | |
| | | $setFields = array_intersect($check, $fields); |
| | | foreach ($setFields as $f) { |
| | |
| | | return false; |
| | | } |
| | | |
| | | public function setAll(array $fields):bool |
| | | public function setAll(array $fields, bool $updatePost = true):bool |
| | | { |
| | | if (empty($fields) || !$this->object_type) { |
| | | return false; |
| | |
| | | return false; |
| | | } |
| | | |
| | | |
| | | // Determine table based on object type |
| | | $check = array_key_exists($this->object_type, $this->wpDefaults) ? $this->wpDefaults[$this->object_type] : []; |
| | | switch ($this->object_type) { |
| | | case 'post': |
| | | $table = $this->wpdb->postmeta; |
| | | $id_column = 'post_id'; |
| | | $check = $this->postFields; |
| | | break; |
| | | case 'term': |
| | | $table = $this->wpdb->termmeta; |
| | | $id_column = 'term_id'; |
| | | $check = $this->termFields; |
| | | break; |
| | | case 'user': |
| | | case 'integrations': |
| | | $table = $this->wpdb->usermeta; |
| | | $id_column = 'user_id'; |
| | | $check = $this->userFields; |
| | | break; |
| | | case 'options': |
| | | try { |
| | |
| | | $temp = []; |
| | | foreach ($setFields as $f) { |
| | | $temp[$f] = $fields[$f]; |
| | | unset($fields[array_search($f, $fields)]); |
| | | unset($fields[$f]); |
| | | } |
| | | $setFields = $temp; |
| | | |
| | |
| | | // Sanitize value |
| | | $sanitized = $this->sanitizer->sanitize($value, $field_config); |
| | | if ($this->checkOverrides($field, $sanitized, $field_config)) { |
| | | return true; |
| | | continue; |
| | | } |
| | | |
| | | if ($field_config['type'] === 'taxonomy' && !array_key_exists('taxonomy_type', $field_config)){ |
| | | $set = wp_set_post_terms($this->object_id, $sanitized, jvbCheckBase($field_config['taxonomy']), false); |
| | | |
| | | if ($field_config['type'] == 'taxonomy' && (!array_key_exists('taxonomy_type', $field_config))) { |
| | | if (empty(trim($sanitized))) { |
| | | // Clear all terms when value is empty |
| | | wp_set_object_terms($this->object_id, [], jvbCheckBase($field_config['taxonomy']), false); |
| | | } else { |
| | | $term_ids = array_map('intval', array_filter(explode(',', $sanitized))); |
| | | wp_set_object_terms($this->object_id, $term_ids, jvbCheckBase($field_config['taxonomy']), false); |
| | | } |
| | | } |
| | | |
| | | if ($field_config['type'] === 'location' && empty($sanitized)) { |
| | |
| | | } |
| | | |
| | | if (!empty($setFields)) { |
| | | foreach ($setFields as $field => $value) { |
| | | $field_config = $this->getFieldConfig($field); |
| | | if ($field_config) { |
| | | $setFields[$field] = $this->sanitizer->sanitize($value, $field_config); |
| | | } |
| | | |
| | | if ($field === 'post_date') { |
| | | $datetime = strtotime($setFields[$field]); |
| | | if ($datetime !== false) { |
| | | $setFields[$field] = date('Y-m-d H:i:s', $datetime); |
| | | } else { |
| | | $setFields[$field] = date('Y-m-d H:i:s', time()); |
| | | } |
| | | |
| | | $setFields['post_date_gmt'] = get_gmt_from_date($setFields[$field]); |
| | | $setFields['edit_date'] = true; |
| | | } |
| | | } |
| | | |
| | | switch ($this->object_type) { |
| | | case 'post': |
| | | if (array_key_exists('post_thumbnail', $setFields)) { |
| | |
| | | } |
| | | |
| | | if (!empty($setFields)) { |
| | | $result = wp_update_post(array_merge(['ID' => $this->object_id], $setFields), true); |
| | | $result = jvb_update_post(array_merge(['ID' => $this->object_id], $setFields)); |
| | | } |
| | | break; |
| | | case 'user': |
| | |
| | | wp_update_term($this->object_id, $this->data->taxonomy, $setFields); |
| | | break; |
| | | } |
| | | } elseif ($updatePost && $this->object_type === 'post' && !empty($this->object_id)) { |
| | | //Update the 'post modified' date with meta updates, for filtering |
| | | jvb_update_post(['ID' => $this->object_id]); |
| | | } |
| | | |
| | | } catch (Exception $e) { |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | private function getOrCreateTerm(string $termName, string $taxonomy):?int |
| | | { |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $term = get_term_by('name', $termName, $taxonomy); |
| | | |
| | | if (!$term) { |
| | | $result = wp_insert_term($termName, $taxonomy); |
| | | if (is_wp_error($result)) { |
| | | return null; |
| | | } |
| | | $termID = $result['term_id']; |
| | | } else { |
| | | $termID = $term->term_id; |
| | | } |
| | | |
| | | if ($termID) { |
| | | return $termID; |
| | | } |
| | | return null; |
| | | } |
| | | } |