Jake Vanderwerf
2026-04-26 86c6cd3cc099d2480932ede03c12cea01e625c94
inc/rest/routes/AdminRoutes.php
@@ -2,8 +2,11 @@
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\meta\MetaManager;
use JVBase\managers\Cache;
use JVBase\managers\IconsManager;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\rest\PermissionHandler;
use WP_Query;
use WP_Error;
use WP_REST_Request;
@@ -13,7 +16,7 @@
    exit; // Exit if accessed directly
}
class AdminRoutes extends RestRouteManager
class AdminRoutes extends Rest
{
    protected array $fields;
    protected $meta;
@@ -22,52 +25,28 @@
    public function __construct()
    {
        $this->cache_name = 'itsme';
        $this->cacheName = 'itsme';
        parent::__construct();
        $this->action = 'itsme';
    }
    public function registerRoutes():void
    {
        if (!current_user_can('manage_options')) {
            return;
        }
        register_rest_route($this->namespace, '/myster', [
            [
                'methods'   => 'GET',
                'callback'  => [$this, 'getItems'],
                'permission_callback'   => [$this, 'checkPermission']
            ],
            [
                'methods'   => 'POST',
                'callback'  => [$this, 'updateItems'],
                'permission_callback'   => [$this, 'checkPermission']
            ]
        ]);
      Route::for('admin-cache')
         ->post([$this, 'handleCacheAction'])
         ->auth('admin')
         ->rateLimit(30)
         ->register();
        register_rest_route($this->namespace, '/admin-action', [
            [
                'methods'   => 'POST',
                'callback'  => [$this, 'adminAction'],
                'permission_callback'   => [$this, 'checkPermission']
            ]
        ]);
    }
      Route::for('admin-icons')
         ->post([$this, 'handleIconAction'])
         ->auth('admin')
         ->rateLimit(30)
         ->register();
    /**
     * @param WP_REST_Request $request The Request object
     *
     * @return bool whether or not we can proceed
     */
    public function checkPermission(WP_REST_Request $request):bool
    {
        if (!current_user_can('manage_options')) {
            return false;
        }
        $this->verifyNonce($request, 'wp_rest');
        if ($this->action!=='') {
            $this->verifyNonce($request, $this->action . get_current_user_id(), $request->get_header('action_nonce'));
        }
        return true;
      Route::for('admin-action')
         ->post([$this, 'adminAction'])
         ->auth('admin')
         ->rateLimit()
         ->register();
    }
    /**
@@ -91,227 +70,6 @@
        );
    }
    /**
     * @param WP_REST_Request $request Request object
     *
     * @return WP_REST_Response
     */
    public function updateItems(WP_REST_Request $request):WP_REST_Response
    {
        error_log('Received Request: '.print_r($request->get_params(), true));
        $content = $request->get_param('content');
        if (!$this->checkContent($content, true)) {
            return new WP_REST_Response([
                'sucess'    => false,
                'message'   => 'Invalid attempt'
            ]);
        }
        $data = $request->get_param('data');
        if (empty($data)) {
            return new WP_REST_Response([
                'success'    => true,
                'message'   => 'Nothing to Update'
            ]);
        }
        $type    = match ($content) {
            'shop',
            'artform',
            'type',
            'media',
            'artstyle',
            'arttheme',
            'city',
            'colour',
            'offerfor',
            'pstyle',
            'style',
            'theme' => 'term',
            default => 'post',
        };
        $errors = [];
        $success = [];
        error_log('Data: '.print_r($data, true));
        foreach ($data as $ID => $fields) {
            $ID = (int)$ID;
            $meta = new MetaManager($ID, $type);
            $allFields = JVB()->getFields($content);
            foreach ($fields as $name => $value) {
                $process = true;
                if (!array_key_exists($name, $allFields)) {
                    $errors[$ID][$name] = __('Field not found', 'jvb');
                    $process = false;
                }
                error_log('Proceeding as Normal...');
                //Switch between config types to extract values for special cases
                $config = $allFields[$name];
                switch ($config['type']) {
                    case 'radio':
                        //Ensure we only chose one option
                        $temp = explode(',', $value);
                        $value = trim($temp[0]);
                        if (!in_array($value, $config['options'])) {
                            $errors[$ID][$name] = __('Invalid Option', 'jvb');
                            $process = false;
                        }
                        break;
                    case 'set':
                        $temp = explode(',', $value);
                        $value = array_map('trim', $temp);
                        break;
                    case 'repeater':
                        error_log('Repeater: '.print_r($name, true));
                        $single = false;
                        switch ($name) {
                            case 'keywords':
                                $single = 'keyword';
                                break;
                            case 'languages':
                                $single = 'language';
                                break;
                            case 'links':
                                $single = 'url';
                                break;
                            case 'followers':
                                $single = 'count';
                                break;
                        }
                        $items = array_keys($config['fields']);
                        //First, separate out any ]
                        if (strpos($value, ']')) {
                            $rows = array_map(function ($item) {
                                return str_replace('[', '', $item);
                            }, explode(']', $value));
                            array_pop($rows);
                            error_log('Rows: '.print_r($rows, true));
                            $value = [];
                            foreach ($rows as $index => $row) {
                                $r = array_map('trim', explode(',', $row));
                                if (count($r) !== count($items)) {
                                    $errors[$ID][$name] = __('Not enough fields set. May not save correctly', 'jvb');
                                }
                                //attempt to save fields
                                $new = [];
                                foreach ($items as $key => $i) {
                                    $new[$i] = (array_key_exists($key, $r)) ? $r[$key] : (($i==='checked') ? date('Y-m-d'): '');
                                }
                                $value[] = $new;
                            }
                            error_log('Processed Repeater Value for sanitizing: '.print_r($value, true));
                        } elseif ($value === '') {
                            $value = [];
                        } else {
                            if (!$single) {
                                $errors[$ID][$name] = __('Must set single key', 'jvb');
                                $process = false;
                            }
                            $rows = array_map('trim', explode(',', $value));
                            $value = [];
                            foreach ($rows as $row) {
                                $new = [];
                                foreach ($items as $i) {
                                    $new[$i] = ($i === $single) ? $row : (($i==='checked') ? date('Y-m-d') : '');
                                }
                                error_log('New Repeater Row Output: '.print_r($new, true));
                                $value[] = $new;
                            }
                        }
                        break;
                }
                switch ($name) {
                    case 'shop':
                    case 'type':
                    case 'city':
                        $terms = array_map('trim', explode(',', $value));
                        $set = [];
                        foreach ($terms as $term) {
                            $t = get_term_by('name', $value, BASE.$name);
                            if (!$t) {
                                $errors[$ID][$name][] = __($value.' does not exist yet', 'jvb');
                            } else {
                                $set[] = $t->term_id;
                            }
                        }
                        if (!empty($set)) {
                            $result = wp_set_post_terms($ID, $set, BASE.$name);
                            $result = is_array($result);
                        }
                        break;
                    case 'owner':
                        error_log('Processing Owner Request from Admin Routes...');
                        $users = array_map('trim', explode(',', $value));
                        foreach ($users as $user) {
                            $t = jvbGetUserByDisplayName($user)??jvbGetUserByFirstName($user)??false;
                            error_log('Got user: '.print_r($t, true));
                            if (!$t) {
                                $errors[$ID][$name][] = __($value.' does not exist yet...', 'jvb');
                                $result = false;
                            } else {
                                $result = JVB()->routes('shop')->setShopOwner($t->ID, $ID, true);
                            }
                        }
                        break;
                    case 'managers':
                        error_log('Processing Manager Request from Admin Routes...');
                        $users = array_map('trim', explode(',', $value));
                        foreach ($users as $user) {
                            $t = jvbGetUserByDisplayName($user)??jvbGetUserByFirstName($user)??false;
                            error_log('Got user: '.print_r($t, true));
                            if (!$t) {
                                $result = $errors[$ID][$name][] = __($value.' does not exist yet...', 'jvb');
                            } else {
                                $result = JVB()->routes('shop')->setShopManager($t->ID, $ID, true);
                            }
                        }
                        break;
                    case 'location':
                        $value = [
                            'address'   => $value,
                            'lat'       => '',
                            'lng'       => '',
                        ];
                        $result = $meta->updateValue($name, $value);
                        break;
                    case 'hours':
                        $temp = [];
                        foreach ($value as $key => $v) {
                            if (strpos($v['days'], '-')) {
                                $temp[$key]['days'] = jvbExpandDayRange($v['days']);
                                $temp[$key]['time_open'] = $v['time_open'];
                                $temp[$key]['time_closes'] = $v['time_closes'];
                            }
                        }
                        $value = $temp;
                        error_log('Final Hours for processing: '.print_r($value, true));
                        $result = $meta->updateValue($name, $value);
                        break;
                    default:
                        $result = $meta->updateValue($name, $value);
                        break;
                }
                //Save the value
                if ($result) {
                    $success[$ID][] = $name;
                } else {
                    $errors[$ID][] = [
                        $name => 'Could not update value'
                    ];
                }
            }
        }
        return new WP_REST_Response([
            'success'       => true,
            'successful'    => $success,
            'errors'        => $errors
        ]);
    }
    /**
     * @param array $filters
@@ -331,350 +89,137 @@
        return $out;
    }
    /**
     * @param WP_REST_Request $request The REST Request
     *
     * @return array
     */
    protected function buildParams(WP_REST_Request $request):array
    {
        $data = $request->get_params();
        $this->setMetaType($data['content']);
        switch ($this->metaType) {
            case 'post':
                $key = 'post_type';
                break;
            case 'term':
                $key = 'taxonomy';
                break;
            case 'user':
                $key = 'role';
                break;
            default:
                return [];
        }
        global $jvb_everything;
        return [
            $key        => (array_key_exists('content', $data) &&
                            array_key_exists($data['content'], $jvb_everything)) ?
                                BASE.$data['content'] : BASE.'artist',
            'order'     => (array_key_exists('order', $data) &&
                            in_array(strtolower($data['order']), ['asc', 'desc'])) ?
                                strtolower($data['order']) : 'desc',
            'orderby'   => (array_key_exists('orderby', $data) &&
                            in_array($data['orderby'], ['name', 'date', 'followers', 'karma'])) ?
                                $data['orderby'] : 'name',
            'paged'     => (array_key_exists('page', $data) && is_numeric($data['page'])) ?
                                (int)$data['page'] : 1,
            'filters'   => (array_key_exists('filters', $data)) ?
                                $this->checkFilters($data['filters']) : null,
        ];
    }
   /**
    * Handle cache-related actions
    */
   public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
   {
      $action = sanitize_text_field($request->get_param('action'));
    protected function setMetaType(string $type):void
    {
        $this->metaType = match (true) {
            array_key_exists($type, JVB_CONTENT) => 'post',
            array_key_exists($type, JVB_TAXONOMY) => 'term',
            array_key_exists($type, JVB_USER) => 'user',
            default => false,
        };
    }
      switch ($action) {
         case 'flush-all':
            $total = Cache::flushAll();
            return new \WP_REST_Response([
               'success' => true,
               'message' => $total.' caches flushed successfully'
            ]);
    /**
     * @param WP_REST_Request $request The REST Request
     *
     * @return WP_REST_Response
     */
    public function getItems(WP_REST_Request $request):WP_REST_Response
    {
         case 'flush-cache':
            $group = sanitize_text_field($request->get_param('group'));
            if (empty($group)) {
               return new \WP_REST_Response([
                  'success' => false,
                  'message' => 'No cache group specified'
               ], 400);
            }
        $args = $this->buildParams($request);
        $key = $this->cache->generateKey($args);
            Cache::for($group)?->flush();
        $cache = $this->cache->get($key);
        if ($cache) {
            return new WP_REST_Response($cache);
        }
            return new \WP_REST_Response([
               'success' => true,
               'message' => "Cache group '{$group}' flushed successfully"
            ]);
        $this->content = $request->get_param('content');
        $args['posts_per_page'] = 50;
        $this->fields = jvbGetFields($this->content);
//        $this->fields = array_filter(JVB()->getFields($data['content']), function($arr){
//            return array_key_exists('quickEdit', $arr);
//        });
        $response = match ($this->metaType) {
            'post'  => $this->getPostItems($args),
            'term'  => $this->getTermItems($args),
            'user'  => $this->getUserItems($args),
            default => []
        };
        $this->cache->set($key, $response);
        return new WP_REST_Response($response);
    }
    /**
     * @param array $args the data from buildParams method
     *
     * @return array
     */
    protected function getTermItems(array $args):array
    {
      if (isJVBContentTax($args['taxonomy'])) {
         return $this->getContentTypeTaxItems($args);
         default:
            return new \WP_REST_Response([
               'success' => false,
               'message' => 'Invalid action'
            ], 400);
      }
        // Build query arguments
        $args = array_merge($args, [
            'hide_empty' => false,
            'number' => $args['posts_per_page'],
            'offset' => ($args['paged'] - 1) * $args['posts_per_page'],
            'fields' => 'ids'
        ]);
   }
        // Add ordering
        switch ($args['orderby']) {
            case 'date':
                $args['orderby'] = 'id'; // Terms don't have date, so use ID as a proxy
                break;
            case 'karma':
                // Terms should be ordered by meta value
                $args['orderby'] = 'meta_value_num';
                $args['meta_key'] = BASE . 'karma';
                break;
            default:
                $args['orderby'] = 'name';
                break;
        }
        $args['order'] = strtoupper($args['order']);
   /**
    * Handle icon-related actions
    */
   public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
   {
      $action = sanitize_text_field($request->get_param('action'));
      $source = sanitize_text_field($request->get_param('source') ?? 'icons');
      $icons = IconsManager::for($source);
        // Add any filters
        if (!empty($args['filters'])) {
            // Term meta filtering would go here
            $meta_query = [];
      switch ($action) {
         case 'refresh-icons':
            // Force regenerate CSS immediately
            $icons->forceRefresh();
            IconsManager::regenerateAllCSS([$source => true]);
            foreach ($args['filters'] as $filter_type => $filter_values) {
                if (!empty($filter_values)) {
                    // Example: filter by parent terms
                    if ($filter_type === 'parent') {
                        $args['parent'] = $filter_values[0]; // Assume single parent filter
                    } else {
                        // For meta-based filters
                        $meta_query[] = [
                            'key' => BASE . $filter_type,
                            'value' => $filter_values,
                            'compare' => 'IN'
                        ];
                    }
                }
            }
            return new \WP_REST_Response([
               'success' => true,
               'message' => "Icon CSS regenerated successfully for '{$source}'"
            ]);
            if (!empty($meta_query)) {
                $args['meta_query'] = $meta_query;
            }
        }
        unset($args['filters']);
         case 'refresh-all-icons':
            // Regenerate all icon sources
            foreach (['icons', 'forms', 'dash'] as $src) {
               IconsManager::for($src)->forceRefresh();
            }
            IconsManager::regenerateAllCSS();
        // Get count first for pagination info
        $count_args = $args;
        unset($count_args['number']);
        unset($count_args['offset']);
        $count_args['fields'] = 'count';
        $total_items = get_terms($count_args);
            return new \WP_REST_Response([
               'success' => true,
               'message' => 'All icon CSS files regenerated successfully'
            ]);
        // Get the actual terms
        $term_ids = get_terms($args);
         case 'restore-icon-version':
            $timestamp = (int)$request->get_param('timestamp');
            if (empty($timestamp)) {
               return new \WP_REST_Response([
                  'success' => false,
                  'message' => 'No timestamp provided'
               ], 400);
            }
        // Error handling
        if (is_wp_error($term_ids)) {
            return [
                'items' => [],
                'has_more' => false,
                'total_items' => 0,
                'total_pages' => 0,
                'error' => $term_ids->get_error_message()
            ];
        }
            if ($icons->restoreVersion($timestamp)) {
               return new \WP_REST_Response([
                  'success' => true,
                  'message' => 'Icon version restored successfully'
               ]);
            }
        // Format each term
        $items = array_map([$this, 'formatItem'], $term_ids);
            return new \WP_REST_Response([
               'success' => false,
               'message' => 'Failed to restore icon version'
            ], 500);
        return [
            'items' => $items,
            'has_more' => ($total_items > ($args['offset'] + $args['number'])),
            'total_items' => (int)$total_items,
            'total_pages' => ceil($total_items / $args['posts_per_page'])
        ];
    }
         case 'merge-icon-versions':
            $timestamps = $request->get_param('timestamps');
    protected function getUserItems(array $args):array
    {
        return [];
    }
            if (empty($timestamps) || !is_array($timestamps)) {
               return new \WP_REST_Response([
                  'success' => false,
                  'message' => 'No versions selected for merging'
               ], 400);
            }
    /**
     * @param array $data the $data built by buildParams
     *
     * @return array
     */
    protected function getContentTypeTaxItems(array $data):array
    {
        global $wpdb;
        $table_name = $wpdb->prefix . BASE . jvbNoBase($data['taxonomy']);
            $timestamps = array_map('intval', $timestamps);
        // Start building the query to get just the term_ids with proper ordering
        $sql_select = "SELECT s.term_id";
        $sql_from = " FROM {$table_name} AS s";
        $sql_where = " WHERE 1=1";
        $sql_orderby = "";
        $sql_limit = "";
            if (count($timestamps) < 2) {
               return new \WP_REST_Response([
                  'success' => false,
                  'message' => 'Please select at least 2 versions to merge'
               ], 400);
            }
        $params = [];
            if ($icons->mergeVersions($timestamps)) {
               // Regenerate CSS after merge
               IconsManager::regenerateAllCSS([$source => true]);
        // Add filters if any
        if (!empty($data['filters'])) {
            foreach ($data['filters'] as $filter_type => $filter_values) {
                if ($filter_type === 'city' && !empty($filter_values)) {
                    $placeholders = implode(',', array_fill(0, count($filter_values), '%d'));
                    $sql_where .= $wpdb->prepare(" AND s.city IN ($placeholders)", ...$filter_values);
                }
                // Add more filter conditions for other fields as needed
            }
        }
               return new \WP_REST_Response([
                  'success' => true,
                  'message' => 'Icon versions merged successfully'
               ]);
            }
        // Determine ORDER BY clause - here we can use any column from the custom table
        $valid_order_columns = ['name', 'updated_at', 'established_year', 'karma'];
        $orderby_column = in_array($data['orderby'], $valid_order_columns) ? $data['orderby'] : 'name';
        $sql_orderby = " ORDER BY s." . $orderby_column;
            return new \WP_REST_Response([
               'success' => false,
               'message' => 'Failed to merge icon versions'
            ], 500);
        // Validate order direction
        $order_direction = strtoupper($data['order']) === 'DESC' ? 'DESC' : 'ASC';
        $sql_orderby .= " " . $order_direction;
        // Calculate offset for pagination
        $per_page = absint($data['posts_per_page']);
        $page = max(1, absint($data['paged']));
        $offset = ($page - 1) * $per_page;
        // Add pagination with prepared statement
        $sql_limit = $wpdb->prepare(" LIMIT %d OFFSET %d", $per_page, $offset);
        // Count query (without limit clause)
        $count_sql = "SELECT COUNT(s.term_id)" . $sql_from . $sql_where;
        $total_items = $wpdb->get_var($count_sql);
        // Final query with all components
        $sql = $sql_select . $sql_from . $sql_where . $sql_orderby . $sql_limit;
        // Execute the query
        $shop_term_ids = $wpdb->get_col($sql);
        // Now get the full term objects for these IDs
        $items = [];
        foreach ($shop_term_ids as $shop_term_id) {
            $items[] = $this->formatItem($shop_term_id);
        }
        return [
            'items' => $items,
            'has_more' => ($total_items > ($offset + $per_page)),
            'total_items' => (int)$total_items,
            'total_pages' => ceil($total_items / $per_page)
        ];
    }
    /**
     * @param array $args array returned by buildParams
     *
     * @return array
     */
    protected function getPostItems(array $args):array
    {
        $args['fields'] = 'ids';
        $args['post_status'] = ['draft', 'trash', 'publish'];
        $query = new WP_Query($args);
        $items = array_map([$this, 'formatItem'], $query->posts);
      return [
         'items'         => $items,
         'has_more'      => $query->max_num_pages > $args['paged'],
         'total_items'   => $query->found_posts,
         'total_pages'   => $query->max_num_pages
      ];
    }
    /**
     * @param int $ID the ID of the item to format
     *
     * @return array
     */
    protected function formatItem(int $ID):array
    {
        $meta = new MetaManager($ID, $this->metaType);
        $item = [
            'id'    => $ID,
            'public'    => !($this->metaType === 'post') || get_post_status($ID) === 'publish',
        ];
        $hierarchical = false;
        if ($this->metaType === 'term') {
            $term = get_term($ID);
            if ($term) {
                $hierarchical = is_taxonomy_hierarchical($term->taxonomy);
            }
        }
        foreach ($this->fields as $key => $config) {
            switch ($key) {
                case 'type':
                case 'city':
                case 'shop':
                    $terms = get_the_terms($ID, BASE.$key);
                    $value = [];
                    if ($terms && !is_wp_error($terms)) {
                        foreach ($terms as $term) {
                            $value[] = htmlspecialchars_decode($term->name);
                        }
                    }
                    $item[$key] = implode(', ', $value);
                    break;
                default:
                    $item[$key] = $meta->getValue($key);
                    break;
            }
            switch ($config['type']) {
                case 'repeater':
                    if (empty($item[$key]) || !is_array($item[$key])) {
                        $item[$key] = '';
                    } else {
                        $temp = '';
                        foreach ($item[$key] as $row) {
                            if (is_array($row)) {
                                // Format each row as [value1,value2,value3]
                                $rowValues = array_values($row);
                                $temp .= '[' . implode(',', $rowValues) . ']';
                            } else {
                                // Handle simpler cases where rows might not be arrays
                                $temp .= '[' . $row . ']';
                            }
                        }
                        $item[$key] = $temp;
                    }
                    break;
                case 'location':
                    $item[$key] = $item[$key]['address'];
                    break;
            }
            if ($hierarchical && $key === 'term_name') {
                $item[$key.'_path'] = JVB()->routes('term')->getTermPath($ID, $term->name, $term->taxonomy);
            }
        }
      error_log('Item: '.print_r($item, true));
        return $item;
    }
         default:
            return new \WP_REST_Response([
               'success' => false,
               'message' => 'Invalid action'
            ], 400);
      }
   }
}