Jake Vanderwerf
5 days ago a9b3b28d001941921aa70d37fdc87c758a163a44
inc/managers/RoleManager.php
@@ -1,7 +1,8 @@
<?php
namespace JVBase\managers;
use JVBase\utility\Features;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use WP_User;
use WP_Role;
@@ -12,23 +13,45 @@
{
    private array $roles;
   private array $content;
   private array $subTypes = [];
    public function __construct()
    {
       $this->roles = array_keys(JVB_USER);
       $this->roles = Registrar::getRegistered('user');
      $this->content = array_map(function($content) {
         return strtolower($content['plural']??$content['singular'].'s');
      },JVB_CONTENT);
         $registrar = Registrar::getInstance($content);
         return strtolower(str_replace(' ', '_', $registrar->getPlural()??$registrar->getSingular().'s'));
      },array_merge(
         Registrar::getRegistered('post'),
         Registrar::getFeatured('is_content', 'term')
      ));
      add_action('set_user_role', [$this, 'updateRoles'], 10, 3);
      $this->checkRoleSubTypes();
    }
   public function checkRoleSubTypes():void
   {
      foreach ($this->roles as $role) {
         $registrar = Registrar::getInstance($role);
         if ($registrar->getUserSubtype()){
            $this->subTypes[jvbCheckBase($registrar->getUserSubtype())] = $registrar->getCreatable();
         }
      }
      if (!empty($this->subTypes)) {
         add_action('set_object_terms', [$this, 'maybeSwitchPermissions'],10, 6);
      }
   }
   public function updateRoles(int $userID, string $role, array $oldRoles):void
   {
      if (doing_action('set_user_role') > 1) {
         return;
      }
      $temp = jvbNoBase($role);
      if (array_key_exists($temp, JVB_USER)) {
      $registrar = Registrar::getInstance($temp);
      if ($registrar) {
         $user = get_userdata($userID);
         if (!$user) {
            return;
@@ -56,17 +79,13 @@
   {
      $type = jvbNoBase($type);
      // Check in JVB_CONTENT array
      if (array_key_exists($type, JVB_CONTENT)) {
      $registrar = Registrar::getInstance($type);
      if ($registrar && $registrar->getType() === 'post') {
         return true;
      }
      // Check in JVB_TAXONOMY for content taxonomies
      if (array_key_exists($type, JVB_TAXONOMY)) {
         $tax_config = JVB_TAXONOMY[$type];
         if ($tax_config['is_content'] ?? false) {
            return true;
         }
      if ($registrar && $registrar->getType() === 'term' && $registrar->hasFeature('is_content')) {
         return true;
      }
      return false;
@@ -90,12 +109,11 @@
      $roles = array_keys($user->roles);
      foreach ($roles as $role) {
         $role = jvbNoBase($role);
         $config = JVB_USER[$role]??false;
         if (!$config) {
         $registrar = Registrar::getInstance($role);
         if (!$registrar) {
            return false;
         }
         foreach ($config as $type) {
         foreach ($registrar->getCreatable() as $type) {
            if (is_array($type) && in_array($content, $type)) {
               return true;
            } elseif ($content === $type) {
@@ -185,15 +203,19 @@
      if ($check) {
         return $check;
      }
      $check = JVB_USER[$role]['can_create'] ??false;
      if (!$check) {
      $registrar = Registrar::getInstance($role);
      if (!$registrar) {
         return false;
      }
      $out = [];
      foreach ($check as $types) {
         foreach ($types as $type => $content) {
            $out[$type] = $content;
      foreach ($registrar->getCreatable() as $types) {
         if (is_array($types)) {
            $out = array_merge($out, $types);
         } else {
            $out = [$types];
         }
         $out = array_unique($out);
      }
      set_transient(BASE.'role_config_'.$role, $out, MONTH_IN_SECONDS);
      return $out;
@@ -210,7 +232,7 @@
        if (!in_array($type, $this->roles)) {
            return false;
        }
        $link = get_user_meta($user->ID, BASE.'link', true);
        $link = get_user_meta($user->ID, BASE.'profile_link', true);
        if ($link === '') {
            $type = BASE.$type;
            $name = $user->display_name;
@@ -223,20 +245,24 @@
                'post_author'   => $user->ID,
            ]);
         if ($link) {
            update_user_meta($user->ID, BASE.'link', $link);
            update_post_meta($link, BASE.'link', $user->ID);
            update_user_meta($user->ID, BASE.'profile_link', $link);
            update_post_meta($link, BASE.'profile_link', $user->ID);
         }
        }
        return $link;
    }
   public function registerRole(string $slug, array $config): void
   public function registerRole(string $slug): void
   {
      $role_name = BASE . $slug;
      $display_name = $config['label'] ?? ucfirst($slug);
      $registrar = Registrar::getInstance($slug);
      if (!$registrar){
         return;
      }
      // Build capabilities for this role
      $capabilities = $this->buildRoleCapabilities($slug, $config);
      $capabilities = $this->buildRoleCapabilities($slug, $registrar);
      // Remove role first to ensure clean slate
      remove_role($role_name);
@@ -245,16 +271,16 @@
      add_role($role_name, $display_name, $capabilities);
      // Add management capabilities to administrator
      if ($config['has_dashboard'] ?? false) {
      if ($registrar->hasFeature('has_dashboard') ?? false) {
         $admin_role = get_role('administrator');
         if ($admin_role) {
            $admin_role->add_cap("manage_{$role_name}s", true);
            $admin_role->add_cap("edit_{$role_name}_settings", true);
            $admin_role->add_cap("manage_{$role_name}s");
            $admin_role->add_cap("edit_{$role_name}_settings");
         }
      }
   }
   private function buildRoleCapabilities(string $slug, array $config): array
   private function buildRoleCapabilities(string $slug, Registrar $registrar): array
   {
      //Everyone can see the things
      $capabilities = [
@@ -262,24 +288,32 @@
      ];
      // Dashboard access
      if ($this->config['has_dashboard'] ?? false) {
      if ($registrar->hasFeature('has_dashboard') ?? false) {
         $capabilities['access_dashboard'] = true;
      }
      if (Features::forSite()->has('favourites') && $config['can_favourite'] ?? true) {
      if (Site::has('favourites') && $registrar->hasFeature('can_favourite') ?? true) {
         $capabilities['can_favourite'] = true;
      }
      // Content creation capabilities
      if (!empty($config['can_create'])) {
         foreach ($config['can_create'] as $content_type) {
            $this->addContentCapabilities($capabilities, $content_type, $config);
      if (!empty($registrar->getCreatable())) {
         $content = [];
         foreach ($registrar->getCreatable() as $content_type) {
            if (is_array($content_type)) {
               $content = array_merge($content, $content_type);
            }else {
               $content[] = $content_type;
            }
         }
         foreach  ($content as $c) {
            $this->addContentCapabilities($capabilities, $c, $registrar);
         }
      }
      // Management capabilities
      if (!empty($this->config['manage_others'])) {
         foreach ($this->config['manage_others'] as $type) {
      if (!empty($registrar->getManageOthers())) {
         foreach ($registrar->getManageOthers() as $type) {
            // Skip if content type doesn't exist
            if (!$this->isValidContentType($type)) {
               error_log("Warning: User role '{$slug}' references non-existent content type '{$type}'");
@@ -298,44 +332,30 @@
   /**
    * Add content capabilities to capability array
    */
   private function addContentCapabilities(array &$capabilities, $content_type, array $config): void
   private function addContentCapabilities(array &$capabilities, $content_type, Registrar $registrar): void
   {
      if (is_array($content_type)) {
         // Handle array format for type-specific permissions
         foreach ($content_type as $sub_type => $types) {
            foreach ($types as $type) {
               if (!$this->isValidContentType($type)) {
                  error_log("Warning: Role references non-existent content type '{$type}'");
                  continue;
               }
               $this->addSingleContentCapabilities($capabilities, $type, $config);
            }
         }
      } else {
         if (!$this->isValidContentType($content_type)) {
            error_log("Warning: Role references non-existent content type '{$content_type}'");
            return;
         }
         $this->addSingleContentCapabilities($capabilities, $content_type, $config);
      if (!$this->isValidContentType($content_type)) {
         error_log("Warning: Role references non-existent content type '{$content_type}'");
         return;
      }
      $this->addSingleContentCapabilities($capabilities, $content_type, $registrar);
   }
   /**
    * Add capabilities for a single content type
    */
   private function addSingleContentCapabilities(array &$capabilities, string $type, array $config): void
   private function addSingleContentCapabilities(array &$capabilities, string $type, Registrar $registrar): void
   {
      $caps = $this->getCapabilities($type);
      foreach ($caps as $cap) {
         $capabilities[$cap] = true;
      }
      if (array_key_exists('approve_new', $config)) {
      if ($registrar->hasFeature('approve_new')) {
         $plural = $this->getContentPlural($type);
         // Publish capability depends on approval setting
         $capabilities["publish_{$plural}"] = !($config['approve_new'] ?? false);
      }
   }
   public function grantRoleCapabilities(string $role_name, string $content_slug, bool $grant = true): void
@@ -387,13 +407,15 @@
      return [
         "edit_{$content}",
         "edit_published_{$content}",
         "read_{$content}",
         "delete_{$content}",
         "delete_published_{$content}",
         "edit_{$plural}",
         "edit_others_{$plural}",
         "publish_{$plural}",
         "read_private_{$plural}",
         "edit_{$plural}",
         "edit_private_{$plural}",
         "delete_private_{$plural}",
      ];
   }
   protected function getOthersCapabilities(string $content):array
@@ -406,9 +428,6 @@
      return [
         "edit_others_{$plural}",
         "delete_others_{$plural}",
         "read_private_{$plural}",
         "edit_private_{$plural}",
         "delete_private_{$plural}",
      ];
   }
@@ -420,24 +439,67 @@
   public function getContentPlural(string $content): string
   {
      $content = jvbNoBase($content);
      $config = Features::getConfig($content);
      $capsMap = $config['capability_type']??[];
      if (empty($capsMap)){
         $capsMap = [
            $content,
            str_replace('-', '_',sanitize_title(strtolower(JVB_CONTENT[$content]['plural']??JVB_TAXONOMY[$content]['plural'])))
         ];
         return $capsMap[1];
      $registrar = Registrar::getInstance($content);
      if ($registrar && $registrar->getPlural()) {
         return str_replace(' ', '_', $registrar->getPlural());
      }
      return str_replace('-', '_', sanitize_title(strtolower($content . 's')));
      return str_replace(' ', '_', $content.'s');
   }
   public function activate(): void
   public static function activate(): void
   {
      foreach (JVB_USER as $slug => $config) {
         $this->registerRole($slug, $config);
      error_log('[RoleManager]::activate');
      error_log('Registering roles...');
      $instance = new self;
      foreach (Registrar::getRegistered('user') as $role) {
         $instance->registerRole($role);
      }
      error_log('Roles registered!');
      error_log('Removing unneeded roles...');
      remove_role('contributor');
      remove_role('author');
      remove_role('editor');
      error_log('Roles removed!');
      error_log('Adding Admin Capabilities');
      $instance->addAdminCaps();
      error_log('Ensuring Existing User\'s Roles...');
      $instance->ensureRoleCaps();
      error_log('Roles activated!');
   }
      protected function addAdminCaps():void
      {
         $users = get_users(['role' => 'administrator']);
         foreach (array_merge(Registrar::getRegistered('post'), Registrar::getFeatured('is_content')) as $slug) {
            $this->grantRoleCapabilities('administrator', $slug);
            $this->grantRoleOthersCapabilities('administrator', $slug);
            foreach ($users as $user) {
               $this->grantContent($user, $slug);
               $this->grantOthersContent($user, $slug);
            }
         }
      }
      protected function ensureRoleCaps():void
      {
         $roles = Registrar::getRegistered('user');
         foreach ($roles as $role) {
            $registrar = Registrar::getInstance($role);
            $creatable = $registrar->getCreatable();
            $manageable = $registrar->getManageOthers();
            if (!empty($creatable) || !empty($manageable)) {
               $users = get_users(['role' => jvbCheckBase($role)]);
               foreach ($users as $user) {
                  foreach ($creatable as $slug) {
                     $this->grantContent($user, $slug);
                  }
                  foreach ($manageable as $slug) {
                     $this->grantOthersContent($user, $slug);
                  }
               }
            }
         }
      }
   /******************************************************************
    * OWNABLE and MANAGABLE terms (ie: tattoo shops)
@@ -458,9 +520,10 @@
      }
      $taxonomy = jvbNoBase($taxonomy);
      $registrar = Registrar::getInstance($taxonomy);
      // Verify this is an ownable content taxonomy
      if (!Features::forTaxonomy($taxonomy)->has('is_content') ||
         !Features::forTaxonomy($taxonomy)->has('is_ownable')) {
      if (!$registrar || !$registrar->hasFeature('is_content') ||
            !$registrar->hasFeature('is_ownable')) {
         return false;
      }
@@ -473,6 +536,15 @@
      $user->add_cap(BASE . 'can_own_' . $termID);
      $user->add_cap(BASE . 'can_manage_' . $termID);
      $owners = get_term_meta($termID, BASE.'owners', true);
      if (empty($owners)) {
         $owners = [];
      }
      $owners[] = $userID;
      $owners = array_unique($owners);
      update_term_meta($termID, BASE.'owners', $owners);
      do_action(BASE . 'granted_ownership', $userID, $termID, $taxonomy);
      return true;
@@ -498,6 +570,16 @@
         return false;
      }
      $owners = get_term_meta($termID, BASE.'owners', true);
      if (empty($owners)) {
         $owners = [];
      }
      if (in_array($userID, $owners)) {
         unset($owners[array_search($userID, $owners)]);
      }
      $owners = array_unique($owners);
      update_term_meta($termID, BASE.'owners', $owners);
      $user->remove_cap(BASE . 'can_own_' . $termID);
      do_action(BASE . 'revoked_ownership', $userID, $termID, $taxonomy);
@@ -521,9 +603,10 @@
      }
      $taxonomy = jvbNoBase($taxonomy);
      $registrar = Registrar::getInstance($taxonomy);
      // Verify this is an ownable content taxonomy
      if (!Features::forTaxonomy($taxonomy)->has('is_content') ||
         !Features::forTaxonomy($taxonomy)->has('is_ownable')) {
      if (!$registrar || !$registrar->hasFeature('is_content') ||
         !$registrar->hasFeature('is_ownable')) {
         return false;
      }
@@ -534,6 +617,14 @@
      $user->add_cap(BASE . 'can_manage_' . $termID);
      $managers = get_term_meta($termID, BASE.'managers', true);
      if (empty($managers)) {
         $managers = [];
      }
      $managers[] = $userID;
      $managers = array_unique($managers);
      update_term_meta($termID, BASE.'managers', $managers);
      do_action(BASE . 'granted_management', $userID, $termID, $taxonomy);
      return true;
@@ -561,6 +652,16 @@
      $user->remove_cap(BASE . 'can_manage_' . $termID);
      $managers = get_term_meta($termID, BASE.'managers', true);
      if (empty($managers)) {
         $managers = [];
      }
      if (in_array($userID, $managers)) {
         unset($managers[array_search($userID, $managers)]);
      }
      $managers = array_unique($managers);
      update_term_meta($termID, BASE.'managers', $managers);
      do_action(BASE . 'revoked_management', $userID, $termID, $taxonomy);
      return true;
@@ -682,13 +783,9 @@
      static $ownable = null;
      if ($ownable === null) {
         $ownable = [];
         foreach (JVB_TAXONOMY as $taxonomy => $config) {
            if (Features::forTaxonomy($taxonomy)->has('is_content') &&
               Features::forTaxonomy($taxonomy)->has('is_ownable')) {
               $ownable[] = $taxonomy;
            }
         }
         $ownable = array_map(function ($instance) {
            return $instance->slug;
         }, Registrar::getFeatured('is_ownable', 'term'));
      }
      return $ownable;
@@ -704,14 +801,111 @@
      static $invitable = null;
      if ($invitable === null) {
         $invitable = [];
         foreach (JVB_TAXONOMY as $taxonomy => $config) {
            if (Features::forTaxonomy($taxonomy)->has('invitable')) {
               $invitable[] = $taxonomy;
         $invitable = array_map(function ($instance) {
            return $instance->slug;
         }, Registrar::getFeatured('invitable', 'term'));
      }
      return $invitable;
   }
   public function getPermission(string $action, string $content, ?int $ID = null):?string
   {
      $plural = $this->getContentPlural($content);
      switch ($action) {
         case 'edit':
            if ($ID) {
               return "edit_{$content}";
            }
            return "edit_{$plural}";
      }
      return null;
   }
   public function maybeSwitchPermissions(int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids):void
   {
      //This shouldn't happen, but whatever
      if (empty($this->subTypes)) {
         return;
      }
      if (!in_array($taxonomy, array_keys($this->subTypes))) {
         return;
      }
      $new = array_diff($tt_ids, $old_tt_ids);
      $old = array_diff($old_tt_ids, $tt_ids);
      $userID = (int)get_post_meta($object_id, BASE.'profile_link',true);
      if ($userID === 0) {
         return;
      }
      $user = get_userdata($userID);
      if (!$user) {
         return;
      }
      //Revoke old first
      if (!empty($old)) {
         $old = array_filter(array_map(function ($id) use ($taxonomy) {
            $termID = $this->getTermIDFromTTID($id);
            return $this->getRootTermSlug($termID, $taxonomy);
         }, $old));
         foreach ($old as $slug) {
            if (!array_key_exists($slug, $this->subTypes[$taxonomy])) {
               error_log('[RoleManager]::maybeSwitchPermissions Could not find creatable types for role subtype '.$slug);
               continue;
            }
            foreach ($this->subTypes[$taxonomy][$slug] ?? [] as $s) {
               $this->grantContent($user, $s, false);
            }
         }
      }
      return $invitable;
      if (!empty($new)) {
         $new = array_filter(array_map(function ($id) use ($taxonomy){
            $termID = $this->getTermIDFromTTID($id);
            return $this->getRootTermSlug($termID, $taxonomy);
         }, $new));
         foreach ($new as $slug) {
            if (!array_key_exists($slug, $this->subTypes[$taxonomy])) {
               error_log('[RoleManager]::maybeSwitchPermissions Could not find creatable types for role subtype '.$slug);
               continue;
            }
            foreach ($this->subTypes[$taxonomy][$slug] ?? [] as $s) {
               $this->grantContent($user, $s);
            }
         }
      }
   }
   /**
    * Helper function to get term_id from term_taxonomy_id
    * @param int $tt_id
    *
    * @return int
    */
   private function getTermIDFromTTID(int $tt_id):int
   {
      global $wpdb;
      return $wpdb->get_var($wpdb->prepare(
         "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d",
         $tt_id
      ));
   }
   private function getRootTermSlug(int $termID, string $taxonomy): string|false
   {
      $term = get_term($termID, $taxonomy);
      if (!$term || is_wp_error($term)) {
         return false;
      }
      while ($term->parent !== 0) {
         $term = get_term($term->parent, $taxonomy);
         if (!$term || is_wp_error($term)) {
            return false;
         }
      }
      return $term->slug;
   }
}