=It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
218 files modified
1 files deleted
47 files added
10 files renamed
| | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\base\Site; |
| | | use JVBase\blocks\CustomBlocks; |
| | | use JVBase\managers\AdminPages; |
| | | use JVBase\managers\ApprovalManager; |
| | | use JVBase\managers\Dashboard\DashboardManager; |
| | | use JVBase\managers\Approvals; |
| | | use JVBase\managers\DirectoryManager; |
| | | use JVBase\managers\EmailManager; |
| | | use JVBase\managers\ErrorHandler; |
| | | use JVBase\managers\FavouritesManager; |
| | | use JVBase\managers\Favourites; |
| | | use JVBase\managers\InvitationsManager; |
| | | use JVBase\managers\LoginManager; |
| | | use JVBase\managers\MagicLinkManager; |
| | | use JVBase\managers\Login; |
| | | use JVBase\managers\NotificationManager; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\ReferralManager; |
| | | use JVBase\managers\RoleManager; |
| | | use JVBase\managers\Referrals; |
| | | use JVBase\managers\SEO\render\SchemaOutput; |
| | | use JVBase\managers\TaxonomyRelationships; |
| | | use JVBase\managers\UserTermsManager; |
| | |
| | | use JVBase\rest\routes\FavouritesRoutes; |
| | | use JVBase\rest\routes\FeedRoutes; |
| | | use JVBase\rest\routes\FormRoutes; |
| | | use JVBase\rest\routes\IntegrationsHelcimRoutes; |
| | | use JVBase\rest\routes\IntegrationsRoutes; |
| | | use JVBase\rest\routes\IntegrationsSquareRoutes; |
| | | use JVBase\rest\routes\Invitations; |
| | | use JVBase\rest\routes\LoginRoutes; |
| | | use JVBase\rest\routes\NewsRoutes; |
| | | use JVBase\rest\routes\NotificationsRoutes; |
| | | use JVBase\rest\routes\OptionsRoutes; |
| | | use JVBase\rest\routes\QueueRoutes; |
| | | use JVBase\rest\routes\ReferralRoutes; |
| | | use JVBase\rest\routes\ResponseRoutes; |
| | | use JVBase\rest\routes\SEORoutes; |
| | |
| | | protected array $managers = []; |
| | | protected array $content = []; |
| | | protected array $taxonomies = []; |
| | | protected array $integrations = []; |
| | | protected array $blocks = []; |
| | | protected array $routes = []; |
| | | protected CustomBlocks $customBlocks; |
| | | |
| | | protected array $serviceMap = [ |
| | | 'maps' => 'JVBase\integrations\services\GoogleMaps', |
| | | 'square' => 'JVBase\integrations\services\Square', |
| | | 'facebook' => 'JVBase\integrations\services\Facebook', |
| | | 'helcim' => 'JVBase\integrations\services\Helcim', |
| | | 'instagram' => 'JVBase\integrations\services\Instagram', |
| | | 'bluesky' => 'JVBase\integrations\services\BlueSky', |
| | | 'gmb' => 'JVBase\integrations\services\GoogleMyBusiness', |
| | | 'cloudflare' => 'JVBase\integrations\services\Cloudflare', |
| | | 'umami' => 'JVBase\integrations\services\Umami', |
| | | 'postmark' => 'JVBase\integrations\services\PostMark', |
| | | ]; |
| | | |
| | | |
| | | public static function getInstance(): JVB |
| | | { |
| | |
| | | |
| | | private function __construct() |
| | | { |
| | | $this->customBlocks = new CustomBlocks(); |
| | | $this->managers = [ |
| | | 'errors' => new ErrorHandler(), |
| | | 'queue' => new Queue(), |
| | | // 'dash' => new DashboardManager(), |
| | | 'roles' => new RoleManager(), |
| | | // 'forms' => new FormManager(), |
| | | // 'schema' => new SchemaOutputManager(), |
| | | 'admin' => new AdminPages(), |
| | | 'seoAdmin' => new SEOAdmin(), |
| | | 'seo' => new SchemaOutput(), |
| | | 'schemaHelper' => new SchemaHelper(), |
| | | // 'uploads' => new UploadManager(), |
| | | // 'userTerms' => new UserTermsManager(), |
| | | 'email' => new EmailManager(), |
| | | // 'terms' => new TaxonomyRelationships(), |
| | | ]; |
| | | |
| | | if (Site::has('feed_block')) { |
| | | $this->managers['terms'] = new TaxonomyRelationships(); |
| | | } |
| | | |
| | | if (Site::hasAll(['dashboard', 'membership', 'feed_block'])) { |
| | | $this->managers['userTerms'] = new UserTermsManager(); |
| | | } |
| | | |
| | | $this->routes = [ |
| | | 'login' => new LoginRoutes(), |
| | | 'integrations' => new IntegrationsRoutes(), |
| | | 'seo' => new SEORoutes(), |
| | | 'queue' => new QueueRoutes(), |
| | | 'settings' => new SettingsRoutes(), |
| | | 'upload' => new UploadRoutes(), |
| | | 'forms' => new FormRoutes(), |
| | | 'user' => new UserRoutes() |
| | | ]; |
| | | |
| | | if (Site::has('magic_link')) { |
| | | // $this->routes['magicLink'] = new MagicLinkRoutes(); |
| | | $this->managers['magicLink'] = new MagicLinkManager(); |
| | | } |
| | | if (Site::has('referrals')) { |
| | | $this->managers['referral'] = new ReferralManager(); |
| | | $this->routes['referral'] = new ReferralRoutes(); |
| | | } |
| | | |
| | | |
| | | if (Site::hasIntegration('square')) { |
| | | $this->routes['square'] = new IntegrationsSquareRoutes(); |
| | | } |
| | | if (Site::hasIntegration('helcim')) { |
| | | $this->routes['helcim'] = new IntegrationsHelcimRoutes(); |
| | | } |
| | | |
| | | if (Site::has('feed_block')) { |
| | | $this->routes['feed'] = new FeedRoutes(); |
| | | } |
| | | $membership = Site::membership(); |
| | | if ($membership && $membership->has('notifications')) { |
| | | $this->managers['notifications'] = new NotificationManager(); |
| | | $this->routes['notifications'] = new NotificationsRoutes(); |
| | | } |
| | | if (!empty(Registrar::withFeature('approve_new'))) { |
| | | $this->managers['approvals'] = new ApprovalManager(); |
| | | } |
| | | if (Site::has('feed_block') || Site::has('dashboard')) { |
| | | $this->routes['term'] = new TermRoutes(); |
| | | } |
| | | |
| | | if (Site::has('is_directory')) { |
| | | $this->managers['directory'] = new DirectoryManager(); |
| | | } |
| | | |
| | | if (Site::has('dashboard')) { |
| | | $this->routes['error'] = new ErrorRoutes(); |
| | | $this->routes['admin'] = new AdminRoutes(); |
| | | $this->routes['content'] = new ContentRoutes(); |
| | | // $this->routes['bio'] = new BioRoutes(); |
| | | // $this->routes['shop'] = new ShopRoutes(); |
| | | $this->routes['contentTax'] = new ContentTermsRoutes(); |
| | | $this->routes['options'] = new OptionsRoutes(); |
| | | } |
| | | |
| | | if (Site::has('favourites')) { |
| | | $this->managers['favourites'] = new FavouritesManager(); |
| | | $this->routes['favourites'] = new FavouritesRoutes(); |
| | | } |
| | | |
| | | if ($membership && $membership->has('forum')) { |
| | | $this->routes['news'] = new NewsRoutes(); |
| | | } |
| | | if ($membership && $membership->has('invitable')) { |
| | | $this->managers['invitations'] = new InvitationsManager(); |
| | | } |
| | | if (!empty(Registrar::withFeature('has_responses'))) { |
| | | $this->routes['comments'] = new ResponseRoutes(); |
| | | } |
| | | if (!empty(Registrar::withFeature('karma'))) { |
| | | $this->routes['vote'] = new VoteRoutes(); |
| | | } |
| | | if (!empty(Registrar::withFeature('karma')) |
| | | || ($membership && $membership->has('member_verified')) || |
| | | ($membership && $membership->has('term_approval'))) { |
| | | $this->routes['approvals'] = new ApprovalRoutes(); |
| | | } |
| | | if ($membership && $membership->has('can_invite')) { |
| | | $this->routes['invites'] = new Invitations(); |
| | | } |
| | | |
| | | |
| | | if (Site::has('dashboard')) { |
| | | $this->managers['dash'] = new DashboardManager(); |
| | | } |
| | | |
| | | $this->setupIntegrations(); |
| | | |
| | | add_action('wp_footer', [$this, 'additionalActions']); |
| | | // $this->managers['notifications'] = new NotificationManager(); |
| | | // Register activation hook |
| | | register_activation_hook(JVB_DIR . '/jvb.php', [$this, 'activate']); |
| | | } |
| | | |
| | | |
| | | protected function setupIntegrations(): void |
| | | { |
| | | foreach(array_keys(Site::getIntegrations()) as $integration) { |
| | | $this->integrations[$integration] = $this->serviceMap[$integration]::getInstance(); |
| | | } |
| | | } |
| | | |
| | | public function registeredContent(): array |
| | | { |
| | | return array_merge(array_keys($this->content), array_keys($this->taxonomies)); |
| | | } |
| | | |
| | | public function favourites(): FavouritesManager|false |
| | | { |
| | | return $this->managers['favourites'] ?? false; |
| | | } |
| | | |
| | | public function dashboard(): DashboardManager|false |
| | | { |
| | | return $this->managers['dash'] ?? false; |
| | | } |
| | | |
| | | public function directories(): DirectoryManager|false |
| | | { |
| | | return $this->managers['directory'] ?? false; |
| | | } |
| | | |
| | | public function error(): ErrorHandler |
| | | { |
| | | return $this->managers['errors']; |
| | | } |
| | | |
| | | public function file() |
| | | { |
| | | return $this->managers['file']; |
| | | } |
| | | |
| | | public function queue(): Queue |
| | | { |
| | | return $this->managers['queue']; |
| | | } |
| | | // public function forms() |
| | | // { |
| | | // return $this->managers['forms']; |
| | | // } |
| | | public function notification(): NotificationManager|false |
| | | { |
| | | return $this->managers['notifications'] ?? false; |
| | | } |
| | | |
| | | public function routes($route): mixed |
| | |
| | | return false; |
| | | } |
| | | |
| | | public function roles(): RoleManager |
| | | { |
| | | return $this->managers['roles']; |
| | | } |
| | | |
| | | public function admin() |
| | | { |
| | | return $this->managers['admin']; |
| | | } |
| | | |
| | | public function seoAdmin() |
| | | { |
| | | return $this->managers['seoAdmin']; |
| | | } |
| | | |
| | | public function getFields($type): array |
| | | { |
| | | return Registrar::getFieldsFor($type)??[]; |
| | | } |
| | | |
| | | /** |
| | | * @deprecated Should use the Registrar::getInstance |
| | | * @param $type |
| | | * @return mixed |
| | | */ |
| | | public function getContent($type): mixed |
| | | { |
| | | return $this->content[$type] ?? $this->taxonomies[$type] ?? $this->blocks[$type] ?? null; |
| | | } |
| | | |
| | | public function connect(string $service, ?int $userID = null): mixed |
| | | { |
| | | // if ($userID) { |
| | | // if (!$this->userCanConnect($service, $userID)) { |
| | | // return null; |
| | | // } |
| | | // |
| | | // if (!array_key_exists($service, $this->integrations)) { |
| | | // return null; |
| | | // } |
| | | // |
| | | // return $this->serviceMap[$service]::getInstance($userID); |
| | | // } |
| | | $instance = $this->integrations[$service]??null; |
| | | if ($instance) { |
| | | $instance->switchUser($userID); |
| | | } |
| | | return $instance; |
| | | } |
| | | |
| | | public function userCanConnect(string $service, int $userID): bool |
| | | { |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | return user_can($userID, 'manage_options') || $registrar->hasIntegration($service); |
| | | } |
| | | |
| | | public function getAvailableServices(bool $keys = true): array |
| | | { |
| | | |
| | | return ($keys) ? array_keys($this->integrations) : $this->integrations; |
| | | } |
| | | |
| | | public function activate(): void |
| | | { |
| | | // Activate roles - will be properly initialized after post types are registered |
| | | $this->roles()->activate(); |
| | | } |
| | | |
| | | public function addRoute($slug, $class): void |
| | | { |
| | | $this->routes[$slug] = $class; |
| | | } |
| | | |
| | | public function email(): EmailManager |
| | | { |
| | | return $this->managers['email']; |
| | | } |
| | | |
| | | public function referrals(): ReferralManager|false |
| | | { |
| | | return $this->managers['referral'] ?? false; |
| | | } |
| | | |
| | | public function magicLink(): MagicLinkManager|false |
| | | { |
| | | return $this->managers['magicLink'] ?? false; |
| | | } |
| | | |
| | | public function invitations(): InvitationsManager|false |
| | | { |
| | | return $this->managers['invitations'] ?? false; |
| | | } |
| | | |
| | | public function additionalActions():void |
| | | { |
| | | if (LoginManager::isLogin()) { |
| | | return; |
| | | } |
| | | $extras = apply_filters('jvbAdditionalActions', []); |
| | | $extras = array_filter($extras, function ($extra) { |
| | | return is_array($extra) && array_key_exists('button', $extra) && array_key_exists('content', $extra); |
| | | }); |
| | | if (empty ($extras)) { |
| | | return; |
| | | } |
| | | $buttons = array_map(function ($extra) { |
| | | return $extra['button']; |
| | | }, $extras); |
| | | $contents = array_map(function($extra) { |
| | | return $extra['content']; |
| | | }, $extras); |
| | | |
| | | if (!empty ($buttons)) { |
| | | ?> |
| | | <section class="main-actions"> |
| | | <div class="buttons col"> |
| | | <?= implode('', $buttons); ?> |
| | | </div> |
| | | <div class="actions"> |
| | | <?= implode('', $contents); ?> |
| | | </div> |
| | | </section> |
| | | <?php |
| | | } |
| | | } |
| | | |
| | | public function seo():SchemaOutput |
| | | { |
| | | return $this->managers['seo']; |
| | | } |
| | | |
| | | public function blocks():CustomBlocks|bool |
| | | { |
| | | return $this->customBlocks??false; |
| | | } |
| | | public function schemaHelper():SchemaHelper |
| | | { |
| | | return $this->managers['schemaHelper']; |
| | | } |
| | | |
| | | public function termRelationships():TaxonomyRelationships |
| | | { |
| | | return $this->managers['terms']; |
| | | } |
| | | |
| | | public function approvals ():ApprovalManager|false |
| | | { |
| | | return $this->managers['approvals']??false; |
| | | } |
| | | } |
| | |
| | | use JVBase\managers\DashboardManager; |
| | | use JVBase\managers\DirectoryManager; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\ReferralManager; |
| | | use JVBase\managers\RoleManager; |
| | | use JVBase\managers\Referrals; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | jvbSchedules(); |
| | | error_log('Schedules done!'); |
| | | |
| | | RoleManager::activate(); |
| | | Roles::activate(); |
| | | |
| | | jvbRegisterAdminPages(); |
| | | // if (array_key_exists('integrations', JVB_SITE) && array_key_exists('umami', JVB_SITE['integrations']) && JVB_SITE['integrations']['umami']=== true) { |
| | |
| | | |
| | | function jvbAddAdminCaps() |
| | | { |
| | | $roleManager = new RoleManager(); |
| | | $roleManager = new Roles(); |
| | | |
| | | |
| | | $role = get_role('administrator'); |
| | |
| | | return; |
| | | } |
| | | |
| | | $roleManager = new RoleManager(); |
| | | $roleManager = new Roles(); |
| | | $users = get_users(['role' => 'administrator']); |
| | | |
| | | foreach (array_merge(Registrar::getRegistered('post'), Registrar::withFeature('is_content', 'term')) as $slug) { |
| | |
| | | function jvbRegisterAdminPages():void |
| | | { |
| | | if (Site::has('referrals')){ |
| | | ReferralManager::addSubpage(); |
| | | Referrals::addSubpage(); |
| | | } |
| | | // SEOAdminPage::addSubpage(); |
| | | } |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | .icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMSwxLTIxLjQ5LTU5LjgyLDYsNiwwLDEsMS05LjI1LDcuNjRBODIsODIsMCwxLDAsMjA5Ljc4LDEzNEgxMjhhNiw2LDAsMCwxLDAtMTJoODhBNiw2LDAsMCwxLDIyMiwxMjhaIi8+PC9zdmc+');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTkuNCwxNjcuODRDMjAxLjcxLDE1NS42OSwxOTgsMTM1LjEyLDE5OCwxMjBjMC0xOC40MiwxMy44Ni0zNC4yOSwyMi4xMi00Mi4xMmE2LDYsMCwwLDAsMC04LjcxQzIwOCw1Ny43LDE4Ny4wNyw1MCwxNjgsNTBhNzAuMjMsNzAuMjMsMCwwLDAtNDAsMTIuNTUsNjkuNiw2OS42LDAsMCwwLTg5LjMxLDguMDhBNzIuNjMsNzIuNjMsMCwwLDAsMTgsMTIzLjM1YTEyNS4xMSwxMjUuMTEsMCwwLDAsMzkuNTMsODguMzNBMzcuODUsMzcuODUsMCwwLDAsODMuNiwyMjJoODcuN0EzNy44MywzNy44MywwLDAsMCwxOTksMjEwLjA3YTEyMi42LDEyMi42LDAsMCwwLDE3LjU0LTI0LjJjNi41NS0xMiw1Ljc3LTEzLjc1LDUtMTUuNDhBNi4wNyw2LjA3LDAsMCwwLDIxOS40LDE2Ny44NFptLTI5LjIzLDM0QTI1LjgyLDI1LjgyLDAsMCwxLDE3MS4zLDIxMEg4My42QTI1Ljg1LDI1Ljg1LDAsMCwxLDY1Ljc4LDIwMywxMTMuMjEsMTEzLjIxLDAsMCwxLDMwLDEyM2E2MC41NSw2MC41NSwwLDAsMSwxNy4yMS00NEE1Ni44Miw1Ni44MiwwLDAsMSw4OCw2MmguODFhNTcuMzUsNTcuMzUsMCwwLDEsMzUuNDQsMTIuNzEsNiw2LDAsMCwwLDcuNSwwQTU3LjM5LDU3LjM5LDAsMCwxLDE2OCw2MmMxMy44OSwwLDI4LjgxLDQuNjgsMzkuMTEsMTItOS40NCwxMC4xNC0yMS4xLDI2LjU5LTIxLjEsNDYsMCwyMy43OCw3LjgxLDQyLjYsMjIuNjYsNTQuNzdBMTA3LjMzLDEwNy4zMywwLDAsMSwxOTAuMTcsMjAxLjg5Wm0tNjAtMTcxLjM5QTM4LDM4LDAsMCwxLDE2NywyaDFhNiw2LDAsMCwxLDAsMTJoLTFhMjYsMjYsMCwwLDAtMjUuMTgsMTkuNSw2LDYsMCwxLDEtMTEuNjItM1oiLz48L3N2Zz4=');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=');}.icon-cloud-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01Mi40NCwzNkE2LDYsMCwwLDAsNDMuNTYsNDRsNDAuMTgsNDQuMmMtLjQ1Ljg3LS45LDEuNzUtMS4zMiwyLjY0QTYyLDYyLDAsMSwwLDcyLDIxNGg4OGE4NS4yMyw4NS4yMywwLDAsMCwzMi4zNS02LjNMMjAzLjU2LDIyMGE2LDYsMCwwLDAsOC44OC04LjA4Wk0xNjAsMjAySDcyYTUwLDUwLDAsMSwxLDUuOS05OS42NEE4Ni4yNSw4Ni4yNSwwLDAsMCw3NCwxMjhhNiw2LDAsMCwwLDEyLDAsNzMuOTIsNzMuOTIsMCwwLDEsNi40NC0zMC4ybDkxLjIyLDEwMC4zNEE3My42NSw3My42NSwwLDAsMSwxNjAsMjAyWm04Ni03NGE4NS44NSw4NS44NSwwLDAsMS0yMS44NSw1Ny4yNyw2LDYsMCwwLDEtNC40NywyLDYsNiwwLDAsMS00LjQ3LTEwLDc0LDc0LDAsMCwwLTk5LTEwOC45Miw2LDYsMCwxLDEtNy4xMS05LjY3QTg2LDg2LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-exclamation-mark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTg1LjI3LDg1LjI3LDAsMCwxLTE3LjIsNTEuNiw2LDYsMCwxLDEtOS42LTcuMkE3NCw3NCwwLDEsMCw4NiwxMjhhNiw2LDAsMCwxLTEyLDAsODUuNTQsODUuNTQsMCwwLDEsMy45MS0yNS42NEE1MC42OCw1MC42OCwwLDAsMCw3MiwxMDJhNTAsNTAsMCwwLDAsMCwxMDBIOTZhNiw2LDAsMCwxLDAsMTJINzJBNjIsNjIsMCwxLDEsODIuNDMsOTAuODgsODYsODYsMCwwLDEsMjQ2LDEyOFptLTY2LjI0LDQzLjc2TDE1OCwxOTMuNTFWMTI4YTYsNiwwLDAsMC0xMiwwdjY1LjUxbC0yMS43Ni0yMS43NWE2LDYsMCwwLDAtOC40OCw4LjQ4bDMyLDMyYTYsNiwwLDAsMCw4LjQ4LDBsMzItMzJhNiw2LDAsMCwwLTguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTAwLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OCwwbC04MC04MGE2LDYsMCwwLDEsOC40OC04LjQ4TDEyOCwxNjcuNTFsNzUuNzYtNzUuNzVhNiw2LDAsMCwxLDguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODguMjQsMTY0LjI0YTYsNiwwLDAsMS04LjQ4LDBMMTU4LDE0Mi40OVYyMDhhNiw2LDAsMCwxLTEyLDBWMTQyLjQ5bC0yMS43NiwyMS43NWE2LDYsMCwwLDEtOC40OC04LjQ4bDMyLTMyYTYsNiwwLDAsMSw4LjQ4LDBsMzIsMzJBNiw2LDAsMCwxLDE4OC4yNCwxNjQuMjRaTTE2MCw0MkE4Ni4xLDg2LjEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDQwYTYsNiwwLDAsMCwwLTEySDcyYTUwLDUwLDAsMCwxLDAtMTAwLDUwLjY4LDUwLjY4LDAsMCwxLDUuOTEuMzZBODUuNTQsODUuNTQsMCwwLDAsNzQsMTI4YTYsNiwwLDAsMCwxMiwwLDc0LDc0LDAsMSwxLDEwMy42LDY3Ljg1LDYsNiwwLDAsMCw0LjgsMTFBODYsODYsMCwwLDAsMTYwLDQyWiIvPjwvc3ZnPg==');}.icon-cloud-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMzYuMjQtOTQuMjRhNiw2LDAsMCwxLDAsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxNDQsMTUxLjUxbDQzLjc2LTQzLjc1QTYsNiwwLDAsMSwxOTYuMjQsMTA3Ljc2WiIvPjwvc3ZnPg==');}.icon-cloud-warning{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptLTYtNzRWODhhNiw2LDAsMCwxLDEyLDB2NDBhNiw2LDAsMCwxLTEyLDBabTE2LDM2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE3MCwxNjRaIi8+PC9zdmc+');}.icon-syncing{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iY3VycmVudENvbG9yIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PHBhdGggaWQ9InJlZnJlc2giIGQ9Ik0xNjAuMDQ3IDEyMi44NzVhMzAuNzg0IDMwLjc4NCAwIDAgMC0yMS43NSA4Ljc5N2MtMi44NDIgMy4wMDMtLjQ2NyA0Ljk3MSAxLjMxMiAzLjE1NiAxMS4wNDMtMTAuNzg2IDI4LjcxLTEwLjY4IDM5LjYyNS4yMzRsNy4yMDMgNy4yMDRoLTEyLjg3NWMtMy4zNDcuMDA4LTMuMTY1IDMuODc1IDAgMy44NzVoMTYuMTFjMi4wNjIgMCAyLjU0LTEuNDE4IDIuNTYyLTQuOTdsLjA5NC0xNC45MjFjLjAyLTMuMjktMy40MzctMy4xNjUtMy40MzcgMHYxMi44NmwtNy4yMDMtNy4xODhhMzAuNzY4IDMwLjc2OCAwIDAgMC0yMS42NDEtOS4wNDd6bS0yOS41OTQgMzkuNzk3Yy0yLjA2MiAwLTIuNTI0IDEuNDAyLTIuNTQ3IDQuOTUzbC0uMDk0IDE0LjkyMmMtLjAyIDMuMjkgMy40MjIgMy4xNjQgMy40MjIgMHYtMTIuODZsNy4yMDMgNy4yMDRjMTEuOTU2IDExLjk1NSAzMS4zMTIgMTIuMDY0IDQzLjQwNy4yNSAyLjg0Mi0zLjAwMy40NTEtNC45ODgtMS4zMjgtMy4xNzItMTEuMDQzIDEwLjc4Ni0yOC43MSAxMC42OC0zOS42MjUtLjIzNWwtNy4xODgtNy4yMDNoMTIuODZjMy4zNDctLjAwOCAzLjE2NS0zLjg2IDAtMy44NmgtMTYuMTF6Ii8+PHBhdGggZD0iTTE2MCA0NGE4NC4xMSA4NC4xMSAwIDAgMC03Ni40MSA0OS4xMkE2MC43MSA2MC43MSAwIDAgMCA3MiA5MmE2MCA2MCAwIDAgMCAwIDEyMGg4OGE4NCA4NCAwIDAgMCAwLTE2OFptMCAxNjBINzJhNTIgNTIgMCAxIDEgOC41NS0xMDMuM0E4My42NiA4My42NiAwIDAgMCA3NiAxMjhhNCA0IDAgMCAwIDggMCA3NiA3NiAwIDEgMSA3NiA3NloiLz48L3N2Zz4=');}.icon-cloud-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMjguMjQtODUuNzZMMTY4LjQ4LDEzNmwxOS43NiwxOS43NmE2LDYsMCwxLDEtOC40OCw4LjQ4TDE2MCwxNDQuNDhsLTE5Ljc2LDE5Ljc2YTYsNiwwLDAsMS04LjQ4LTguNDhMMTUxLjUyLDEzNmwtMTkuNzYtMTkuNzZhNiw2LDAsMCwxLDguNDgtOC40OEwxNjAsMTI3LjUybDE5Ljc2LTE5Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=');}.icon-share-fat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTA3Ljc2bC04MC04MEE2LDYsMCwwLDAsMTQ2LDMyVjc0LjJjLTU0LjQ4LDMuNTktMTIwLjM5LDU1LTEyNy45MywxMjAuNjZhMTAsMTAsMCwwLDAsMTcuMjMsOGgwQzQ2LjU2LDE5MC44NSw4NywxNTIuNiwxNDYsMTUwLjEzVjE5MmE2LDYsMCwwLDAsMTAuMjQsNC4yNGw4MC04MEE2LDYsMCwwLDAsMjM2LjI0LDEwNy43NlpNMTU4LDE3Ny41MlYxNDRhNiw2LDAsMCwwLTYtNmMtMjcuNzMsMC01NC43Niw3LjI1LTgwLjMyLDIxLjU1YTE5My4zOCwxOTMuMzgsMCwwLDAtNDAuODEsMzAuNjVjNC43LTI2LjU2LDIwLjE2LTUyLDQ0LTcyLjI3Qzk4LjQ3LDk3Ljk0LDEyNy4yOSw4NiwxNTIsODZhNiw2LDAsMCwwLDYtNlY0Ni40OUwyMjMuNTEsMTEyWiIvPjwvc3ZnPg==');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBIMTc0VjQwYTIyLDIyLDAsMCwwLTIyLTIySDEwNEEyMiwyMiwwLDAsMCw4Miw0MFY1MEg0MGE2LDYsMCwwLDAsMCwxMkg1MFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRIMTkyYTE0LDE0LDAsMCwwLDE0LTE0VjYyaDEwYTYsNiwwLDAsMCwwLTEyWk05NCw0MGExMCwxMCwwLDAsMSwxMC0xMGg0OGExMCwxMCwwLDAsMSwxMCwxMFY1MEg5NFpNMTk0LDIwOGEyLDIsMCwwLDEtMiwySDY0YTIsMiwwLDAsMS0yLTJWNjJIMTk0Wk0xMTAsMTA0djY0YTYsNiwwLDAsMS0xMiwwVjEwNGE2LDYsMCwwLDEsMTIsMFptNDgsMHY2NGE2LDYsMCwwLDEtMTIsMFYxMDRhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-sun-dim{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMzJhNiw2LDAsMCwxLDEyLDB2OGE2LDYsMCwwLDEtMTIsMFptNjgsODhhNjIsNjIsMCwxLDEtNjItNjJBNjIuMDcsNjIuMDcsMCwwLDEsMTkwLDEyOFptLTEyLDBhNTAsNTAsMCwxLDAtNTAsNTBBNTAuMDYsNTAuMDYsMCwwLDAsMTc4LDEyOFpNNTkuNzYsNjguMjRhNiw2LDAsMSwwLDguNDgtOC40OGwtOC04YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwxLDAtOC40OC04LjQ4Wm0xMzYtMTM2LTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwwLDAtOC40OC04LjQ4Wm0uNDgsMTM2YTYsNiwwLDAsMC04LjQ4LDguNDhsOCw4YTYsNiwwLDAsMCw4LjQ4LTguNDhaTTQwLDEyMkgzMmE2LDYsMCwwLDAsMCwxMmg4YTYsNiwwLDAsMCwwLTEyWm04OCw4OGE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LThBNiw2LDAsMCwwLDEyOCwyMTBabTk2LTg4aC04YTYsNiwwLDAsMCwwLDEyaDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-moon{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==');}.icon-house{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNmg2NGE2LDYsMCwwLDAsNi02VjE1OGgzNnY1OGE2LDYsMCwwLDAsNiw2aDY0YTYsNiwwLDAsMCw2LTZWMTIwQTEzLjkyLDEzLjkyLDAsMCwwLDIxNy45LDExMC4xWk0yMTAsMjEwSDE1OFYxNTJhNiw2LDAsMCwwLTYtNkgxMDRhNiw2LDAsMCwwLTYsNnY1OEg0NlYxMjBhMiwyLDAsMCwxLC41OC0xLjQybDgwLTgwYTIsMiwwLDAsMSwyLjg0LDBsODAsODBBMiwyLDAsMCwxLDIxMCwxMjBaIi8+PC9zdmc+');}.icon-star-half-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMCwyNC41LDE3LjgyTDEyOCwxOTguNDlsNTAuNTMsMzEuMDhBMTYuNCwxNi40LDAsMCwwLDIwMywyMTEuNzVsLTEzLjc2LTU4LjA3LDQ1LTM4LjgzQTE2LjQzLDE2LjQzLDAsMCwwLDIzOS4xOCw5Ny4yNlptLTE1LjM0LDUuNDctNDguNyw0MmE4LDgsMCwwLDAtMi41Niw3LjkxbDE0Ljg4LDYyLjhhLjM3LjM3LDAsMCwxLS4xNy40OGMtLjE4LjE0LS4yMy4xMS0uMzgsMGwtNTQuNzItMzMuNjVBOCw4LDAsMCwwLDEyOCwxODEuMVYzMmMuMjQsMCwuMjcuMDguMzUuMjZMMTUzLDkxLjg2YTgsOCwwLDAsMCw2Ljc1LDQuOTJsNjMuOTEsNS4xNmMuMTYsMCwuMjUsMCwuMzQuMjlTMjI0LDEwMi42MywyMjMuODQsMTAyLjczWiIvPjwvc3ZnPg==');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');} |
| | | .icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsMTI4YTk2LDk2LDAsMSwxLTIxLjk1LTYxLjA5LDgsOCwwLDEsMS0xMi4zMywxMC4xOEE4MCw4MCwwLDEsMCwyMDcuNiwxMzZIMTI4YTgsOCwwLDAsMSwwLTE2aDg4QTgsOCwwLDAsMSwyMjQsMTI4WiIvPjwvc3ZnPg==');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMywxNjkuNTlhOC4wNyw4LjA3LDAsMCwwLTIuOC0zLjRDMjAzLjUzLDE1NC41MywyMDAsMTM0LjY0LDIwMCwxMjBjMC0xNy42NywxMy40Ny0zMy4wNiwyMS41LTQwLjY3YTgsOCwwLDAsMCwwLTExLjYyQzIwOC44Miw1NS43NCwxODcuODIsNDgsMTY4LDQ4YTcyLjIsNzIuMiwwLDAsMC00MCwxMi4xMyw3MS41Niw3MS41NiwwLDAsMC05MC43MSw5LjA5QTc0LjYzLDc0LjYzLDAsMCwwLDE2LDEyMy40YTEyNy4wNiwxMjcuMDYsMCwwLDAsNDAuMTQsODkuNzNBMzkuOCwzOS44LDAsMCwwLDgzLjU5LDIyNGg4Ny42OGEzOS44NCwzOS44NCwwLDAsMCwyOS4xMi0xMi41NywxMjUsMTI1LDAsMCwwLDE3LjgyLTI0LjZDMjI1LjIzLDE3NCwyMjQuMzMsMTcyLDIyMy4zLDE2OS41OVptLTM0LjYzLDMwLjk0YTIzLjc2LDIzLjc2LDAsMCwxLTE3LjQsNy40N0g4My41OWEyMy44MiwyMy44MiwwLDAsMS0xNi40NC02LjUxQTExMS4xNCwxMTEuMTQsMCwwLDEsMzIsMTIzLDU4LjUsNTguNSwwLDAsMSw0OC42NSw4MC40Nyw1NC44MSw1NC44MSwwLDAsMSw4OCw2NGguNzhBNTUuNDUsNTUuNDUsMCwwLDEsMTIzLDc2LjI4YTgsOCwwLDAsMCwxMCwwQTU1LjQ0LDU1LjQ0LDAsMCwxLDE2OCw2NGE3MC42NCw3MC42NCwwLDAsMSwzNiwxMC4zNWMtMTMsMTQuNTItMjAsMzAuNDctMjAsNDUuNjUsMCwyMy43Nyw3LjY0LDQyLjczLDIyLjE4LDU1LjNBMTA1LjgyLDEwNS44MiwwLDAsMSwxODguNjcsMjAwLjUzWk0xMjguMjMsMzBBNDAsNDAsMCwwLDEsMTY3LDBoMWE4LDgsMCwwLDEsMCwxNmgtMWEyNCwyNCwwLDAsMC0yMy4yNCwxOCw4LDgsMCwxLDEtMTUuNS00WiIvPjwvc3ZnPg==');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzMuNjYsOTguMzRhOCw4LDAsMCwxLDAsMTEuMzJsLTU2LDU2YTgsOCwwLDAsMS0xMS4zMiwwbC0yNC0yNGE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTEyLDE0OC42OWw1MC4zNC01MC4zNUE4LDgsMCwwLDEsMTczLjY2LDk4LjM0Wk0yMzIsMTI4QTEwNCwxMDQsMCwxLDEsMTI4LDI0LDEwNC4xMSwxMDQuMTEsMCwwLDEsMjMyLDEyOFptLTE2LDBhODgsODgsMCwxLDAtODgsODhBODguMSw4OC4xLDAsMCwwLDIxNiwxMjhaIi8+PC9zdmc+');}details.all-filters summary::after,.icon-faders-horizontal{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzYsODBhOCw4LDAsMCwxLDgtOGgzMmE4LDgsMCwwLDEsMCwxNkgxODRBOCw4LDAsMCwxLDE3Niw4MFpNNDAsODhIMTQ0djE2YTgsOCwwLDAsMCwxNiwwVjU2YTgsOCwwLDAsMC0xNiwwVjcySDQwYTgsOCwwLDAsMCwwLDE2Wm0xNzYsODBIMTIwYTgsOCwwLDAsMCwwLDE2aDk2YTgsOCwwLDAsMCwwLTE2Wk04OCwxNDRhOCw4LDAsMCwwLTgsOHYxNkg0MGE4LDgsMCwwLDAsMCwxNkg4MHYxNmE4LDgsMCwwLDAsMTYsMFYxNTJBOCw4LDAsMCwwLDg4LDE0NFoiLz48L3N2Zz4=');}.icon-cloud-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01My45MiwzNC42MkE4LDgsMCwxLDAsNDIuMDgsNDUuMzhMODEuMzIsODguNTVsLS4wNi4xMkE2NSw2NSwwLDAsMCw3Miw4OGE2NCw2NCwwLDAsMCwwLDEyOGg4OGE4Ny4zNCw4Ny4zNCwwLDAsMCwzMS44LTUuOTNsMTAuMjgsMTEuMzFhOCw4LDAsMSwwLDExLjg0LTEwLjc2Wk0xNjAsMjAwSDcyYTQ4LDQ4LDAsMCwxLDAtOTZjMS4xLDAsMi4yLDAsMy4zLjEyQTg4LjQsODguNCwwLDAsMCw3MiwxMjhhOCw4LDAsMCwwLDE2LDAsNzIuMjUsNzIuMjUsMCwwLDEsNS4wNi0yNi41NGw4Nyw5NS43QTcxLjY2LDcxLjY2LDAsMCwxLDE2MCwyMDBabTg4LTcyYTg3Ljg5LDg3Ljg5LDAsMCwxLTIyLjM1LDU4LjYxQTgsOCwwLDAsMSwyMTMuNzEsMTc2LDcyLDcyLDAsMCwwLDExNy4zNyw3MGE4LDgsMCwwLDEtOS40OC0xMi44OUE4OCw4OCwwLDAsMSwyNDgsMTI4WiIvPjwvc3ZnPg==');}.icon-exclamation-mark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDQsMjAwYTE2LDE2LDAsMSwxLTE2LTE2QTE2LDE2LDAsMCwxLDE0NCwyMDBabS0xNi00MGE4LDgsMCwwLDAsOC04VjQ4YTgsOCwwLDAsMC0xNiwwVjE1MkE4LDgsMCwwLDAsMTI4LDE2MFoiLz48L3N2Zz4=');}.icon-cloud-arrow-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDgsMTI4YTg3LjM0LDg3LjM0LDAsMCwxLTE3LjYsNTIuODEsOCw4LDAsMSwxLTEyLjgtOS42MkE3MS4zNCw3MS4zNCwwLDAsMCwyMzIsMTI4YTcyLDcyLDAsMCwwLTE0NCwwLDgsOCwwLDAsMS0xNiwwLDg4LDg4LDAsMCwxLDMuMjktMjMuODhDNzQuMiwxMDQsNzMuMSwxMDQsNzIsMTA0YTQ4LDQ4LDAsMCwwLDAsOTZIOTZhOCw4LDAsMCwxLDAsMTZINzJBNjQsNjQsMCwxLDEsODEuMjksODguNjgsODgsODgsMCwwLDEsMjQ4LDEyOFptLTY5LjY2LDQyLjM0TDE2MCwxODguNjlWMTI4YTgsOCwwLDAsMC0xNiwwdjYwLjY5bC0xOC4zNC0xOC4zNWE4LDgsMCwwLDAtMTEuMzIsMTEuMzJsMzIsMzJhOCw4LDAsMCwwLDExLjMyLDBsMzItMzJhOCw4LDAsMCwwLTExLjMyLTExLjMyWiIvPjwvc3ZnPg==');}details summary::after,.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuNjYsMTAxLjY2bC04MCw4MGE4LDgsMCwwLDEtMTEuMzIsMGwtODAtODBBOCw4LDAsMCwxLDUzLjY2LDkwLjM0TDEyOCwxNjQuNjlsNzQuMzQtNzQuMzVhOCw4LDAsMCwxLDExLjMyLDExLjMyWiIvPjwvc3ZnPg==');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzguMzQsMTY1LjY2LDE2MCwxNDcuMzFWMjA4YTgsOCwwLDAsMS0xNiwwVjE0Ny4zMWwtMTguMzQsMTguMzVhOCw4LDAsMCwxLTExLjMyLTExLjMybDMyLTMyYTgsOCwwLDAsMSwxMS4zMiwwbDMyLDMyYTgsOCwwLDAsMS0xMS4zMiwxMS4zMlpNMTYwLDQwQTg4LjA4LDg4LjA4LDAsMCwwLDgxLjI5LDg4LjY4LDY0LDY0LDAsMSwwLDcyLDIxNmg0MGE4LDgsMCwwLDAsMC0xNkg3MmE0OCw0OCwwLDAsMSwwLTk2YzEuMSwwLDIuMiwwLDMuMjkuMTJBODgsODgsMCwwLDAsNzIsMTI4YTgsOCwwLDAsMCwxNiwwLDcyLDcyLDAsMSwxLDEwMC44LDY2LDgsOCwwLDAsMCwzLjIsMTUuMzQsNy45LDcuOSwwLDAsMCwzLjItLjY4QTg4LDg4LDAsMCwwLDE2MCw0MFoiLz48L3N2Zz4=');}.icon-cloud-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDBBODguMDksODguMDksMCwwLDAsODEuMjksODguNjcsNjQsNjQsMCwxLDAsNzIsMjE2aDg4YTg4LDg4LDAsMCwwLDAtMTc2Wm0wLDE2MEg3MmE0OCw0OCwwLDAsMSwwLTk2YzEuMSwwLDIuMiwwLDMuMjkuMTFBODgsODgsMCwwLDAsNzIsMTI4YTgsOCwwLDAsMCwxNiwwLDcyLDcyLDAsMSwxLDcyLDcyWm0zNy42Ni05My42NmE4LDgsMCwwLDEsMCwxMS4zMmwtNDgsNDhhOCw4LDAsMCwxLTExLjMyLDBsLTI0LTI0YTgsOCwwLDAsMSwxMS4zMi0xMS4zMkwxNDQsMTQ4LjY5bDQyLjM0LTQyLjM1QTgsOCwwLDAsMSwxOTcuNjYsMTA2LjM0WiIvPjwvc3ZnPg==');}.icon-cloud-warning{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDBBODguMDksODguMDksMCwwLDAsODEuMjksODguNjcsNjQsNjQsMCwxLDAsNzIsMjE2aDg4YTg4LDg4LDAsMCwwLDAtMTc2Wm0wLDE2MEg3MmE0OCw0OCwwLDAsMSwwLTk2YzEuMSwwLDIuMiwwLDMuMjkuMTFBODgsODgsMCwwLDAsNzIsMTI4YTgsOCwwLDAsMCwxNiwwLDcyLDcyLDAsMSwxLDcyLDcyWm0tOC03MlY4OGE4LDgsMCwwLDEsMTYsMHY0MGE4LDgsMCwwLDEtMTYsMFptMjAsMzZhMTIsMTIsMCwxLDEtMTItMTJBMTIsMTIsMCwwLDEsMTcyLDE2NFoiLz48L3N2Zz4=');}.icon-syncing{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iY3VycmVudENvbG9yIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PHBhdGggaWQ9InJlZnJlc2giIGQ9Ik0xNjAuMDQ3IDEyMi44NzVhMzAuNzg0IDMwLjc4NCAwIDAgMC0yMS43NSA4Ljc5N2MtMi44NDIgMy4wMDMtLjQ2NyA0Ljk3MSAxLjMxMiAzLjE1NiAxMS4wNDMtMTAuNzg2IDI4LjcxLTEwLjY4IDM5LjYyNS4yMzRsNy4yMDMgNy4yMDRoLTEyLjg3NWMtMy4zNDcuMDA4LTMuMTY1IDMuODc1IDAgMy44NzVoMTYuMTFjMi4wNjIgMCAyLjU0LTEuNDE4IDIuNTYyLTQuOTdsLjA5NC0xNC45MjFjLjAyLTMuMjktMy40MzctMy4xNjUtMy40MzcgMHYxMi44NmwtNy4yMDMtNy4xODhhMzAuNzY4IDMwLjc2OCAwIDAgMC0yMS42NDEtOS4wNDd6bS0yOS41OTQgMzkuNzk3Yy0yLjA2MiAwLTIuNTI0IDEuNDAyLTIuNTQ3IDQuOTUzbC0uMDk0IDE0LjkyMmMtLjAyIDMuMjkgMy40MjIgMy4xNjQgMy40MjIgMHYtMTIuODZsNy4yMDMgNy4yMDRjMTEuOTU2IDExLjk1NSAzMS4zMTIgMTIuMDY0IDQzLjQwNy4yNSAyLjg0Mi0zLjAwMy40NTEtNC45ODgtMS4zMjgtMy4xNzItMTEuMDQzIDEwLjc4Ni0yOC43MSAxMC42OC0zOS42MjUtLjIzNWwtNy4xODgtNy4yMDNoMTIuODZjMy4zNDctLjAwOCAzLjE2NS0zLjg2IDAtMy44NmgtMTYuMTF6Ii8+PHBhdGggZD0iTTE2MCA0NGE4NC4xMSA4NC4xMSAwIDAgMC03Ni40MSA0OS4xMkE2MC43MSA2MC43MSAwIDAgMCA3MiA5MmE2MCA2MCAwIDAgMCAwIDEyMGg4OGE4NCA4NCAwIDAgMCAwLTE2OFptMCAxNjBINzJhNTIgNTIgMCAxIDEgOC41NS0xMDMuM0E4My42NiA4My42NiAwIDAgMCA3NiAxMjhhNCA0IDAgMCAwIDggMCA3NiA3NiAwIDEgMSA3NiA3NloiLz48L3N2Zz4=');}.icon-cloud-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDBBODguMDksODguMDksMCwwLDAsODEuMjksODguNjcsNjQsNjQsMCwxLDAsNzIsMjE2aDg4YTg4LDg4LDAsMCwwLDAtMTc2Wm0wLDE2MEg3MmE0OCw0OCwwLDAsMSwwLTk2YzEuMSwwLDIuMiwwLDMuMjkuMTFBODgsODgsMCwwLDAsNzIsMTI4YTgsOCwwLDAsMCwxNiwwLDcyLDcyLDAsMSwxLDcyLDcyWm0yOS42Ni04Mi4zNEwxNzEuMzEsMTM2bDE4LjM1LDE4LjM0YTgsOCwwLDAsMS0xMS4zMiwxMS4zMkwxNjAsMTQ3LjMxbC0xOC4zNCwxOC4zNWE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTQ4LjY5LDEzNmwtMTguMzUtMTguMzRhOCw4LDAsMCwxLDExLjMyLTExLjMyTDE2MCwxMjQuNjlsMTguMzQtMTguMzVhOCw4LDAsMCwxLDExLjMyLDExLjMyWiIvPjwvc3ZnPg==');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhWOTZhOCw4LDAsMCwxLTgsOEgxNjhhOCw4LDAsMCwxLDAtMTZoMjguNjlMMTgyLjA2LDczLjM3YTc5LjU2LDc5LjU2LDAsMCwwLTU2LjEzLTIzLjQzaC0uNDVBNzkuNTIsNzkuNTIsMCwwLDAsNjkuNTksNzIuNzEsOCw4LDAsMCwxLDU4LjQxLDYxLjI3YTk2LDk2LDAsMCwxLDEzNSwuNzlMMjA4LDc2LjY5VjQ4YTgsOCwwLDAsMSwxNiwwWk0xODYuNDEsMTgzLjI5YTgwLDgwLDAsMCwxLTExMi40Ny0uNjZMNTkuMzEsMTY4SDg4YTgsOCwwLDAsMCwwLTE2SDQwYTgsOCwwLDAsMC04LDh2NDhhOCw4LDAsMCwwLDE2LDBWMTc5LjMxbDE0LjYzLDE0LjYzQTk1LjQzLDk1LjQzLDAsMCwwLDEzMCwyMjIuMDZoLjUzYTk1LjM2LDk1LjM2LDAsMCwwLDY3LjA3LTI3LjMzLDgsOCwwLDAsMC0xMS4xOC0xMS40NFoiLz48L3N2Zz4=');}.icon-share-fat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuNjYsMTA2LjM1bC04MC04MEE4LDgsMCwwLDAsMTQ0LDMyVjcyLjM1Yy0yNS45NCwyLjIyLTU0LjU5LDE0LjkyLTc4LjE2LDM0LjkxLTI4LjM4LDI0LjA4LTQ2LjA1LDU1LjExLTQ5Ljc2LDg3LjM3YTEyLDEyLDAsMCwwLDIwLjY4LDkuNThoMGMxMS0xMS43MSw1MC4xNC00OC43NCwxMDcuMjQtNTJWMTkyYTgsOCwwLDAsMCwxMy42Niw1LjY1bDgwLTgwQTgsOCwwLDAsMCwyMzcuNjYsMTA2LjM1Wk0xNjAsMTcyLjY5VjE0NGE4LDgsMCwwLDAtOC04Yy0yOC4wOCwwLTU1LjQzLDcuMzMtODEuMjksMjEuOGExOTYuMTcsMTk2LjE3LDAsMCwwLTM2LjU3LDI2LjUyYzUuOC0yMy44NCwyMC40Mi00Ni41MSw0Mi4wNS02NC44NkM5OS40MSw5OS43NywxMjcuNzUsODgsMTUyLDg4YTgsOCwwLDAsMCw4LThWNTEuMzJMMjIwLjY5LDExMloiLz48L3N2Zz4=');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDhIMTc2VjQwYTI0LDI0LDAsMCwwLTI0LTI0SDEwNEEyNCwyNCwwLDAsMCw4MCw0MHY4SDQwYTgsOCwwLDAsMCwwLDE2aDhWMjA4YTE2LDE2LDAsMCwwLDE2LDE2SDE5MmExNiwxNiwwLDAsMCwxNi0xNlY2NGg4YTgsOCwwLDAsMCwwLTE2Wk05Niw0MGE4LDgsMCwwLDEsOC04aDQ4YTgsOCwwLDAsMSw4LDh2OEg5NlptOTYsMTY4SDY0VjY0SDE5MlpNMTEyLDEwNHY2NGE4LDgsMCwwLDEtMTYsMFYxMDRhOCw4LDAsMCwxLDE2LDBabTQ4LDB2NjRhOCw4LDAsMCwxLTE2LDBWMTA0YTgsOCwwLDAsMSwxNiwwWiIvPjwvc3ZnPg==');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjM4LDE2LjM4LDAsMCwwLDI0LjUsMTcuODJMMTI4LDE5OC40OWw1MC41MywzMS4wOEExNi40LDE2LjQsMCwwLDAsMjAzLDIxMS43NWwtMTMuNzYtNTguMDcsNDUtMzguODNBMTYuNDMsMTYuNDMsMCwwLDAsMjM5LjE4LDk3LjI2Wm0tMTUuMzQsNS40Ny00OC43LDQyYTgsOCwwLDAsMC0yLjU2LDcuOTFsMTQuODgsNjIuOGEuMzcuMzcsMCwwLDEtLjE3LjQ4Yy0uMTguMTQtLjIzLjExLS4zOCwwbC01NC43Mi0zMy42NWE4LDgsMCwwLDAtOC4zOCwwTDY5LjA5LDIxNS45NGMtLjE1LjA5LS4xOS4xMi0uMzgsMGEuMzcuMzcsMCwwLDEtLjE3LS40OGwxNC44OC02Mi44YTgsOCwwLDAsMC0yLjU2LTcuOTFsLTQ4LjctNDJjLS4xMi0uMS0uMjMtLjE5LS4xMy0uNXMuMTgtLjI3LjMzLS4yOWw2My45Mi01LjE2QTgsOCwwLDAsMCwxMDMsOTEuODZsMjQuNjItNTkuNjFjLjA4LS4xNy4xMS0uMjUuMzUtLjI1cy4yNy4wOC4zNS4yNUwxNTMsOTEuODZhOCw4LDAsMCwwLDYuNzUsNC45Mmw2My45Miw1LjE2Yy4xNSwwLC4yNCwwLC4zMy4yOVMyMjQsMTAyLjYzLDIyMy44NCwxMDIuNzNaIi8+PC9zdmc+');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-star-half-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMCwyNC41LDE3LjgyTDEyOCwxOTguNDlsNTAuNTMsMzEuMDhBMTYuNCwxNi40LDAsMCwwLDIwMywyMTEuNzVsLTEzLjc2LTU4LjA3LDQ1LTM4LjgzQTE2LjQzLDE2LjQzLDAsMCwwLDIzOS4xOCw5Ny4yNlptLTE1LjM0LDUuNDctNDguNyw0MmE4LDgsMCwwLDAtMi41Niw3LjkxbDE0Ljg4LDYyLjhhLjM3LjM3LDAsMCwxLS4xNy40OGMtLjE4LjE0LS4yMy4xMS0uMzgsMGwtNTQuNzItMzMuNjVBOCw4LDAsMCwwLDEyOCwxODEuMVYzMmMuMjQsMCwuMjcuMDguMzUuMjZMMTUzLDkxLjg2YTgsOCwwLDAsMCw2Ljc1LDQuOTJsNjMuOTEsNS4xNmMuMTYsMCwuMjUsMCwuMzQuMjlTMjI0LDEwMi42MywyMjMuODQsMTAyLjczWiIvPjwvc3ZnPg==');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');} |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | namespace JVBase\base; |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | /** |
| | | * Custom settings for the login page can be set here |
| | | */ |
| | |
| | | namespace JVBase\base; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * JVB_MEMBERSHIP defines the structure of the directory |
| | | * Options include: |
| | |
| | | <?php |
| | | namespace JVBase\base; |
| | | use JVBase\Instance; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\config\seo\Resolver; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | class SchemaHelper |
| | | { |
| | | use Instance; |
| | | protected static array $allowedTypes; |
| | | protected static array $allowedFormats = ['schema', 'archive', 'meta', 'reference']; |
| | | protected static array $schemas = []; |
| | | protected static array $metas = []; |
| | | protected static array $archives = []; |
| | | protected static array $references = []; |
| | | public function __construct() |
| | | protected function __construct() |
| | | { |
| | | self::$allowedTypes = array_merge(['website', 'organization'], Registrar::getRegistered()); |
| | | } |
| | |
| | | } |
| | | return $class; |
| | | } |
| | | |
| | | public static function reset(string $type, string $format):bool |
| | | { |
| | | $default = self::getDefault($type, $format); |
| | | return self::update($type, $format, $default); |
| | | } |
| | | } |
| | |
| | | public static function hasIntegration(string $integration):bool |
| | | { |
| | | $integration = strtolower($integration); |
| | | return array_key_exists($integration, self::$integrations); |
| | | return array_key_exists($integration, self::$integrations) && self::$integrations[$integration] === true; |
| | | } |
| | | |
| | | public static function hasAnyIntegration(array $integrations = []):bool |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\base\Site; |
| | | |
| | | require(JVB_DIR.'/base/Site.php'); |
| | | |
| | | |
| | | add_action('plugins_loaded', 'jvbDefineSite', 1); |
| | | function jvbDefineSite() { |
| | | function jvbDefineSite():void { |
| | | Site::getInstance(); |
| | | } |
| | | //require(JVB_DIR.'/base/content.php'); |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | $defaults = [ |
| | | 'action-0' => '#ff0080', |
| | | 'action-50' => '#ff2492', |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | /** |
| | | * JVB_CONTENT: Base post type definitions |
| | | * Structure: slug=> [$options] |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | $name = get_bloginfo('name'); |
| | | $desc = get_bloginfo('description'); |
| | | $defaults = [ |
| | |
| | | use JVBase\meta\Form; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Fields; |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * Custom options for the site can be set here. They will need to be handled by the child plugin, but you can make use of the Meta.php this way |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | /** |
| | | * JVB_TAXONOMY : defines the custom taxonomies |
| | | * structure: slug => [$options] |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | /** |
| | | * JVB_USER -> defines user roles. Mainly for Dashboard/Directory sites |
| | | * Structure: slug => $options |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | function() use($taxonomy, $termID) { |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $tax = get_taxonomy($taxonomy); |
| | | $query = new WP_Query([ |
| | | 'post_type' => $tax->object_type, |
| | | 'posts_per_page' => 1, |
| | | 'fields' => 'ids', |
| | | 'tax_query' => [ |
| | | [ |
| | | 'taxonomy' => $taxonomy, |
| | | 'terms' => $termID |
| | | if ($tax) { |
| | | $query = new WP_Query([ |
| | | 'post_type' => $tax->object_type, |
| | | 'posts_per_page' => 1, |
| | | 'fields' => 'ids', |
| | | 'tax_query' => [ |
| | | [ |
| | | 'taxonomy' => $taxonomy, |
| | | 'terms' => $termID |
| | | ] |
| | | ] |
| | | ] |
| | | ]); |
| | | $result = ($query->have_posts()) ? 'true' : 'false'; |
| | | wp_reset_postdata(); |
| | | return $result; |
| | | ]); |
| | | $result = ($query->have_posts()) ? 'true' : 'false'; |
| | | wp_reset_postdata(); |
| | | return $result; |
| | | |
| | | } |
| | | return 'false'; |
| | | } |
| | | ); |
| | | } |
| File was renamed from inc/integrations/Instance.php |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | namespace JVBase; |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | $results['total'] = count($terms); |
| | | $results['messages'][] = "Found {$results['total']} terms to process"; |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'sync_content_taxonomy_tables', |
| | | 0, |
| | | [ |
| | |
| | | <?php |
| | | namespace JVBase\admin; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\integrations\ServiceMap; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | |
| | | |
| | | class Integrations |
| | | { |
| | | use ServiceMap; |
| | | |
| | | public function __construct() |
| | | { |
| | | // Hook into WordPress admin |
| | |
| | | ); |
| | | |
| | | // Add submenu pages for configured integrations |
| | | $integrations = JVB()->getAvailableServices(false); |
| | | |
| | | foreach($integrations as $service => $integration) { |
| | | // if ($integration->isSetUp() && !empty($integration->enabledContentTypes())) { |
| | | if (!empty($integration->enabledContentTypes())) { |
| | | foreach ($this->getAvailableServices() as $service) { |
| | | $integration = $this->getService($service); |
| | | if ($integration->isSetUp() && method_exists($integration, 'getAllowedContent') && !empty($integration->getAllowedContent())) { |
| | | add_submenu_page( |
| | | 'jvb-integrations', |
| | | $integration->getTitle() . ' Defaults', |
| | | $integration->getTitle(), |
| | | 'manage_options', |
| | | 'jvb-integration-'.$service, |
| | | 'jvb-integration-'.$integration->getServiceName(), |
| | | [$integration, 'renderDefaults'] |
| | | ); |
| | | } |
| | |
| | | |
| | | <div class="item-grid integrations"> |
| | | <?php |
| | | $integrations = JVB()->getAvailableServices(false); |
| | | foreach ($this->getAvailableServices() as $service) { |
| | | $integration = $this->getService($service); |
| | | if ($integration->isSetUp() && method_exists($integration, 'renderConnection')) { |
| | | $integration->renderConnection(); |
| | | } |
| | | } |
| | | |
| | | foreach ($integrations as $service => $integration): |
| | | $integration->renderConnection(); |
| | | endforeach; |
| | | ?> |
| | | </div> |
| | | </div> |
| | |
| | | namespace JVBase\admin; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\AdminPages; |
| | | use JVBase\meta\Form; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\ui\Tabs; |
| | |
| | | } |
| | | |
| | | class SEOAdmin { |
| | | use Instance; |
| | | protected string $admin_page; |
| | | protected array $types = [ |
| | | 'JVBase\managers\SEO\render\Thing' => 'Thing', |
| | |
| | | |
| | | protected array $checks; |
| | | |
| | | public function __construct() |
| | | protected function __construct() |
| | | { |
| | | $this->setChecks(); |
| | | add_action(BASE.'dashboard_page_seo', [$this, 'addDashboardSection'], 10); |
| | |
| | | |
| | | public function addAdminMenu():void |
| | | { |
| | | $main = JVB()->admin()->getMainConfig(); |
| | | $main = AdminPages::getInstance()->getMainConfig(); |
| | | $this->admin_page = add_submenu_page( |
| | | $main['menu_slug'], |
| | | 'SEO', |
| | |
| | | ]; |
| | | } |
| | | |
| | | $success = JVB()->schemaHelper()->updateSchema($action, $data); |
| | | $success = SchemaHelper::getInstance()->updateSchema($action, $data); |
| | | return $success ? [ |
| | | 'jvb_notice' => 'success', |
| | | 'jvb_message' => 'Saved changes successfully' |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | require(JVB_DIR . '/inc/admin/Integrations.php'); |
| | |
| | | <?php |
| | | namespace JVBase\blocks; |
| | | |
| | | use DateTime; |
| | | use DOMDocument; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\LoginManager; |
| | | use JVBase\managers\Login; |
| | | use JVBase\managers\SEO\BreadcrumbManager; |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\VideoObject; |
| | | use JVBase\utility\Image; |
| | | use JVBase\utility\Video; |
| | | use WP_Block; |
| | | use WP_Query; |
| | | |
| | |
| | | |
| | | class CustomBlocks |
| | | { |
| | | use Instance; |
| | | protected Cache $cache; |
| | | |
| | | protected static ?WP_Query $currentLoop = null; |
| | |
| | | protected static array $pendingClass = []; |
| | | protected static bool $renderGallery = false; |
| | | |
| | | public function __construct() |
| | | private function __construct() |
| | | { |
| | | $this->cache = Cache::for('blocks', WEEK_IN_SECONDS); |
| | | $this->cache->connect('post')->connect('taxonomy'); |
| | | $this->cache->flush(); |
| | | // $this->cache = Cache::for('blocks', null,WEEK_IN_SECONDS); |
| | | |
| | | add_filter('pre_render_block', [$this, 'prerender'], 10, 3); |
| | | add_filter('render_block', [$this, 'render'], 10, 2); |
| | | |
| | |
| | | protected function checkMethods(?string $content, array $block, ?WP_Block $parent = null, bool $isPrerender = false):?string |
| | | { |
| | | $blockName = $this->sanitizeBlockName($block); |
| | | if (str_starts_with($blockName, 'woocommerce')) { |
| | | return $content; |
| | | } |
| | | |
| | | $base = ($isPrerender) ? 'prerender_' : 'render_'; |
| | | $method = $base.$blockName; |
| | |
| | | //Ignore for both |
| | | $base = [ |
| | | 'core/null', |
| | | 'core/pattern', |
| | | 'core/list-item', |
| | | 'core/post-content', |
| | | 'jvb/drawer-menu', |
| | |
| | | } |
| | | |
| | | if (array_key_exists('displayLoginAsForm', $attrs) && $attrs['displayLoginAsForm']) { |
| | | LoginManager::getInstance()->setAction($action); |
| | | return LoginManager::getInstance()->renderLoginForm($action, $redirect, '<h2>Login</h2>'); |
| | | Login::getInstance()->setAction($action); |
| | | return Login::getInstance()->renderLoginForm($action, $redirect, '<h2>Login</h2>'); |
| | | |
| | | } |
| | | |
| | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\ui\Navigation; |
| | | use WP_Block; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | class DrawerMenuBlock { |
| | | protected Cache $cache; |
| | | public function __construct() |
| | |
| | | } |
| | | $items = array_map(function($item) { return $item['text'];}, $menu_items); |
| | | |
| | | $key = $cache->generateKey($items); |
| | | $menu = $cache->remember($key, |
| | | $menu = $cache->remember($items, |
| | | function () use ($menu_items, $menu_id, $collapsed) { |
| | | $menu = new Navigation($menu_id); |
| | | $menu->asDrawer($collapsed)->populateFromArray($menu_items); |
| | |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Block; |
| | | use WP_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | class FAQBlock { |
| | | protected Cache $cache; |
| | | // protected Cache $cache; |
| | | protected string $postType; |
| | | protected string $section; |
| | | protected string $slug; |
| | | protected bool $isDirectory; |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('faq_block', WEEK_IN_SECONDS)->connect('post', true)->connect('taxonomy', true); |
| | | // $this->cache = Cache::for('faq_block', 'post', WEEK_IN_SECONDS); |
| | | add_action('init', [ $this, 'registerBlock' ]); |
| | | add_action('enqueue_block_editor_assets', [$this, 'localizeData']); |
| | | } |
| | |
| | | } |
| | | $buttons = ''; |
| | | if ($this->isDirectory) { |
| | | $directory = JVB()->directories()->directories($this->slug); |
| | | $directory = DirectoryManager::getInstance()->directories($this->slug); |
| | | $url = $directory['url']??''; |
| | | if (!empty($url)) { |
| | | $buttons .= '<li><a href="'.$url.'">'.__('See Alphabetical List', 'jvb').'</a></li>'; |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | // Initialize cache with connections |
| | | $this->cache = Cache::for('feed_block', WEEK_IN_SECONDS); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | $this->cache->flush(); |
| | | // // Initialize cache with connections |
| | | // $this->cache = Cache::for('feed_block','post', WEEK_IN_SECONDS); |
| | | // if (JVB_TESTING) { |
| | | // $this->cache->flush(); |
| | | // } |
| | | |
| | | add_action('init', [$this, 'registerBlock']); |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\blocks; |
| | | |
| | | use JVBase\integrations\services\Cloudflare; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Form; |
| | | use JVBase\base\Site; |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('forms', WEEK_IN_SECONDS); |
| | | $this->cache = Cache::for('forms', null, WEEK_IN_SECONDS); |
| | | |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | |
| | | |
| | | // Enqueue Turnstile if needed |
| | | if (Site::hasIntegration('cloudflare')) { |
| | | $cloudflare = JVB()->connect('cloudflare'); |
| | | $cloudflare = Cloudflare::getInstance(); |
| | | if ($cloudflare->isSetUp()) { |
| | | $cloudflare->enqueueTurnstileScripts(); |
| | | } |
| | |
| | | return '<div class="jvb-form-error">No valid form type selected. Please edit this block and select a form type.</div>'; |
| | | } |
| | | |
| | | $cache_key = $this->cache->generateKey($block); |
| | | return $this->cache->remember( |
| | | $cache_key, |
| | | $block, |
| | | function() use ($form_type, $block) { |
| | | return $this->renderForm($form_type, $block); |
| | | } |
| | |
| | | return; |
| | | } |
| | | |
| | | $cloudflare = JVB()->connect('cloudflare'); |
| | | $cloudflare = Cloudflare::getInstance(); |
| | | if ($cloudflare->isSetUp()) { |
| | | $cloudflare->renderTurnstile(); |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\blocks; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\base\Site; |
| | | use JVBase\integrations\services\GoogleMyBusiness; |
| | | //use JVBase\managers\Cache; |
| | | use WP_Block; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | class GMBReviewBlock { |
| | | protected Cache $cache; |
| | | // protected Cache $cache; |
| | | public function __construct() |
| | | { |
| | | if (!Site::hasIntegration('gmb')){ |
| | | return; |
| | | } |
| | | add_action('init', [ $this, 'registerBlock' ]); |
| | | } |
| | | |
| | |
| | | */ |
| | | public function render(array $attributes, string $content, WP_Block $block):string |
| | | { |
| | | if (!Site::hasIntegration('gmb')) { |
| | | return ''; |
| | | } |
| | | $count = $attributes['count'] ?? 5; |
| | | $showRating = $attributes['showRating'] ?? true; |
| | | $showDate = $attributes['showDate'] ?? true; |
| | |
| | | $inheritUser = null; |
| | | } |
| | | try { |
| | | $gmb = JVB()->connect('gmb', $inheritUser); |
| | | if (!$gmb->isSetUp()) { |
| | | $gmb = GoogleMyBusiness::getInstance(); |
| | | $gmb->switchUser($inheritUser); |
| | | if (!$gmb->isSetUp() |
| | | || !method_exists($gmb, 'getReviews') |
| | | || !method_exists($gmb, 'getReviewsViewUrl') |
| | | || !method_exists($gmb, 'getReviewUrl')) { |
| | | error_log('GMB Not set up for: '.(int)$inheritUser); |
| | | return ''; |
| | | } |
| | | |
| | | $gotReviews = $gmb->getReviews(); |
| | | // Get all data |
| | | $allReviews = $gotReviews['reviews']??[]; |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('glossary_terms', WEEK_IN_SECONDS)->connect('post', true); |
| | | $this->cache = Cache::for('glossary_terms', 'post', WEEK_IN_SECONDS); |
| | | add_action('init', [ $this, 'registerBlock' ]); |
| | | } |
| | | |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('menu', WEEK_IN_SECONDS)->connect('post'); |
| | | $this->cache = Cache::for('menu', 'post', WEEK_IN_SECONDS); |
| | | if (Site::hasIntegration('square')) { |
| | | $this->base = '_square_'; |
| | | } else if (Site::hasIntegration('helcim')) { |
| | |
| | | |
| | | protected function buildParams():void |
| | | { |
| | | if (is_post_type_archive(BASE.'menu_item')) { |
| | | // if (is_post_type_archive(BASE.'menu_item') || |
| | | if (is_post_type_archive('product') || |
| | | (function_exists('is_shop') && is_shop())) { |
| | | $this->params = ['all']; |
| | | } elseif (is_tax(BASE.'section')) { |
| | | // } elseif (is_tax(BASE.'section')) { |
| | | } elseif (is_tax('product_cat')) { |
| | | $this->params = []; |
| | | } else { |
| | | $this->params = []; |
| | |
| | | return ''; |
| | | } |
| | | jvbInlineStyles('forms'); |
| | | $key = $this->cache->generateKey($this->params); |
| | | $this->cache->flush(); |
| | | return $this->cache->remember( |
| | | $key, |
| | | $this->params, |
| | | function() { |
| | | ob_start(); |
| | | $this->renderBlock(); |
| | |
| | | <?php |
| | | $sections = $this->getSections(); |
| | | foreach ($sections as $section) { |
| | | if (!jvbTermHasPosts($section['id'], BASE.'section')) { |
| | | // if (!jvbTermHasPosts($section['id'], BASE.'section')) { |
| | | if (!jvbTermHasPosts($section['id'], 'product_cat')) { |
| | | continue; |
| | | } |
| | | ?> |
| | |
| | | { |
| | | $sections = $this->getSections(); |
| | | foreach ($sections as $section) { |
| | | if($section['name'] === 'Uncategorized') { |
| | | continue; |
| | | } |
| | | $items = new WP_Query([ |
| | | 'post_type' => BASE.'menu_item', |
| | | 'post_type' => 'product', |
| | | // 'post_type' => BASE.'menu_item', |
| | | 'order' => 'ASC', |
| | | 'posts_per_page' => -1, |
| | | 'tax_query' => [ |
| | | [ |
| | | 'taxonomy' => BASE.'section', |
| | | 'taxonomy' => 'product_cat', |
| | | // 'taxonomy' => BASE.'section', |
| | | 'terms' => (int) $section['id'], |
| | | ] |
| | | ], |
| | |
| | | } |
| | | } |
| | | |
| | | protected function renderMenuItem(int $ID, string $slug, string $postType = 'menu_item') { |
| | | /** |
| | | * Renders from WooCommerce product |
| | | * @param int $ID |
| | | * @param string $slug |
| | | * @param string $postType |
| | | * @return void |
| | | */ |
| | | protected function renderMenuItem(int $ID, string $slug, string $postType = 'product'):void { |
| | | $meta = Meta::forPost($ID); |
| | | |
| | | $fields = $meta->getAll(); |
| | | |
| | | // jvbDump($fields, 'Fields'); |
| | | |
| | | $excerpt = get_the_excerpt($ID); |
| | | $isVariable = !empty($fields['variations']??[]); |
| | | $variations = $isVariable ? array_map('floatval', array_column($fields['variations'], 'price')) : []; |
| | | $priceRange = $isVariable ? |
| | | (min($variations).' - '.max($variations)) : |
| | | $fields['_price']??''; |
| | | |
| | | $out = sprintf( |
| | | '<div class="menu-item%s" data-section="%s" data-label="%s" data-id="%s"> |
| | | <div class="header row x-btw"> |
| | | <h3>%s</h3> |
| | | <p class="price">$%s</p> |
| | | </div> |
| | | <div class="description">%s</div> |
| | | <div class="info col right">', |
| | | $isVariable ? ' variable' : '', |
| | | $slug, |
| | | $fields['post_title'], |
| | | $ID, |
| | | $fields['post_title'], |
| | | $priceRange, |
| | | $excerpt |
| | | ); |
| | | |
| | | if (!empty($fields['variations']??[])) { |
| | | foreach ($fields['variations'] as $index => $row) { |
| | | $out .= sprintf( |
| | | '<div class="row right nowrap"> |
| | | <div> |
| | | <h4>%s — %s</h4> |
| | | %s |
| | | </div>', //Note: we had 3 %s%s%s before the last </div> last time. This was to add optional ingredients or preparation time duration, not default in WooCommerce |
| | | $row['title'], |
| | | jvbCurrency($row['price']), |
| | | empty($row['description']) ? '' : wpautop($row['description']), |
| | | ); |
| | | |
| | | $out .= Form::render( |
| | | $row['id'], |
| | | '', |
| | | [ |
| | | 'type' => 'quantity', |
| | | 'label' => $row['title'], |
| | | 'min' => 0, |
| | | 'max' => 30, |
| | | 'data' => [ |
| | | 'price' => $row['price'], |
| | | 'id' => $row['id'], |
| | | 'sku' => $row['sku'], |
| | | 'name' => $row['title'], |
| | | 'add-to-cart' => '' |
| | | ], |
| | | 'add' => 'Add 1 '.$row['title'].' to cart', |
| | | 'remove' => 'Remove 1 '.$row['title']. ' from cart', |
| | | ] |
| | | ); |
| | | $out .= '</div>'; |
| | | } |
| | | } else { |
| | | $out .= Form::render( |
| | | $ID, |
| | | '', |
| | | [ |
| | | 'type' => 'quantity', |
| | | 'label' => 'Add to Order', |
| | | 'min' => 0, |
| | | 'max' => 30, |
| | | 'step' => 1, |
| | | 'data' => [ |
| | | 'price' => $fields['_price'], |
| | | 'id' => $ID, |
| | | 'name' => $fields['post_title'], |
| | | 'step' => 1, |
| | | 'add-to-cart' => '', |
| | | ], |
| | | 'add' => 'Add 1 '.$fields['post_title'].' to cart', |
| | | 'remove' => 'Remove 1 '.$fields['title']. ' from cart', |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | $out .= '</div></div>'; |
| | | |
| | | echo $out; |
| | | |
| | | } |
| | | /** |
| | | protected function renderMenuItemOLD(int $ID, string $slug, string $postType = 'menu_item') { |
| | | $meta = Meta::forPost($ID); |
| | | $fields = $meta->getAll(); |
| | | $values = $meta->getAll([ |
| | |
| | | |
| | | </div> |
| | | <?php |
| | | } |
| | | } **/ |
| | | |
| | | |
| | | |
| | | protected function renderOnThisPage():void |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('menu', DAY_IN_SECONDS)->connect('post'); |
| | | $this->cache = Cache::for('calendar', 'post', DAY_IN_SECONDS); |
| | | add_action('init', [ $this, 'registerBlock' ]); |
| | | } |
| | | |
| | |
| | | return ''; |
| | | } |
| | | |
| | | $key = $this->cache->generateKey($this->params); |
| | | $this->cache->flush(); |
| | | return $this->cache->remember( |
| | | $key, |
| | | $this->params, |
| | | function() { |
| | | ob_start(); |
| | | $this->renderBlock(); |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('summary_block', WEEK_IN_SECONDS); |
| | | $this->cache = Cache::for('summary_block', 'post', WEEK_IN_SECONDS); |
| | | add_action('init', [ $this, 'registerBlock' ]); |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); |
| | | if (JVB_TESTING) { |
| | |
| | | { |
| | | $this->config = $this->getConfig(); |
| | | $key = $this->generateKey(); |
| | | $cache = $this->cache->get($key); |
| | | if (is_tax()) { |
| | | $this->cache = Cache::for('summary_tax', 'term', WEEK_IN_SECONDS); |
| | | } elseif (is_author()) { |
| | | $this->cache = Cache::for('summary_auth', 'user', WEEK_IN_SECONDS); |
| | | } else { |
| | | $this->cache = Cache::for('summary_post', 'post', WEEK_IN_SECONDS); |
| | | } |
| | | |
| | | $cache = $this->cache->get(get_queried_object_id()); |
| | | |
| | | if ($cache) { |
| | | return $cache; |
| | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use WP_Block; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('timelines', WEEK_IN_SECONDS)->connect('post'); |
| | | $this->cache = Cache::for('timelines', 'post', WEEK_IN_SECONDS); |
| | | if (JVB_TESTING){ |
| | | $this->cache->flush(); |
| | | } |
| | |
| | | namespace JVBase\forms; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Roles; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Query; |
| | |
| | | public function __construct(string $post_type, array $config = []) |
| | | { |
| | | $this->post_type = $post_type; |
| | | $this->cache = Cache::for(jvbNoBase($post_type), WEEK_IN_SECONDS)->connect('post', true); |
| | | $this->cache = Cache::for(jvbNoBase($post_type), 'post',WEEK_IN_SECONDS); |
| | | |
| | | $this->config = wp_parse_args($config, [ |
| | | 'multiple' => true, |
| | |
| | | 'methods' => 'GET', |
| | | 'callback' => [new self('post'), 'searchPosts'], |
| | | 'permission_callback' => function($request) { |
| | | return JVB()->roles()->checkRole(get_current_user(), $request->get_param('post_type')); |
| | | return Roles::getInstance()()->checkRole(get_current_user(), $request->get_param('post_type')); |
| | | }, |
| | | 'args' => [ |
| | | 'post_type' => [ |
| | |
| | | |
| | | if (is_singular(BASE.'directory')) { |
| | | $type = get_post_meta(get_the_ID(), BASE.'for_type_slug', true); |
| | | return JVB()->directories()?->getDirectoryList()[$type] ?? []; |
| | | return DirectoryManager::getInstance()?->getDirectoryList()[$type] ?? []; |
| | | } |
| | | |
| | | $obj = get_queried_object(); |
| | | $directories = JVB()->directories()?->directories(); |
| | | $directories = DirectoryManager::getInstance()?->directories(); |
| | | if (is_tax()) { |
| | | $tax = jvbNoBase($obj->taxonomy); |
| | | return array_key_exists($tax, $directories) ? $directories[$tax] : []; |
| | |
| | | } |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\RoleManager; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\meta\Form; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | |
| | | 'label' => 'Scrapped', |
| | | ] |
| | | ]; |
| | | $permission = JVB()->roles()->getContentPlural($content); |
| | | $permission = Roles::getInstance()()->getContentPlural($content); |
| | | $canPublish = current_user_can("publish_{$permission}") && current_user_can('skip_moderation'); |
| | | $out = '<div class="filters">'; |
| | | if (!empty($statusFilters)) { |
| | |
| | | 'disabled' => true, |
| | | ] |
| | | ]; |
| | | $permission = JVB()->roles()->getContentPlural($content); |
| | | $permission = Roles::getInstance()()->getContentPlural($content); |
| | | if (current_user_can('skip_moderation') && current_user_can("publish_{$permission}")) { |
| | | $bulkActions['publish']['disabled'] = false; |
| | | } |
| | |
| | | */ |
| | | function jvbRenderDateFilter(string $content):string |
| | | { |
| | | $cache = Cache::for('date_filter')->connect('post', true); |
| | | $cache = Cache::for('date_filter'); |
| | | $check = $cache->get($content); |
| | | if ($check) { |
| | | return $check; |
| | |
| | | |
| | | $temp = []; |
| | | foreach ($types as $t) { |
| | | $permission = JVB()->roles()->getPermission('edit',$t); |
| | | $permission = Roles::getInstance()()->getPermission('edit',$t); |
| | | if (user_can($link, $permission)){ |
| | | $temp[] = $t; |
| | | } |
| | |
| | | <?php |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\managers\DirectoryManager; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | function jvbIsDirectory():bool |
| | | { |
| | | |
| | | return JVB()->directories() && JVB()->directories()->isDirectory(); |
| | | return Site::has('is_directory') && DirectoryManager::getInstance()->isDirectory(); |
| | | } |
| | |
| | | */ |
| | | function jvbFormatRating(int $ID, string $type = 'post'):string |
| | | { |
| | | $cache = Cache::for('rating', WEEK_IN_SECONDS)->connect('post')->connect('taxonomy')->connect('user'); |
| | | $cache = Cache::for('rating', $type, WEEK_IN_SECONDS); |
| | | |
| | | $cached = $cache->get($ID); |
| | | if ($cached) { |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | |
| | | */ |
| | | function jvbShareName(int $userID):string |
| | | { |
| | | $cache = Cache::for('usernames')->connect('user'); |
| | | $cache = Cache::for('usernames', 'user'); |
| | | return $cache->remember( |
| | | $userID, |
| | | function() use ($userID) { |
| | |
| | | */ |
| | | function jvbGetUserByFirstName(string $first_name):WP_User|false |
| | | { |
| | | $cache = Cache::for('userFirstname')->connect('user', true); |
| | | $cache = Cache::for('userFirstname', 'user'); |
| | | return $cache->remember( |
| | | $first_name, |
| | | function() use ($first_name) { |
| | |
| | | */ |
| | | function jvbGetUserByDisplayName(string $display_name):WP_User|false |
| | | { |
| | | $cache = Cache::for('displayNames')->connect('user', true); |
| | | $cache = Cache::for('displayNames', 'user'); |
| | | $cached = $cache->get($display_name)??false; |
| | | |
| | | if ($cached && is_int($cached)) { |
| | |
| | | function jvbGetUsername(int $user_id):string |
| | | { |
| | | $key = 'user_display_names'; |
| | | $cache = Cache::for('userNames', WEEK_IN_SECONDS)->connect('user'); |
| | | $cache = Cache::for('userNames', 'user',WEEK_IN_SECONDS); |
| | | $cached = $cache->get($user_id); |
| | | |
| | | if ($cached) { |
| | |
| | | return false; |
| | | } |
| | | |
| | | $cache = Cache::for('artist', 3600)->connect('post'); |
| | | $cache = Cache::for('artist', 'post', 3600); |
| | | $cached = $cache->get($userID); |
| | | if ($cached) { |
| | | return match ($return) { |
| | |
| | | |
| | | function jvbUserProfileLink(int $userID):string|false |
| | | { |
| | | $cache = Cache::for('userLink')->connect('user'); |
| | | $cache = Cache::for('userLink', 'user'); |
| | | return $cache->remember( |
| | | $userID, |
| | | function() use ($userID) { |
| | |
| | | $link = get_user_meta($userID, BASE.'profile_link', true); |
| | | //Try to create it |
| | | if (empty($link)) { |
| | | $link = JVB()->roles()->addUserLink($user, $role); |
| | | $link = Roles::getInstance()()->addUserLink($user, $role); |
| | | if (!$link) { |
| | | return false; |
| | | } |
| | |
| | | * @param string $type |
| | | * @return string |
| | | */ |
| | | function jvbRenderLinks(int $ID, string $type =''):string |
| | | function jvbRenderLinks(int $ID, string $type ='post'):string |
| | | { |
| | | $cache = Cache::for('user_links', WEEK_IN_SECONDS)->connect('post')->connect('taxonomy')->connect('user'); |
| | | $cache = Cache::for('user_links_'.$type, $type,WEEK_IN_SECONDS); |
| | | $cached = $cache->get($ID); |
| | | if ($cached) { |
| | | return $cached; |
| | |
| | | |
| | | /** |
| | | * @param int $ID |
| | | * @param string $type |
| | | * @param ?string $type |
| | | * |
| | | * @return string |
| | | */ |
| | | function jvbRenderContactInfo(int $ID, string $type = ''):string |
| | | function jvbRenderContactInfo(int $ID, ?string $type = null):string |
| | | { |
| | | $cache = Cache::for('contact', WEEK_IN_SECONDS)->connect('post')->connect('taxonomy'); |
| | | if (is_null($type)) { |
| | | return ''; |
| | | } |
| | | $cache = Cache::for('contact_'.$type, $type,WEEK_IN_SECONDS); |
| | | |
| | | $cached = $cache->get($ID); |
| | | if($cached){ |
| | |
| | | return ''; |
| | | } |
| | | } |
| | | $cache = Cache::for($term->taxonomy.'_link')->connect('taxonomy'); |
| | | $cache = Cache::for($term->taxonomy.'_link', 'term'); |
| | | $key = $term->term_id; |
| | | return $cache->remember( |
| | | $key, |
| | |
| | | return ''; |
| | | } |
| | | |
| | | $cache = Cache::for('locations')->connect('taxonomy'); |
| | | $cache = Cache::for('tax_locations', 'term'); |
| | | $key = $cache->generateKey($location); |
| | | |
| | | $cached = false; |
| | |
| | | */ |
| | | function jvbRenderHours(int $ID, string $type = ''):string |
| | | { |
| | | $cache = Cache::for('hours_display', WEEK_IN_SECONDS)->connect('taxonomy')->connect('post')->connect('user'); |
| | | if (!$type) { |
| | | return ''; |
| | | } |
| | | $cache = Cache::for('hours_display_'.$type, $type, WEEK_IN_SECONDS); |
| | | |
| | | $cached = $cache->get($ID); |
| | | |
| | |
| | | namespace JVBase\importers; |
| | | |
| | | use WP_Error; |
| | | use JVBase\managers\ReferralManager; |
| | | use JVBase\managers\Referrals; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | protected string $referrals_table; |
| | | protected string $treatments_table; |
| | | protected string $rewards_table; |
| | | protected ReferralManager $referral_manager; |
| | | protected Referrals $referral_manager; |
| | | protected array $import_stats = []; |
| | | protected array $skipped_details = []; |
| | | |
| | |
| | | $this->referrals_table = $wpdb->prefix . BASE . 'referrals'; |
| | | $this->treatments_table = $wpdb->prefix . BASE . 'referral_treatments'; |
| | | $this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards'; |
| | | $this->referral_manager = new ReferralManager(); |
| | | $this->referral_manager = Referrals::getInstance(); |
| | | } |
| | | |
| | | /** |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | require(JVB_DIR . '/inc/importers/JaneAppClientImporter.php'); |
| | | require(JVB_DIR . '/inc/importers/JaneAppSalesImporter.php'); |
| | |
| | | |
| | | public function renderConnection(bool $return = false):string |
| | | { |
| | | if ($this->userID && !JVB()->userCanConnect($this->service_name, $this->userID)) { |
| | | if ($this->userID && !$this->userCanConnect($this->userID)) { |
| | | return ''; |
| | | } |
| | | |
| | |
| | | array $data, |
| | | array $options = [] |
| | | ):bool { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | |
| | | $queued = $queue->queueOperation( |
| | | $type, |
| | |
| | | $this->addSavePost(); |
| | | } |
| | | |
| | | add_action('transition_post_status', [$this, 'handlePostStatusTransition'], 10, 3); |
| | | // add_action('transition_post_status', [$this, 'handlePostStatusTransition'], 10, 3); |
| | | |
| | | if ($this->canSync['delete']) { |
| | | add_action('before_delete_post', [$this, 'handleDeletePost'], 10, 1); |
| | |
| | | if ($jpegVersion !== '' && is_int($jpegVersion)) { |
| | | return $jpegVersion; |
| | | } |
| | | $uploader = new UploadManager(); |
| | | $uploader = UploadManager::getInstance(); |
| | | $converted = $uploader->convertImageTo($imgID, 'jpeg', 80, false); |
| | | if ($converted && !is_wp_error($converted)) { |
| | | update_post_meta($imgID, BASE . 'jpeg_version', $converted['attachment_id']); |
| | |
| | | |
| | | /** |
| | | * Used by JVBase\registrar\helpers\AddIntegrationFields.php |
| | | * @param string|null $content_type |
| | | * @return array |
| | | */ |
| | | public function getAdditionalFields(?string $content_type = null):array |
| | |
| | | |
| | | trait OAuth |
| | | { |
| | | use _Base, Admin; |
| | | use _Base, Admin, ServiceMap; |
| | | protected OAuthURLs $oauth; |
| | | /** |
| | | * Child classes must set this (e.g. 'https://api.example.com/v1/') |
| | |
| | | { |
| | | return $this->getRequestHeaders(); |
| | | } |
| | | public function getOAuthBaseURL():?string |
| | | { |
| | | return $this->baseOAuthUrl; |
| | | } |
| | | |
| | | /** |
| | | * Default to the base handleResponse method, but extensions can modify accordingly |
| | |
| | | } |
| | | |
| | | // Get integration instance |
| | | $integration = JVB()->connect($this->service_name, $user_id); |
| | | $integration = $this->getService($this->service_name); |
| | | |
| | | if (!$integration) { |
| | | if (!$integration || !method_exists($integration, 'switchUser')) { |
| | | wp_die('Invalid service: ' . esc_html($this->service_name), 'OAuth Error'); |
| | | } |
| | | |
| | | $integration->switchUser($user_id); |
| | | |
| | | |
| | | // Exchange code for tokens |
| | | $result = $integration->handleOAuthCode($code, $state); |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | use JVBase\base\Site; |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait ServiceMap { |
| | | protected array $serviceMap = [ |
| | | 'maps' => 'JVBase\integrations\services\GoogleMaps', |
| | | 'square' => 'JVBase\integrations\services\Square', |
| | | 'facebook' => 'JVBase\integrations\services\Facebook', |
| | | 'helcim' => 'JVBase\integrations\services\Helcim', |
| | | 'instagram' => 'JVBase\integrations\services\Instagram', |
| | | 'bluesky' => 'JVBase\integrations\services\BlueSky', |
| | | 'gmb' => 'JVBase\integrations\services\GoogleMyBusiness', |
| | | 'cloudflare' => 'JVBase\integrations\services\Cloudflare', |
| | | 'umami' => 'JVBase\integrations\services\Umami', |
| | | 'postmark' => 'JVBase\integrations\services\PostMark', |
| | | ]; |
| | | |
| | | public function getService(string $service):Integrations|false |
| | | { |
| | | return array_key_exists($service, $this->serviceMap) ? |
| | | $this->serviceMap[$service]::getInstance() |
| | | : false; |
| | | } |
| | | |
| | | public function getAvailableServices():array |
| | | { |
| | | return array_filter(array_keys($this->serviceMap), function ($service) { |
| | | return Site::hasIntegration($service); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | |
| | | use JVBase\managers\queue\executors\IntegrationExecutor; |
| | | use JVBase\managers\queue\mergers\DefaultMerger; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\meta\Meta; |
| | | use WP_Query; |
| | |
| | | self::$syncTo = $this->service_name.'_sync_to'; |
| | | self::$deleteFrom = $this->service_name.'_delete_from'; |
| | | self::$syncFrom = $this->service_name.'_sync_from'; |
| | | self::$syncCustomer = $this->service_name.'_sync_customer'; |
| | | //TODO: What is the difference between sync_from and import? |
| | | self::$import = $this->service_name.'import_from'; |
| | | } |
| | |
| | | default => '' |
| | | }; |
| | | } |
| | | public function setServiceItemID(int $itemID, string $id, string $type = 'post'):bool |
| | | { |
| | | |
| | | $result = match($type) { |
| | | 'post' => Meta::forPost($itemID)->set("_{$this->service_name}_item_id", $id), |
| | | 'term','taxonomy' => Meta::forTerm($itemID)->set("_{$this->service_name}_item_id", $id), |
| | | 'user' => Meta::forUser($itemID)->set("_{$this->service_name}_item_id", $id), |
| | | default => false |
| | | }; |
| | | return $result !== false; |
| | | } |
| | | protected function updateItemStatus(int|array $items, string $status = 'pending', string $type = 'post'):void |
| | | { |
| | | if (!is_array($items)) { |
| | |
| | | if (empty($this->syncTaxonomies) && empty($this->syncPostTypes)) { |
| | | return; |
| | | } |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $executor = new IntegrationExecutor(); |
| | | |
| | | $queue->registry()->register(self::$syncTo, new TypeConfig( |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | |
| | | return; |
| | | } |
| | | |
| | | JVB()->queue()->add( |
| | | Queue::getInstance()->add( |
| | | self::$deleteFrom, |
| | | $userID, |
| | | [ |
| | |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | Queue::getInstance()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | Queue::getInstance()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | |
| | | } |
| | | public function switchUser(?int $userID = null):bool |
| | | { |
| | | if (!$this->userCanConnect($userID)) { |
| | | if (!is_null($userID) && !$this->userCanConnect($userID)) { |
| | | return false; |
| | | } |
| | | $this->clearUser(); |
| | |
| | | if (method_exists($this, 'initializeRequestLimiter')) { |
| | | $this->initializeRequestLimiter(); |
| | | } |
| | | if (method_exists($this, 'intializeOAuthRateLimiter')) { |
| | | if (method_exists($this, 'initializeOAuthRateLimiter')) { |
| | | $this->initializeOAuthRateLimiter(); |
| | | } |
| | | } |
| | |
| | | } |
| | | return $response; |
| | | } |
| | | |
| | | public function getAllowedContent():array |
| | | { |
| | | return array_keys($this->contentTypes); |
| | | } |
| | | |
| | | public function getServiceName():string |
| | | { |
| | | return $this->service_name; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | use JVBase\base\Site; |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/integrations/_Base.php'); |
| | | |
| | | require(JVB_DIR . '/inc/integrations/Admin.php'); |
| | | require(JVB_DIR . '/inc/integrations/AdminActionResult.php'); |
| | | require(JVB_DIR . '/inc/integrations/AdminActions.php'); |
| | |
| | | require(JVB_DIR . '/inc/integrations/Endpoint.php'); |
| | | require(JVB_DIR . '/inc/integrations/EndpointField.php'); |
| | | require(JVB_DIR . '/inc/integrations/Field.php'); |
| | | require(JVB_DIR . '/inc/integrations/Instance.php'); |
| | | |
| | | require(JVB_DIR . '/inc/integrations/OAuth.php'); |
| | | require(JVB_DIR . '/inc/integrations/OAuthCredentials.php'); |
| | | require(JVB_DIR . '/inc/integrations/OAuthURLs.php'); |
| | |
| | | require(JVB_DIR . '/inc/integrations/Webhooks.php'); |
| | | require(JVB_DIR . '/inc/integrations/Integrations.php'); |
| | | |
| | | if (Site::hasIntegration('maps')) { |
| | | require(JVB_DIR . '/inc/integrations/services/GoogleMaps.php'); |
| | | } |
| | | add_action('jvbloaded', 'jvbLoadIntegrations'); |
| | | function jvbLoadIntegrations():void |
| | | { |
| | | if (Site::hasIntegration('maps')) { |
| | | require(JVB_DIR . '/inc/integrations/services/GoogleMaps.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('umami')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Umami.php'); |
| | | } |
| | | if (Site::hasIntegration('cloudflare')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Cloudflare.php'); |
| | | } |
| | | if (Site::hasIntegration('umami')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Umami.php'); |
| | | } |
| | | if (Site::hasIntegration('cloudflare')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Cloudflare.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('square')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Square.php'); |
| | | } |
| | | if (Site::hasIntegration('square')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Square.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('facebook')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Facebook.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('instagram')) { |
| | | if (!Site::hasIntegration('facebook')) { |
| | | if (Site::hasIntegration('facebook')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Facebook.php'); |
| | | } |
| | | require(JVB_DIR . '/inc/integrations/services/Instagram.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('bluesky')) { |
| | | require(JVB_DIR . '/inc/integrations/services/BlueSky.php'); |
| | | } |
| | | if (Site::hasIntegration('instagram')) { |
| | | if (!Site::hasIntegration('facebook')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Facebook.php'); |
| | | } |
| | | require(JVB_DIR . '/inc/integrations/services/Instagram.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('gmb')) { |
| | | require(JVB_DIR . '/inc/integrations/services/GoogleMyBusiness.php'); |
| | | } |
| | | if (Site::hasIntegration('bluesky')) { |
| | | require(JVB_DIR . '/inc/integrations/services/BlueSky.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('postmark')) { |
| | | require(JVB_DIR . '/inc/integrations/services/PostMark.php'); |
| | | } |
| | | if (Site::hasIntegration('gmb')) { |
| | | require(JVB_DIR . '/inc/integrations/services/GoogleMyBusiness.php'); |
| | | } |
| | | |
| | | if (Site::hasIntegration('postmark')) { |
| | | require(JVB_DIR . '/inc/integrations/services/PostMark.php'); |
| | | } |
| | | |
| | | |
| | | if (Site::hasIntegration('helcim')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Helcim.php'); |
| | | if (Site::hasIntegration('helcim')) { |
| | | require(JVB_DIR . '/inc/integrations/services/Helcim.php'); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | /** |
| | | * Verify the Turnstile response |
| | | * Can be called directly via JVB()->connect('cloudflare')->verifyTurnstile($token) |
| | | * or Cloudflare::getInstance()->verifyTurnstile($token) |
| | | * Can be called directly via Cloudflare::getInstance()->verifyTurnstile($token) |
| | | * @param string|null $token |
| | | * @param string $remote_ip |
| | | * @return bool |
| | |
| | | |
| | | /** |
| | | * Verify Turnstile response |
| | | * Can be called directly: JVB()->connect('cloudflare')->verifyTurnstile($token) |
| | | * Can be called directly: Cloudflare::getInstance()->verifyTurnstile($token) |
| | | */ |
| | | public function verifyTurnstile(?string $token = null, string $remote_ip = ''): bool |
| | | { |
| | |
| | | namespace JVBase\integrations\services; |
| | | use Exception; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\OAuth; |
| | | use JVBase\integrations\OAuthCredentials; |
| | |
| | | use JVBase\integrations\SyncFrom; |
| | | use JVBase\integrations\SyncTo; |
| | | use JVBase\integrations\UserConnection; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\meta\Sanitizer; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | $this->title = 'Facebook'; |
| | | $this->icon = 'facebook-logo'; |
| | | |
| | | $this->apiVersion = 'v25.0'; |
| | | $this->apiVersion = 'v26.0'; |
| | | |
| | | $this->canSync = [ |
| | | 'create' => true, |
| | | 'update' => true, |
| | | 'delete' => true |
| | | 'update' => false, |
| | | 'delete' => false |
| | | ]; |
| | | |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | | $this->defineOAuth(); |
| | | $this->defineActions(); |
| | | $this->defineContentTypes(); |
| | | |
| | | $this->baseUrl = 'https://graph.facebook.com/'.$this->apiVersion; |
| | | $this->baseOAuthUrl = 'https://www.facebook.com/'.$this->apiVersion; |
| | | |
| | | $this->supportsWebp = false; |
| | | |
| | | parent::__construct(); |
| | | } |
| | | |
| | |
| | | { |
| | | return 'Connect your Facebook Page to share posts, events, and engage with your audience.'; |
| | | } |
| | | |
| | | protected function defineContentTypes():void |
| | | { |
| | | $base = $this->getBaseFields(); |
| | | $this->contentTypes['post'] = array_merge($base, $this->contentTypePostFields()); |
| | | $this->contentTypes['image'] = array_merge($base, $this->contentTypeImageFields()); |
| | | //Looks like product requires some major heavy lifting. Best to skip for now |
| | | // $this->contentTypes['product'] = array_merge($base, $this->contentTypeProductFields()); |
| | | |
| | | |
| | | } |
| | | protected function getBaseFields():array |
| | | { |
| | | return [ |
| | | |
| | | ]; |
| | | } |
| | | protected function contentTypePostFields():array |
| | | { |
| | | return []; |
| | | } |
| | | protected function contentTypeImageFields():array |
| | | { |
| | | return []; |
| | | } |
| | | protected function initialize(): void |
| | | { |
| | | // TODO: Implement initialize() method. |
| | |
| | | { |
| | | return [ |
| | | 'Content-Type' => 'application/json', |
| | | 'Accept' => 'application/json' |
| | | 'Accept' => 'application/json', |
| | | ]; |
| | | } |
| | | |
| | | |
| | | protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array |
| | | protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array |
| | | { |
| | | if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') { |
| | | return $this->response(false, 'Rate limit exceeded'); |
| | | } |
| | | |
| | | $errors = $this->checkErrors($response); |
| | | if (!empty($errors)) { |
| | | return $errors; |
| | | } |
| | | |
| | | $endpoint = str_replace($this->baseUrl, '', $endpoint); |
| | | return match ($endpoint) { |
| | | '/me/accounts?access_token='.$this->getAccessToken() => $this->handleGetPagesResponse($response, $method), |
| | | default => $this->response(false, 'Unhandled Response for ' . $endpoint . ' endpoint, with method: ' . $method), |
| | | default => $response |
| | | }; |
| | | } |
| | | protected function checkErrors(WP_Error|array $response):array|false |
| | | { |
| | | if (!isset($response['error'])) { |
| | | return false; |
| | | } |
| | | $error = (string)$response['error']['code']; |
| | | switch ($error) { |
| | | //Invalid parameter |
| | | case '100': |
| | | //Too many calls by this account. Wait for a bit |
| | | case '80001': |
| | | //Permissions error |
| | | case '200': |
| | | //Action has been deemed abusive, or otherwise disallowed |
| | | case '368': |
| | | //Missing or invalid image file |
| | | case '324': |
| | | //Invalid OAuth 2.0 Access Token |
| | | case '190': |
| | | //Incorrect Signature |
| | | case '104': |
| | | //Desktop applications cannot call this function for other users |
| | | case '240': |
| | | //Requires additional permissions |
| | | case '283': |
| | | //Session is invalid; user has been checkpointed |
| | | case '459': |
| | | default: |
| | | $this->logError( |
| | | 'handleResponse', |
| | | 'Something went wrong: '.$response['Message']??'', |
| | | $response |
| | | ); |
| | | return $this->response(false, 'Something went wrong: '.$response['Message']??''); |
| | | } |
| | | } |
| | | /************************************************************ |
| | | * UTILITY METHODS |
| | | ************************************************************/ |
| | |
| | | $resp = $this->getRequest( |
| | | $this->baseUrl.'/me?access_token='.$cred['access_token'], |
| | | ); |
| | | $tmp = []; |
| | | if ($resp && !is_wp_error($resp)) { |
| | | $tmp['user_id'] = $resp['id']; |
| | | $tmp['user_name'] = $resp['name']; |
| | |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | public function getOrCreateImage(int $imgID, string $caption = '', bool $publish = true, bool $returnResponse = false):string|array|false |
| | | { |
| | | $imgID = $this->getSupportedImage($imgID); |
| | | |
| | | $id = $this->getServiceItemID($imgID); |
| | | if (!empty($id)) { |
| | | return $id; |
| | | } |
| | | |
| | | $url = wp_get_attachment_url($imgID); |
| | | if (!file_exists($url)) { |
| | | return false; |
| | | } |
| | | |
| | | $args = [ |
| | | 'url' => $url |
| | | ]; |
| | | if (!empty($caption)) { |
| | | $caption = html_entity_decode(wp_strip_all_tags($caption)); |
| | | if (!empty($caption)) { |
| | | $args['caption'] = $caption; |
| | | } |
| | | } |
| | | |
| | | $alt_text = get_post_meta($imgID, '_wp_attachment_image_alt', true); |
| | | if (!empty($alt_text)) { |
| | | $args['alt_text_custom'] = esc_html($alt_text); |
| | | } |
| | | |
| | | if (!$publish) { |
| | | $args['published'] = false; |
| | | } |
| | | |
| | | $args['access_token'] = $this->getAccessToken(); |
| | | |
| | | $response = $this->postRequest( |
| | | sprintf( |
| | | '%s/%s/photos', |
| | | $this->baseUrl, |
| | | $this->getPageID(), |
| | | ), |
| | | $args |
| | | ); |
| | | |
| | | if (isset($response['id'])) { |
| | | $this->setServiceItemID($imgID, $response['id']); |
| | | } |
| | | |
| | | return $returnResponse ? $response : $response['id']; |
| | | |
| | | |
| | | } |
| | | |
| | | /********************************************************** |
| | | * POSTING UTILITIES |
| | | **********************************************************/ |
| | | public function post(string $message):array |
| | | { |
| | | if (!$this->isSetUp()) { |
| | | return $this->response(false, 'Not set up.'); |
| | | } |
| | | $message = html_entity_decode(wp_strip_all_tags($message)); |
| | | if (empty($message)) { |
| | | return $this->response(false, 'Message cannot be empty.'); |
| | | } |
| | | $args = [ |
| | | 'message' => $message, |
| | | ]; |
| | | |
| | | $args['access_token'] = $this->getAccessToken(); |
| | | |
| | | return $this->postRequest( |
| | | sprintf( |
| | | '%s/%s/feed', |
| | | $this->baseUrl, |
| | | $this->getPageID(), |
| | | ), |
| | | $args |
| | | ); |
| | | } |
| | | /** |
| | | * From https://developers.facebook.com/docs/graph-api/reference/photo/#u_0_o_gP |
| | | * |
| | | * @param int $imgID |
| | | * @param string $caption |
| | | * @return array |
| | | */ |
| | | public function postSingleImage(int $imgID, string $caption):array |
| | | { |
| | | if (!$this->isSetUp()) { |
| | | return $this->response(false, 'Not set up yet'); |
| | | } |
| | | |
| | | $response = $this->getOrCreateImage($imgID, $caption); |
| | | |
| | | return $this->response(true, 'Posted Image', $response); |
| | | } |
| | | |
| | | public function postGalleryImages(array $imgIDs, string $caption, int $postID = 0):array |
| | | { |
| | | if (!$this->isSetUp()) { |
| | | return $this->response(false, 'Not set up yet'); |
| | | } |
| | | |
| | | $post = []; |
| | | $i = 0; |
| | | foreach ($imgIDs as $imgID) { |
| | | $id = $this->getOrCreateImage($imgID, '', false); |
| | | if ($id) { |
| | | $post['attached_media['.$i.']'] = json_encode(['media_fbid' => $id]); |
| | | $i++; |
| | | } |
| | | } |
| | | |
| | | if (empty($post)) { |
| | | return $this->response(false, 'No images posted'); |
| | | } |
| | | |
| | | $post['access_token'] = $this->getAccessToken(); |
| | | |
| | | $caption = html_entity_decode(wp_strip_all_tags($caption)); |
| | | if (!empty($caption)) { |
| | | $post['message'] = $caption; |
| | | } |
| | | |
| | | $response = $this->postRequest( |
| | | sprintf( |
| | | '%s/%s/feed', |
| | | $this->baseUrl, |
| | | $this->getPageID(), |
| | | ), |
| | | $post |
| | | ); |
| | | |
| | | if ($postID > 0 && isset($response['id'])) { |
| | | $this->setServiceItemID($postID, $response['id']); |
| | | } |
| | | |
| | | return $this->response(true, 'Successfully posted to Facebook', $response); |
| | | } |
| | | |
| | | public function getReviews():array |
| | | { |
| | | if (!$this->isSetUp() || !$this->getPageID()) { |
| | | return []; |
| | | } |
| | | $ratings = $this->getRequest( |
| | | sprintf( |
| | | '%s/%s/ratings', |
| | | $this->baseUrl, |
| | | $this->getPageID(), |
| | | ) |
| | | ); |
| | | if (!isset($ratings['data'])) { |
| | | return []; |
| | | } |
| | | return $ratings['data']; |
| | | } |
| | | |
| | | /** |
| | | * https://developers.facebook.com/docs/graph-api/reference/page/#parameters-3 |
| | | * @param array $data allowed keys are: about, description, phone, website, emails, location {json object with street, city, state, zip, country} |
| | | * @return array |
| | | * TODO: Cover photo? (cover) |
| | | * TODO: Food style (food_styles) |
| | | */ |
| | | public function updateBusiness(array $data):array |
| | | { |
| | | if (!$this->isSetUp() || !$this->getPageID()) { |
| | | return $this->response(false, 'Not set up yet'); |
| | | } |
| | | $data = array_filter($data, function($item) use ($data) { |
| | | if (!in_array($item, ['about', 'description', 'phone', 'website', 'emails', 'location', 'hours'])) { |
| | | $this->logError('updateBusiness', 'Attempted to set property that is not allowed: '.$item.' with value: '.print_r($data[$item], true)); |
| | | return false; |
| | | } |
| | | return true; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | $out = []; |
| | | foreach ($data as $prop => $val) { |
| | | $out[$prop] = match($prop) { |
| | | 'about', 'description' => sanitize_text_field($val), |
| | | 'phone' => Sanitizer::sanitizePhone($val), |
| | | 'emails' => $this->sanitizeEmails($val), |
| | | 'website' => sanitize_url($val), |
| | | 'location' => $this->sanitizeLocation($val), |
| | | 'hours' => $this->format_hours($val), |
| | | default => null |
| | | }; |
| | | } |
| | | $out = array_filter($out); |
| | | |
| | | if (empty($out)) { |
| | | return $this->response(false, 'No data to update'); |
| | | } |
| | | |
| | | $out['access_token'] = $this->getAccessToken(); |
| | | |
| | | $response = $this->postRequest( |
| | | sprintf( |
| | | '%s/%s', |
| | | $this->baseUrl, |
| | | $this->getPageID() |
| | | ), |
| | | $out |
| | | ); |
| | | |
| | | $success = $response['success']??false; |
| | | return $this->response($success, $success ? 'Successfully updated information' : 'Failed to update information'); |
| | | } |
| | | protected function format_hours(array $days):array|null |
| | | { |
| | | //FB expects it to be: |
| | | // - first 3 characters of day of week (mon) |
| | | // - either 1 or 2, to allow for two set hours (ex: noon break) |
| | | // - open or close |
| | | //example: mon_1_open: 11:00, mon_1_close: 18:00 |
| | | //To accomplish this, we will assume the hours are formatted as with the SEO schema |
| | | $days = array_filter($days, function ($day) { |
| | | return in_array($day, ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $out = []; |
| | | foreach ($days as $day => $conf) { |
| | | if (array_key_exists('isOpen', $conf) && $conf['isOpen'] === 1 |
| | | && array_key_exists('opens', $conf) && !empty($conf['opens']) |
| | | && array_key_exists('closes', $conf) && !empty($conf['closes']) |
| | | ) { |
| | | $short = substr($day, 0, 3); |
| | | |
| | | $out[$short.'_1_open'] = $conf['opens']; //Should be stored in 24hr time already |
| | | $out[$short.'_1_close'] = $conf['closes']; |
| | | } |
| | | } |
| | | |
| | | return empty($out) ? null : $out; |
| | | } |
| | | protected function sanitizeLocation(array $location):array|null |
| | | { |
| | | if (array_key_exists('province', $location)) { |
| | | $location['state'] = $location['province']; |
| | | unset($location['province']); |
| | | } |
| | | if (array_key_exists('postal_code', $location)) { |
| | | $location['zip'] = $location['postal_code']; |
| | | unset($location['postal_code']); |
| | | } |
| | | $location = array_filter($location, function ($item) use ($location) { |
| | | if (!in_array($item, ['street', 'city', 'state', 'zip', 'country'])) { |
| | | $this->logError('sanitizeLocation', 'Attempted to set property that is not allowed: '.$item.', with value: '.print_r($location[$item], true)); |
| | | return false; |
| | | } |
| | | if (!is_string($location[$item])) { |
| | | $this->logError('sanitizeLocation', 'Unexpected value for '.$item.': '.print_r($location[$item], true)); |
| | | return false; |
| | | } |
| | | return true; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | return empty($location) ? null : $location; |
| | | } |
| | | protected function sanitizeEmails(array $emails):array|null |
| | | { |
| | | $emails = array_filter($emails, function($item) { |
| | | return (bool)filter_var($item, FILTER_VALIDATE_EMAIL); |
| | | }); |
| | | return empty($emails) ? null : $emails; |
| | | } |
| | | } |
| | |
| | | |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\Requests; |
| | | use WP_Error; |
| | |
| | | namespace JVBase\integrations\services; |
| | | use Exception; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\OAuth; |
| | | use JVBase\integrations\OAuthCredentials; |
| | | use JVBase\integrations\OAuthURLs; |
| | | use JVBase\integrations\Requests; |
| | | use JVBase\integrations\SyncTo; |
| | | use JVBase\integrations\UserConnection; |
| | | |
| | | |
| | |
| | | |
| | | class GoogleMyBusiness extends Integrations |
| | | { |
| | | use Admin, Instance, OAuth, Requests, UserConnection; |
| | | use Admin, Instance, OAuth, Requests, UserConnection, SyncTo; |
| | | |
| | | protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours'; |
| | | |
| | |
| | | } |
| | | return $options; |
| | | } |
| | | |
| | | protected function setContentTypes(): void |
| | | { |
| | | // TODO: Implement setContentTypes() method. |
| | | } |
| | | |
| | | protected function formatForService(int $itemID, string $type = 'post'): array |
| | | { |
| | | // TODO: Implement formatForService() method. |
| | | } |
| | | } |
| | |
| | | $gmb_item = $this->buildMenuItem($item, $defaults); |
| | | |
| | | // Get section for this item |
| | | $categories = wp_get_post_terms($item->ID, BASE.'section'); |
| | | $categories = wp_get_post_terms($item->ID, 'product_cat'); |
| | | // $categories = wp_get_post_terms($item->ID, BASE.'section'); |
| | | $section_slug = !empty($categories) ? |
| | | $categories[0]->slug : |
| | | 'menu-items'; |
| | |
| | | |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\managers\queue\executors\IntegrationExecutor; |
| | | use JVBase\managers\queue\TypeConfig; |
| | |
| | | |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | |
| | | $queue->registry()->register(self::$import, new TypeConfig( |
| | | executor: $executor, |
| | |
| | | |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\OAuth; |
| | | use JVBase\integrations\OAuthCredentials; |
| | |
| | | |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\Requests; |
| | | use JVBase\meta\Meta; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | return $this->response(false, 'Rate limit exceeded'); |
| | | } |
| | | $endpoint = str_replace($this->apiBase, '', $endpoint); |
| | | $error = $response['ErrorCode'] ?? false; |
| | | if ($error) { |
| | | switch ($error) { |
| | | case '0': |
| | | //Do nothing, no error found |
| | | break; |
| | | //Bad or missing API Token |
| | | case '10': |
| | | //Multiple errors |
| | | case '11': |
| | | //Resource Not found |
| | | case '12': |
| | | //Invalid Pagination Key |
| | | case '13': |
| | | //Maintenance; PostMark API down for a sec |
| | | case '100': |
| | | //Invalid Email Request |
| | | case '300': |
| | | //Sender Signature not found |
| | | case '400': |
| | | //Sender signature not confirmed |
| | | case '401': |
| | | //Invalid JSON |
| | | case '402': |
| | | //Invalid request field(s) - {FIELD_NAME} |
| | | case '403': |
| | | //Not allowed to send (no more credits in account) |
| | | case '405': |
| | | //Inactive recipient. Recipient hard bounces, or filed spam complaint |
| | | case '406': |
| | | //JSON required |
| | | case '409': |
| | | //Too many requests (500 max) |
| | | case '410': |
| | | //Forbidden Attachment type |
| | | case '411': |
| | | //Account is pending |
| | | case '412': |
| | | //Account may not exist |
| | | case '413': |
| | | //Invalid query string parameter |
| | | case '500': |
| | | default: |
| | | $this->logError( |
| | | 'handleResponse', |
| | | 'Something went wrong: '.$response['Message']??'', |
| | | $response |
| | | ); |
| | | return $this->response(false, 'Something went wrong: '.$response['Message']??''); |
| | | } |
| | | $checkErrors = $this->checkErrors($response); |
| | | if (!empty($checkErrors)) { |
| | | return $checkErrors; |
| | | } |
| | | |
| | | return match ($endpoint) { |
| | | '/email' => $this->handleEmailResponse($response, $method), |
| | | '/email/bulk' => $this->handleBulkEmailResponse($response, $method), |
| | | '/email/batch' => $this->handleBatchEmailResponse($response, $method), |
| | | default => $this->response(false, 'Unhandled Response for '. $endpoint . ' endpoint, with method: '. $method), |
| | | }; |
| | | } |
| | | |
| | | protected function checkErrors(WP_Error|array $response):array|false |
| | | { |
| | | $error = $response['ErrorCode'] ?? false; |
| | | if ($error) { |
| | | switch ($error) { |
| | | case '0': |
| | | //Do nothing, no error found |
| | | return false; |
| | | //Bad or missing API Token |
| | | case '10': |
| | | //Multiple errors |
| | | case '11': |
| | | //Resource Not found |
| | | case '12': |
| | | //Invalid Pagination Key |
| | | case '13': |
| | | //Maintenance; PostMark API down for a sec |
| | | case '100': |
| | | //Maybe schedule a retry? |
| | | //Invalid Email Request |
| | | case '300': |
| | | //Sender Signature not found |
| | | case '400': |
| | | //Sender signature not confirmed |
| | | case '401': |
| | | //Invalid JSON |
| | | case '402': |
| | | //Invalid request field(s) - {FIELD_NAME} |
| | | case '403': |
| | | //Not allowed to send (no more credits in account) |
| | | case '405': |
| | | //Inactive recipient. Recipient hard bounces, or filed spam complaint |
| | | case '406': |
| | | //JSON required |
| | | case '409': |
| | | //Too many requests (500 max) |
| | | case '410': |
| | | //Maybe schedule a retry? |
| | | //Forbidden Attachment type |
| | | case '411': |
| | | //Account is pending |
| | | case '412': |
| | | //Account may not exist |
| | | case '413': |
| | | //Some errors in bulk request |
| | | case '422': |
| | | //Invalid query string parameter |
| | | case '500': |
| | | default: |
| | | $this->logError( |
| | | 'handleResponse', |
| | | 'Something went wrong: '.$response['Message']??'', |
| | | $response |
| | | ); |
| | | return $this->response(false, 'Something went wrong: '.$response['Message']??''); |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /********************************************************** |
| | | * UTILITY |
| | | **********************************************************/ |
| | |
| | | |
| | | $payload = $this->buildPayload($to, $subject, $message, $is_html, $headers, $attachments); |
| | | |
| | | $response = $this->postRequest( |
| | | return $this->postRequest( |
| | | sprintf('%s/email', $this->apiBase), |
| | | $payload |
| | | ); |
| | | } |
| | | protected function handleEmailResponse(WP_Error|array $response, string $method):array |
| | | { |
| | | |
| | | return $this->response(true, 'Successfully sent request to PostMark', $response); |
| | | } |
| | | protected function buildPayload(string|array $to, string $subject, string $message, bool $is_html = true, array $headers = [], array $attachments = []):array |
| | | public function buildPayload(string|array $to, string $subject, string $message, bool $is_html = true, array $headers = [], array $attachments = []):array |
| | | { |
| | | $to = is_array($to) ? implode(',',$to) : $to; |
| | | |
| | |
| | | |
| | | if (!empty($headers['reply-to'])) { |
| | | $payload['ReplyTo'] = $headers['reply-to']; |
| | | } else { |
| | | $payload['ReplyTo'] = $this->getFromEmail(); |
| | | } |
| | | |
| | | if (!empty($attachments)) { |
| | |
| | | |
| | | return $payload; |
| | | } |
| | | |
| | | /** |
| | | * Sends the same message to multiple emails. Can use templates |
| | | * @param array<int> $userIDs |
| | | * @param string $subject |
| | | * @param string $message |
| | | * @param bool $is_html |
| | | * @param array $headers |
| | | * @param array $attachments |
| | | * @return array |
| | | */ |
| | | public function sendMessageToMany(array $userIDs, string $subject, string $message, bool $is_html = true, array $headers = [], array $attachments = []):array |
| | | { |
| | | if (!$this->isSetUp()) { |
| | | return $this->response(false, 'Not set up'); |
| | | } |
| | | |
| | | $tags = []; |
| | | $pattern = '/\{\{(.*?)\}\}/'; |
| | | |
| | | if (str_contains($subject, '{{')){ |
| | | preg_match_all($pattern, $subject, $tags); |
| | | } |
| | | if (str_contains($message, '{{')) { |
| | | preg_match_all($pattern, $message, $tags); |
| | | } |
| | | |
| | | $tags = array_unique($tags); |
| | | foreach ($tags as $tag) { |
| | | $t = implode( |
| | | array_map(function($word) { |
| | | return ucfirst($word); |
| | | }, explode('_', $tag) |
| | | )); |
| | | str_replace('{{'.$tag.'}}', '{{'.$t.'}}', $subject); |
| | | str_replace('{{'.$tag.'}}', '{{'.$t.'}}', $message); |
| | | } |
| | | |
| | | |
| | | $to = array_map(function ($userID) use ($tags, $subject, $message) { |
| | | $meta = Meta::forUser($userID); |
| | | $email = $meta->get('user_email'); |
| | | $out = []; |
| | | if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) { |
| | | $out['To'] = $email; |
| | | } |
| | | |
| | | if (empty($out)) { |
| | | $this->logError('sendMessageToMany', 'User does not have an email address',['user' => $userID]); |
| | | return []; |
| | | } |
| | | |
| | | $tmp = []; |
| | | foreach ($tags as $tag) { |
| | | $v = $meta->get($tag); |
| | | |
| | | if (!empty($v)) { |
| | | $tag = implode( |
| | | array_map(function($word) { |
| | | return ucfirst($word); |
| | | }, explode('_', $tag) |
| | | )); |
| | | $tmp[$tag] = $v; |
| | | } |
| | | } |
| | | if (!empty($tmp)) { |
| | | $out['TemplateModel'] = $tmp; |
| | | } |
| | | return $out; |
| | | }, $userIDs); |
| | | $to = array_filter($to, function($result) { |
| | | return array_key_exists('To', $result) && !empty($result['To']); |
| | | }); |
| | | |
| | | if (empty($to)) { |
| | | $this->logError('sendMessageToMany', 'No users set',['users' => $userIDs]); |
| | | } |
| | | |
| | | //Onward! |
| | | $payload = [ |
| | | 'From' => sprintf( |
| | | '%s <%s>', |
| | | $headers['from-name'] ?? $this->getFromName(), |
| | | $headers['from'] ?? $this->getFromEmail() |
| | | ), |
| | | 'Subject' => sanitize_text_field($subject), |
| | | 'TrackOpens' => $this->getTrackOpens() === 'true', |
| | | 'TrackLinks' => $this->getTrackLinks(), |
| | | 'MessageStream' => 'broadcast' |
| | | ]; |
| | | |
| | | if ($is_html) { |
| | | $payload['HtmlBody'] = $message; |
| | | $payload['TextBody'] = $this->textFromHtml($message); |
| | | } else { |
| | | $payload['TextBody'] = $message; |
| | | } |
| | | |
| | | if (!empty($headers['reply-to'])) { |
| | | $payload['ReplyTo'] = $headers['reply-to']; |
| | | } else { |
| | | $payload['ReplyTo'] = $this->getFromEmail(); |
| | | } |
| | | if (!empty($attachments)) { |
| | | $payload['Attachments'] = $this->processAttachments($attachments); |
| | | } |
| | | if (!empty($headers['x-headers'])) { |
| | | $payload['Headers'] = $headers['x-headers']; |
| | | } |
| | | $payload['Tag'] = $this->determineEmailTag($subject, $headers); |
| | | |
| | | $payload['Messages'] = $to; |
| | | |
| | | return $this->postRequest( |
| | | sprintf('%s/email/bulk', $this->apiBase), |
| | | $payload |
| | | ); |
| | | } |
| | | protected function handleBulkEmailResponse(WP_Error|array $response, string $method):array |
| | | { |
| | | return $this->response($response['Status']??'' === 'Accepted', 'Successfully sent request to Postmark', $response); |
| | | } |
| | | |
| | | /** |
| | | * @param array $emails An array of results from buildPayload, ready to batch send to Postmark. Limit of 500 at a time |
| | | * @return array |
| | | */ |
| | | public function sendMany(array $emails):array |
| | | { |
| | | if (!$this->isSetUp()) { |
| | | return $this->response(false, 'Not set up'); |
| | | } |
| | | $emails = array_filter($emails, function ($email) { |
| | | if (!array_key_exists('To', $email) || !filter_var($email['To'], FILTER_VALIDATE_EMAIL)) { |
| | | $this->logError('sendMany', 'Email does not have a To email address', $email); |
| | | return false; |
| | | } |
| | | //This shouldn't happen, but if it does, we'll just remove it for now. |
| | | if (!array_key_exists('From', $email)) { |
| | | $this->logError('sendMany', 'Email does not have a From email address', $email); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }); |
| | | |
| | | if (empty($emails)) { |
| | | $this->logError('sendMany', 'No emails have been sent', $emails); |
| | | return $this->response(false, 'No emails have been sent', $emails); |
| | | } |
| | | |
| | | return $this->postRequest( |
| | | sprintf( |
| | | '%s/email/batch', |
| | | $this->apiBase |
| | | ), |
| | | $emails |
| | | ); |
| | | } |
| | | protected function handleBatchEmailResponse(WP_Error|array $response, string $method):array |
| | | { |
| | | $main = $response[0]; |
| | | $checkErrors = $this->checkErrors($main); |
| | | if (!empty($checkErrors)) { |
| | | return $checkErrors; |
| | | } |
| | | if (count($response) > 1) { |
| | | $errors = $response; |
| | | unset($errors[0]); |
| | | foreach ($errors as $error) { |
| | | $this->checkErrors($error); |
| | | } |
| | | } |
| | | return $this->response($main['Message']??'' === 'OK', 'Successfully sent request to Postmark', $response); |
| | | } |
| | | } |
| | |
| | | use Exception; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\APIEndpoint; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\OAuth; |
| | | use JVBase\integrations\OAuthCredentials; |
| | |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\meta\Sanitizer; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\ui\CRUDSkeleton; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | add_action(BASE.'dashboard_page_orders', [$this, 'renderDashPage']); |
| | | } |
| | | |
| | | public function addDashboardPages():void |
| | | { |
| | | $page = Dashboard::getInstance()->addPage('Your Orders', 'orders', 'receipt'); |
| | | $page->setScripts(['jvb-crud']); |
| | | } |
| | | |
| | | public function renderDashPage():void |
| | | { |
| | | $this->determineUser(); |
| | | |
| | | $crud = new CRUDSkeleton(); |
| | | $crud->icon('receipt'); |
| | | $crud->title('Your Orders','Here you can see your past orders, and reorder from there if you\'d like'); |
| | | $crud->content($this->orderPostType, 'Order', 'Orders'); |
| | | $crud->addSearch(); |
| | | $crud->addCapabilities(['view']); |
| | | $crud->setEmptyState(sprintf( |
| | | '<div class="empty-state"> |
| | | <h3>%sNothing here%s</h3> |
| | | <p>It doesn\'t look like you have any orders yet.</p> |
| | | <p>Head on over to <a href="%s">our menu</a> and make your first order!</p> |
| | | </div>', |
| | | jvbDashIcon($crud->getIcon()), |
| | | jvbDashIcon($crud->getIcon()), |
| | | get_post_type_archive_link(BASE.'menu_item') |
| | | )); |
| | | |
| | | |
| | | $crud->render(); |
| | | } |
| | | |
| | | protected function initialize(): void |
| | | { |
| | | $this->defineEndpoints(); |
| | |
| | | ********************************************************************/ |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $queue->registry()->register( |
| | | self::$syncCustomer, |
| | | new TypeConfig( |
| | |
| | | { |
| | | return $this->loadCredentials()['location_id']??false; |
| | | } |
| | | protected function getLocations():array|false |
| | | public function getLocations():array|false |
| | | { |
| | | if (!$this->getAccessToken()) { |
| | | return false; |
| | |
| | | return $this->contentTypes[$content_type]; |
| | | } |
| | | |
| | | public function handleImportFromSquare():array |
| | | { |
| | | |
| | | return $this->response(true, 'Did a thing'); |
| | | } |
| | | |
| | | public function handleSyncToSquare():array |
| | | { |
| | | |
| | | return $this->response(true, 'Did a thing'); |
| | | } |
| | | |
| | | |
| | | } |
| | |
| | | |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | |
| | | $queue->registry()->register(self::$syncCustomer, new TypeConfig( |
| | | executor: $executor, |
| | |
| | | "If you didn't create this account, please ignore this email.\n\n" . |
| | | "Thanks,\n", |
| | | $site_name, |
| | | JVB()->email()->button('Reset Password', $reset_url), |
| | | JVB()->email()->link($reset_url), |
| | | Emails::getInstance()->button('Reset Password', $reset_url), |
| | | Emails::getInstance()->link($reset_url), |
| | | ); |
| | | |
| | | JVB()->email()->sendEmail( |
| | | Emails::getInstance()->sendEmail( |
| | | $user->user_email, |
| | | sprintf('[%s] Welcome! Set Your Password', $site_name), |
| | | $message |
| | |
| | | |
| | | public function addDashboardPages():void |
| | | { |
| | | $page = JVB()->dashboard()->addPage('Your Orders', 'orders', 'receipt'); |
| | | $page = Dashboard::getInstance()->addPage('Your Orders', 'orders', 'receipt'); |
| | | $page->setScripts(['jvb-crud']); |
| | | } |
| | | public function renderDashPage():void |
| | |
| | | <?php |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | class Umami extends Integrations |
| | | { |
| | | use Admin, Instance; |
| | | |
| | | protected function __construct() |
| | | { |
| | | $this->service_name = 'umami'; |
| | | $this->title = 'umami'; |
| | | $this->icon = 'chart-line'; |
| | | |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | | |
| | | parent::__construct(); |
| | | $this->defineTables(); |
| | | } |
| | | |
| | | protected function defineFields(): void |
| | | { |
| | | $id = $this->addField('website_id', 'Website ID', 'text', 'manage_options'); |
| | | $id->setHint('Found in your Umami dashboard under Settings->Websites'); |
| | | $id->setRequired(); |
| | | |
| | | $api_url = $this->addField('api_url', 'API URL', 'text', 'manage_options'); |
| | | $api_url->setHint('Your Umami server URL. Leave blank if using umami.is cloud service.'); |
| | | |
| | | $api_token = $this->addField('api_token', 'API Token (Optional)', 'text', 'manage_options'); |
| | | $api_token->setSubType('password'); |
| | | $api_token->setHint('Required only if you want to retrieve analytics data for dashboards.'); |
| | | } |
| | | protected function defineInstructions():void |
| | | { |
| | | $this->instructions = [ |
| | | 'Login to your <a href="https://cloud.umami.is/settings/websites" target="_blank">umami.is</a> account, select your website, and copy the website ID' |
| | | ]; |
| | | } |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | //TODO: Once we have an actual umami setup, we can store the fetched data here for reference |
| | | //Ideally, we have the type (term, user, post), WP ID, created_date, and count |
| | | } |
| | | |
| | | protected function initialize(): void |
| | | { |
| | | // TODO: Implement initialize() method. |
| | | // Add tracking script to frontend |
| | | add_action('wp_head', [$this, 'renderTrackingScript'], 5); |
| | | |
| | | // Schedule data collection if API is configured |
| | | if ($this->getApiToken()) { |
| | | add_action('jvb_daily_umami_collection', [$this, 'collectDailyData']); |
| | | |
| | | if (!wp_next_scheduled('jvb_daily_umami_collection')) { |
| | | wp_schedule_event(time(), 'daily', 'jvb_daily_umami_collection'); |
| | | } |
| | | } |
| | | } |
| | | |
| | | public function renderTrackingScript():void |
| | | { |
| | | if (JVB_TESTING || !$this->isSetUp() || is_admin()) { |
| | | return; |
| | | } |
| | | |
| | | $script_url = $this->getTrackingScriptUrl(); |
| | | if (!$this->getWebsiteID() || empty($script_url)) { |
| | | return; |
| | | } |
| | | |
| | | echo sprintf( |
| | | '<link rel="preconnect" href="https://%s"/>' . "\n", |
| | | esc_attr(parse_url($script_url, PHP_URL_HOST)) |
| | | ); |
| | | |
| | | $attributes = [ |
| | | 'defer', |
| | | sprintf( |
| | | 'src="%s"', |
| | | esc_url($script_url) |
| | | ), |
| | | sprintf( |
| | | 'data-website-id="%s"', |
| | | esc_attr($this->getWebsiteID()) |
| | | ) |
| | | ]; |
| | | |
| | | $tags = $this->getCurrentTag(); |
| | | if (!empty($tags)) { |
| | | $attributes[] = $tags; |
| | | } |
| | | |
| | | echo sprintf( |
| | | '<script %s></script>', |
| | | implode(' ', $attributes) |
| | | ); |
| | | |
| | | $this->outputCustomEvent(); |
| | | } |
| | | protected function getCurrentTag():string |
| | | { |
| | | $contentTax = array_map('jvbCheckBase', Registrar::withFeature('is_content', 'term')); |
| | | if (!empty($contentTax) && is_tax($contentTax)) { |
| | | $tax = get_queried_object(); |
| | | $reg = Registrar::getInstance($tax->taxonomy); |
| | | return 'data-tag="'.$reg->getUmamiEvent().'"'; |
| | | } |
| | | |
| | | $profiles = array_map('jvbCheckBase', Registrar::withFeature('profile_link')); |
| | | if (!empty($profiles) && is_singular($profiles)) { |
| | | $usr = get_queried_object(); |
| | | $reg = Registrar::getInstance($usr->post_type); |
| | | return 'data-tag="'.$reg->getUmamiEvent().'"'; |
| | | } |
| | | |
| | | return ''; |
| | | } |
| | | |
| | | protected function outputCustomEvent():void |
| | | { |
| | | $args = []; |
| | | $event = false; |
| | | $contentTax = array_map('jvbCheckBase', Registrar::withFeature('is_content', 'term')); |
| | | if (!empty($contentTax) && is_tax($contentTax)) { |
| | | $tax = get_queried_object(); |
| | | $reg = Registrar::getInstance($tax->taxonomy); |
| | | |
| | | $event = $reg->getUmamiEvent(); |
| | | $args = [ |
| | | 'id' => $tax->term_id, |
| | | ]; |
| | | if ($reg->isOwnable()) { |
| | | $owners = jvbGetTermOwners($tax->term_id); |
| | | if (!empty($owners)) { |
| | | $args['owners'] = $owners; |
| | | } |
| | | } |
| | | } |
| | | |
| | | $profiles = array_map('jvbCheckBase', Registrar::withFeature('profile_link')); |
| | | if (!empty($profiles) && is_singular($profiles)) { |
| | | $usr = get_queried_object(); |
| | | $reg = Registrar::getInstance($usr->post_type); |
| | | $event = $reg->getUmamiEvent(); |
| | | |
| | | global $post; |
| | | $args['id'] = get_the_ID(); |
| | | $args['owner_id'] = $post->post_author; |
| | | } |
| | | |
| | | if ($event && !empty($args)) { |
| | | echo sprintf( |
| | | '<script defer>umami.track(event_name: %s, data: %s);</script>', |
| | | $event, |
| | | json_encode($args) |
| | | ); |
| | | } |
| | | } |
| | | |
| | | /********************************************************* |
| | | * UTILITY |
| | | *********************************************************/ |
| | | public function getWebsiteID(): string|false |
| | | { |
| | | return $this->loadCredentials()['website_id']??false; |
| | | } |
| | | protected function getApiUrl():string|false |
| | | { |
| | | $api = $this->loadCredentials()['api_url']; |
| | | return empty($api) ? false : $api; |
| | | } |
| | | protected function getApiToken():string|false |
| | | { |
| | | $api = $this->loadCredentials()['api_token']; |
| | | return empty($api) ? false : $api; |
| | | } |
| | | protected function getTrackingScriptUrl():string |
| | | { |
| | | if ($this->getApiUrl()) { |
| | | return rtrim($this->getApiUrl(), '/').'/script.js'; |
| | | } |
| | | // Default to Umami cloud |
| | | return 'https://cloud.umami.is/script.js'; |
| | | } |
| | | |
| | | public function buildEvent(string $event, array $args = []):string |
| | | { |
| | | $base = 'data-umami-event'; |
| | | $out = [ |
| | | $base => esc_attr($event), |
| | | ]; |
| | | |
| | | foreach ($args as $k => $v) { |
| | | $out[$base.$k] = esc_attr(jvbNoBase($v)); |
| | | } |
| | | |
| | | return $this->attributesToString($out); |
| | | } |
| | | |
| | | protected function attributesToString(array $attributes):string |
| | | { |
| | | $strings = []; |
| | | foreach ($attributes as $key => $value) { |
| | | $strings[] = sprintf( |
| | | '%s="%s"', |
| | | $key, |
| | | $value |
| | | ); |
| | | } |
| | | |
| | | return implode(' ', $strings); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | $event = ''; |
| | | $type = ''; |
| | | |
| | | |
| | | if (is_singular([BASE . 'artist', BASE . 'partner'])) { |
| | | // Profile view |
| | | global $post; |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use WP_REST_Response; |
| | |
| | | */ |
| | | class AdminPages |
| | | { |
| | | use Instance; |
| | | protected array $subpages; |
| | | protected array $actions; |
| | | protected string $page_hook; |
| | |
| | | protected function renderStatusItems():void |
| | | { |
| | | // Get queue stats |
| | | $queue_status = JVB()->queue()->getStatus(); |
| | | $queue_status = Queue::getInstance()->getStatus(); |
| | | error_log('Queue Status: '.print_r($queue_status, true)); |
| | | |
| | | // Other system checks |
| | |
| | | } |
| | | |
| | | $group = sanitize_text_field($request->get_param('group')); |
| | | Cache::invalidateGroup($group); |
| | | Cache::for($group)->flush(); |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| File was renamed from inc/managers/ApprovalManager.php |
| | |
| | | |
| | | use DateTime; |
| | | use JVBase\base\Site; |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\ApprovalRoutes; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class ApprovalManager { |
| | | class Approvals { |
| | | use Instance; |
| | | protected array $tables =[]; |
| | | protected int $expiresAt = 21; //days for request |
| | | protected int $requiredVotes = 3; //Number of votes before a term is approved |
| | | protected bool $hasApproval = false; |
| | | public function __construct() |
| | | protected Rest $route; |
| | | private function __construct() |
| | | { |
| | | $this->route = ApprovalRoutes::getInstance(); |
| | | $this->defineTables(); |
| | | if (empty($this->tables)) { |
| | | return; |
| | |
| | | ]); |
| | | |
| | | |
| | | JVB()->notification()->notify($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | NotificationManager::getInstance()->notify($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | } |
| | | public function finalizeDenial(int $request_id, string $type):void |
| | | { |
| | |
| | | 'request_id' => $request_id |
| | | ]); |
| | | |
| | | JVB()->notification()->notify($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | NotificationManager::getInstance()->notify($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | } |
| | | |
| | | public function getApprovalRequests(int $userID, ?string $type = null):array |
| | |
| | | if (!$this->registrar) { |
| | | return; |
| | | } |
| | | $this->cache = Cache::for('crud_date_ranges','post')->user(); |
| | | |
| | | $this->user_id = get_current_user_id(); |
| | | $this->content = $content; |
| | | $this->cache = Cache::for('crud')->connect('post')->connect('taxonomy'); |
| | | |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | // Create and configure skeleton |
| | | $this->skeleton = new CRUDSkeleton(); |
| | |
| | | |
| | | protected function addDateRanges():array |
| | | { |
| | | return $this->cache->user()->remember( |
| | | return $this->cache->remember( |
| | | 'dateRanges', |
| | | function() { |
| | | $postType = jvbCheckBase($this->content); |
| | |
| | | { |
| | | private string $group; |
| | | private int $ttl; |
| | | private static array $timestamps = []; |
| | | private const TS_GROUP = 'jvb_http_ts'; |
| | | |
| | | private const CONNECTIONS_OPTION = 'jvb_cache_connections'; |
| | | private static ?array $connections = null; |
| | | protected array $tags = []; |
| | | |
| | | private static array $instances = []; |
| | | private bool $hasRedis; |
| | | private bool $varyByAuth = false; |
| | | private bool $varyByUser = false; |
| | | |
| | | private function __construct(string $group, int $ttl) |
| | | private array $index=[]; |
| | | private ?string $target = null; |
| | | |
| | | private function __construct(string $group, ?string $target = null, int $ttl = WEEK_IN_SECONDS) |
| | | { |
| | | if (!is_null($target) && empty($this->index)) { |
| | | $this->target = null; |
| | | $this->index = get_option(BASE."cache_{$target}_index",[]); |
| | | } |
| | | $this->group = $group; |
| | | $this->ttl = $ttl; |
| | | $this->hasRedis = (bool) wp_using_ext_object_cache(); |
| | | $this->logGroup(); |
| | | $this->hasRedis = (bool)wp_using_ext_object_cache(); |
| | | } |
| | | |
| | | protected function logGroup():void |
| | | { |
| | | $changed = false; |
| | | $groups = get_option(BASE."cache_groups", []); |
| | | if (!array_key_exists($this->group, $groups)) { |
| | | $groups[$this->group] = [ |
| | | 'auth' => $this->varyByAuth, |
| | | 'user' => $this->varyByUser, |
| | | 'target'=> $this->target, |
| | | 'ttl' => $this->ttl, |
| | | ]; |
| | | $changed = true; |
| | | } |
| | | |
| | | if ($groups[$this->group]['auth'] && !$this->varyByAuth) { |
| | | error_log('[Cache]Group '.$this->group.' previously defined as varying by auth, but is not set. Setting it now.'); |
| | | $this->auth(); |
| | | } |
| | | if ($groups[$this->group]['user'] && !$this->varyByUser) { |
| | | error_log('[Cache]Group '.$this->group.' previously defined as varying by user, but is not set. Setting it now.'); |
| | | $this->user(); |
| | | } |
| | | if (is_null($this->target) && !is_null($groups[$this->group]['target'])) { |
| | | error_log('[Cache]Group '.$this->group. ' previously defined with target '.$groups[$this->group]['target'].'... Setting it now.'); |
| | | $this->target = $groups[$this->group]['target']; |
| | | } elseif ($this->target !== $groups[$this->group]['target']) { |
| | | error_log('[Cache]WARNING Group previously defined with target '.$groups[$this->group]['target'].' - but is currently defined with '.$this->target. '. You may want to look into this.'); |
| | | } |
| | | |
| | | if ($this->ttl !== $groups[$this->group]['ttl']) { |
| | | error_log('[Cache]Group '. $this->group. ' previously defined with ttl '.$groups[$this->group]['ttl']. ' - but is currently defined with '. $this->ttl); |
| | | $groups[$this->group]['ttl'] = $this->ttl; |
| | | $changed = true; |
| | | } |
| | | |
| | | if ($this->varyByAuth && !$groups[$this->group]['auth']) { |
| | | $groups[$this->group]['auth'] = true; |
| | | $changed = true; |
| | | } |
| | | if ($this->varyByUser && !$groups[$this->group]['user']) { |
| | | $groups[$this->group]['user'] = true; |
| | | $changed = true; |
| | | } |
| | | if ($this->target !== $groups[$this->group]['target']) { |
| | | $groups[$this->group]['target'] = $this->target; |
| | | $changed = true; |
| | | } |
| | | |
| | | if ($changed) { |
| | | update_option(BASE."cache_groups", $groups, false); |
| | | } |
| | | } |
| | | |
| | | public static function registerHooks(): void |
| | |
| | | add_action('delete_term', [self::class, 'onTermDelete'], 10, 3); |
| | | |
| | | // Term meta updates |
| | | add_action('updated_term_meta', [self::class, 'onTermMetaChange'], 10, 2); |
| | | add_action('added_term_meta', [self::class, 'onTermMetaChange'], 10, 2); |
| | | add_action('deleted_term_meta', [self::class, 'onTermMetaDelete'], 10, 2); |
| | | // add_action('updated_term_meta', [self::class, 'onTermMetaChange'], 10, 2); |
| | | // add_action('added_term_meta', [self::class, 'onTermMetaChange'], 10, 2); |
| | | // add_action('deleted_term_meta', [self::class, 'onTermMetaDelete'], 10, 2); |
| | | |
| | | // User updates |
| | | add_action('profile_update', [self::class, 'onUserChange'], 10, 2); |
| | |
| | | add_action('deleted_user', [self::class, 'onUserDelete']); |
| | | |
| | | // User meta updates |
| | | add_action('updated_user_meta', [self::class, 'onUserMetaChange'], 10, 2); |
| | | add_action('added_user_meta', [self::class, 'onUserMetaChange'], 10, 2); |
| | | add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 2); |
| | | // add_action('updated_user_meta', [self::class, 'onUserMetaChange'], 10, 2); |
| | | // add_action('added_user_meta', [self::class, 'onUserMetaChange'], 10, 2); |
| | | // add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 2); |
| | | } |
| | | |
| | | public function auth(): static |
| | |
| | | return (string) $key; |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Factory |
| | | * ------------------------------------------------------------------- */ |
| | | |
| | | public static function for(string $group, int $ttl = HOUR_IN_SECONDS): self |
| | | public static function for(string $group, ?string $target = null, int $ttl = HOUR_IN_SECONDS): self |
| | | { |
| | | $group = sanitize_key($group); |
| | | |
| | | if (!isset(self::$instances[$group])) { |
| | | self::$instances[$group] = new self($group, $ttl); |
| | | self::$instances[$group] = new self($group, $target, $ttl); |
| | | } |
| | | |
| | | return self::$instances[$group]; |
| | | } |
| | | |
| | | /** |
| | | * To help facilitate cache flushing on object updates, we track the keys that are used for a particular id |
| | | * Generally, $this->target will either be 'post', 'user', 'term', or 'custom' |
| | | * @param int $ID |
| | | * @param string $key |
| | | * @return void |
| | | */ |
| | | protected function trackID(int $ID, string $key):void |
| | | { |
| | | if (is_null($this->target)) { |
| | | return; |
| | | } |
| | | if (!array_key_exists($ID, $this->index[$this->target])) { |
| | | $this->index[$ID] = []; |
| | | } |
| | | if (!array_key_exists($this->group, $this->index[$ID])) { |
| | | $this->index[$ID][$this->group] = []; |
| | | } |
| | | if (!in_array($key, $this->index[$ID][$this->group])) { |
| | | $this->index[$this->target][$ID][$this->group][] = $key; |
| | | update_option(BASE."cache_{$this->target}_index", $this->index, false); |
| | | } |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Core operations |
| | | * ------------------------------------------------------------------- */ |
| | | |
| | | public function remember(int|string|array $key, callable $callback, ?int $ttl = null): mixed |
| | | { |
| | | //TODO: Redis is fucking this all up for some reason. While we figure it out, let's just return the callback |
| | | return $callback(); |
| | | |
| | | if (is_array($key)) { |
| | | $key = $this->generateKey($key); |
| | | } |
| | | if (!empty($this->tags)) { |
| | | return $this->rememberTagged( |
| | | $key, |
| | | $this->tags, |
| | | $callback, |
| | | $ttl |
| | | ); |
| | | } |
| | | $key = $this->generateKey($key); |
| | | |
| | | $value = $this->get($key); |
| | | |
| | |
| | | |
| | | return $value; |
| | | } |
| | | |
| | | public function get(int|string|array $id): mixed |
| | | protected function transientKey(string $key):string |
| | | { |
| | | if (is_array($id)) { |
| | | $id = $this->generateKey($id); |
| | | } |
| | | if ($this->hasRedis) { |
| | | $value = wp_cache_get($id, $this->group); |
| | | } else { |
| | | $value = get_transient("jvb_{$this->group}_{$id}"); |
| | | } |
| | | |
| | | return $value; |
| | | return BASE."{$this->group}_{$key}"; |
| | | } |
| | | |
| | | public function set(int|string|array $id, mixed $value, ?int $ttl = null): void |
| | | public function get(int|string|array $key): mixed |
| | | { |
| | | if (is_array($id)) { |
| | | $id = $this->generateKey($id); |
| | | } |
| | | $ttl = $ttl ?? $this->ttl; |
| | | $key = $this->generateKey($key, false); |
| | | |
| | | $value = $this->hasRedis ? |
| | | wp_cache_get($key, $this->group) : |
| | | get_transient($this->transientKey($key)); |
| | | |
| | | return $value['value']??false; |
| | | } |
| | | public function getTimestamp(int|string|array $key): mixed |
| | | { |
| | | $key = $this->generateKey($key, false); |
| | | |
| | | $value = $this->hasRedis ? |
| | | wp_cache_get($key, $this->group) : |
| | | get_transient($this->transientKey($key)); |
| | | return $value['modified']??false; |
| | | } |
| | | |
| | | public function set(int|string|array $key, mixed $value, ?int $ttl = null): void |
| | | { |
| | | $key = $this->generateKey($key); |
| | | $ttl = $ttl??$this->ttl; |
| | | $store = [ |
| | | 'value' => $value, |
| | | 'modified' => time() |
| | | ]; |
| | | if ($this->hasRedis) { |
| | | wp_cache_set($id, $value, $this->group, $ttl); |
| | | wp_cache_set($key, $store, $this->group, $ttl); |
| | | } else { |
| | | set_transient("jvb_{$this->group}_{$id}", $value, $ttl); |
| | | set_transient($this->transientKey($key), $store, $ttl); |
| | | } |
| | | } |
| | | |
| | | public function forget(int|string|array $id): void |
| | | public function forget(int|string|array $key): void |
| | | { |
| | | if (is_array($id)) { |
| | | $id = $this->generateKey($id); |
| | | } |
| | | $key = $this->generateKey($key); |
| | | if ($this->hasRedis) { |
| | | wp_cache_delete($id, $this->group); |
| | | wp_cache_delete($key, $this->group); |
| | | } else { |
| | | delete_transient("jvb_{$this->group}_{$id}"); |
| | | delete_transient($this->transientKey($key)); |
| | | } |
| | | |
| | | } |
| | | |
| | | public function flush(): void |
| | |
| | | } |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Invalidation |
| | | * ------------------------------------------------------------------- */ |
| | | |
| | | public static function invalidateItem(string $group, int|string|array $id): void |
| | | public static function clearPost(int $ID):void |
| | | { |
| | | if (is_array($id)) { |
| | | $id = self::for($group)->generateKey($id); |
| | | $index = get_option(BASE."cache_post_index", []); |
| | | if (!array_key_exists($ID, $index)) { |
| | | return; |
| | | } |
| | | $group = sanitize_key($group); |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_delete($id, $group); |
| | | } else { |
| | | delete_transient("jvb_{$group}_{$id}"); |
| | | } |
| | | self::touch($group); |
| | | |
| | | foreach (self::connections()[$group] ?? [] as $conn) { |
| | | $target = $conn['target'] ?? $conn; // Backwards compat if still string |
| | | $flush = $conn['flush'] ?? false; |
| | | |
| | | if ($flush) { |
| | | // Flush entire target group |
| | | self::invalidateGroup($target); |
| | | } else { |
| | | // Just delete this item ID |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_delete($id, $target); |
| | | } else { |
| | | delete_transient("jvb_{$target}_{$id}"); |
| | | } |
| | | self::touch($target); |
| | | foreach ($index[$ID] as $group => $keys) { |
| | | $cache = self::for($group, 'post'); |
| | | foreach ($keys as $key) { |
| | | $cache->forget($key); |
| | | } |
| | | } |
| | | self::invalidateByTag($group, $id); |
| | | } |
| | | public static function clearUser(int $ID):void |
| | | { |
| | | $index = get_option(BASE."cache_user_index", []); |
| | | if (!array_key_exists($ID, $index)) { |
| | | return; |
| | | } |
| | | foreach ($index[$ID] as $group => $keys) { |
| | | $cache = self::for($group, 'user'); |
| | | foreach ($keys as $key) { |
| | | $cache->forget($key); |
| | | } |
| | | } |
| | | } |
| | | public static function clearTerm(int $ID):void |
| | | { |
| | | $index = get_option(BASE."cache_term_index", []); |
| | | if (!array_key_exists($ID, $index)) { |
| | | return; |
| | | } |
| | | foreach ($index[$ID] as $group => $keys) { |
| | | $cache = self::for($group, 'term'); |
| | | foreach ($keys as $key) { |
| | | $cache->forget($key); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | public static function invalidateGroup(string $group): void |
| | | { |
| | | $group = sanitize_key($group); |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_flush_group($group); |
| | | } else { |
| | | $instance = self::for($group); |
| | | $instance->clearGroupTransients(); |
| | | } |
| | | |
| | | self::touch($group); |
| | | |
| | | foreach (self::connections()[$group] ?? [] as $conn) { |
| | | $target = $conn['target'] ?? $conn; // Backwards compat |
| | | |
| | | // When flushing entire source group, always flush connected targets |
| | | // (regardless of flush flag - we don't know which items to delete) |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_flush_group($target); |
| | | } else { |
| | | $instance = self::for($target); |
| | | $instance->clearGroupTransients(); |
| | | } |
| | | self::touch($target); |
| | | } |
| | | } |
| | | |
| | | public static function touch(string $group): int |
| | | { |
| | | $group = sanitize_key($group); |
| | | $time = time(); |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_set($group, $time, self::TS_GROUP, WEEK_IN_SECONDS); |
| | | } else { |
| | | set_transient('jvb_ts_' . $group, $time, WEEK_IN_SECONDS); |
| | | } |
| | | |
| | | self::$timestamps[$group] = $time; |
| | | return $time; |
| | | } |
| | | |
| | | public static function lastModified(string|array $groups): int |
| | | { |
| | | if (is_array($groups)) { |
| | | return max(array_map([self::class, 'lastModified'], $groups)); |
| | | } |
| | | |
| | | $group = sanitize_key($groups); |
| | | |
| | | if (isset(self::$timestamps[$group])) { |
| | | return self::$timestamps[$group]; |
| | | } |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | $ts = (int) wp_cache_get($group, self::TS_GROUP); |
| | | } else { |
| | | $ts = (int) get_transient('jvb_ts_' . $group); |
| | | } |
| | | |
| | | if (!$ts) { |
| | | $ts = time(); |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_set($group, $ts, self::TS_GROUP, WEEK_IN_SECONDS); |
| | | } else { |
| | | set_transient('jvb_ts_' . $group, $ts, WEEK_IN_SECONDS); |
| | | } |
| | | } |
| | | |
| | | return self::$timestamps[$group] = $ts; |
| | | } |
| | | |
| | | public function getLastModifiedForTags(array $tags): ?int |
| | | { |
| | | if (!$this->hasRedis) { |
| | | return null; |
| | | } |
| | | $redis = self::redis(); |
| | | if (!$redis) { |
| | | return null; |
| | | } |
| | | |
| | | $lastModified = 0; |
| | | |
| | | foreach ($tags as $tag) { |
| | | $ts = $redis->get("jvb:tag:{$tag}:lastModified"); |
| | | if ($ts) { |
| | | $lastModified = max($lastModified, (int) $ts); |
| | | } |
| | | } |
| | | |
| | | return $lastModified ?: null; |
| | | } |
| | | |
| | | /**************************************************** |
| | | * CONNECTIONS |
| | | ****************************************************/ |
| | | private static function connections(): array |
| | | { |
| | | if (self::$connections === null) { |
| | | self::$connections = get_option(self::CONNECTIONS_OPTION, []); |
| | | } |
| | | return self::$connections; |
| | | self::for($group)->flush(); |
| | | } |
| | | |
| | | |
| | | public function connect(string $source, bool $flush = false): self |
| | | { |
| | | $source = sanitize_key($source); |
| | | $target = $this->group; |
| | | |
| | | $all = self::connections(); |
| | | $all[$source] ??= []; |
| | | |
| | | $before = count($all[$source]); |
| | | |
| | | // Add the connection |
| | | $all[$source][] = ['target' => $target, 'flush' => $flush]; |
| | | |
| | | // Remove duplicates by serializing for comparison |
| | | $all[$source] = array_values(array_unique($all[$source], SORT_REGULAR)); |
| | | |
| | | // Only update if something actually changed |
| | | if (count($all[$source]) !== $before) { |
| | | update_option(self::CONNECTIONS_OPTION, $all, false); |
| | | self::$connections = $all; |
| | | } |
| | | |
| | | return $this; |
| | | } |
| | | /**************************************************** |
| | | * REDIS |
| | | ****************************************************/ |
| | | private static function redis(): ?\Redis |
| | | { |
| | | global $wp_object_cache; |
| | | |
| | | return $wp_object_cache instanceof \WP_Object_Cache |
| | | && isset($wp_object_cache->redis) |
| | | ? $wp_object_cache->redis |
| | | : null; |
| | | } |
| | | |
| | | /** |
| | | * Remember with tags for complex invalidation scenarios |
| | | * |
| | | * Example: Cache user favorites tagged by each post ID |
| | | * When any post updates, this cache entry auto-invalidates |
| | | * |
| | | * @param int|string|array $key Cache key |
| | | * @param array $tags Array of [group, id] pairs: [['post', 123], ['user', 456]] |
| | | * @param callable $callback Function to generate value if cache miss |
| | | * @return mixed Cached or generated value |
| | | */ |
| | | public function rememberTagged( |
| | | int|string|array $key, |
| | | array $tags, |
| | | callable $callback, |
| | | ?int $ttl = null |
| | | ): mixed { |
| | | if (is_array($key)) { |
| | | $key = $this->generateKey($key); |
| | | } |
| | | $instanceTags = array_map(fn($tag) => [$this->group, $tag], $this->getTags()); |
| | | $tags = array_unique(array_merge( |
| | | $instanceTags, |
| | | array_map(fn($tag) => [sanitize_key($tag[0]), $tag[1]], $tags) |
| | | ), SORT_REGULAR); |
| | | |
| | | |
| | | $value = wp_cache_get($key, $this->group); |
| | | if ($value !== false) { |
| | | return $value; |
| | | } |
| | | |
| | | $value = $callback(); |
| | | if ($value === null || $value === false) { |
| | | return $value; |
| | | } |
| | | |
| | | wp_cache_set($key, $value, $this->group, $this->ttl); |
| | | |
| | | if ($redis = self::redis()) { |
| | | foreach ($tags as [$tagGroup, $tagId]) { |
| | | $redis->sAdd("tag:$tagGroup:$tagId", "{$this->group}:$key"); |
| | | } |
| | | } |
| | | |
| | | return $value; |
| | | } |
| | | |
| | | private static function invalidateByTag(string $group, int|string|array $id): void |
| | | { |
| | | if (is_array($id)) { |
| | | $id = self::for($group)->generateKey($id); |
| | | } |
| | | if (!$redis = self::redis()) { |
| | | return; |
| | | } |
| | | |
| | | $key = "tag:$group:$id"; |
| | | $targets = $redis->sMembers($key); |
| | | |
| | | foreach ($targets as $target) { |
| | | [$group, $id] = explode(':', $target, 2); |
| | | wp_cache_delete($id, $group); |
| | | } |
| | | |
| | | $redis->del($key); |
| | | } |
| | | |
| | | public function tag(string $tag): static |
| | | { |
| | | $this->tags[] = sanitize_key($tag); |
| | | return $this; |
| | | } |
| | | |
| | | public function getTags(): array |
| | | { |
| | | return array_unique($this->tags); |
| | | } |
| | | /**************************************************** |
| | | * TRANSIENT HELPER |
| | | ****************************************************/ |
| | |
| | | { |
| | | global $wpdb; |
| | | |
| | | $pattern = '_transient_jvb_' . $this->group . '_%'; |
| | | $timeout_pattern = '_transient_timeout_jvb_' . $this->group . '_%'; |
| | | $pattern = '_transient_' .BASE. $this->group . '%'; |
| | | $timeout_pattern = '_transient_timeout_' . BASE . $this->group . '%'; |
| | | |
| | | // Remove LIMIT to avoid table locks, add retry for deadlocks |
| | | $attempts = 0; |
| | |
| | | } |
| | | |
| | | // error_log('[Clearing cache for post change: '.$postId.']'); |
| | | self::invalidateItem('post', $postId); |
| | | self::clearPost($postId); |
| | | } |
| | | |
| | | public static function onPostDelete(int $postId): void |
| | | { |
| | | // error_log('[Clearing cache for post delete: '.$postId.']'); |
| | | self::invalidateItem('post', $postId); |
| | | self::clearPost($postId); |
| | | } |
| | | |
| | | // public static function onPostMetaChange(int $metaId, int $objectId): void |
| | |
| | | public static function onTermChange(int $termId, int $ttId, string $taxonomy): void |
| | | { |
| | | error_log('[Clearing cache for term change: '.$termId.']'); |
| | | self::invalidateItem('taxonomy', $termId); |
| | | self::clearTerm($termId); |
| | | } |
| | | |
| | | public static function onTermDelete(int $termId): void |
| | | { |
| | | // error_log('[Clearing cache for term delete: '.$termId.']'); |
| | | self::invalidateItem('taxonomy', $termId); |
| | | self::clearTerm($termId); |
| | | } |
| | | |
| | | public static function onTermMetaChange(int $metaId, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for term meta change: '.$objectId.']'); |
| | | self::invalidateItem('taxonomy', $objectId); |
| | | self::clearTerm($objectId); |
| | | } |
| | | |
| | | public static function onTermMetaDelete(array $metaIds, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for term meta delete: '.$objectId.']'); |
| | | self::invalidateItem('taxonomy', $objectId); |
| | | self::clearTerm($objectId); |
| | | } |
| | | |
| | | /**************************************************** |
| | |
| | | public static function onUserChange(int $userId): void |
| | | { |
| | | // error_log('[Clearing cache for user change: '.$userId.']'); |
| | | self::invalidateItem('user', $userId); |
| | | self::clearUser($userId); |
| | | } |
| | | |
| | | public static function onUserDelete(int $userId): void |
| | | { |
| | | // error_log('[Clearing cache for user delete: '.$userId.']'); |
| | | self::invalidateItem('user', $userId); |
| | | self::clearUser($userId); |
| | | } |
| | | |
| | | public static function onUserMetaChange(int $metaId, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for user meta change: '.$objectId.']'); |
| | | self::invalidateItem('user', $objectId); |
| | | self::clearUser($objectId); |
| | | } |
| | | |
| | | public static function onUserMetaDelete(array $metaIds, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for user meta delete: '.$objectId.']'); |
| | | self::invalidateItem('user', $objectId); |
| | | self::clearUser($objectId); |
| | | } |
| | | /*************************************************** |
| | | * UTILITY |
| | |
| | | * Generate a cache key from parameters |
| | | * Useful for caching based on multiple variables |
| | | * |
| | | * @param array $params Key-value pairs that uniquely identify this cache entry |
| | | * @param int|string|array $key Key-value pairs that uniquely identify this cache entry |
| | | * @return string MD5 hash of sorted parameters |
| | | */ |
| | | public function generateKey(array $params): string |
| | | public function generateKey(int|string|array $key, bool $trackIt = true): string |
| | | { |
| | | ksort($params); |
| | | return md5(serialize($params)); |
| | | $trackID = false; |
| | | $ID = false; |
| | | |
| | | if (!is_null($this->target) && is_int($key)) { |
| | | //Likely an item id. Store it for reference |
| | | $trackID = true; |
| | | $ID = $key; |
| | | } |
| | | if (is_array($key)) { |
| | | ksort($key); |
| | | if (!is_null($this->target) && array_key_exists('id', $key)) { |
| | | $trackID = true; |
| | | $ID = $key['id']; |
| | | } |
| | | $key = md5(serialize($key)); |
| | | } |
| | | |
| | | $key = $this->resolveKey($key); |
| | | if ($trackIt && !is_null($this->target) && $ID && $trackID) { |
| | | $this->trackID($ID, $key); |
| | | } |
| | | return $key; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public static function flushAll(): int |
| | | { |
| | | $all = self::connections(); |
| | | $groups = []; |
| | | |
| | | // Collect all unique groups from connections |
| | | foreach ($all as $source => $targets) { |
| | | $groups[$source] = true; |
| | | foreach ($targets as $conn) { |
| | | $target = $conn['target'] ?? $conn; |
| | | $groups[$target] = true; |
| | | } |
| | | } |
| | | |
| | | // Add any instantiated groups not in connections |
| | | foreach (array_keys(self::$instances) as $group) { |
| | | $groups[$group] = true; |
| | | } |
| | | |
| | | // Flush each group |
| | | $groups = get_option(BASE."cache_groups", []); |
| | | $count = 0; |
| | | foreach (array_keys($groups) as $group) { |
| | | self::invalidateGroup($group); |
| | | $count++; |
| | | foreach ($groups as $group => $conf) { |
| | | $cache = Cache::for($group, $conf['target']??null, $conf['ttl']); |
| | | $cache->flush(); |
| | | $count ++ ; |
| | | |
| | | usleep(50000); // Wait 50ms before retry |
| | | } |
| | | |
| | | // Also flush timestamp cache |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_flush_group(self::TS_GROUP); |
| | | } |
| | | |
| | | // Clear in-memory caches |
| | | self::$timestamps = []; |
| | | |
| | | return $count; |
| | | } |
| | | |
| | | /** |
| | | * Get all cache groups and their connections for admin display |
| | | * Get all cache groups |
| | | * |
| | | * @return array Format: ['group' => ['connects_to' => [...], 'connected_from' => [...]]] |
| | | * @return array |
| | | */ |
| | | public static function getAllGroups(): array |
| | | { |
| | | $connections = self::connections(); |
| | | $groups = []; |
| | | |
| | | // Build bidirectional view |
| | | foreach ($connections as $source => $targets) { |
| | | if (!isset($groups[$source])) { |
| | | $groups[$source] = ['connects_to' => [], 'connected_from' => []]; |
| | | } |
| | | |
| | | foreach ($targets as $conn) { |
| | | $target = $conn['target'] ?? $conn; |
| | | $flush = $conn['flush'] ?? false; |
| | | |
| | | // Source connects to target |
| | | $groups[$source]['connects_to'][] = [ |
| | | 'group' => $target, |
| | | 'flush' => $flush |
| | | ]; |
| | | |
| | | // Target is connected from source |
| | | if (!isset($groups[$target])) { |
| | | $groups[$target] = ['connects_to' => [], 'connected_from' => []]; |
| | | } |
| | | $groups[$target]['connected_from'][] = [ |
| | | 'group' => $source, |
| | | 'flush' => $flush |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | // Add any instantiated groups not in connections |
| | | foreach (array_keys(self::$instances) as $group) { |
| | | if (!isset($groups[$group])) { |
| | | $groups[$group] = ['connects_to' => [], 'connected_from' => []]; |
| | | } |
| | | } |
| | | |
| | | return $groups; |
| | | } |
| | | |
| | | public function hasRedis():bool |
| | | { |
| | | return $this->hasRedis; |
| | | return get_option(BASE."cache_groups", []); |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class CacheBack |
| | | { |
| | | private string $group; |
| | | private int $ttl; |
| | | private static array $timestamps = []; |
| | | private const TS_GROUP = 'jvb_http_ts'; |
| | | |
| | | private const CONNECTIONS_OPTION = 'jvb_cache_connections'; |
| | | private static ?array $connections = null; |
| | | protected array $tags = []; |
| | | |
| | | private static array $instances = []; |
| | | private bool $hasRedis = false; |
| | | private bool $varyByAuth = false; |
| | | private bool $varyByUser = false; |
| | | |
| | | private function __construct(string $group, int $ttl) |
| | | { |
| | | return; |
| | | $this->group = $group; |
| | | $this->ttl = $ttl; |
| | | $this->hasRedis = (bool) wp_using_ext_object_cache(); |
| | | } |
| | | |
| | | public static function registerHooks(): void |
| | | { |
| | | // Post updates (all post types including core) |
| | | add_action('save_post', [self::class, 'onPostChange'], 10, 2); |
| | | add_action('delete_post', [self::class, 'onPostDelete']); |
| | | |
| | | // Post meta updates, now handled via Meta.php? |
| | | // add_action('updated_post_meta', [self::class, 'onPostMetaChange'], 10, 2); |
| | | // add_action('added_post_meta', [self::class, 'onPostMetaChange'], 10, 2); |
| | | // add_action('deleted_post_meta', [self::class, 'onPostMetaDelete'], 10, 2); |
| | | |
| | | // Term updates (all taxonomies) |
| | | add_action('edited_term', [self::class, 'onTermChange'], 10, 3); |
| | | add_action('create_term', [self::class, 'onTermChange'], 10, 3); |
| | | add_action('delete_term', [self::class, 'onTermDelete'], 10, 3); |
| | | |
| | | // Term meta updates |
| | | add_action('updated_term_meta', [self::class, 'onTermMetaChange'], 10, 2); |
| | | add_action('added_term_meta', [self::class, 'onTermMetaChange'], 10, 2); |
| | | add_action('deleted_term_meta', [self::class, 'onTermMetaDelete'], 10, 2); |
| | | |
| | | // User updates |
| | | add_action('profile_update', [self::class, 'onUserChange'], 10, 2); |
| | | add_action('user_register', [self::class, 'onUserChange'], 10, 1); |
| | | add_action('deleted_user', [self::class, 'onUserDelete']); |
| | | |
| | | // User meta updates |
| | | add_action('updated_user_meta', [self::class, 'onUserMetaChange'], 10, 2); |
| | | add_action('added_user_meta', [self::class, 'onUserMetaChange'], 10, 2); |
| | | add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 2); |
| | | } |
| | | |
| | | public function auth(): static |
| | | { |
| | | $this->varyByAuth = true; |
| | | return $this; |
| | | } |
| | | |
| | | public function user(): static |
| | | { |
| | | $this->varyByUser = true; |
| | | return $this; |
| | | } |
| | | |
| | | private function resolveKey(int|string $key): string |
| | | { |
| | | if ($this->varyByUser) { |
| | | return $key . ':u' . get_current_user_id(); |
| | | } |
| | | if ($this->varyByAuth) { |
| | | return $key . ':' . (is_user_logged_in() ? 'auth' : 'guest'); |
| | | } |
| | | return (string) $key; |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Factory |
| | | * ------------------------------------------------------------------- */ |
| | | |
| | | public static function for(string $group, int $ttl = HOUR_IN_SECONDS): self |
| | | { |
| | | $group = sanitize_key($group); |
| | | |
| | | if (!isset(self::$instances[$group])) { |
| | | self::$instances[$group] = new self($group, $ttl); |
| | | } |
| | | |
| | | return self::$instances[$group]; |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Core operations |
| | | * ------------------------------------------------------------------- */ |
| | | |
| | | public function remember(int|string|array $key, callable $callback, ?int $ttl = null): mixed |
| | | { |
| | | //TODO: Redis is fucking this all up for some reason. While we figure it out, let's just return the callback |
| | | return $callback(); |
| | | |
| | | if (is_array($key)) { |
| | | $key = $this->generateKey($key); |
| | | } |
| | | if (!empty($this->tags)) { |
| | | return $this->rememberTagged( |
| | | $key, |
| | | $this->tags, |
| | | $callback, |
| | | $ttl |
| | | ); |
| | | } |
| | | |
| | | $value = $this->get($key); |
| | | |
| | | if ($value !== false) { |
| | | return $value; |
| | | } |
| | | |
| | | $value = $callback(); |
| | | |
| | | if ($value !== null && $value !== false) { |
| | | $this->set($key, $value); |
| | | } |
| | | |
| | | return $value; |
| | | } |
| | | |
| | | public function get(int|string|array $id): mixed |
| | | { |
| | | return false; |
| | | if (is_array($id)) { |
| | | $id = $this->generateKey($id); |
| | | } |
| | | if ($this->hasRedis) { |
| | | $value = wp_cache_get($id, $this->group); |
| | | } else { |
| | | $value = get_transient("jvb_{$this->group}_{$id}"); |
| | | } |
| | | |
| | | return $value; |
| | | } |
| | | |
| | | public function set(int|string|array $id, mixed $value, ?int $ttl = null): void |
| | | { |
| | | return; |
| | | if (is_array($id)) { |
| | | $id = $this->generateKey($id); |
| | | } |
| | | $ttl = $ttl ?? $this->ttl; |
| | | if ($this->hasRedis) { |
| | | wp_cache_set($id, $value, $this->group, $ttl); |
| | | } else { |
| | | set_transient("jvb_{$this->group}_{$id}", $value, $ttl); |
| | | } |
| | | } |
| | | |
| | | public function forget(int|string|array $id): void |
| | | { |
| | | if (is_array($id)) { |
| | | $id = $this->generateKey($id); |
| | | } |
| | | if ($this->hasRedis) { |
| | | wp_cache_delete($id, $this->group); |
| | | } else { |
| | | delete_transient("jvb_{$this->group}_{$id}"); |
| | | } |
| | | } |
| | | |
| | | public function flush(): void |
| | | { |
| | | if ($this->hasRedis) { |
| | | if (function_exists('wp_cache_flush_group')) { |
| | | wp_cache_flush_group($this->group); |
| | | } else { |
| | | wp_cache_flush(); |
| | | } |
| | | } else { |
| | | $this->clearGroupTransients(); |
| | | } |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Invalidation |
| | | * ------------------------------------------------------------------- */ |
| | | |
| | | public static function invalidateItem(string $group, int|string|array $id): void |
| | | { |
| | | if (is_array($id)) { |
| | | $id = self::for($group)->generateKey($id); |
| | | } |
| | | $group = sanitize_key($group); |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_delete($id, $group); |
| | | } else { |
| | | delete_transient("jvb_{$group}_{$id}"); |
| | | } |
| | | self::touch($group); |
| | | |
| | | foreach (self::connections()[$group] ?? [] as $conn) { |
| | | $target = $conn['target'] ?? $conn; // Backwards compat if still string |
| | | $flush = $conn['flush'] ?? false; |
| | | |
| | | if ($flush) { |
| | | // Flush entire target group |
| | | self::invalidateGroup($target); |
| | | } else { |
| | | // Just delete this item ID |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_delete($id, $target); |
| | | } else { |
| | | delete_transient("jvb_{$target}_{$id}"); |
| | | } |
| | | self::touch($target); |
| | | } |
| | | } |
| | | self::invalidateByTag($group, $id); |
| | | } |
| | | |
| | | public static function invalidateGroup(string $group): void |
| | | { |
| | | $group = sanitize_key($group); |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_flush_group($group); |
| | | } else { |
| | | $instance = self::for($group); |
| | | $instance->clearGroupTransients(); |
| | | } |
| | | |
| | | self::touch($group); |
| | | |
| | | foreach (self::connections()[$group] ?? [] as $conn) { |
| | | $target = $conn['target'] ?? $conn; // Backwards compat |
| | | |
| | | // When flushing entire source group, always flush connected targets |
| | | // (regardless of flush flag - we don't know which items to delete) |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_flush_group($target); |
| | | } else { |
| | | $instance = self::for($target); |
| | | $instance->clearGroupTransients(); |
| | | } |
| | | self::touch($target); |
| | | } |
| | | } |
| | | |
| | | public static function touch(string $group): int |
| | | { |
| | | $group = sanitize_key($group); |
| | | $time = time(); |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_set($group, $time, self::TS_GROUP, WEEK_IN_SECONDS); |
| | | } else { |
| | | set_transient('jvb_ts_' . $group, $time, WEEK_IN_SECONDS); |
| | | } |
| | | |
| | | self::$timestamps[$group] = $time; |
| | | return $time; |
| | | } |
| | | |
| | | public static function lastModified(string|array $groups): int |
| | | { |
| | | if (is_array($groups)) { |
| | | return max(array_map([self::class, 'lastModified'], $groups)); |
| | | } |
| | | |
| | | $group = sanitize_key($groups); |
| | | |
| | | if (isset(self::$timestamps[$group])) { |
| | | return self::$timestamps[$group]; |
| | | } |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | $ts = (int) wp_cache_get($group, self::TS_GROUP); |
| | | } else { |
| | | $ts = (int) get_transient('jvb_ts_' . $group); |
| | | } |
| | | |
| | | if (!$ts) { |
| | | $ts = time(); |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_set($group, $ts, self::TS_GROUP, WEEK_IN_SECONDS); |
| | | } else { |
| | | set_transient('jvb_ts_' . $group, $ts, WEEK_IN_SECONDS); |
| | | } |
| | | } |
| | | |
| | | return self::$timestamps[$group] = $ts; |
| | | } |
| | | |
| | | public function getLastModifiedForTags(array $tags): ?int |
| | | { |
| | | if (!$this->hasRedis) { |
| | | return null; |
| | | } |
| | | $redis = self::redis(); |
| | | if (!$redis) { |
| | | return null; |
| | | } |
| | | |
| | | $lastModified = 0; |
| | | |
| | | foreach ($tags as $tag) { |
| | | $ts = $redis->get("jvb:tag:{$tag}:lastModified"); |
| | | if ($ts) { |
| | | $lastModified = max($lastModified, (int) $ts); |
| | | } |
| | | } |
| | | |
| | | return $lastModified ?: null; |
| | | } |
| | | |
| | | /**************************************************** |
| | | * CONNECTIONS |
| | | ****************************************************/ |
| | | private static function connections(): array |
| | | { |
| | | if (self::$connections === null) { |
| | | self::$connections = get_option(self::CONNECTIONS_OPTION, []); |
| | | } |
| | | return self::$connections; |
| | | } |
| | | |
| | | |
| | | public function connect(string $source, bool $flush = false): self |
| | | { |
| | | $source = sanitize_key($source); |
| | | $target = $this->group; |
| | | |
| | | $all = self::connections(); |
| | | $all[$source] ??= []; |
| | | |
| | | $before = count($all[$source]); |
| | | |
| | | // Add the connection |
| | | $all[$source][] = ['target' => $target, 'flush' => $flush]; |
| | | |
| | | // Remove duplicates by serializing for comparison |
| | | $all[$source] = array_values(array_unique($all[$source], SORT_REGULAR)); |
| | | |
| | | // Only update if something actually changed |
| | | if (count($all[$source]) !== $before) { |
| | | update_option(self::CONNECTIONS_OPTION, $all, false); |
| | | self::$connections = $all; |
| | | } |
| | | |
| | | return $this; |
| | | } |
| | | /**************************************************** |
| | | * REDIS |
| | | ****************************************************/ |
| | | private static function redis(): ?\Redis |
| | | { |
| | | global $wp_object_cache; |
| | | |
| | | return $wp_object_cache instanceof \WP_Object_Cache |
| | | && isset($wp_object_cache->redis) |
| | | ? $wp_object_cache->redis |
| | | : null; |
| | | } |
| | | |
| | | /** |
| | | * Remember with tags for complex invalidation scenarios |
| | | * |
| | | * Example: Cache user favorites tagged by each post ID |
| | | * When any post updates, this cache entry auto-invalidates |
| | | * |
| | | * @param int|string|array $key Cache key |
| | | * @param array $tags Array of [group, id] pairs: [['post', 123], ['user', 456]] |
| | | * @param callable $callback Function to generate value if cache miss |
| | | * @return mixed Cached or generated value |
| | | */ |
| | | public function rememberTagged( |
| | | int|string|array $key, |
| | | array $tags, |
| | | callable $callback, |
| | | ?int $ttl = null |
| | | ): mixed { |
| | | if (is_array($key)) { |
| | | $key = $this->generateKey($key); |
| | | } |
| | | $instanceTags = array_map(fn($tag) => [$this->group, $tag], $this->getTags()); |
| | | $tags = array_unique(array_merge( |
| | | $instanceTags, |
| | | array_map(fn($tag) => [sanitize_key($tag[0]), $tag[1]], $tags) |
| | | ), SORT_REGULAR); |
| | | |
| | | |
| | | $value = wp_cache_get($key, $this->group); |
| | | if ($value !== false) { |
| | | return $value; |
| | | } |
| | | |
| | | $value = $callback(); |
| | | if ($value === null || $value === false) { |
| | | return $value; |
| | | } |
| | | |
| | | wp_cache_set($key, $value, $this->group, $this->ttl); |
| | | |
| | | if ($redis = self::redis()) { |
| | | foreach ($tags as [$tagGroup, $tagId]) { |
| | | $redis->sAdd("tag:$tagGroup:$tagId", "{$this->group}:$key"); |
| | | } |
| | | } |
| | | |
| | | return $value; |
| | | } |
| | | |
| | | private static function invalidateByTag(string $group, int|string|array $id): void |
| | | { |
| | | if (is_array($id)) { |
| | | $id = self::for($group)->generateKey($id); |
| | | } |
| | | if (!$redis = self::redis()) { |
| | | return; |
| | | } |
| | | |
| | | $key = "tag:$group:$id"; |
| | | $targets = $redis->sMembers($key); |
| | | |
| | | foreach ($targets as $target) { |
| | | [$group, $id] = explode(':', $target, 2); |
| | | wp_cache_delete($id, $group); |
| | | } |
| | | |
| | | $redis->del($key); |
| | | } |
| | | |
| | | public function tag(string $tag): static |
| | | { |
| | | $this->tags[] = sanitize_key($tag); |
| | | return $this; |
| | | } |
| | | |
| | | public function getTags(): array |
| | | { |
| | | return array_unique($this->tags); |
| | | } |
| | | /**************************************************** |
| | | * TRANSIENT HELPER |
| | | ****************************************************/ |
| | | private function clearGroupTransients(): void |
| | | { |
| | | global $wpdb; |
| | | |
| | | $pattern = '_transient_jvb_' . $this->group . '_%'; |
| | | $timeout_pattern = '_transient_timeout_jvb_' . $this->group . '_%'; |
| | | |
| | | // Remove LIMIT to avoid table locks, add retry for deadlocks |
| | | $attempts = 0; |
| | | $max_attempts = 3; |
| | | |
| | | while ($attempts < $max_attempts) { |
| | | $result = $wpdb->query( |
| | | $wpdb->prepare( |
| | | "DELETE FROM $wpdb->options |
| | | WHERE option_name LIKE %s OR option_name LIKE %s", |
| | | $pattern, |
| | | $timeout_pattern |
| | | ) |
| | | ); |
| | | |
| | | // Success or non-deadlock error |
| | | if ($result !== false || !str_contains($wpdb->last_error, 'Deadlock')) { |
| | | break; |
| | | } |
| | | |
| | | $attempts++; |
| | | if ($attempts < $max_attempts) { |
| | | usleep(50000); // Wait 50ms before retry |
| | | } |
| | | } |
| | | } |
| | | /**************************************************** |
| | | * HOOKS |
| | | ****************************************************/ |
| | | /**************************************************** |
| | | * HOOKS - Posts |
| | | ****************************************************/ |
| | | public static function onPostChange(int $postId, \WP_Post $post): void |
| | | { |
| | | if (wp_is_post_revision($postId) || wp_is_post_autosave($postId)) { |
| | | return; |
| | | } |
| | | |
| | | // error_log('[Clearing cache for post change: '.$postId.']'); |
| | | self::invalidateItem('post', $postId); |
| | | } |
| | | |
| | | public static function onPostDelete(int $postId): void |
| | | { |
| | | // error_log('[Clearing cache for post delete: '.$postId.']'); |
| | | self::invalidateItem('post', $postId); |
| | | } |
| | | |
| | | // public static function onPostMetaChange(int $metaId, int $objectId): void |
| | | // { |
| | | //// error_log('[Clearing cache for post meta change: '.$objectId.']'); |
| | | // self::invalidateItem('post', $objectId); |
| | | // } |
| | | // |
| | | // public static function onPostMetaDelete(array $metaIds, int $objectId): void |
| | | // { |
| | | //// error_log('[Clearing cache for post meta delete: '.$objectId.']'); |
| | | // self::invalidateItem('post', $objectId); |
| | | // } |
| | | |
| | | /**************************************************** |
| | | * HOOKS - Terms |
| | | ****************************************************/ |
| | | public static function onTermChange(int $termId, int $ttId, string $taxonomy): void |
| | | { |
| | | error_log('[Clearing cache for term change: '.$termId.']'); |
| | | self::invalidateItem('taxonomy', $termId); |
| | | } |
| | | |
| | | public static function onTermDelete(int $termId): void |
| | | { |
| | | // error_log('[Clearing cache for term delete: '.$termId.']'); |
| | | self::invalidateItem('taxonomy', $termId); |
| | | } |
| | | |
| | | public static function onTermMetaChange(int $metaId, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for term meta change: '.$objectId.']'); |
| | | self::invalidateItem('taxonomy', $objectId); |
| | | } |
| | | |
| | | public static function onTermMetaDelete(array $metaIds, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for term meta delete: '.$objectId.']'); |
| | | self::invalidateItem('taxonomy', $objectId); |
| | | } |
| | | |
| | | /**************************************************** |
| | | * HOOKS - Users |
| | | ****************************************************/ |
| | | public static function onUserChange(int $userId): void |
| | | { |
| | | // error_log('[Clearing cache for user change: '.$userId.']'); |
| | | self::invalidateItem('user', $userId); |
| | | } |
| | | |
| | | public static function onUserDelete(int $userId): void |
| | | { |
| | | // error_log('[Clearing cache for user delete: '.$userId.']'); |
| | | self::invalidateItem('user', $userId); |
| | | } |
| | | |
| | | public static function onUserMetaChange(int $metaId, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for user meta change: '.$objectId.']'); |
| | | self::invalidateItem('user', $objectId); |
| | | } |
| | | |
| | | public static function onUserMetaDelete(array $metaIds, int $objectId): void |
| | | { |
| | | // error_log('[Clearing cache for user meta delete: '.$objectId.']'); |
| | | self::invalidateItem('user', $objectId); |
| | | } |
| | | /*************************************************** |
| | | * UTILITY |
| | | **************************************************/ |
| | | /** |
| | | * Generate a cache key from parameters |
| | | * Useful for caching based on multiple variables |
| | | * |
| | | * @param array $params Key-value pairs that uniquely identify this cache entry |
| | | * @return string MD5 hash of sorted parameters |
| | | */ |
| | | public function generateKey(array $params): string |
| | | { |
| | | ksort($params); |
| | | return md5(serialize($params)); |
| | | } |
| | | |
| | | /** |
| | | * Nuclear option: Flush ALL registered cache groups |
| | | * Use for debugging or after major updates |
| | | * |
| | | * @return int Number of groups flushed |
| | | */ |
| | | public static function flushAll(): int |
| | | { |
| | | $all = self::connections(); |
| | | $groups = []; |
| | | |
| | | // Collect all unique groups from connections |
| | | foreach ($all as $source => $targets) { |
| | | $groups[$source] = true; |
| | | foreach ($targets as $conn) { |
| | | $target = $conn['target'] ?? $conn; |
| | | $groups[$target] = true; |
| | | } |
| | | } |
| | | |
| | | // Add any instantiated groups not in connections |
| | | foreach (array_keys(self::$instances) as $group) { |
| | | $groups[$group] = true; |
| | | } |
| | | |
| | | // Flush each group |
| | | $count = 0; |
| | | foreach (array_keys($groups) as $group) { |
| | | self::invalidateGroup($group); |
| | | $count++; |
| | | } |
| | | |
| | | // Also flush timestamp cache |
| | | if (wp_using_ext_object_cache()) { |
| | | wp_cache_flush_group(self::TS_GROUP); |
| | | } |
| | | |
| | | // Clear in-memory caches |
| | | self::$timestamps = []; |
| | | |
| | | return $count; |
| | | } |
| | | |
| | | /** |
| | | * Get all cache groups and their connections for admin display |
| | | * |
| | | * @return array Format: ['group' => ['connects_to' => [...], 'connected_from' => [...]]] |
| | | */ |
| | | public static function getAllGroups(): array |
| | | { |
| | | $connections = self::connections(); |
| | | $groups = []; |
| | | |
| | | // Build bidirectional view |
| | | foreach ($connections as $source => $targets) { |
| | | if (!isset($groups[$source])) { |
| | | $groups[$source] = ['connects_to' => [], 'connected_from' => []]; |
| | | } |
| | | |
| | | foreach ($targets as $conn) { |
| | | $target = $conn['target'] ?? $conn; |
| | | $flush = $conn['flush'] ?? false; |
| | | |
| | | // Source connects to target |
| | | $groups[$source]['connects_to'][] = [ |
| | | 'group' => $target, |
| | | 'flush' => $flush |
| | | ]; |
| | | |
| | | // Target is connected from source |
| | | if (!isset($groups[$target])) { |
| | | $groups[$target] = ['connects_to' => [], 'connected_from' => []]; |
| | | } |
| | | $groups[$target]['connected_from'][] = [ |
| | | 'group' => $source, |
| | | 'flush' => $flush |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | // Add any instantiated groups not in connections |
| | | foreach (array_keys(self::$instances) as $group) { |
| | | if (!isset($groups[$group])) { |
| | | $groups[$group] = ['connects_to' => [], 'connected_from' => []]; |
| | | } |
| | | } |
| | | |
| | | return $groups; |
| | | } |
| | | |
| | | public function hasRedis():bool |
| | | { |
| | | return $this->hasRedis; |
| | | } |
| | | } |
| | |
| | | protected static string $termIDType; |
| | | protected static string $postIDType; |
| | | protected static string $postTable; |
| | | protected ?CustomTable $karmaTable = null; |
| | | protected ?CustomTable $favouritesTable = null; |
| | | |
| | | protected Cache $cache; |
| | | |
| | |
| | | return self::$instances[$tableName]; |
| | | } |
| | | |
| | | public function setKarma(CustomTable $karma):void |
| | | { |
| | | $this->karmaTable = $karma; |
| | | } |
| | | public function setFavourite(CustomTable $favourites):void |
| | | { |
| | | $this->favouritesTable = $favourites; |
| | | } |
| | | |
| | | public function ensureDefined() { |
| | | $this->ensureUserTable(); |
| | | $this->ensureUserIDType(); |
| | |
| | | return $this; |
| | | } |
| | | |
| | | public function getColumnKeys():array |
| | | { |
| | | return array_keys($this->columns); |
| | | } |
| | | |
| | | /** |
| | | * @param array $keys An array of {string} $keys. If a $key is an array, you can define a custom $key => $value, example: 'UNIQUE' => $value, or 'PRIMARY' => $value |
| | | * @return $this |
| | |
| | | return $this; |
| | | } |
| | | |
| | | public function orderByKarma(string $metric = 'karmic_score', string $direction = 'DESC'): self |
| | | { |
| | | if (!$this->karmaTable || !in_array($metric, ['upvotes', 'downvotes', 'karmic_score'])) { |
| | | return $this->orderBy('created_at'); |
| | | } |
| | | $this->builder['orderby'] = $metric; |
| | | $this->builder['order'] = strtoupper($direction); |
| | | return $this; |
| | | } |
| | | |
| | | public function orderByFavourites(string $direction = 'DESC'): self |
| | | { |
| | | if (!$this->favouritesTable) { |
| | | return $this->orderBy('created_at'); |
| | | } |
| | | $this->builder['orderby'] = 'favourites_count'; |
| | | $this->builder['order'] = strtoupper($direction); |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Set pagination |
| | | * |
| | |
| | | public function get(array $where, string $output = OBJECT): object|array|null |
| | | { |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey(array_merge(['output' => $output], $where)), |
| | | array_merge(['output' => $output], $where), |
| | | function() use ($where, $output) { |
| | | $query = "SELECT * FROM {$this->fullTableName}"; |
| | | $where = $this->buildWhereClause($where); |
| | |
| | | public function getMany(array $args = [], bool $itemsOnly = true, string $output = OBJECT): array |
| | | { |
| | | $items = $this->cache->remember( |
| | | $this->cache->generateKey(array_merge($args, ['output' => $output])), |
| | | array_merge($args, ['output' => $output]), |
| | | function () use ($args, $output) { |
| | | $query = "SELECT * FROM {$this->fullTableName}"; |
| | | $values = []; |
| | |
| | | |
| | | // ORDER BY |
| | | if (!empty($args['orderby'])) { |
| | | $orderby = sanitize_sql_orderby($args['orderby']); |
| | | $orderby = $this->resolveOrderby($args['orderby']); |
| | | $order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC'; |
| | | $query .= " ORDER BY {$orderby} {$order}"; |
| | | } |
| | |
| | | ]; |
| | | } |
| | | |
| | | protected function resolveOrderby(string $orderby):string |
| | | { |
| | | $orderby = strtolower($orderby); |
| | | if (in_array($orderby, ['upvotes', 'downvotes', 'karmic_score']) && $this->karmaTable) { |
| | | $tableName = $this->karmaTable->getFullTableName(); |
| | | return match ($orderby) { |
| | | 'upvotes' => "(SELECT COUNT(*) FROM {$tableName} k WHERE k.item_id = {$this->fullTableName}.id AND k.vote = 'up')", |
| | | 'downvotes' => "(SELECT COUNT(*) FROM {$tableName} k WHERE k.item_id = {$this->fullTableName}.id AND k.vote = 'down')", |
| | | 'karmic_score' => "(SELECT COALESCE(SUM(CASE WHEN k.vote = 'up' THEN 1 WHEN k.vote = 'down' THEN -1 ELSE 0 END), 0) FROM {$tableName} k WHERE k.item_id = {$this->fullTableName}.id)", |
| | | }; |
| | | } |
| | | |
| | | if ($orderby === 'favourites_count' && $this->favouritesTable) { |
| | | $tableName = $this->favouritesTable->getFullTableName(); |
| | | return "(SELECT COUNT(*) FROM {$tableName} f WHERE f.target_id = {$this->fullTableName}.id)"; |
| | | } |
| | | |
| | | return sanitize_sql_orderby($orderby); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Get a specific column value from all matches |
| | |
| | | $key = array_merge($where, ['column' => $column, 'orderby' => $orderby, 'order' => $order, 'limit' => $limit]); |
| | | |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey($key), |
| | | $key, |
| | | function() use ($column, $where, $orderby, $order, $limit) { |
| | | if (!empty($this->columns) && !array_key_exists($column, $this->columns)) { |
| | | $this->logError('pluck', ['reason' => "Column '{$column}' not in definition"]); |
| | |
| | | } |
| | | $args = ['where' => $where]; |
| | | if ($orderby) { |
| | | $args['orderby'] = $orderby; |
| | | $args['orderby'] = $this->resolveOrderby($orderby); |
| | | } |
| | | if ($order) { |
| | | $args['order'] = $order; |
| | |
| | | public function count(array $where = []): int |
| | | { |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey(array_merge(['source' => 'count'], $where)), |
| | | array_merge(['source' => 'count'], $where), |
| | | function() use ($where) { |
| | | $query = "SELECT COUNT(*) FROM {$this->fullTableName}"; |
| | | $values = []; |
| | |
| | | foreach ($where as $column => $value) { |
| | | $column_safe = esc_sql($column); |
| | | |
| | | // OR grouping: ['OR' => [ [...cond1], [...cond2] ]] |
| | | if ($column === 'OR' && is_array($value)) { |
| | | $orConditions = []; |
| | | foreach ($value as $orWhere) { |
| | | $orClause = $this->buildWhereClause($orWhere); |
| | | $orConditions[] = $orClause['sql']; |
| | | $values = array_merge($values, $orClause['values']); |
| | | } |
| | | $conditions[] = '(' . implode(' OR ', $orConditions) . ')'; |
| | | continue; |
| | | } |
| | | |
| | | if ($value === null) { |
| | | $conditions[] = "`{$column_safe}` IS NULL"; |
| | | } elseif (is_array($value) && count($value) === 2 && is_string($value[0])) { |
| | | [$operator, $operand] = $value; |
| | | $operator = strtoupper(trim($operator)); |
| | | |
| | | if (in_array($operator, ['IN', 'NOT IN']) && is_array($operand)) { |
| | | if ($operator === 'IS NOT NULL') { |
| | | $conditions[] = "`{$column_safe}` IS NOT NULL"; |
| | | } elseif (in_array($operator, ['IN', 'NOT IN']) && is_array($operand)) { |
| | | $placeholders = implode(',', array_map([$this, 'getPlaceholder'], $operand)); |
| | | $conditions[] = "`{$column_safe}` {$operator} ({$placeholders})"; |
| | | $values = array_merge($values, $operand); |
| | | } elseif (in_array($operator, ['BETWEEN', 'NOT BETWEEN']) && is_array($operand) && count($operand) === 2) { |
| | | $conditions[] = "`{$column_safe}` {$operator} " . $this->getPlaceholder($operand[0]) . " AND " . $this->getPlaceholder($operand[1]); |
| | | $values[] = $operand[0]; |
| | | $values[] = $operand[1]; |
| | | } else { |
| | | // <, >, <=, >=, !=, LIKE, etc. |
| | | $conditions[] = "`{$column_safe}` {$operator} " . $this->getPlaceholder($operand); |
| | |
| | | error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . $this->wpdb->last_error); |
| | | } |
| | | } |
| | | |
| | | public function sum(string $column, array $where):float |
| | | { |
| | | $rows = $this->pluck($column, $where); |
| | | return array_sum($rows); |
| | | } |
| | | } |
| File was renamed from inc/managers/Dashboard/DashboardManager.php |
| | |
| | | <?php |
| | | namespace JVBase\managers\Dashboard; |
| | | |
| | | use JetBrains\PhpStorm\NoReturn;use JVBase\base\Site;use JVBase\forms\TaxonomySelector;use JVBase\managers\Cache;use JVBase\managers\IconsManager;use JVBase\managers\LoginManager;use JVBase\managers\RoleManager;use JVBase\meta\Form;use JVBase\meta\Meta;use JVBase\registrar\Fields;use JVBase\registrar\Registrar;use JVBase\ui\Navigation;use JVBase\ui\Tabs; |
| | | use JetBrains\PhpStorm\NoReturn;use JVBase\base\Site;use JVBase\forms\TaxonomySelector;use JVBase\Instance;use JVBase\integrations\ServiceMap;use JVBase\managers\Cache;use JVBase\managers\IconsManager;use JVBase\managers\Login;use JVBase\managers\Roles;use JVBase\meta\Form;use JVBase\meta\Meta;use JVBase\registrar\Fields;use JVBase\registrar\Registrar;use JVBase\ui\Navigation;use JVBase\ui\Tabs; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | class DashboardManager { |
| | | class Dashboard { |
| | | use Instance, ServiceMap; |
| | | public static string $based; |
| | | protected string $slug = 'dash'; |
| | | protected string $baseURl; |
| | |
| | | protected array $pages = []; |
| | | protected array $sections = []; |
| | | protected Cache $cache; |
| | | public function __construct() { |
| | | |
| | | private function __construct() { |
| | | self::$based = BASE.$this->slug; |
| | | $this->cache = Cache::for('dashboard')->connect('post'); |
| | | $this->cache = Cache::for('dashboard', 'post'); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | |
| | | return; |
| | | } |
| | | //Redirect to custom dashboard |
| | | if (is_user_logged_in() && isOurPeople()) { |
| | | if (is_admin() && isOurPeople()) { |
| | | $this->redirectToDashboard(); |
| | | } |
| | | } |
| | |
| | | foreach ($content as $c) { |
| | | $registrar = Registrar::getInstance($c); |
| | | $page = $this->addPage($registrar->getPlural(), $c, $registrar->getIcon()); |
| | | $page->setPermission(RoleManager::getPermissionName('edit', $c)); |
| | | $page->setPermission(Roles::getPermissionName('edit', $c)); |
| | | $page->setSection('content'); |
| | | $page->setScripts(['jvb-crud', 'jvb-creator']); |
| | | $this->sections[$c] = new Section($registrar->getPlural(), $c, $registrar->getIcon(), 'content'); |
| | | $this->sections[$c]->setPermission(RoleManager::getPermissionName('edit', $c)); |
| | | $this->sections[$c]->setPermission(Roles::getPermissionName('edit', $c)); |
| | | $this->sections[$c]->setIsLink(true); |
| | | |
| | | $taxonomies = $registrar->registrar->taxonomies; |
| | |
| | | $taxReg = Registrar::getInstance($tax); |
| | | if ($taxReg) { |
| | | $taxPage = $this->addPage($taxReg->getPlural(), $tax, $registrar->getIcon(), $page->getID()); |
| | | $taxPage->setPermission(RoleManager::getPermissionName('edit', $c)); |
| | | $taxPage->setPermission(Roles::getPermissionName('edit', $c)); |
| | | $taxPage->setSection($c); |
| | | |
| | | $taxPage->setScripts(['jvb-crud', 'jvb-creator']); |
| | |
| | | foreach ($contentTax as $c) { |
| | | $registrar = Registrar::getInstance($c); |
| | | $page = $this->addPage($registrar->getPlural(), $c, $registrar->getIcon()); |
| | | $page->setPermission(RoleManager::getPermissionName('edit', $c)); |
| | | $page->setPermission(Roles::getPermissionName('edit', $c)); |
| | | $page->setSection($c); |
| | | //TODO: add artist management and invitations, if applicable |
| | | } |
| | |
| | | $this->sections['integrations']->setIsLink(true); |
| | | |
| | | foreach (array_keys(Site::getIntegrations()) as $integration) { |
| | | $integration = match($integration) { |
| | | 'maps' => 'JVBase\integrations\services\GoogleMaps', |
| | | 'square' => 'JVBase\integrations\services\Square', |
| | | 'facebook' => 'JVBase\integrations\services\Facebook', |
| | | 'helcim' => 'JVBase\integrations\services\Helcim', |
| | | 'instagram' => 'JVBase\integrations\services\Instagram', |
| | | 'bluesky' => 'JVBase\integrations\services\BlueSky', |
| | | 'gmb' => 'JVBase\integrations\services\GoogleMyBusiness', |
| | | 'cloudflare' => 'JVBase\integrations\services\Cloudflare', |
| | | 'umami' => 'JVBase\integrations\services\Umami', |
| | | 'postmark' => 'JVBase\integrations\services\PostMark', |
| | | }; |
| | | $integration = $this->getService($integration); |
| | | $title = $integration::title(); |
| | | $icon = $integration::icon(); |
| | | if ($integration::hasExtraOptions()) { |
| | |
| | | echo '<p>Alternatively, you can always login using the magic link, which sets a temporary password for 15 minutes.</p>'; |
| | | } |
| | | ?> |
| | | <?= LoginManager::getInstance()->renderLoginForm('resetpass'); ?> |
| | | <?= Login::getInstance()->renderLoginForm('resetpass'); ?> |
| | | |
| | | <?php |
| | | } |
| | |
| | | $this->slug.'_ID', |
| | | function() use ($parentID) { |
| | | $existing = new WP_Query([ |
| | | 'post_type' => DashboardManager::$based, |
| | | 'post_type' => Dashboard::$based, |
| | | 'name' => $this->slug, |
| | | 'fields' => 'ids', |
| | | 'posts_per_page'=> 1, |
| | |
| | | $args = [ |
| | | 'post_title' => $this->title, |
| | | 'post_name' => $this->slug, |
| | | 'post_type' => DashboardManager::$based, |
| | | 'post_type' => Dashboard::$based, |
| | | 'post_status' => 'publish' |
| | | ]; |
| | | if ($parentID > 0) { |
| | |
| | | { |
| | | |
| | | if ($this->slug === 'dash') { |
| | | $this->URL = get_post_type_archive_link(DashboardManager::$based); |
| | | $this->URL = get_post_type_archive_link(Dashboard::$based); |
| | | return; |
| | | } |
| | | $this->URL = get_the_permalink($this->ID); |
| | |
| | | exit; |
| | | } |
| | | |
| | | require_once JVB_DIR . '/inc/managers/Dashboard/DashboardManager.php'; |
| | | require_once JVB_DIR . '/inc/managers/Dashboard/Dashboard.php'; |
| | | require_once JVB_DIR . '/inc/managers/Dashboard/DashboardPage.php'; |
| | | require_once JVB_DIR . '/inc/managers/Dashboard/Section.php'; |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | |
| | | use JetBrains\PhpStorm\NoReturn;use JVBase\forms\TaxonomySelector; |
| | | use JVBase\base\Site; |
| | | use JVBase\meta\Form; |
| | | use JVBase\integrations\ServiceMap;use JVBase\meta\Form; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\ui\Navigation; |
| | | use WP_Error;use WP_Query;use WP_User; |
| | |
| | | */ |
| | | class DashboardManager |
| | | { |
| | | use ServiceMap; |
| | | |
| | | protected WP_User $user; |
| | | protected Cache $cache; |
| | | protected string $role; |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('dashboard', WEEK_IN_SECONDS)->connect('user')->user(); |
| | | $this->cache = Cache::for('dashboard', WEEK_IN_SECONDS)->user(); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | |
| | | } |
| | | |
| | | public function excludeDashboard(array $IDs):array { |
| | | $this->cache->flush(); |
| | | $exclude = $this->cache->remember( |
| | | 'dashboardIDs', |
| | | function() { |
| | |
| | | */ |
| | | protected function buildIntegrationPages(int $parentID):void |
| | | { |
| | | $integrations = JVB()->getAvailableServices(false); |
| | | |
| | | foreach ($integrations as $name => $integration) { |
| | | $integrations = $this->getAvailableServices(); |
| | | foreach ($integrations as $service) { |
| | | $integration = $this->getService($service); |
| | | $title = $integration->getTitle(); |
| | | |
| | | $slug = sanitize_title($title); |
| | | $this->createDashboardPage($slug, $title, $parentID); |
| | | } |
| | |
| | | } |
| | | $integrationSlugs = array_map(function($name) { |
| | | return sanitize_title(str_replace('_', '-', $name)); |
| | | }, array_keys(JVB()->getAvailableServices(false))); |
| | | }, array_keys($this->getAvailableServices())); |
| | | |
| | | // Check if this is an integration subpage |
| | | if (in_array($page, $integrationSlugs)) { |
| | |
| | | Registrar::withFeature('is_content', 'term') |
| | | ); |
| | | $availableContent = array_filter($pages, function($page, $key) use($all) { |
| | | return !is_numeric($key) && in_array($key, $all) && JVB()->roles()->checkRole($this->user, $key); |
| | | return !is_numeric($key) && in_array($key, $all) && Roles::getInstance()()->checkRole($this->user, $key); |
| | | }, ARRAY_FILTER_USE_BOTH); |
| | | if (!empty ($availableContent)){ |
| | | $content = $menu->addItem('Your Content', jvbDashIcon('book-bookmark')) |
| | |
| | | } |
| | | |
| | | if (Site::has('referrals')) { |
| | | $whatever = JVB()->referrals()->getReferralWelcomeMessage($this->user->ID); |
| | | $whatever = Referrals::getInstance()->getReferralWelcomeMessage($this->user->ID); |
| | | if (!empty($whatever)) { |
| | | return $whatever; |
| | | } |
| | |
| | | |
| | | protected function getIntegrationsMenu():string |
| | | { |
| | | $integrations = JVB()->getAvailableServices(false); |
| | | $integrations = $this->getAvailableServices(); |
| | | $out = ''; |
| | | if (!empty($integrations)) { |
| | | $out = '<nav class="integrations"><ul>'; |
| | | |
| | | $url = get_home_url(null, '/dash/integrations/'); |
| | | $out .= '<li><a href="'.$url.'">'.jvbDashIcon('plugs-connected').'Integrations</a></li>'; |
| | | foreach ($integrations as $name=> $integration) { |
| | | if (!JVB()->userCanConnect($name, $this->user->ID) || !$integration->hasDefaults()) { |
| | | foreach ($integrations as $service) { |
| | | $integration = $this->getService($service); |
| | | if (method_exists($integration, 'userCanConnect') && !$integration->userCanConnect($this->user->ID)) { |
| | | continue; |
| | | } |
| | | $link = sanitize_title(str_replace('_', '-',$name)); |
| | | $link = sanitize_title(str_replace('_', '-',$service)); |
| | | $out .= '<li><a href="'.$url.$link.'">'.jvbDashIcon($integration->icon).$integration->getTitle().'</a></li>'; |
| | | } |
| | | $out .= '</ul></nav>'; |
| | |
| | | ?> |
| | | <section class="item-grid integrations"> |
| | | <?php |
| | | $all = JVB()->getAvailableServices(); |
| | | foreach ($all as $name) { |
| | | |
| | | foreach ($this->getAvailableServices() as $name) { |
| | | if (current_user_can('manage_options')) { |
| | | $userID = null; |
| | | } else { |
| | | $userID = $this->user->ID; |
| | | } |
| | | |
| | | JVB()->connect($name, $userID)->renderConnection(); |
| | | $integration = $this->getService($name); |
| | | if (method_exists($integration, 'determineUser') |
| | | && method_exists($integration, 'renderConnection')) { |
| | | $integration->determineUser(); |
| | | $integration->renderConnection(); |
| | | } |
| | | } |
| | | ?> |
| | | </section> |
| | |
| | | $membership = Site::membership(); |
| | | $type = $registrar? $registrar->getType() : false; |
| | | if ($type) { |
| | | $permission = JVB()->roles()->getPermission('edit', $key); |
| | | $permission = Roles::getInstance()()->getPermission('edit', $key); |
| | | } |
| | | switch ($type) { |
| | | case 'content': |
| | |
| | | break; |
| | | case 'taxonomy': |
| | | $registrar = Registrar::getInstance($key); |
| | | if ($registrar && $registrar->hasFeature('is_content') && (!empty(JVB()->roles()->getOwnedTerms($userID, $key)) || !empty(JVB()->roles()->getManagedTerms($userID, $key)))){ |
| | | if ($registrar && $registrar->hasFeature('is_content') && (!empty(Roles::getInstance()()->getOwnedTerms($userID, $key)) || !empty(Roles::getInstance()()->getManagedTerms($userID, $key)))){ |
| | | $remove = false; |
| | | } else if (count(array_intersect($registrar->registrar->for, array_keys($pages))) > 0) { |
| | | $remove = false; |
| | |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use WP_Block; |
| | |
| | | |
| | | class DirectoryManager |
| | | { |
| | | use Instance; |
| | | protected array $directories; |
| | | protected array $directoryPageIDs; |
| | | protected array $directoryList; |
| | |
| | | protected static string $slug = BASE.'for_type_slug'; |
| | | protected Cache $cache; |
| | | |
| | | public function __construct($perPage = 100) |
| | | private function __construct($perPage = 100) |
| | | { |
| | | $this->directories = $this->getDirectories(); |
| | | if (empty($this->directories)) { |
| | |
| | | } |
| | | $this->perPage = $perPage; |
| | | $this->cache = Cache::for('directory', WEEK_IN_SECONDS); |
| | | $this->cache->connect('post', true) |
| | | ->connect('taxonomy', true) |
| | | ->connect('user', true); |
| | | |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | |
| | | jvb_register_do_once('buildDirectories', [$this, 'activate']); |
| | | add_action('init', [$this, 'registerDirectories']); |
| | | add_filter('pre_render_block', [$this, 'renderBlock'], 20, 3); |
| | | add_action('save_post', [$this, 'flushCache']); |
| | | add_action('edit_terms', [$this, 'flushCache']); |
| | | } |
| | | |
| | | public function flushPostCache(int $postID):void |
| | | { |
| | | //TODO: We can probably use a bit more finesse here. |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | public function registerDirectories():void |
| | | { |
| | | |
| File was renamed from inc/managers/EmailManager.php |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\Instance; |
| | | use WP_User; |
| | | |
| | | /** |
| | |
| | | * Hooks into wp_mail to ensure a consistent look in our automatic emails |
| | | */ |
| | | |
| | | class EmailManager |
| | | class Emails |
| | | { |
| | | |
| | | use Instance; |
| | | public array $colours; |
| | | private string $title = JVB_EMAIL['content']['title']; |
| | | private string $prefix = JVB_EMAIL['content']['subjectPrefix']; |
| File was renamed from inc/managers/ErrorHandler.php |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\Instance; |
| | | use WP_Error; |
| | | use Exception; |
| | | use WP_REST_Request; |
| | |
| | | exit; // Exit if accessed directly |
| | | } |
| | | |
| | | class ErrorHandler |
| | | class Errors |
| | | { |
| | | use Instance; |
| | | protected int $notification_threshold = 5; // Critical errors within 1 hour |
| | | |
| | | protected array $error_levels = [ |
| | |
| | | |
| | | protected CustomTable $table; |
| | | |
| | | public function __construct() |
| | | private function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | // global $wpdb; |
| | |
| | | } |
| | | public function registerAdminAction():void |
| | | { |
| | | $admin = JVB()->admin(); |
| | | $admin = AdminPages::getInstance(); |
| | | |
| | | $admin->registerSubpage( |
| | | 'Error Log', |
| | |
| | | $admin_email = get_option('admin_email'); |
| | | $subject = "[" . get_bloginfo('name') . " Critical Error] {$component}"; |
| | | |
| | | $body = JVB()->email()->alert( |
| | | $body = Emails::getInstance()->alert( |
| | | 'A critical error has occurred and requires immediate attention', |
| | | 'error' |
| | | ); |
| | | |
| | | $body .= JVB()->email()->h2('Error Details'); |
| | | $body .= JVB()->email()->card( |
| | | $body .= Emails::getInstance()->h2('Error Details'); |
| | | $body .= Emails::getInstance()->card( |
| | | '<p><strong>Component:</strong> ' . esc_html($component) . '</p>' . |
| | | '<p><strong>Message:</strong></p>' . |
| | | JVB()->email()->codeBlock($message), |
| | | Emails::getInstance()->codeBlock($message), |
| | | 'Error Information' |
| | | ); |
| | | |
| | | if (!empty($context)) { |
| | | $body .= JVB()->email()->h3('Additional Context'); |
| | | $body .= JVB()->email()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); |
| | | $body .= Emails::getInstance()->h3('Additional Context'); |
| | | $body .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); |
| | | } |
| | | |
| | | return JVB()->email()->sendEmail($admin_email, $subject, $body, 'CRITICAL ERROR'); |
| | | return Emails::getInstance()->sendEmail($admin_email, $subject, $body, 'CRITICAL ERROR'); |
| | | } |
| | | |
| | | /** |
| | |
| | | $subject = "[{$site_name}] Daily Error Summary - " . date('Y-m-d'); |
| | | |
| | | // Header with alert |
| | | $body = JVB()->email()->h1('Daily Error Summary'); |
| | | $body = Emails::getInstance()->h1('Daily Error Summary'); |
| | | $body .= sprintf('<p>Error summary for <strong>%s</strong></p>', $yesterday); |
| | | |
| | | // Summary stats in a grid |
| | | if (!empty($summary['stats'])) { |
| | | $stats = [ |
| | | JVB()->email()->stat($summary['stats']->total_errors, 'Total Errors'), |
| | | JVB()->email()->stat($summary['stats']->critical_count, 'Critical', 'Requires attention'), |
| | | JVB()->email()->stat($summary['stats']->error_count, 'Errors'), |
| | | JVB()->email()->stat($summary['stats']->warning_count, 'Warnings') |
| | | Emails::getInstance()->stat($summary['stats']->total_errors, 'Total Errors'), |
| | | Emails::getInstance()->stat($summary['stats']->critical_count, 'Critical', 'Requires attention'), |
| | | Emails::getInstance()->stat($summary['stats']->error_count, 'Errors'), |
| | | Emails::getInstance()->stat($summary['stats']->warning_count, 'Warnings') |
| | | ]; |
| | | $body .= JVB()->email()->grid($stats, 4); |
| | | $body .= Emails::getInstance()->grid($stats, 4); |
| | | } |
| | | |
| | | // Alert if critical errors exist |
| | | if (!empty($summary['critical'])) { |
| | | $body .= JVB()->email()->alert( |
| | | $body .= Emails::getInstance()->alert( |
| | | sprintf('Found %d critical errors that need immediate attention', count($summary['critical'])), |
| | | 'error' |
| | | ); |
| | | } |
| | | |
| | | $body .= JVB()->email()->spacer(20); |
| | | $body .= Emails::getInstance()->spacer(20); |
| | | |
| | | // Frequent errors section |
| | | if (!empty($summary['frequent'])) { |
| | | $body .= JVB()->email()->h2('Most Frequent Errors'); |
| | | $body .= Emails::getInstance()->h2('Most Frequent Errors'); |
| | | |
| | | foreach ($summary['frequent'] as $error) { |
| | | $cardContent = JVB()->email()->badge($error->count . 'x', 'warning') . ' '; |
| | | $cardContent = Emails::getInstance()->badge($error->count . 'x', 'warning') . ' '; |
| | | $cardContent .= '<strong>' . esc_html($error->error_type) . '</strong>'; |
| | | $cardContent .= '<p style="margin:10px 0 5px 0;font-size:13px;">' . esc_html(wp_trim_words($error->message, 15)) . '</p>'; |
| | | $cardContent .= '<p style="margin:0;font-size:12px;color:' . JVB()->email()->colours['dark-200'] . ';"> |
| | | $cardContent .= '<p style="margin:0;font-size:12px;color:' . Emails::getInstance()->colours['dark-200'] . ';"> |
| | | Source: ' . esc_html($error->source) . ' | |
| | | Logged in: ' . $error->logged_in_count . ' | |
| | | Logged out: ' . $error->logged_out_count . ' |
| | | </p>'; |
| | | |
| | | $body .= JVB()->email()->card($cardContent, $error->component); |
| | | $body .= Emails::getInstance()->card($cardContent, $error->component); |
| | | } |
| | | } |
| | | |
| | | // Critical errors section |
| | | if (!empty($summary['critical'])) { |
| | | $body .= JVB()->email()->spacer(30); |
| | | $body .= JVB()->email()->h2('Recent Critical Errors'); |
| | | $body .= Emails::getInstance()->spacer(30); |
| | | $body .= Emails::getInstance()->h2('Recent Critical Errors'); |
| | | |
| | | foreach ($summary['critical'] as $error) { |
| | | $cardContent = '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>'; |
| | | $cardContent .= '<p><strong>Message:</strong></p>'; |
| | | $cardContent .= JVB()->email()->codeBlock($error->message); |
| | | $cardContent .= Emails::getInstance()->codeBlock($error->message); |
| | | |
| | | // Include context if available |
| | | if (!empty($error->context)) { |
| | | $context = json_decode($error->context, true); |
| | | if (is_array($context)) { |
| | | $cardContent .= '<p><strong>Context:</strong></p>'; |
| | | $cardContent .= JVB()->email()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); |
| | | $cardContent .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); |
| | | } |
| | | } |
| | | |
| | | $body .= JVB()->email()->card($cardContent, $error->component . ': ' . $error->error_type); |
| | | $body .= Emails::getInstance()->card($cardContent, $error->component . ': ' . $error->error_type); |
| | | } |
| | | } |
| | | |
| | | // Dashboard link |
| | | $admin_url = admin_url('admin.php?page=jvb-error-logs'); |
| | | $body .= JVB()->email()->spacer(30); |
| | | $body .= JVB()->email()->button($admin_url, 'View Detailed Logs'); |
| | | $body .= Emails::getInstance()->spacer(30); |
| | | $body .= Emails::getInstance()->button($admin_url, 'View Detailed Logs'); |
| | | |
| | | return JVB()->email()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY'); |
| | | return Emails::getInstance()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY'); |
| | | } |
| | | |
| | | /** |
| File was renamed from inc/managers/FavouritesManager.php |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\FavouritesRoutes; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | class FavouritesManager |
| | | class Favourites |
| | | { |
| | | private Cache $cache; |
| | | use Instance; |
| | | |
| | | protected CustomTable $favourites; |
| | | protected CustomTable $lists; |
| | | protected CustomTable $listItems; |
| | | protected CustomTable $listShares; |
| | | protected Rest $route; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->route = FavouritesRoutes::getInstance(); |
| | | $this->defineTables(); |
| | | $this->registerHooks(); |
| | | } |
| | |
| | | add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3); |
| | | add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3); |
| | | add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']); |
| | | |
| | | //adds favourite ordering to wp_query/tax_query |
| | | add_filter('posts_join', [$this, 'maybeJoinFavouriteOrderby'], 10, 2); |
| | | add_filter('posts_orderby', [$this, 'maybeApplyFavouriteOrderby'], 10, 2); |
| | | add_filter('terms_clauses', [$this, 'maybeApplyFavouriteTermOrderby'], 10, 3); |
| | | } |
| | | protected function defineTables():void |
| | | { |
| | |
| | | ); |
| | | |
| | | if ($owner) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $owner, |
| | | 'new_favourite', |
| | | $user_id, |
| | |
| | | ]); |
| | | |
| | | if (in_array($registerType, ['term', 'user'])) { |
| | | JVB()->notification()->deleteUserPreference($user_id, $target_id, $type); |
| | | NotificationManager::getInstance()->deleteUserPreference($user_id, $target_id, $type); |
| | | } |
| | | if ($owner) { |
| | | JVB()->notification()->removeNotification( |
| | | NotificationManager::getInstance()->removeNotification( |
| | | $owner, |
| | | [ |
| | | 'from_user' => $user_id, |
| | |
| | | return $this->toggleFavourite(false, $user_id, $target_id, $type); |
| | | } |
| | | |
| | | public function hasFavourited(int $user_id, int $target_id, string $type):bool |
| | | { |
| | | return (bool) $this->favourites->get([ |
| | | 'user_id' => $user_id, |
| | | 'target_id' => $target_id, |
| | | 'type' => $type |
| | | ]); |
| | | } |
| | | |
| | | public function batchToggleFavourites(int $user_id, array $items):array |
| | | { |
| | | $items = array_filter($items, function ($item) { |
| | | $keys = ['target_id', 'type', 'action']; |
| | | foreach ($keys as $key) { |
| | | if (!array_key_exists($key, $item)) { |
| | | return false; |
| | | } |
| | | if (empty($item[$key])) { |
| | | return false; |
| | | } |
| | | } |
| | | if (!in_array($item['action'], ['add', 'remove'])) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }); |
| | | |
| | | if (empty($items)) { |
| | | return []; |
| | | } |
| | | |
| | | $results = []; |
| | | foreach ($items as $item) { |
| | | $results[$item['target_id']] =$this->toggleFavourite($item['action'] === 'add', $user_id, $item['target_id'], $item['type']); |
| | | } |
| | | return $results; |
| | | } |
| | | |
| | | |
| | | public function addNoteFromTarget(int $user_id, int $target_id, string $type, string $note):bool |
| | | { |
| | | $favourite = $this->favourites->get([ |
| | |
| | | $args = []; |
| | | if ($shareWithUser) { |
| | | $args['user_id'] = $shareWithUser; |
| | | JVB()->notification()->addNotification($shareWithUser, 'list_shared', $user_id, '', $listId); |
| | | NotificationManager::getInstance()->addNotification($shareWithUser, 'list_shared', $user_id, '', $listId); |
| | | } elseif ($newUser) { |
| | | $email = sanitize_email($newUser['email']); |
| | | $name = sanitize_text_field($newUser['name']); |
| | |
| | | if (is_int($userIDorEmail)) { |
| | | $where['user_id'] = $userIDorEmail; |
| | | |
| | | $notification = JVB()->notification()->table(); |
| | | $notification = NotificationManager::getInstance()->table(); |
| | | $n = $notification->get([ |
| | | 'for_user' => $userIDorEmail, |
| | | 'from_user' => $user_id, |
| | |
| | | |
| | | $message = $accept ? 'accepted' : 'rejected'; |
| | | $message = $user->first_name . ' ' . $message . ' your list share invitation.'; |
| | | JVB()->notification()->addNotification($list->user_id, 'list_share_status', $userIDorEmail, $message); |
| | | NotificationManager::getInstance()->addNotification($list->user_id, 'list_share_status', $userIDorEmail, $message); |
| | | } |
| | | }else if (is_email($userIDorEmail)) { |
| | | $args['email'] = sanitize_email($userIDorEmail); |
| | |
| | | |
| | | public function getListDetails(int $list_id, int $user_id, int $page = 1):array |
| | | { |
| | | $list = JVB()->favourites()->getLists(['id' => $list_id, 'user_id' => $user_id]); |
| | | $list = Favourites::getInstance()->getLists(['id' => $list_id, 'user_id' => $user_id]); |
| | | if (!$list) { |
| | | return []; |
| | | } |
| | |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Join favourite counts for WP_Query when orderby => 'favourite_count' |
| | | */ |
| | | public function maybeJoinFavouriteOrderby(string $join, \WP_Query $query): string |
| | | { |
| | | if ($query->get('orderby') !== 'favourites_count') { |
| | | return $join; |
| | | } |
| | | global $wpdb; |
| | | $table = $this->favourites->getFullTableName(); |
| | | $type = $query->get('post_type') ?: 'post'; |
| | | |
| | | $join .= $wpdb->prepare( |
| | | " LEFT JOIN (SELECT target_id, COUNT(*) as fav_count FROM {$table} WHERE type = %s GROUP BY target_id) jvb_fav_counts ON jvb_fav_counts.target_id = {$wpdb->posts}.ID", |
| | | $type |
| | | ); |
| | | return $join; |
| | | } |
| | | |
| | | public function maybeApplyFavouriteOrderby(string $orderby, \WP_Query $query): string |
| | | { |
| | | if ($query->get('orderby') !== 'favourites_count') { |
| | | return $orderby; |
| | | } |
| | | $order = strtoupper($query->get('order')) === 'ASC' ? 'ASC' : 'DESC'; |
| | | return "COALESCE(jvb_fav_counts.fav_count, 0) {$order}"; |
| | | } |
| | | |
| | | /** |
| | | * Join + order for WP_Term_Query when orderby => 'favourite_count' |
| | | */ |
| | | public function maybeApplyFavouriteTermOrderby(array $clauses, array $taxonomies, array $args): array |
| | | { |
| | | if (($args['orderby'] ?? '') !== 'favourites_count') { |
| | | return $clauses; |
| | | } |
| | | global $wpdb; |
| | | $table = $this->favourites->getFullTableName(); |
| | | $type = jvbCheckBase($taxonomies[0] ?? ''); |
| | | |
| | | $clauses['join'] .= $wpdb->prepare( |
| | | " LEFT JOIN (SELECT target_id, COUNT(*) as fav_count FROM {$table} WHERE type = %s GROUP BY target_id) jvb_fav_counts ON jvb_fav_counts.target_id = t.term_id", |
| | | $type |
| | | ); |
| | | $order = strtoupper($args['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC'; |
| | | $clauses['orderby'] = "ORDER BY COALESCE(jvb_fav_counts.fav_count, 0)"; |
| | | $clauses['order'] = $order; |
| | | |
| | | return $clauses; |
| | | } |
| | | } |
| | |
| | | private function __construct(string $source) |
| | | { |
| | | $this->source = $source; |
| | | $this->cache = Cache::for('icons_' . $source, WEEK_IN_SECONDS); |
| | | $this->cache = Cache::for('icons_' . $source); |
| | | $this->style = Site::icon(); |
| | | |
| | | $this->addMap(); |
| | |
| | | $instance->archiveCurrentVersion($css); |
| | | |
| | | if (file_put_contents($css_path, $css) !== false) { |
| | | Cache::touch('icons_' . $source); |
| | | Cache::for('icons_'.$source)->flush(); |
| | | error_log("[IconsManager] Updated {$source}.css"); |
| | | } else { |
| | | error_log("[IconsManager] Could not write {$source}.css"); |
| | |
| | | $instance->archiveCurrentVersion($css); |
| | | |
| | | if (file_put_contents($css_path, $css) !== false) { |
| | | Cache::touch('icons_' . $source); |
| | | Cache::for('icons_'.$source)->flush(); |
| | | error_log("[IconsManager] Updated {$source}.css"); |
| | | } else { |
| | | error_log("[IconsManager] Could not write {$source}.css"); |
| | |
| | | |
| | | public function registerStyle(): void |
| | | { |
| | | $timestamp = Cache::lastModified('icons_' . $this->source); |
| | | $timestamp = Cache::for('icons_'.$this->source)->getTimestamp(); |
| | | $handle = 'jvb-icons-' . $this->source; |
| | | |
| | | wp_register_style( |
| | |
| | | if (file_put_contents($css_path, $entry['css']) !== false) { |
| | | $this->icons = $entry['iconList']; |
| | | $this->saveIcons(); |
| | | Cache::touch('icons_' . $this->source); |
| | | Cache::for('icons_'.$this->source)->flush(); |
| | | return true; |
| | | } |
| | | |
| | |
| | | } |
| | | $needsUpdate[$this->source] = true; |
| | | update_option(BASE.'icons_needs_update', $needsUpdate); |
| | | Cache::touch('icons_' . $this->source); |
| | | |
| | | Cache::for('icons_'.$this->source)->flush(); |
| | | } |
| | | |
| | | public function mergeVersions(array $timestamps): bool |
| | |
| | | * |
| | | * Generates artist feature cards as webp images with artist information |
| | | * Uses Imagick with GD fallback and leverages FileUploadManager storage |
| | | * TODO: Integrate better with UploadManager.php |
| | | */ |
| | | class ImageGenerator |
| | | { |
| | |
| | | |
| | | // Get directory structure from FileUploadManager if available |
| | | if ($this->fileManager) { |
| | | $user_dir = $this->fileManager->getUploadDirectory(); |
| | | $user_dir = $this->fileManager->generateDirectory('image', $this->data); |
| | | $relative_dir = str_replace($base_dir, '', dirname($file_path)); |
| | | $destination_dir = $base_dir . '/' . $relative_dir; |
| | | } else { |
| | |
| | | $user_data = get_userdata($this->data['user_id']); |
| | | $this->outputFilename = $this->fileManager->generateFilename( |
| | | $title.'.webp', |
| | | $user_data |
| | | $this->data |
| | | ); |
| | | } else { |
| | | // Fallback filename generation |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\queue\executors\InvitationExecutor; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\Invitations; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | class InvitationsManager |
| | | { |
| | | use Instance; |
| | | protected array $inviteConfig; |
| | | protected CustomTable $table; |
| | | protected int $expiryDays = 14; |
| | | protected Cache $cache; |
| | | public function __construct() |
| | | protected Rest $route; |
| | | |
| | | private function __construct() |
| | | { |
| | | $this->defineTable(); |
| | | if (!isset($this->table)) { |
| | | return; |
| | | } |
| | | if (Site::membership() && Site::membership()->has('can_invite')) { |
| | | $this->route = Invitations::getInstance(); |
| | | } |
| | | $this->setInviteConfig(); |
| | | $this->cache = Cache::for('invitations')->user(); |
| | | add_action('init', [$this, 'registerInvitationExecutors'], 5); |
| | |
| | | */ |
| | | public function registerInvitationExecutors(): void |
| | | { |
| | | $registry = JVB()->queue()->registry(); |
| | | $registry = Queue::getInstance()->registry(); |
| | | $executor = new InvitationExecutor(); |
| | | |
| | | // Create invitations - chunked at 20 |
| | |
| | | } |
| | | |
| | | // User must be owner or manager of the term |
| | | return JVB()->roles()->isManager($userID, $termID); |
| | | return Roles::getInstance()()->isManager($userID, $termID); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | $termText = implode('', $termContent); |
| | | |
| | | $button = JVB()->email()->button($signupUrl, 'Join the Scene!'); |
| | | $link = JVB()->email()->link($signupUrl); |
| | | $signature = JVB()->email()->signature(); |
| | | $button = Emails::getInstance()->button($signupUrl, 'Join the Scene!'); |
| | | $link = Emails::getInstance()->link($signupUrl); |
| | | $signature = Emails::getInstance()->signature(); |
| | | |
| | | $message = sprintf( |
| | | '<p>Hi %s!</p> |
| | |
| | | $signature |
| | | ); |
| | | |
| | | JVB()->email()->sendEmail($email, $subject, $message); |
| | | Emails::getInstance()->sendEmail($email, $subject, $message); |
| | | } |
| | | |
| | | /** |
| | |
| | | $this->notifyInvitersOfAcceptance($invitation, $userID); |
| | | |
| | | // Invalidate cache |
| | | Cache::for('invitations')->forget('user_' . $userID); |
| | | $this->cache->forget('user_' . $userID); |
| | | |
| | | return true; |
| | | } |
| | |
| | | */ |
| | | protected function processTermMembership(int $userID, object $invitation): void |
| | | { |
| | | foreach (JVB()->roles()->getInvitableTaxonomies() as $taxonomy) { |
| | | foreach (Roles::getInstance()()->getInvitableTaxonomies() as $taxonomy) { |
| | | $column = 'to_' . $taxonomy; |
| | | if (isset($invitation->$column) && $invitation->$column) { |
| | | $termID = (int) $invitation->$column; |
| | |
| | | $userData = get_userdata($userID); |
| | | |
| | | foreach ($inviters as $inviter) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $inviter['user_id'], |
| | | 'invitation_accepted', |
| | | [ |
| | |
| | | ); |
| | | |
| | | // Clear cache after cleanup |
| | | Cache::for('invitations')->flush(); |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | /***************************************************************** |
| | |
| | | $name |
| | | ); |
| | | |
| | | JVB()->email()->sendEmail($email, $subject, $content, 'INVITATION REVOKED'); |
| | | Emails::getInstance()->sendEmail($email, $subject, $content, 'INVITATION REVOKED'); |
| | | } |
| | | |
| | | /** |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\ApprovalRoutes; |
| | | use JVBase\rest\routes\VoteRoutes; |
| | | use WP_Error; |
| | | use WP_Post; |
| | | use WP_Term; |
| | |
| | | protected string $references; |
| | | protected CustomTable $table; |
| | | protected static Cache $cache; |
| | | protected Rest $route; |
| | | |
| | | private function __construct(string $key, ?string $references = null) |
| | | { |
| | | $this->route = VoteRoutes::getInstance(); |
| | | $this->key = $key; |
| | | if (is_null($references)) { |
| | | $references = match($key){ |
| | | 'post' => 'post', |
| | | 'term' => 'term', |
| | | 'user' => 'user', |
| | | 'post', 'posts' => 'post', |
| | | 'term','terms', 'taxonomy' => 'term', |
| | | 'user', 'users' => 'user', |
| | | 'response', 'responses' => 'response', |
| | | default => false |
| | | }; |
| | | if (!$references) { |
| | |
| | | // break; |
| | | // } |
| | | if (!isset(self::$cache)) { |
| | | self::$cache = Cache::for('user_karma')->connect('user')->user(); |
| | | self::$cache = Cache::for('user_karma')->user(); |
| | | } |
| | | |
| | | $this->registerHooks(); |
| | | |
| | | } |
| | | |
| | | protected function registerHooks():void |
| | | { |
| | | if ($this->references === 'post') { |
| | | add_filter('posts_join', [$this, 'maybeJoinKarmaOrderby'], 10, 2); |
| | | add_filter('posts_orderby', [$this, 'maybeApplyKarmaOrderby'], 10, 2); |
| | | } elseif ($this->references === 'term') { |
| | | add_filter('terms_clauses', [$this, 'maybeApplyKarmaTermOrderby'], 10, 3); |
| | | } |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | } |
| | | |
| | | /** |
| | | * Supported orderby keys: 'karma_upvotes', 'karma_downvotes', 'karma_score' |
| | | */ |
| | | protected function karmaOrderExpr(string $orderby, string $alias = 'jvb_karma'): ?string |
| | | { |
| | | return match ($orderby) { |
| | | 'upvotes' => "COALESCE({$alias}.upvotes, 0)", |
| | | 'downvotes' => "COALESCE({$alias}.downvotes, 0)", |
| | | 'karmic_score' => "COALESCE({$alias}.upvotes, 0) - COALESCE({$alias}.downvotes, 0)", |
| | | default => null, |
| | | }; |
| | | } |
| | | |
| | | public function maybeJoinKarmaOrderby(string $join, \WP_Query $query): string |
| | | { |
| | | if (!in_array($query->get('orderby'), ['upvotes', 'downvotes', 'karmic_score'])) { |
| | | return $join; |
| | | } |
| | | |
| | | global $wpdb; |
| | | $table = $this->table->getFullTableName(); |
| | | $content = $query->get('post_type') ?: 'post'; |
| | | |
| | | $join .= $wpdb->prepare( |
| | | " LEFT JOIN (SELECT item_id, |
| | | SUM(vote = 'up') as upvotes, |
| | | SUM(vote = 'down') as downvotes |
| | | FROM {$table} WHERE content = %s GROUP BY item_id) jvb_karma |
| | | ON jvb_karma.item_id = {$wpdb->posts}.ID", |
| | | $content |
| | | ); |
| | | return $join; |
| | | } |
| | | |
| | | public function maybeApplyKarmaOrderby(string $orderby, \WP_Query $query): string |
| | | { |
| | | $expr = $this->karmaOrderExpr($query->get('orderby')); |
| | | if (!$expr) { |
| | | return $orderby; |
| | | } |
| | | $order = strtoupper($query->get('order')) === 'ASC' ? 'ASC' : 'DESC'; |
| | | return "{$expr} {$order}"; |
| | | } |
| | | |
| | | public function maybeApplyKarmaTermOrderby(array $clauses, array $taxonomies, array $args): array |
| | | { |
| | | $expr = $this->karmaOrderExpr($args['orderby'] ?? ''); |
| | | if (!$expr) { |
| | | return $clauses; |
| | | } |
| | | global $wpdb; |
| | | $table = $this->table->getFullTableName(); |
| | | $content = jvbNoBase($taxonomies[0] ?? ''); |
| | | |
| | | $clauses['join'] .= $wpdb->prepare( |
| | | " LEFT JOIN (SELECT item_id, |
| | | SUM(vote = 'up') as upvotes, |
| | | SUM(vote = 'down') as downvotes |
| | | FROM {$table} WHERE content = %s GROUP BY item_id) jvb_karma |
| | | ON jvb_karma.item_id = t.term_id", |
| | | $content |
| | | ); |
| | | $clauses['orderby'] = "ORDER BY {$expr}"; |
| | | $clauses['order'] = strtoupper($args['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC'; |
| | | |
| | | return $clauses; |
| | | } |
| | | |
| | | public function karmaTable():CustomTable |
| | | { |
| | | return $this->table; |
| | | } |
| | | |
| | | public static function getInstance(string $key):KarmaManager|false |
| | | { |
| | | if (!isset(self::$instances[$key])) { |
| | |
| | | { |
| | | $key = sanitize_key($key); |
| | | if (!isset(self::$instances[$key])) { |
| | | self::$instances[$key] = new KarmaManager($key, $references); |
| | | self::$instances[$key] = new self($key, $references); |
| | | } |
| | | |
| | | return self::$instances[$key]; |
| | |
| | | 'post' => get_post($itemID), |
| | | 'term' => get_term($itemID, $based), |
| | | 'user' => get_userdata($itemID), |
| | | 'response' => JVB()->responses()->response->get(['id' => $itemID])??false, |
| | | 'response' => Responses::getInstance()->responseExists($itemID), |
| | | default => false //TODO if we do have custom tables, we'll have to set that up here |
| | | }; |
| | | } |
| | |
| | | } |
| | | self::$cache->forget($userID); |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'karmic', |
| | | 0, |
| | | [ |
| | |
| | | update_user_meta($item['itemID'], BASE.'karma', $karma); |
| | | break; |
| | | case 'response': |
| | | JVB()->responses()->response->update( |
| | | [ |
| | | 'upvotes' => $upvotes, |
| | | 'downvotes' => $downvotes, |
| | | 'karma' => $karma |
| | | ], |
| | | [ |
| | | 'id' => $item['itemID'] |
| | | ] |
| | | ); |
| | | Responses::getInstance()->updateKarma($item['itemID'], $upvotes, $downvotes, $karma); |
| | | break; |
| | | } |
| | | $results[$item['itemID']] = true; |
| File was renamed from inc/managers/LoginManager.php |
| | |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\meta\Form; |
| | | use JVBase\Instance;use JVBase\integrations\services\Cloudflare;use JVBase\meta\Form; |
| | | |
| | | use JVBase\registrar\Registrar;use WP_Error; |
| | | use JVBase\registrar\Registrar;use JVBase\rest\routes\LoginRoutes;use WP_Error; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class LoginManager |
| | | class Login |
| | | { |
| | | use Instance; |
| | | |
| | | protected ?Form $form = null; |
| | | protected Cache $cache; |
| | | |
| | |
| | | protected array $fields = []; |
| | | protected ?string $action = null; |
| | | protected string $title = ''; |
| | | protected static LoginManager $instance; |
| | | |
| | | // Token handlers registry |
| | | protected array $messageHandlers = []; |
| | |
| | | ]; |
| | | private int $max_file_size = 5242880; // 5MB in bytes |
| | | |
| | | private LoginRoutes $routes; |
| | | |
| | | public function __construct() |
| | | protected function __construct() |
| | | { |
| | | self::$instance = $this; |
| | | $this->cache = Cache::for('login'); |
| | | $this->cache->flush(); |
| | | $this->routes = new LoginRoutes(); |
| | | // $this->cache = Cache::for('login'); |
| | | // $this->cache->flush(); |
| | | // Initialize magic link support if enabled |
| | | if (Site::has('magic_link')) { |
| | | $this->initMagicLinkSupport(); |
| | |
| | | add_action('user_register', [$this, 'saveRegistrationFields'], 50, 2); |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 8, 1); |
| | | } |
| | | public static function getInstance():self |
| | | { |
| | | return self::$instance; |
| | | } |
| | | |
| | | public function excludeLoginSitemap(array $ids): array |
| | | { |
| | |
| | | exit; |
| | | } |
| | | $this->setup(); |
| | | $page = $this->cache->remember( |
| | | $this->getAction(), |
| | | function() { |
| | | return $this->renderPage(); |
| | | }, |
| | | 5 |
| | | ); |
| | | |
| | | echo $page; |
| | | echo $this->renderPage(); |
| | | return ''; |
| | | } |
| | | protected function renderPage() { |
| | |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | return; |
| | | } |
| | | JVB()->connect('cloudflare')->enqueueTurnstileScripts(); |
| | | Cloudflare::getInstance()->enqueueTurnstileScripts(); |
| | | } |
| | | |
| | | protected function verifyTurnstile(): bool |
| | | { |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | if (!Site::hasIntegration('cloudflare') || Cloudflare::getInstance()->isSetUp()) { |
| | | return true; // Not enabled, pass verification |
| | | } |
| | | |
| | |
| | | return false; |
| | | } |
| | | |
| | | return JVB()->connect('cloudflare')->verifyTurnstile($token); |
| | | return Cloudflare::getInstance()->verifyTurnstile($token); |
| | | } |
| | | |
| | | /************************************************************************ |
| | |
| | | |
| | | protected function maybeMagicLink(): void |
| | | { |
| | | if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) { |
| | | if (!MagicLinks::getInstance() || !in_array($this->action, ['login', 'lostpassword'])) { |
| | | return; |
| | | } |
| | | ?> |
| | |
| | | } |
| | | } |
| | | |
| | | // Initialize the login manager |
| | | new LoginManager(); |
| File was renamed from inc/managers/MagicLinkManager.php |
| | |
| | | |
| | | use JetBrains\PhpStorm\NoReturn; |
| | | use JVBase\base\Site; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Cloudflare; |
| | | use JVBase\meta\Form; |
| | | use JVBase\ui\Tabs; |
| | | use WP_User; |
| | |
| | | * NOTE: Login form integration is now handled by LoginManager.php |
| | | * This class focuses solely on magic link generation and verification |
| | | */ |
| | | class MagicLinkManager |
| | | class MagicLinks |
| | | { |
| | | use Instance; |
| | | protected Cache $cache; |
| | | protected Cache $referral_cache; |
| | | protected Cache $rateLimitCache; |
| | | |
| | | // Token settings |
| | | protected int $token_expiry = 900; // 15 minutes in seconds |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('magic_links', $this->token_expiry); |
| | | $this->referral_cache = Cache::for('referral_magic_links', 14 * DAY_IN_SECONDS); |
| | | $this->cache = Cache::for('magic_links',null, $this->token_expiry); |
| | | $this->referral_cache = Cache::for('referral_magic_links', null, 14 * DAY_IN_SECONDS); |
| | | $this->rateLimitCache = Cache::for('magic_link_rate_limit', null, $this->rate_limit_window); |
| | | |
| | | // Hook into WordPress auth flow |
| | | add_action('template_redirect', [$this, 'handleMagicLinkClick']); |
| | |
| | | */ |
| | | protected function checkRateLimit(string $email): bool|WP_Error |
| | | { |
| | | $cache_key = 'rate_limit_' . md5($email); |
| | | $attempts = $this->cache->get($cache_key); |
| | | $key = md5($email); |
| | | $attempts = $this->rateLimitCache->get($key); |
| | | |
| | | if (!$attempts) { |
| | | $attempts = ['count' => 0, 'timestamp' => time()]; |
| | | } |
| | | |
| | | // Reset counter if window has passed |
| | | if (time() - $attempts['timestamp'] > $this->rate_limit_window) { |
| | | $attempts = ['count' => 0, 'timestamp' => time()]; |
| | | $attempts = 1; |
| | | } |
| | | |
| | | // Check if limit exceeded |
| | | if ($attempts['count'] >= $this->max_attempts_per_hour) { |
| | | if ($attempts >= $this->max_attempts_per_hour) { |
| | | return new WP_Error( |
| | | 'rate_limit_exceeded', |
| | | 'Too many magic link requests. Please try again in an hour.' |
| | |
| | | } |
| | | |
| | | // Increment counter |
| | | $attempts['count']++; |
| | | $this->cache->set($cache_key, $attempts, $this->rate_limit_window); |
| | | $attempts++; |
| | | $this->rateLimitCache->set($key, $attempts); |
| | | |
| | | return true; |
| | | } |
| | |
| | | $subject = 'Sign in to ' . get_bloginfo('name'); |
| | | $message = $this->getLoginEmailTemplate($user->display_name, $magic_url); |
| | | |
| | | $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Log in to '. get_bloginfo('name')); |
| | | $sent = Emails::getInstance()->sendEmail($email, $subject, $message, 'Log in to '. get_bloginfo('name')); |
| | | |
| | | return $sent ? true : new WP_Error('email_failed', 'Failed to send magic link'); |
| | | } |
| | |
| | | $subject = 'Complete your ' . get_bloginfo('name') . ' registration'; |
| | | $message = $this->getSignupEmailTemplate($context['name'] ?? '', $magic_url); |
| | | |
| | | $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Complete Registration'); |
| | | $sent = Emails::getInstance()->sendEmail($email, $subject, $message, 'Complete Registration'); |
| | | |
| | | return $sent ? true : new WP_Error('email_failed', 'Failed to send signup link'); |
| | | } |
| | |
| | | $subject = (array_key_exists('subject', $context) && $context['subject'] !== '') ? $context['subject'] : $referrer_name . ' invited you to join ' . get_bloginfo('name'); |
| | | $message = $this->getReferralEmailTemplate($context['name'] ?? '', $referrer_name, $magic_url, $reward_text, $context); |
| | | |
| | | $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Accept Invitation'); |
| | | $sent = Emails::getInstance()->sendEmail($email, $subject, $message, 'Accept Invitation'); |
| | | |
| | | return $sent ? true : new WP_Error('email_failed', 'Failed to send referral link'); |
| | | } |
| | |
| | | $subject = 'Reset your password'; |
| | | $message = $this->getResetEmailTemplate($user->display_name, $magic_url); |
| | | |
| | | $sent = JVB()->email()->sendEmail($email, $subject, $message, 'Reset Password'); |
| | | $sent = Emails::getInstance()->sendEmail($email, $subject, $message, 'Reset Password'); |
| | | |
| | | return $sent ? true : new WP_Error('email_failed', 'Failed to send reset link'); |
| | | } |
| | |
| | | $this->processSignup($token_data); |
| | | } |
| | | |
| | | $role = JVB()->referrals()->getRole(); |
| | | $role = Referrals::getInstance()->getRole(); |
| | | $pass = wp_generate_password(20, true, true); |
| | | $name = sanitize_text_field($token_data['name']); |
| | | $user_id = wp_insert_user([ |
| | |
| | | |
| | | protected function getLoginEmailTemplate(string $name, string $magic_url): string |
| | | { |
| | | $content = JVB()->email()->h2('Hey ' . esc_html($name) . '!'); |
| | | $content = Emails::getInstance()->h2('Hey ' . esc_html($name) . '!'); |
| | | $content .= '<p>Click the button below to sign in to your account instantly - no password needed!</p>'; |
| | | $content .= JVB()->email()->button($magic_url, 'Sign In Now'); |
| | | $content .= Emails::getInstance()->button($magic_url, 'Sign In Now'); |
| | | $content .= '<p>Or copy and paste this link into your browser:</p>'; |
| | | $content .= JVB()->email()->link($magic_url); |
| | | $content .= JVB()->email()->divider(); |
| | | $content .= Emails::getInstance()->link($magic_url); |
| | | $content .= Emails::getInstance()->divider(); |
| | | $content .= '<p>If you didn\'t request this, you can safely ignore this email. This link expires in 15 minutes.</p>'; |
| | | return $content; |
| | | } |
| | | |
| | | protected function getSignupEmailTemplate(string $name, string $magic_url): string |
| | | { |
| | | $content = JVB()->email()->h2('Welcome' . ($name ? ', ' . esc_html($name) : '') . '!'); |
| | | $content = Emails::getInstance()->h2('Welcome' . ($name ? ', ' . esc_html($name) : '') . '!'); |
| | | $content .= '<p>Click the button below to complete your registration and access your account!</p>'; |
| | | $content .= JVB()->email()->button($magic_url, 'Complete Registration'); |
| | | $content .= Emails::getInstance()->button($magic_url, 'Complete Registration'); |
| | | $content .= '<p>Or copy and paste this link:</p>'; |
| | | $content .= JVB()->email()->link($magic_url); |
| | | $content .= JVB()->email()->spacer(10); |
| | | $content .= Emails::getInstance()->link($magic_url); |
| | | $content .= Emails::getInstance()->spacer(10); |
| | | $content .= '<p><small>This link expires in 24 hours.</small></p>'; |
| | | return $content; |
| | | } |
| | | |
| | | protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text, array $context): string |
| | | { |
| | | $content = JVB()->email()->h2('Hey' . ($name ? ' ' . esc_html($name) : '') . '!'); |
| | | $content = Emails::getInstance()->h2('Hey' . ($name ? ' ' . esc_html($name) : '') . '!'); |
| | | $content .= sprintf( |
| | | '<p><strong>%s</strong> thinks you\'d love %s!</p>', |
| | | esc_html($referrer_name), |
| | |
| | | ); |
| | | |
| | | if (!empty($context['message'])) { |
| | | $content .= JVB()->email()->callout(nl2br(esc_html($context['message']))); |
| | | $content .= Emails::getInstance()->callout(nl2br(esc_html($context['message']))); |
| | | } |
| | | |
| | | if ($reward_text) { |
| | | $content .= JVB()->email()->alert( |
| | | $content .= Emails::getInstance()->alert( |
| | | '<strong>Special Welcome Offer:</strong> ' . esc_html($reward_text), |
| | | 'success' |
| | | ); |
| | | } |
| | | |
| | | $content .= JVB()->email()->button($magic_url, 'Join Now'); |
| | | $content .= Emails::getInstance()->button($magic_url, 'Join Now'); |
| | | $content .= '<p>Or copy and paste this link:</p>'; |
| | | $content .= JVB()->email()->link($magic_url); |
| | | $content .= JVB()->email()->spacer(10); |
| | | $content .= Emails::getInstance()->link($magic_url); |
| | | $content .= Emails::getInstance()->spacer(10); |
| | | $content .= '<p><small>This invitation expires in 14 days.</small></p>'; |
| | | |
| | | return $content; |
| | |
| | | public function renderForm():string |
| | | { |
| | | ob_start(); |
| | | JVB()->connect('cloudflare')->renderTurnstile(); |
| | | Cloudflare::getInstance()->renderTurnstile(); |
| | | $turnstile = ob_get_clean(); |
| | | global $wp; |
| | | |
| | |
| | | // wp_login_url() |
| | | ); |
| | | |
| | | $login = LoginManager::getInstance()->renderLoginForm('login', get_home_url(null,'/'.$wp->request)); |
| | | $login = Login::getInstance()->renderLoginForm('login', get_home_url(null,'/'.$wp->request)); |
| | | |
| | | $tabs = new Tabs(); |
| | | $tabs->addTab('magic') |
| | |
| | | */ |
| | | class NewsRelationships |
| | | { |
| | | private string $table_name; |
| | | private Cache $cache; |
| | | private CustomTable $relationships; |
| | | |
| | | public function __construct() |
| | | { |
| | |
| | | $wpdb->query("TRUNCATE TABLE {$this->table_name}"); |
| | | |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'rebuild_news', |
| | | 0, |
| | | [ |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Notifications\Content; |
| | | use JVBase\managers\Notifications\EmailDigests; |
| | | use JVBase\managers\Notifications\Notifications; |
| | | use JVBase\managers\Notifications\Preferences; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\NotificationsRoutes; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | |
| | | */ |
| | | class NotificationManager |
| | | { |
| | | use Instance; |
| | | protected Notifications $notifications; |
| | | protected Content $content; |
| | | protected EmailDigests $digest; |
| | | protected Preferences $preferences; |
| | | |
| | | public function __construct() |
| | | protected Rest $route; |
| | | private function __construct() |
| | | { |
| | | $this->route = NotificationsRoutes::getInstance(); |
| | | |
| | | $this->notifications = new Notifications(); |
| | | $this->content = new Content(); |
| | | $this->digest = new EmailDigests(); |
| | |
| | | protected static Cache $cache; |
| | | public function __construct() |
| | | { |
| | | self::$cache = Cache::for('content_notifications')->connect('post'); |
| | | self::$cache = Cache::for('content_notifications', 'post'); |
| | | } |
| | | public static function getSinceDate(string $date):string |
| | | { |
| | |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\managers\Emails; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Error; |
| | |
| | | return; |
| | | } |
| | | |
| | | JVB()->queue()->add( |
| | | Queue::getInstance()->add( |
| | | 'email_notification_digest', |
| | | 0, |
| | | [ |
| | |
| | | if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) { |
| | | return []; |
| | | } |
| | | return JVB()->notification()->getUsersByFrequency($frequency); |
| | | return NotificationManager::getInstance()->getUsersByFrequency($frequency); |
| | | } |
| | | |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | |
| | | |
| | | protected function generateUserDigest(int $userID, string $frequency):string|false |
| | | { |
| | | $subscription = JVB()->notification()->getUserSubscriptions($userID, $frequency); |
| | | $subscription = NotificationManager::getInstance()->getUserSubscriptions($userID, $frequency); |
| | | |
| | | //TODO |
| | | $notifications = JVB()->notification()->getUserNotifications($userID); |
| | | $notifications = NotificationManager::getInstance()->getUserNotifications($userID); |
| | | |
| | | if (empty($subscription) && empty($notifications)) { |
| | | return false; |
| | |
| | | $image = sprintf( |
| | | '<a href="%s">%s</a>', |
| | | $url, |
| | | JVB()->email()->image($image[0], get_post_meta($img, '_wp_attachment_image_alt', true)) |
| | | Emails::getInstance()->image($image[0], get_post_meta($img, '_wp_attachment_image_alt', true)) |
| | | ); |
| | | $t = sprintf( |
| | | '<a href="%s">%s</a>', |
| | | $url, |
| | | $m->get('post_title') |
| | | ); |
| | | $output[] = JVB()->email()->card($image, $t); |
| | | $output[] = Emails::getInstance()->card($image, $t); |
| | | } |
| | | |
| | | $final = JVB()->email()->button($goTo, 'Want to See More?'); |
| | | return JVB()->email()->grid($output, 3, 'New from '.$title,'',$final); |
| | | $final = Emails::getInstance()->button($goTo, 'Want to See More?'); |
| | | return Emails::getInstance()->grid($output, 3, 'New from '.$title,'',$final); |
| | | } |
| | | |
| | | protected function getTermUpdates(int $termID, string $type, string $frequency):string|false |
| | |
| | | $image = sprintf( |
| | | '<a href="%s">%s</a>', |
| | | $url, |
| | | JVB()->email()->image($image[0], get_post_meta($img, '_wp_attachment_image_alt', true)) |
| | | Emails::getInstance()->image($image[0], get_post_meta($img, '_wp_attachment_image_alt', true)) |
| | | ); |
| | | $t = sprintf( |
| | | '<a href="%s">%s</a>', |
| | | $url, |
| | | $m->get('post_title') |
| | | ); |
| | | $output[] = JVB()->email()->card($image, $t); |
| | | $output[] = Emails::getInstance()->card($image, $t); |
| | | } |
| | | |
| | | $final = JVB()->email()->button($goTo, 'See More'); |
| | | return JVB()->email()->grid($output, 3, 'New in '.$title, '', $final); |
| | | $final = Emails::getInstance()->button($goTo, 'See More'); |
| | | return Emails::getInstance()->grid($output, 3, 'New in '.$title, '', $final); |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | } |
| | | public function registerAdminAction():void |
| | | { |
| | | $admin = JVB()->admin(); |
| | | $admin = AdminPages::getInstance(); |
| | | $admin->registerAction( |
| | | 'Restart Stuck Operations', |
| | | 'restart-stuck-operations', |
| | |
| | | |
| | | $message .= "Please check the error logs for more details."; |
| | | |
| | | return JVB()->email()->sendEmail($admin_email, $subject, $message); |
| | | return Emails::getInstance()->sendEmail($admin_email, $subject, $message); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | $success_rate = round(($total_success / max(1, $total_ops)) * 100, 1); |
| | | |
| | | $message = JVB()->email()->h1('Daily Queue Performance Report'); |
| | | $message = Emails::getInstance()->h1('Daily Queue Performance Report'); |
| | | $message .= sprintf('<p>Report for <strong>%s</strong></p>', $yesterday); |
| | | |
| | | // Summary stats in grid |
| | | $stats = [ |
| | | JVB()->email()->stat($total_ops, 'Operations'), |
| | | JVB()->email()->stat($total_success, 'Successful', '✓'), |
| | | JVB()->email()->stat($total_failed, 'Failed', $total_failed > 0 ? 'âš ' : ''), |
| | | JVB()->email()->stat($success_rate . '%', 'Success Rate') |
| | | Emails::getInstance()->stat($total_ops, 'Operations'), |
| | | Emails::getInstance()->stat($total_success, 'Successful', '✓'), |
| | | Emails::getInstance()->stat($total_failed, 'Failed', $total_failed > 0 ? 'âš ' : ''), |
| | | Emails::getInstance()->stat($success_rate . '%', 'Success Rate') |
| | | ]; |
| | | $message .= JVB()->email()->grid($stats, 4); |
| | | $message .= Emails::getInstance()->grid($stats, 4); |
| | | |
| | | $message .= JVB()->email()->spacer(20); |
| | | $message .= Emails::getInstance()->spacer(20); |
| | | |
| | | // Alert if success rate is low |
| | | if ($success_rate < 90) { |
| | | $message .= JVB()->email()->alert( |
| | | $message .= Emails::getInstance()->alert( |
| | | sprintf('Success rate of %s%% is below the 90%% threshold', $success_rate), |
| | | 'warning' |
| | | ); |
| | | } |
| | | |
| | | $message .= JVB()->email()->h2('Details by Operation Type'); |
| | | $message .= Emails::getInstance()->h2('Details by Operation Type'); |
| | | |
| | | // Details for each operation type |
| | | foreach ($metrics as $metric) { |
| | | $details = []; |
| | | $details[] = ['label' => 'Total', 'value' => $metric->total_operations]; |
| | | $details[] = ['label' => 'Success', 'value' => JVB()->email()->badge($metric->successful_operations, 'success')]; |
| | | $details[] = ['label' => 'Failed', 'value' => $metric->failed_operations > 0 ? JVB()->email()->badge($metric->failed_operations, 'error') : '0']; |
| | | $details[] = ['label' => 'Success', 'value' => Emails::getInstance()->badge($metric->successful_operations, 'success')]; |
| | | $details[] = ['label' => 'Failed', 'value' => $metric->failed_operations > 0 ? Emails::getInstance()->badge($metric->failed_operations, 'error') : '0']; |
| | | |
| | | if ($metric->average_duration) { |
| | | $details[] = ['label' => 'Avg Duration', 'value' => round($metric->average_duration, 2) . 's']; |
| | |
| | | $details[] = ['label' => 'Peak Memory', 'value' => $memory_mb . ' MB']; |
| | | } |
| | | |
| | | $message .= JVB()->email()->card( |
| | | JVB()->email()->table($details), |
| | | $message .= Emails::getInstance()->card( |
| | | Emails::getInstance()->table($details), |
| | | esc_html($metric->type) |
| | | ); |
| | | } |
| | |
| | | // Current queue status |
| | | $pending_count = $this->getCurrentQueueSize(); |
| | | if ($pending_count > 0) { |
| | | $message .= JVB()->email()->spacer(20); |
| | | $message .= JVB()->email()->notice( |
| | | $message .= Emails::getInstance()->spacer(20); |
| | | $message .= Emails::getInstance()->notice( |
| | | sprintf('<strong>Current Queue:</strong> %d operations pending', $pending_count) |
| | | ); |
| | | } |
| | | |
| | | $message .= JVB()->email()->spacer(20); |
| | | $message .= JVB()->email()->button(admin_url('admin.php?page=jvb-queue'), 'View Queue Dashboard'); |
| | | $message .= Emails::getInstance()->spacer(20); |
| | | $message .= Emails::getInstance()->button(admin_url('admin.php?page=jvb-queue'), 'View Queue Dashboard'); |
| | | |
| | | JVB()->email()->sendEmail($admin_email, $subject, $message, 'QUEUE REPORT'); |
| | | Emails::getInstance()->sendEmail($admin_email, $subject, $message, 'QUEUE REPORT'); |
| | | } |
| | | |
| | | /** |
| File was renamed from inc/managers/ReferralManager.php |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Cloudflare; |
| | | use JVBase\meta\Form; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\meta\Sanitizer; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\ReferralRoutes; |
| | | use JVBase\ui\CRUDSkeleton; |
| | | use JVBase\ui\Tabs; |
| | | use JVBase\base\Site; |
| | |
| | | * Handles user referral codes, tracking, rewards, and reporting. |
| | | * Keeps referrals separate from invitations system. |
| | | */ |
| | | class ReferralManager |
| | | class Referrals |
| | | { |
| | | protected MagicLinkManager $magic_link; |
| | | use Instance; |
| | | protected MagicLinks $magic_link; |
| | | protected Cache $cache; |
| | | protected Cache $requestCache; |
| | | protected Cache $statsCache; |
| | |
| | | protected CustomTable $janeClients; |
| | | protected CustomTable $rewards; |
| | | protected CustomTable $treatments; |
| | | protected Rest $route; |
| | | |
| | | // Default reward settings |
| | | protected array $default_settings = [ |
| | |
| | | |
| | | protected array $settings; |
| | | |
| | | public function __construct() |
| | | private function __construct() |
| | | { |
| | | $this->route = ReferralRoutes::getInstance(); |
| | | $this->defineTables(); |
| | | $this->role = Site::getDefaultReferralRole(); |
| | | $this->role = Site::getReferralRole(); |
| | | $this->default_settings['referral_role'] = $this->role; |
| | | |
| | | $this->cache = Cache::for('referrals', WEEK_IN_SECONDS); |
| | |
| | | // Schedule cron jobs for reports |
| | | $this->registerCronJobs(); |
| | | |
| | | // Add admin bar label for referral page |
| | | add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999); |
| | | |
| | | // Add admin notice to referral page edit screen |
| | | add_action('admin_notices', [$this, 'showReferralPageNotice']); |
| | | |
| | |
| | | add_action('admin_init', [$this, 'registerSettings']); |
| | | // Handle admin page form submission |
| | | add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3); |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | } |
| | | |
| | | protected function defineTables():void |
| | |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'from_user' => "{$table->getUserIDType()} NOT NULL", |
| | | 'to_user' => "{$table->getUserIDType()} NOT NULL", |
| | | 'to_name' => 'varchar(255) NOT NULL', |
| | | 'to_email' => 'varchar(255) NOT NULL', |
| | | 'to_phone' => 'varchar(50) NOT NULL', |
| | | 'from_user' => "{$table->getUserIDType()} NOT NULL", //The person doing the referring |
| | | 'to_user' => "{$table->getUserIDType()} DEFAULT NULL", //The person being referred |
| | | 'to_name' => 'varchar(255) DEFAULT NULL', |
| | | 'to_email' => 'varchar(255) DEFAULT NULL', |
| | | 'to_phone' => 'varchar(50) DEFAULT NULL', |
| | | 'referral_code'=> 'varchar(50) NOT NULL', |
| | | 'status' => "ENUM('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'", |
| | | 'status' => "ENUM('invited', 'pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'", |
| | | 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'last_invited'=> 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'consulted_at'=> 'datetime DEFAULT NULL', |
| | | 'treated_at'=> 'datetime DEFAULT NULL', |
| | | 'treatment_count' => 'int DEFAULT 0', |
| | |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => '(`to_user`)'], |
| | | // ['key' => 'UNIQUE', 'value' => '(`to_user`)'], |
| | | 'from_user (`from_user`)', |
| | | 'to_user (`to_user`)', |
| | | 'status (`status`)', |
| | | 'code (`referral_code`)', |
| | | 'date (`created_at`)', |
| | |
| | | 'jvb-data-store', |
| | | ]; |
| | | |
| | | if (Site::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) { |
| | | JVB()->connect('cloudflare')->enqueueTurnstileScripts(); |
| | | if (Site::hasIntegration('cloudflare') && Cloudflare::getInstance()->isSetUp()) { |
| | | Cloudflare::getInstance()->enqueueTurnstileScripts(); |
| | | } |
| | | if (is_singular(BASE.'dash')) { |
| | | $requirements[] = 'jvb-form'; |
| | |
| | | * |
| | | * @param int $user_id |
| | | * @param array $userData |
| | | * @return bool; |
| | | * @return void |
| | | */ |
| | | public function processReferral(int $user_id, array $userData): bool |
| | | public function processReferral(int $user_id, array $userData):void |
| | | { |
| | | $referral = $this->referrals->get(['to_user' => $user_id]); |
| | | |
| | | if (empty($referral)) { |
| | | $referral = $this->referrals->get(['to_email' => $userData['email']]); |
| | | if (!array_key_exists('ref', $_POST) || empty($_POST['ref'])) { |
| | | return; |
| | | } |
| | | if (empty($referral)) { |
| | | // Check session/cookie if not in meta |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | } |
| | | $referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? ''; |
| | | if (!empty ($referral_code)) { |
| | | $referral = [ |
| | | 'to_user' => $user_id, |
| | | 'referral_code' => $referral_code, |
| | | 'to_email' => $userData['user_email'] |
| | | ]; |
| | | } |
| | | $code = sanitize_text_field($_POST['ref']); |
| | | $referredBy = $this->codes->pluck('user_id', ['code' => $code]); |
| | | if (!empty($referredBy)) { |
| | | $this->createReferralFromID($referredBy[0], $user_id, $code); |
| | | } |
| | | |
| | | if (empty($referral)) { |
| | | return false; // No referral code - regular registration |
| | | } |
| | | |
| | | // Find the from_user |
| | | $from_user = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]); |
| | | if (empty($from_user)) { |
| | | //This should not happen, but whatever |
| | | return false; |
| | | } |
| | | |
| | | // Check if this email already has a referral record |
| | | if ($this->isEmailInvited($referral['to_email'])) { |
| | | return false; |
| | | } |
| | | |
| | | $from_user = $from_user[0]; |
| | | $record = $this->referrals->findOrCreate([ |
| | | 'to_user' => $user_id, |
| | | 'referral_code' => $referral['referral_code'], |
| | | ], [ |
| | | 'from_user' => $from_user, |
| | | 'to_email' => $referral['to_email'], |
| | | 'to_name' => $userData['first_name'], |
| | | // 'to_phone' => |
| | | 'status' => 'pending' |
| | | ]); |
| | | |
| | | if (!$record) { |
| | | error_log('[ReferralManager]::processReferral Could not update record for user: '.print_r($referral, true)); |
| | | return false; |
| | | } |
| | | |
| | | |
| | | // Clean up temp data |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | | if (isset($_SESSION[BASE . 'referral_code'])) { |
| | | unset($_SESSION[BASE . 'referral_code']); |
| | | } |
| | | if (isset($_COOKIE[BASE . 'referral_code'])) { |
| | | setcookie(BASE . 'referral_code', '', time() - 3600, '/'); |
| | | } |
| | | |
| | | // Clear caches |
| | | $this->cache->flush(); |
| | | |
| | | // Fire action for tracking |
| | | do_action('jvb_referral_processed', $user_id, $from_user->ID, $referral['referral_code']); |
| | | |
| | | // Send notification to from_user |
| | | $this->sendReferrerNotification($from_user->ID, $userData['display_name']); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Create a referral record in the database |
| | | * |
| | | * @param int $from_user_id |
| | | * @param int $to_user_id |
| | | * @param string $code |
| | | * @return int|false |
| | | * @param int $from_user_id The person sending the referral |
| | | * @param string $name The person who was referred |
| | | * @param string $email The person who was referred's email |
| | | * @param ?string $phone The person who was referred's phone (optional) |
| | | * @param ?string $code The person sending the referral's custom code |
| | | * @return array |
| | | */ |
| | | public function createReferral(int $from_user_id, int $to_user_id, string $code) |
| | | public function createReferralFromFields(int $from_user_id, string $name, string $email, ?string $phone = null, ?string $code = null):array |
| | | { |
| | | $user = get_user_by('ID', $to_user_id); |
| | | if (!get_userdata($from_user_id)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'From user does not exist', |
| | | ]; |
| | | } |
| | | |
| | | return $this->referrals->findOrCreate([ |
| | | [ |
| | | 'to_user' => $to_user_id, |
| | | ], |
| | | [ |
| | | 'from_user' => $from_user_id, |
| | | 'to_name' => $user->display_name, |
| | | 'to_email' => $user->user_email, |
| | | 'to_phone' => get_user_meta($to_user_id, BASE . 'phone', true) ?: '', |
| | | 'referral_code' => $code, |
| | | 'status' => 'pending', |
| | | ] |
| | | ]); |
| | | if (filter_var($email, FILTER_VALIDATE_EMAIL)) { |
| | | if(email_exists($email)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Email already exists', |
| | | ]; |
| | | } |
| | | } |
| | | if ($phone && !empty($this->referrals->pluck('to_phone', ['to_phone' => Sanitizer::sanitizePhone($phone)]))) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Phone already used' |
| | | ]; |
| | | } |
| | | $insert = [ |
| | | 'from_user' => $from_user_id, |
| | | 'referral_code' => is_null($code)? $this->getUserReferralCode($from_user_id) : sanitize_text_field($code), |
| | | 'to_name' => $name, |
| | | 'to_email' => $email, |
| | | 'status' => 'invited', |
| | | ]; |
| | | if ($phone) { |
| | | $insert['to_phone'] = $phone; |
| | | } |
| | | |
| | | $created = $this->referrals->insert($insert); |
| | | $this->statsCache->forget($from_user_id); |
| | | return [ |
| | | 'success' => (bool) $created, |
| | | 'message' => 'Referral created' |
| | | ]; |
| | | } |
| | | /** |
| | | * Create a referral record in the database |
| | | * |
| | | * @param int $from_user_id The person sending the referral |
| | | * @param int $to_user_id The person who was referred |
| | | * @param ?string $code Custom code, if not default |
| | | * @return array |
| | | */ |
| | | public function createReferralFromID(int $from_user_id, int $to_user_id, ?string $code = null):array |
| | | { |
| | | $user = get_userdata($to_user_id); |
| | | |
| | | $code = !is_null($code) ? sanitize_text_field($code) : $this->getUserReferralCode($to_user_id); |
| | | |
| | | $where = [ |
| | | 'to_email' => $user->user_email, |
| | | 'to_name' => $user->first_name ?: $user->display_name |
| | | ]; |
| | | $phone = false; |
| | | if (array_key_exists('phone', $_POST) && !empty($_POST['phone'])) { |
| | | $phone = Sanitizer::sanitizePhone($_POST['phone']); |
| | | if (!empty($phone)) { |
| | | $where = [ |
| | | 'to_phone' => $phone, |
| | | ]; |
| | | } |
| | | } elseif (!empty(Meta::forUser($to_user_id)->get('phone'))) { |
| | | $phone = Meta::forUser($to_user_id)->get('phone'); |
| | | } |
| | | $where = [ |
| | | 'OR' => $where, |
| | | 'status' => 'invited' |
| | | ]; |
| | | |
| | | $update = [ |
| | | 'from_user' => $from_user_id, |
| | | 'to_user' => $to_user_id, |
| | | 'to_name' => $user->first_name ?: $user->display_name, |
| | | 'to_email' => $user->user_email, |
| | | 'status' => 'pending', |
| | | 'referral_code' => $code |
| | | ]; |
| | | if ($phone) { |
| | | $update ['to_phone'] = $phone; |
| | | } |
| | | $existing = $this->referrals->get($where); |
| | | if ($existing) { |
| | | if ($existing->from_user != $from_user_id) { |
| | | $this->referrals->update([ |
| | | 'status' => 'cancelled', |
| | | ], $where); |
| | | |
| | | } else { |
| | | $updated = $this->referrals->update($update, $where); |
| | | $this->sendReferrerNotification($from_user_id, $update['to_name']); |
| | | return [ |
| | | 'success' => (bool) $updated, |
| | | 'message' => $updated ? 'Successfully created referral' : 'Referral could not be updated' |
| | | ]; |
| | | } |
| | | } |
| | | $created = $this->referrals->insert($update); |
| | | |
| | | $this->statsCache->forget($from_user_id); |
| | | return [ |
| | | 'success' => (bool) $created, |
| | | 'message' => $created ? 'Successfully created referral' : 'Referral could not be added' |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | |
| | | 'order' => 'DESC' |
| | | ]; |
| | | |
| | | $where = [ |
| | | 'from_user' => $user_id, |
| | | ]; |
| | | |
| | | foreach ($this->referrals->getColumnKeys() as $col) { |
| | | if (array_key_exists($col, $args)) { |
| | | if ($col === 'status' && $args[$col] !== 'all') { |
| | | $where[$col] = $args[$col]; |
| | | } else { |
| | | $where[$col] = $args[$col]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | $args = wp_parse_args($args, $defaults); |
| | | $args['status'] = strtolower($args['status']); |
| | | $tableArgs = [ |
| | | 'where' => [ |
| | | 'from_user' => $user_id, |
| | | ], |
| | | 'where' => $where, |
| | | 'per_page' => $args['per_page'], |
| | | 'page' => $args['page'], |
| | | 'orderby' => $args['orderby'], |
| | | 'order' => $args['order'] |
| | | ]; |
| | | if (in_array($args['status'], ['pending', 'consulted', 'treated', 'cancelled'])){ |
| | | |
| | | if ($args['status'] !== 'all'){ |
| | | $tableArgs['status'] = $args['status']; |
| | | } |
| | | $referrals = $this->referrals->getMany($tableArgs); |
| | | $referrals = $this->referrals->getMany($tableArgs, false); |
| | | |
| | | return array_map(function($referral) { |
| | | $last_invite = get_transient('referral_last_invite_' . md5($referral->to_email)); |
| | | $can_resend = !$last_invite || (time() - $last_invite) > WEEK_IN_SECONDS; |
| | | $can_resend = $this->checkCanResendEmail($referral->id); |
| | | $status = match($referral->status) { |
| | | 'consulted' => 'Awaiting Treatment', |
| | | 'treated' => 'Rewarded!', |
| | | 'invited' => 'Invited, but has not used', |
| | | default => 'Pending', |
| | | }; |
| | | return [ |
| | | 'id' => $referral->id, |
| | | 'to_name' => $referral->to_name, |
| | | 'to_email' => $referral->to_email, |
| | | 'referred_at' => JVB()->routes('referral')->formatTimestamp($referral->referred_at), |
| | | 'referred_at' => ReferralRoutes::getInstance()->formatTimestamp($referral->referred_at), |
| | | 'referral_status'=> $status, |
| | | 'can_resend' => $can_resend |
| | | ]; |
| | |
| | | |
| | | } |
| | | |
| | | protected function checkCanResendEmail(int|object $referral): bool |
| | | { |
| | | if (is_int($referral)) { |
| | | $referral = $this->referrals->get(['id'=>$referral]); |
| | | if (!$referral) { |
| | | return false; |
| | | } |
| | | } |
| | | return (time() - $referral->updated_at) > WEEK_IN_SECONDS; |
| | | } |
| | | |
| | | /** |
| | | * Get referral statistics for a user |
| | | * |
| | |
| | | return $this->statsCache->remember( |
| | | $user_id, |
| | | function() use ($user_id) { |
| | | $stats = $this->referrals->queryResults( |
| | | "SELECT |
| | | COUNT(*) as code_used, |
| | | SUM(CASE WHEN status IN ('consulted', 'treated') THEN 1 ELSE 0 END) as consultations, |
| | | SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treatments, |
| | | SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending |
| | | FROM {table} |
| | | WHERE from_user = %d", |
| | | [$user_id], |
| | | ARRAY_A |
| | | ); |
| | | $stats = $stats[0] ?? []; |
| | | |
| | | $rewards = $this->rewards->queryVar( |
| | | "SELECT SUM(amount) FROM {table} WHERE user_id = %d AND reward_type = 'from_user'", |
| | | [$user_id] |
| | | ); |
| | | |
| | | $stats['total_rewards'] = floatval($rewards ?? 0); |
| | | $stats = [ |
| | | 'total' => $this->referrals->count(['user_id' => $user_id]), |
| | | 'invited' => $this->referrals->count(['user_id' => $user_id, 'status' => 'invited']), |
| | | 'pending' => $this->referrals->count(['user_id' => $user_id, 'status' => 'pending']), |
| | | 'consulted' =>$this->referrals->count(['user_id' => $user_id, 'status' => 'consulted']), |
| | | 'treated' => $this->referrals->count(['user_id' => $user_id, 'status' => 'treated']), |
| | | 'total_rewards' => $this->rewards->sum('amount', ['user_id' => $user_id]), |
| | | 'available_rewards' => $this->rewards->sum('amount', ['user_id' => $user_id, 'redeemed_at' => ['NOT NULL', null]]), |
| | | ]; |
| | | $stats['user_id'] = $user_id; |
| | | return $stats; |
| | | } |
| | |
| | | return; |
| | | } |
| | | |
| | | $content = JVB()->email()->h1('Daily Referral Report'); |
| | | $content .= JVB()->email()->stat( |
| | | $content = Emails::getInstance()->h1('Daily Referral Report'); |
| | | $content .= Emails::getInstance()->stat( |
| | | count($new_referrals), |
| | | count($new_referrals) === 1 ? 'New Referral' : 'New Referrals', |
| | | 'From ' . $yesterday |
| | | ); |
| | | |
| | | $content .= JVB()->email()->spacer(20); |
| | | $content .= JVB()->email()->h2('New Referrals'); |
| | | $content .= Emails::getInstance()->spacer(20); |
| | | $content .= Emails::getInstance()->h2('New Referrals'); |
| | | |
| | | // Build list of referrals |
| | | foreach ($new_referrals as $ref) { |
| | |
| | | ); |
| | | $cardContent .= sprintf( |
| | | '<p style="font-size:13px;color:%s;">Referred by: %s | Code: %s</p>', |
| | | JVB()->email()->colours['dark-200'], |
| | | Emails::getInstance()->colours['dark-200'], |
| | | esc_html($ref->from_name), |
| | | JVB()->email()->badge($ref->referral_code, 'info') |
| | | Emails::getInstance()->badge($ref->referral_code, 'info') |
| | | ); |
| | | |
| | | $content .= JVB()->email()->card($cardContent); |
| | | $content .= Emails::getInstance()->card($cardContent); |
| | | } |
| | | |
| | | $to = get_option('admin_email'); |
| | |
| | | count($new_referrals) !== 1 ? 's' : '' |
| | | ); |
| | | |
| | | JVB()->email()->sendEmail($to, $subject, $content, 'DAILY REPORT'); |
| | | Emails::getInstance()->sendEmail($to, $subject, $content, 'DAILY REPORT'); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function sendWeeklyReport(): void |
| | | { |
| | | $top_from_users = $this->getTopReferrers(10, 'week'); |
| | | $total_referrals = $this->referrals->queryVar( |
| | | "SELECT COUNT(*) FROM {table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)" |
| | | ); |
| | | $total_referrals = $this->referrals->count(['created_at' => ['>=', "DATE_SUB(NOW(), INTERVAL 1 WEEK)"]]); |
| | | |
| | | if ($total_referrals == 0) { |
| | | return; |
| | | } |
| | | $top_from_users = $this->getTopReferrers(10, 'week'); |
| | | |
| | | $content = JVB()->email()->h1('Weekly Referral Summary'); |
| | | $content .= JVB()->email()->stat( |
| | | $content = Emails::getInstance()->h1('Weekly Referral Summary'); |
| | | $content .= Emails::getInstance()->stat( |
| | | $total_referrals, |
| | | 'Total Referrals', |
| | | 'This week' |
| | | ); |
| | | |
| | | $content .= JVB()->email()->spacer(30); |
| | | $content .= JVB()->email()->h2('Top 10 Referrers'); |
| | | $content .= Emails::getInstance()->spacer(30); |
| | | $content .= Emails::getInstance()->h2('Top 10 Referrers'); |
| | | |
| | | // Leaderboard style |
| | | $rank = 1; |
| | | foreach ($top_from_users as $from_user) { |
| | | $rankBadge = $rank <= 3 |
| | | ? JVB()->email()->badge('#' . $rank, $rank === 1 ? 'success' : 'info') |
| | | : '<span style="font-weight:600;color:' . JVB()->email()->colours['dark-200'] . ';">#' . $rank . '</span>'; |
| | | ? Emails::getInstance()->badge('#' . $rank, $rank === 1 ? 'success' : 'info') |
| | | : '<span style="font-weight:600;color:' . Emails::getInstance()->colours['dark-200'] . ';">#' . $rank . '</span>'; |
| | | |
| | | $cardContent = sprintf( |
| | | '<p>%s <strong>%s</strong></p>', |
| | |
| | | ); |
| | | |
| | | $stats = [ |
| | | JVB()->email()->stat($from_user->referral_count, 'Total Referrals'), |
| | | JVB()->email()->stat($from_user->treated_count, 'Treated') |
| | | Emails::getInstance()->stat($from_user->referral_count, 'Total Referrals'), |
| | | Emails::getInstance()->stat($from_user->treated_count, 'Treated') |
| | | ]; |
| | | $cardContent .= JVB()->email()->grid($stats, 2); |
| | | $cardContent .= Emails::getInstance()->grid($stats, 2); |
| | | |
| | | $content .= JVB()->email()->card($cardContent); |
| | | $content .= Emails::getInstance()->card($cardContent); |
| | | $rank++; |
| | | } |
| | | |
| | | $to = get_option('admin_email'); |
| | | $subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y'); |
| | | |
| | | JVB()->email()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY'); |
| | | Emails::getInstance()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY'); |
| | | } |
| | | |
| | | /** |
| | |
| | | ]; |
| | | } |
| | | |
| | | $content .= JVB()->email()->table($from_users, 'Top 10 Referrers This Week'); |
| | | $content .= Emails::getInstance()->table($from_users, 'Top 10 Referrers This Week'); |
| | | |
| | | return $content; |
| | | } |
| | |
| | | function getUnloggedInReferral(): string |
| | | { |
| | | ob_start(); |
| | | JVB()->connect('cloudflare')->renderTurnstile(); |
| | | Cloudflare::getInstance()->renderTurnstile(); |
| | | $turnstile = ob_get_clean(); |
| | | |
| | | $reward_text = $this->getRewardText(true); |
| | |
| | | <p>Or click the button below:</p> |
| | | %s |
| | | </div>', |
| | | JVB()->email()->link($code), |
| | | JVB()->email()->button($share_url, 'Share Your Code') |
| | | Emails::getInstance()->link($code), |
| | | Emails::getInstance()->button($share_url, 'Share Your Code') |
| | | ); |
| | | } |
| | | |
| | |
| | | return new WP_Error('invalid_email', 'Invalid email address'); |
| | | } |
| | | |
| | | |
| | | |
| | | // Check if already registered |
| | | if (email_exists($invitee_email)) { |
| | | return new WP_Error('user_exists', 'This person already has an account'); |
| | | } |
| | | |
| | | // Get from_user info |
| | | $from_user = get_user_by('ID', $user_id); |
| | | $from_user = get_userdata($user_id); |
| | | $referral_code = $this->getUserReferralCode($user_id); |
| | | |
| | | if ($referral_code) { |
| | | return $referral_code; |
| | | // Record the invitation attempt (for rate limiting only) |
| | | if ($this->emailAlreadyTreated($invitee_email)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'User already treated' |
| | | ]; |
| | | } |
| | | |
| | | // Record the invitation attempt (for rate limiting only) |
| | | $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name); |
| | | |
| | | // Create registration URL with token (opens sidebar with prefilled form) |
| | | $token_data = [ |
| | | 'name' => sanitize_text_field($invitee_name), |
| | | 'email' => $invitee_email, |
| | | 'expires' => time() + (30 * DAY_IN_SECONDS) |
| | | ]; |
| | | $record = $this->referrals->findOrCreate( |
| | | [ |
| | | 'from_user' => $user_id, |
| | | 'to_email' => $invitee_email, |
| | | 'referral_code' => $referral_code, |
| | | ], |
| | | [ |
| | | 'to_name' => $invitee_name, |
| | | 'status' => 'invited', |
| | | 'last_invited' => current_time('mysql') |
| | | ] |
| | | ); |
| | | |
| | | // Encode the token |
| | | $token = base64_encode(json_encode($token_data)); |
| | | $registration_url = add_query_arg([ |
| | | 'ref' => $referral_code, |
| | | 'rname' => sanitize_text_field($invitee_name), |
| | |
| | | esc_html(get_bloginfo('name')), |
| | | nl2br(esc_html($message)), |
| | | esc_html($reward_text), |
| | | JVB()->email()->button($registration_url, 'Register & Get Your Reward') |
| | | Emails::getInstance()->button($registration_url, 'Register & Get Your Reward') |
| | | ); |
| | | |
| | | // Send email |
| | | $sent = JVB()->email()->sendEmail( |
| | | $sent = Emails::getInstance()->sendEmail( |
| | | $invitee_email, |
| | | $subject, |
| | | $email_content |
| | |
| | | 'failed' => [] |
| | | ]; |
| | | |
| | | foreach ($invitations as $invite) { |
| | | $email = $invite['email'] ?? ''; |
| | | $name = $invite['name'] ?? ''; |
| | | |
| | | if (empty($email) || empty($name)) { |
| | | $results['failed'][] = [ |
| | | 'email' => $email, |
| | | 'name' => $name, |
| | | 'reason' => 'Missing email or name' |
| | | ]; |
| | | continue; |
| | | $invitations = array_filter($invitations, function ($invite) { |
| | | if (!array_key_exists('email', $invite) || !filter_var($invite['email'], FILTER_VALIDATE_EMAIL)) { |
| | | return false; |
| | | } |
| | | if (!array_key_exists('name', $invite) || empty ($invite['name'])) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }); |
| | | |
| | | foreach ($invitations as $invite) { |
| | | $email = $invite['email']; |
| | | $name = $invite['name']; |
| | | |
| | | $result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message); |
| | | |
| | | if (is_wp_error($result)) { |
| | | if (!$result['success']) { |
| | | $results['failed'][] = [ |
| | | 'email' => $email, |
| | | 'name' => $name, |
| | | 'reason' => $result->get_error_message() |
| | | 'reason' => $result['message'] |
| | | ]; |
| | | } else { |
| | | $results['success'][] = [ |
| | |
| | | */ |
| | | protected function checkEmailRateLimit(int $user_id):bool|string |
| | | { |
| | | $hourly_key = 'referral_invites_hour_' . $user_id; |
| | | $count = (int) get_transient($hourly_key); |
| | | $key = BASE.'referral_email_limit'; |
| | | $stored = get_user_meta($user_id, $key, true); |
| | | $count = empty($stored) ? 0 : count($stored); |
| | | |
| | | if ($count >= 15) { |
| | | return 'hourly_limit_reached'; |
| | | return false; |
| | | } |
| | | |
| | | set_transient($hourly_key, $count + 1, HOUR_IN_SECONDS); |
| | | $stored = empty($stored) ? [] : $stored; |
| | | $stored[] = time(); |
| | | $stored = array_filter($stored, function($time) { |
| | | return time() - $time < HOUR_IN_SECONDS; |
| | | }); |
| | | |
| | | update_user_meta($user_id, $key, $stored); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Check if an email has already been invited |
| | | * |
| | | * @param string $email |
| | | * @return bool |
| | | */ |
| | | protected function isEmailInvited(string $email): bool |
| | | { |
| | | // Check invitation tracking table |
| | | $invitation_key = 'referral_invite_' . md5($email); |
| | | $invited = get_transient($invitation_key); |
| | | |
| | | if ($invited) { |
| | | return true; |
| | | } |
| | | |
| | | // Check if there's a pending referral for this email |
| | | $existing = $this->referrals->pluck('id', ['to_email' => $email]); |
| | | |
| | | return !empty($existing); |
| | | } |
| | | |
| | | /** |
| | | * Record invitation attempt (for tracking and preventing duplicates) |
| | | * |
| | | * @param int $user_id |
| | | * @param string $email |
| | | * @param string $name |
| | | */ |
| | | protected function recordInvitationAttempt(int $user_id, string $email, string $name): void |
| | | { |
| | | // Store for 30 days (same as magic link invitation validity) |
| | | $invitation_key = 'referral_invite_' . md5($email); |
| | | $data = [ |
| | | 'inviter_id' => $user_id, |
| | | 'email' => $email, |
| | | 'name' => $name, |
| | | 'sent_at' => current_time('mysql') |
| | | ]; |
| | | |
| | | set_transient($invitation_key, $data, 30 * DAY_IN_SECONDS); |
| | | |
| | | // Also log in user meta for tracking |
| | | $sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: []; |
| | | $sent_invites[] = [ |
| | | 'email' => $email, |
| | | 'name' => $name, |
| | | 'sent_at' => current_time('mysql') |
| | | ]; |
| | | update_user_meta($user_id, BASE . 'referral_invites_sent', $sent_invites); |
| | | } |
| | | |
| | | /** |
| | | * Get user's invitation stats |
| | | * |
| | | * @param int $user_id |
| | | * @return array |
| | | */ |
| | | public function getUserInvitationStats(int $user_id): array |
| | | { |
| | | $sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: []; |
| | | |
| | | // Count invites sent in last hour |
| | | $one_hour_ago = strtotime('-1 hour'); |
| | | $recent_count = 0; |
| | | |
| | | foreach ($sent_invites as $invite) { |
| | | if (strtotime($invite['sent_at']) > $one_hour_ago) { |
| | | $recent_count++; |
| | | } |
| | | } |
| | | |
| | | return [ |
| | | 'total_sent' => count($sent_invites), |
| | | 'sent_last_hour' => $recent_count, |
| | | 'remaining_this_hour' => max(0, 15 - $recent_count), |
| | | 'can_send_more' => $recent_count < 15 |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Export referrals for Jane App cross-reference |
| | | * |
| | | * @param string $start_date Y-m-d format |
| | |
| | | 'menu_title' => 'Referrals', |
| | | 'capability' => 'manage_options', |
| | | 'menu_slug' => BASE . 'referral-admin', |
| | | 'callback' => [self::class, 'renderAdminPageStatic'] |
| | | 'callback' => [self::class, 'renderAdminPage'] |
| | | ]; |
| | | AdminPages::addSubPage(BASE.'referral-admin', $subpage); |
| | | } |
| | | |
| | | /** |
| | | * Static wrapper for renderAdminPage |
| | | * Called by WordPress when admin page is rendered |
| | | */ |
| | | public static function renderAdminPageStatic(): void |
| | | { |
| | | // Get the properly initialized instance from JVB singleton |
| | | JVB()->referrals()->renderAdminPage(); |
| | | } |
| | | |
| | | /** |
| | | * Register settings |
| | | */ |
| | | public function registerSettings(): void |
| | |
| | | */ |
| | | protected function renderReferralStats(bool $wrapCard = false): string |
| | | { |
| | | global $wpdb; // Use fresh global instead of stored reference |
| | | $isAdmin = current_user_can('manage_options'); |
| | | $current = get_current_user_id(); |
| | | $types = ['total' => 'Total Referrals', 'invited' => 'Invited', 'pending' => 'Pending', 'treated' => 'Treated']; |
| | | |
| | | ob_start(); |
| | | $out = '<table class="widefat">'; |
| | | foreach ($types as $type => $label) { |
| | | $status = $type === 'total' ? [] : ['status' => $type]; |
| | | |
| | | // Get fresh table name references |
| | | $referrals_table = $wpdb->prefix . BASE . 'referrals'; |
| | | |
| | | // Use proper WordPress prepare even for COUNT |
| | | $total_referrals = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | "SELECT COUNT(*) FROM `{$referrals_table}` WHERE 1=%d", |
| | | 1 |
| | | ) |
| | | ); |
| | | |
| | | $pending_referrals = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s", |
| | | 'pending' |
| | | ) |
| | | ); |
| | | |
| | | $treated_referrals = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s", |
| | | 'treated' |
| | | ) |
| | | ); |
| | | |
| | | ?> |
| | | <table class="widefat"> |
| | | <tr> |
| | | <th>Total Referrals</th> |
| | | <td><?= esc_html($total_referrals ?? 0) ?></td> |
| | | </tr> |
| | | <tr> |
| | | <th>Pending</th> |
| | | <td><?= esc_html($pending_referrals ?? 0) ?></td> |
| | | </tr> |
| | | <tr> |
| | | <th>Treated</th> |
| | | <td><?= esc_html($treated_referrals ?? 0) ?></td> |
| | | </tr> |
| | | </table> |
| | | <?php |
| | | $table = ob_get_clean(); |
| | | $count = $isAdmin ? |
| | | $this->referrals->count($status) : |
| | | $this->referrals->count(array_merge( |
| | | ['from_user' => $current], |
| | | $status |
| | | )); |
| | | $out .= sprintf( |
| | | '<tr> |
| | | <th>%s</th> |
| | | <td>%s</td> |
| | | </tr>', |
| | | $label, |
| | | esc_html($count) |
| | | ); |
| | | } |
| | | $out .= '</table>'; |
| | | |
| | | if ($wrapCard) { |
| | | $table = '<div class="card"> |
| | | $out = '<div class="card"> |
| | | <h2>Referral Statistics</h2> |
| | | ' . $table . ' |
| | | ' . $out . ' |
| | | </div>'; |
| | | } |
| | | |
| | | return $table; |
| | | return $out; |
| | | } |
| | | |
| | | /** |
| | | * Add "Referral Page" label to admin bar |
| | | * |
| | | * @param WP_Admin_Bar $wp_admin_bar |
| | | */ |
| | | public function addReferralPageLabel($wp_admin_bar): void |
| | | { |
| | | if (!is_admin()) { |
| | | return; |
| | | } |
| | | |
| | | if (!$this->referralPage) { |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | } |
| | | if (!$this->referralPage) { |
| | | return; |
| | | } |
| | | |
| | | global $pagenow, $post; |
| | | |
| | | // Check if we're editing the referral page |
| | | if ('post.php' === $pagenow && $post && $post->ID === $this->referralPage) { |
| | | $wp_admin_bar->add_node([ |
| | | 'id' => 'referral-page', |
| | | 'parent' => 'top-secondary', |
| | | 'title' => __('Referral Page', 'jvbase'), |
| | | 'meta' => [ |
| | | 'class' => 'referral-page-notice' |
| | | ] |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the referral page ID |
| | |
| | | } |
| | | if ($post->ID === $this->referralPage) { |
| | | echo '<div class="notice notice-info">'; |
| | | echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvbase') . '</p>'; |
| | | echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvb') . '</p>'; |
| | | echo '</div>'; |
| | | } |
| | | } |
| | |
| | | * @param array $post_data POST data |
| | | * @return array|null Result array or null if not our page |
| | | */ |
| | | public function handleAdminSubmission($result, string $page_slug, array $post_data): ?array |
| | | public function handleAdminSubmission(mixed $result, string $page_slug, array $post_data): ?array |
| | | { |
| | | // Only handle our page |
| | | if ($page_slug !== BASE . 'referral-admin') { |
| | |
| | | home_url('/dash/referrals') |
| | | ); |
| | | |
| | | JVB()->email()->sendEmail( |
| | | Emails::getInstance()->sendEmail( |
| | | $from_user->user_email, |
| | | $subject, |
| | | $message |
| | |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | public function updateStatus(int $referral_id, string $status): bool|WP_Error |
| | | public function updateStatus(int $referral_id, string $status):array |
| | | { |
| | | $referral = $this->referrals->get(['id' => $referral_id]); |
| | | if (!$referral) { |
| | | return new WP_Error('not_found', 'Referral not found'); |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Referral not found' |
| | | ]; |
| | | } |
| | | |
| | | $data = ['status' => $status, "{$status}_at" => current_time('mysql')]; |
| | |
| | | } |
| | | |
| | | $result = $this->referrals->update($data, ['id' => $referral_id]); |
| | | if ($result === false) { |
| | | return new WP_Error('update_failed', 'Failed to update referral status'); |
| | | if (!$result) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Failed to update referral status' |
| | | ]; |
| | | } |
| | | |
| | | if ($status === 'treated') { |
| | | $this->createRewardRecords($referral_id); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | return true; |
| | | $this->cache->forget($referral_id); |
| | | return [ |
| | | 'success' => true, |
| | | 'message' => 'Successfully set status to '.$status |
| | | ]; |
| | | } |
| | | |
| | | public function removeReferral(int $referral_id, int $user_id): bool|WP_Error |
| | | public function removeReferral(int $referral_id, int $user_id):array |
| | | { |
| | | $referral = $this->referrals->get(['id' => $referral_id]); |
| | | if (!$referral) { |
| | | return new WP_Error('not_found', 'Referral not found'); |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Referral not found' |
| | | ]; |
| | | } |
| | | |
| | | if ($referral->from_user != $user_id && !current_user_can('manage_options')) { |
| | | return new WP_Error('unauthorized', 'Unauthorized'); |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Unauthorized' |
| | | ]; |
| | | } |
| | | |
| | | if ($referral->status !== 'pending') { |
| | | return new WP_Error('invalid_status', 'Can only remove pending referrals'); |
| | | if (!in_array($referral->status, ['invited', 'pending'])) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Cannot remove a completed or cancelled referral' |
| | | ]; |
| | | } |
| | | |
| | | $this->referrals->delete(['id' => $referral_id]); |
| | | $this->cache->flush(); |
| | | return true; |
| | | $this->cache->forget($referral_id); |
| | | return [ |
| | | 'success' => true, |
| | | 'message' => 'Successfully removed referral' |
| | | ]; |
| | | } |
| | | |
| | | public function resendInvitation(int $referral_id, int $user_id): bool|WP_Error |
| | | public function resendInvitation(int $referral_id, int $user_id):array |
| | | { |
| | | $referral = $this->referrals->where(['id' => $referral_id, 'from_user' => $user_id])->first(); |
| | | if (!$referral) { |
| | | return new WP_Error('not_found', 'Referral not found'); |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Referral not found' |
| | | ]; |
| | | } |
| | | |
| | | $transient_key = 'referral_last_invite_' . md5($referral->to_email); |
| | | if (get_transient($transient_key)) { |
| | | return new WP_Error('rate_limit', 'Can only resend once per week'); |
| | | if (!$this->checkCanResendEmail($referral)) { |
| | | $days = (time() - $referral->last_invited) / DAY_IN_SECONDS; |
| | | $days = match (true) { |
| | | $days <= 1 => 'until tomorrow', |
| | | $days <= 3 => 'a couple days', |
| | | default => absint($days).' days' |
| | | }; |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Can\'t resend email. Wait '.$days.'.' |
| | | ]; |
| | | } |
| | | |
| | | $result = $this->sendReferralInvitation( |
| | |
| | | 'Just a friendly reminder about my invitation!' |
| | | ); |
| | | |
| | | if (is_wp_error($result)) { |
| | | return $result; |
| | | } |
| | | |
| | | set_transient($transient_key, time(), WEEK_IN_SECONDS); |
| | | return true; |
| | | return [ |
| | | 'success' => (bool) $result, |
| | | 'message' => $result ? 'Sent email' : 'Could not send email' |
| | | ]; |
| | | } |
| | | |
| | | public function getAllReferrals(array $args = []): array |
| | | public function getReferrals(array $args = []): array |
| | | { |
| | | $conditions = ['1=1']; |
| | | $values = []; |
| | |
| | | $values |
| | | ); |
| | | } |
| | | |
| | | public function emailAlreadyTreated(string $email):bool |
| | | { |
| | | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { |
| | | return false; |
| | | } |
| | | if (email_exists($email)) { |
| | | return false; |
| | | } |
| | | $referral = $this->referrals->get([ |
| | | 'to_email' => $email, |
| | | 'status' => [ |
| | | 'IN', |
| | | ['pending', 'consulted', 'treated', 'cancelled'] |
| | | ] |
| | | ]); |
| | | return !empty($referral); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Process queued referral operations |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error |
| | | { |
| | | if ($operation->type !== 'referral_invite') { |
| | | return $result; |
| | | } |
| | | |
| | | $result = $this->sendBatchReferralInvitations( |
| | | $operation->user_id, |
| | | $data['invitations'], |
| | | $data['subject'], |
| | | $data['message'] |
| | | ); |
| | | |
| | | if ($result['success']) { |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'message' => sprintf( |
| | | 'Sent invitations. Success: %d. Failed: %d.', |
| | | count($result['result']['success']), |
| | | count($result['result']['failed']) |
| | | ), |
| | | 'details' => [ |
| | | 'successful' => $result['result']['success'], |
| | | 'failed' => $result['result']['failed'], |
| | | 'total' => count($data['invitations']) |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | } |
| | | |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\ResponseRoutes; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Response Manager |
| | | * |
| | | * Handles a responses |
| | | * TODO |
| | | */ |
| | | class Responses |
| | | { |
| | | use Instance; |
| | | protected CustomTable $responses; |
| | | protected KarmaManager $karma; |
| | | protected ResponseRoutes $route; |
| | | protected int $perPage = 20; |
| | | |
| | | private function __construct() |
| | | { |
| | | $this->route = ResponseRoutes::getInstance(); |
| | | |
| | | $this->defineTables(); |
| | | $this->karma = KarmaManager::for('responses', $this->responses->getFullTableName()); |
| | | $this->responses->setKarma($this->karma->karmaTable()); |
| | | add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1); |
| | | } |
| | | protected function defineTables():void |
| | | { |
| | | $table = CustomTable::for('responses'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'item_id' => "{$table->getPostIDType()} NOT NULL", |
| | | 'type' => 'varchar(50) NOT NULL', |
| | | 'user_id' => "{$table->getUserIdType()} NOT NULL", |
| | | 'parent_id' => 'bigint(20) unsigned DEFAULT 0', |
| | | 'response' => 'text NOT NULL', |
| | | 'upvotes' => 'int NOT NULL DEFAULT 0', |
| | | 'downvotes' => 'int NOT NULL DEFAULT 0', |
| | | 'karmic_score' => 'int NOT NULL DEFAULT 0', |
| | | 'history' => 'json DEFAULT {}', |
| | | 'status' => "ENUM('publish', 'hidden','flagged','deleted') DEFAULT 'publish'", |
| | | 'is_user_deleted'=> 'tinyint(1) DEFAULT 0', |
| | | 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '`id`'], |
| | | '`item_lookup` (`item_id`)', |
| | | '`user_lookup` (`user_id`)', |
| | | '`parent_lookup` (`parent_id`)', |
| | | '`karma` (`karmic_score`)', |
| | | '`upvote` (`upvotes`)', |
| | | '`downvote` (`downvotes`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}responses_item` FOREIGN KEY (`item_id`) |
| | | REFERENCES `{$table->getPostTable()}` (`ID`) ON DELETE CASCADE", |
| | | ]); |
| | | $table->defineTable(); |
| | | |
| | | $this->responses = $table; |
| | | } |
| | | public function responseExists(int $id):bool |
| | | { |
| | | return (bool) $this->responses->get([ |
| | | 'id' => $id, |
| | | ]); |
| | | } |
| | | |
| | | public function updateKarma(int $id, int $upvotes, int $downvotes, int $score):bool |
| | | { |
| | | return $this->responses->update([ |
| | | 'upvotes' => $upvotes, |
| | | 'downvotes' => $downvotes, |
| | | 'karmic_score' => $score, |
| | | ], [ |
| | | 'id' => $id |
| | | ]); |
| | | } |
| | | |
| | | protected function itemExists(int $item_id, string $type):bool |
| | | { |
| | | return (bool)$this->getOriginalItem($item_id, $type); |
| | | } |
| | | public function getOriginalItem(int $item_id, string $type):object|false |
| | | { |
| | | [$method, $type] = $this->determineType($type); |
| | | if (!$method) { |
| | | return false; |
| | | } |
| | | $item = match($method) { |
| | | 'post' => get_post($item_id), |
| | | 'term' => get_term($item_id, $type), |
| | | 'user' => get_userdata($item_id), |
| | | 'response' => $this->responses->get(['id' => $item_id]), |
| | | default => false |
| | | }; |
| | | return (is_wp_error($item)) ? false : $item; |
| | | } |
| | | public function getOriginalItemAuthors(int $item_id, string $type):array |
| | | { |
| | | [$method, $type] = $this->determineType($type); |
| | | $item = $this->getOriginalItem($item_id, $type); |
| | | return match ($method) { |
| | | 'post' => [$item->post_author], |
| | | 'term' => jvbGetTermOwners($item_id), |
| | | 'user' => [$item->ID], |
| | | 'response' => [$item->user_id], |
| | | }; |
| | | } |
| | | protected function determineType(string $type):array |
| | | { |
| | | $method = match ($type) { |
| | | 'post', 'page', 'product' => 'post', |
| | | 'post_tag', 'category', 'term', 'taxonomy' => 'term', |
| | | 'user' => 'user', |
| | | 'response' => 'response', |
| | | default => false |
| | | }; |
| | | if (!$method) { |
| | | $registrar = Registrar::getInstance($type); |
| | | $type = jvbCheckBase($type); |
| | | if ($registrar) { |
| | | $method = $registrar->getType(); |
| | | } |
| | | } |
| | | if (!$method) { |
| | | error_log('[Responses]itemExists Invalid type: '.$type); |
| | | return [false, false]; |
| | | } |
| | | return [$method, $type]; |
| | | } |
| | | public function respondTo(int $item_id, string $type, int $user_id, string $response, ?int $parent_id = null):array |
| | | { |
| | | if (!$this->itemExists($item_id, $type)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Item does not exist', |
| | | ]; |
| | | } |
| | | if (empty($response)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Response content is empty' |
| | | ]; |
| | | } |
| | | $created = $this->responses->insert([ |
| | | 'item_id' => $item_id, |
| | | 'user_id' => $user_id, |
| | | 'parent_id' => $parent_id, |
| | | 'response' => $response, |
| | | 'type' => $type |
| | | ]); |
| | | |
| | | |
| | | $users = $this->getOriginalItemAuthors($item_id, $type); |
| | | if ($parent_id) { |
| | | $users = array_merge($users, $this->getNotifyableUsers($parent_id)); |
| | | } |
| | | $users = array_unique($users); |
| | | NotificationManager::getInstance()->unnotify( |
| | | $users, |
| | | 'new_response', |
| | | $user_id, |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => (bool)$created, |
| | | 'message' => $created ? 'Response posted successfully' : 'Response not posted' |
| | | ]; |
| | | } |
| | | |
| | | public function editResponse(int $id, string $response):array |
| | | { |
| | | $original = $this->responses->get(['id' => $id]); |
| | | if (!$original) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Original response not found' |
| | | ]; |
| | | } |
| | | if (get_current_user_id() !== $original->user_id) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'This is not your response to edit' |
| | | ]; |
| | | } |
| | | $history = $this->pushHistory($original); |
| | | |
| | | $updated = $this->responses->update([ |
| | | 'history' => $history, |
| | | 'response' => sanitize_text_field($response) |
| | | ], [ |
| | | 'user_id' => $original->user_id, |
| | | 'id' => $id |
| | | ]); |
| | | return [ |
| | | 'success' => (bool)$updated, |
| | | 'message' => $updated ? 'Response edited successfully' : 'Response not edited' |
| | | ]; |
| | | } |
| | | |
| | | public function deleteResponse(int $id):array |
| | | { |
| | | $original = $this->responses->get(['id' => $id]); |
| | | if (!$original) { |
| | | return [ |
| | | 'success' => true, |
| | | 'message' => 'Item does not exist', |
| | | ]; |
| | | } |
| | | if ($original->user_id !== get_current_user_id()) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'This is not your response to delete' |
| | | ]; |
| | | } |
| | | |
| | | $deleted = $this->responses->update([ |
| | | 'status' => 'deleted', |
| | | 'history' => $this->pushHistory($original), |
| | | 'response' => '[ deleted response ]' |
| | | ], [ |
| | | 'id' => $id, |
| | | ]); |
| | | |
| | | NotificationManager::getInstance()->unnotify( |
| | | $this->getNotifyableUsers($id), |
| | | 'new_response', |
| | | $original->user_id, |
| | | ); |
| | | return [ |
| | | 'success' => (bool)$deleted, |
| | | 'message' => $deleted ? 'Response deleted successfully' : 'Response not deleted' |
| | | ]; |
| | | } |
| | | |
| | | public function hideResponse(int $id):array |
| | | { |
| | | $original = $this->responses->get(['id' => $id]); |
| | | if (!$original) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Item does not exist', |
| | | ]; |
| | | } |
| | | $currentUser = get_current_user_id(); |
| | | $isCommentOwner = $original->user_id === $currentUser; |
| | | if ($isCommentOwner) { |
| | | return $this->deleteResponse($id); |
| | | } |
| | | $item = $this->getOriginalItem($original->item_id, $original->type); |
| | | $authorIDs = $this->getOriginalItemAuthors($original->item_id, $original->type); |
| | | |
| | | $isAuthor = in_array($currentUser, $authorIDs); |
| | | if (!$isAuthor) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'You cannot hide this response.' |
| | | ]; |
| | | } |
| | | |
| | | $updated = $this->responses->update([ |
| | | 'status' => 'hidden', |
| | | ], [ |
| | | 'id' => $id, |
| | | ]); |
| | | |
| | | return [ |
| | | 'success' => (bool)$updated, |
| | | 'message' => $updated ? 'Response hidden successfully' : 'Response not hidden' |
| | | ]; |
| | | } |
| | | |
| | | public function getUserResponses(int $userID, $args = [], bool $includeChildren = true):array |
| | | { |
| | | $args = wp_parse_args($args, [ |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC', |
| | | 'page' => 1, |
| | | 'per_page' => 20, |
| | | ]); |
| | | |
| | | $where = [ |
| | | 'user_id' => $userID, |
| | | 'parent_id' => null |
| | | ]; |
| | | if (array_key_exists('item_id', $args)) { |
| | | $where['item_id'] = $args['item_id']; |
| | | } |
| | | $responses = $this->responses->getMany(array_merge([ |
| | | 'where' => $where, |
| | | ], $args), false, ARRAY_A); |
| | | |
| | | if ($includeChildren) { |
| | | $responses['items'] = array_map(function ($r) use ($args) { |
| | | $r['children'] = $this->getChildResponses($r['id'], $args); |
| | | return $r; |
| | | }, $responses['items']); |
| | | } |
| | | return ['items'=> $responses]; |
| | | } |
| | | public function getChildResponses(int $parentID, array $args, bool $includeChildren = true):array |
| | | { |
| | | $args = wp_parse_args($args, [ |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC', |
| | | 'page' => 1, |
| | | 'per_page' => 20, |
| | | ]); |
| | | |
| | | $responses = $this->responses->getMany(array_merge([ |
| | | 'where' => [ |
| | | 'id' => $parentID, |
| | | ] |
| | | ], $args), true, ARRAY_A); |
| | | |
| | | if ($includeChildren) { |
| | | $responses = array_map(function ($r) use ($args) { |
| | | $r['children'] = $this->getChildResponses($r['id'], $args); |
| | | return $r; |
| | | }, $responses); |
| | | } |
| | | return $responses; |
| | | } |
| | | |
| | | public function getAllItemResponses(int $itemID, array $args = [], bool $includeChildren = true):array |
| | | { |
| | | $args = wp_parse_args($args, [ |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC', |
| | | 'page' => 1, |
| | | 'per_page' => 20, |
| | | ]); |
| | | |
| | | $where = [ |
| | | 'item_id' => $itemID, |
| | | 'parent_id' => null |
| | | ]; |
| | | |
| | | $responses = $this->responses->getMany(array_merge([ |
| | | 'where' => $where, |
| | | ], $args), false, ARRAY_A); |
| | | |
| | | if ($includeChildren) { |
| | | $responses['items'] = array_map(function ($r) use ($args) { |
| | | $r['children'] = $this->getChildResponses($r['id'], $args); |
| | | return $r; |
| | | }, $responses['items']); |
| | | } |
| | | return ['items'=> $responses]; |
| | | } |
| | | |
| | | public function handleUserDeletion(int $user_id):void |
| | | { |
| | | $updated = $this->responses->update([ |
| | | 'is_user_deleted' => 1, |
| | | ], [ |
| | | 'user_id' => $user_id, |
| | | ]); |
| | | |
| | | Errors::getInstance()->log( |
| | | 'Responses', |
| | | 'Anonymized responses for deleted user', |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'count' => $updated, |
| | | ], |
| | | 'info' |
| | | ); |
| | | } |
| | | |
| | | protected function getNotifyableUsers(int $id):array |
| | | { |
| | | $original = $this->responses->get(['id' => $id]); |
| | | if (!$original) { |
| | | return []; |
| | | } |
| | | $users = [$original->user_id]; |
| | | if ($original->parent_id) { |
| | | $users = array_merge($users, $this->getNotifyableUsers($original->parent_id)); |
| | | } |
| | | $users = array_merge($users, $this->getOriginalItemAuthors($original->item_id, $original->type)); |
| | | |
| | | return array_unique($users); |
| | | |
| | | } |
| | | |
| | | protected function pushHistory(object $original):array |
| | | { |
| | | $history = json_decode($original->history, true); |
| | | $history[] = [ |
| | | 'edited_at' => $original->updated_at, |
| | | 'response' => $original->response, |
| | | 'upvotes' => $original->upvotes, |
| | | 'downvotes' => $original->downvotes, |
| | | ]; |
| | | return $history; |
| | | } |
| | | } |
| File was renamed from inc/managers/RoleManager.php |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use WP_User; |
| | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | class RoleManager |
| | | class Roles |
| | | { |
| | | use Instance; |
| | | |
| | | private array $roles; |
| | | private array $content; |
| | | private array $subTypes = []; |
| | |
| | | namespace JVBase\managers\SEO; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\DirectoryManager; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use WP_Post; |
| | |
| | | class BreadcrumbManager |
| | | { |
| | | private Cache $cache; |
| | | private Cache $termCache; |
| | | private Cache $postCache; |
| | | private Cache $userCache; |
| | | private static ?self $instance = null; |
| | | |
| | | private function __construct() |
| | | { |
| | | $this->cache = Cache::for('breadcrumbs', MONTH_IN_SECONDS)->connect('post')->connect('taxonomy')->connect('user'); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | $this->cache->flush(); |
| | | $this->cache = Cache::for('breadcrumbs'); |
| | | $this->termCache = Cache::for('breadcrumbs_tax', 'term'); |
| | | $this->postCache = Cache::for('breadcrumbs_post', 'post'); |
| | | $this->userCache = Cache::for('breadcrumbs_user', 'user'); |
| | | } |
| | | |
| | | public static function getInstance(): self |
| | |
| | | |
| | | $key = match(true) { |
| | | is_singular() => get_queried_object_id(), |
| | | is_tax() => get_queried_object()->taxonomy, |
| | | is_tax() => get_queried_object()->term_id, |
| | | is_post_type_archive() => get_queried_object()->name, |
| | | default => 'home' |
| | | }; |
| | | $cache = match(true) { |
| | | is_singular() => $this->postCache, |
| | | is_tax() => $this->termCache, |
| | | is_author() => $this->userCache, |
| | | default => $this->cache |
| | | }; |
| | | |
| | | return $this->cache->remember( |
| | | return $cache->remember( |
| | | $key, |
| | | function() { |
| | | $crumbs = $this->buildCrumbs(); |
| | |
| | | $content = is_array($registrar->registrar->for) ? $registrar->registrar->for[0] : $registrar->registrar->for; |
| | | $contentRegistrar = Registrar::getInstance($content); |
| | | |
| | | if($contentRegistrar && $contentRegistrar->hasFeature('show_directory')) { |
| | | $directory = JVB()->directories(); |
| | | if ($directory && !empty($directory->directories($content)??[])){ |
| | | if(Site::has('is_directory') && $contentRegistrar && $contentRegistrar->hasFeature('show_directory')) { |
| | | $directory = DirectoryManager::getInstance(); |
| | | if (!empty($directory->directories($content)??[])){ |
| | | $crumbs[] = [ |
| | | 'name' => $directory->directories($content)['title'], |
| | | 'url' =>$directory->directories($content)['url'] |
| | |
| | | |
| | | // Add directory if exists |
| | | if ($registrar && $registrar->hasFeature('show_directory')) { |
| | | $directory = JVB()->directories(); |
| | | $directory = DirectoryManager::getInstance(); |
| | | if ($directory && !empty($directory->directories($tax)??[])) { |
| | | $crumbs[] = [ |
| | | 'name' => $directory->directories($tax)['title'], |
| | |
| | | } |
| | | |
| | | // Handle directory posts specially |
| | | if (JVB()->directories() && JVB()->directories()->isDirectory()) { |
| | | if (Site::has('is_directory') && DirectoryManager::getInstance()->isDirectory()) { |
| | | $pos = jvbGetDirectoryInfo(); |
| | | if (!empty($pos)) { |
| | | // Special case for map |
| | | if ($pos['title'] == 'Map') { |
| | | $crumbs[] = [ |
| | | 'name' => 'Tattoo Shops', |
| | | 'url' => JVB()->directories()->directories(BASE.'shop')['url'] |
| | | 'url' => DirectoryManager::getInstance()->directories(BASE.'shop')['url'] |
| | | ]; |
| | | } |
| | | |
| | |
| | | |
| | | $registrar = Registrar::getInstance($name); |
| | | |
| | | if($registrar && $registrar->hasFeature('show_directory') && JVB()->directories()) { |
| | | $directory = JVB()->directories(); |
| | | if($registrar && $registrar->hasFeature('show_directory') && DirectoryManager::getInstance()) { |
| | | $directory = DirectoryManager::getInstance(); |
| | | |
| | | if ($directory && !empty($directory->directories($name)??[])){ |
| | | $crumbs[] = [ |
| | |
| | | } elseif (Site::has('is_directory') && $name === 'directory') { |
| | | |
| | | $crumbs[] = [ |
| | | 'name' => JVB()->directories()->referAs(true), |
| | | 'name' => DirectoryManager::getInstance()->referAs(true), |
| | | 'url' => get_post_type_archive_link($type) |
| | | ]; |
| | | } elseif ($registrar) { |
| | |
| | | 'url' => get_post_type_archive_link($type) |
| | | ]; |
| | | } else { |
| | | $postTypeObject = get_post_type_object($type); |
| | | $crumbs[] = [ |
| | | 'name' => $postTypeObject->label, |
| | | 'url' => get_post_type_archive_link($type) |
| | | ]; |
| | | |
| | | if (function_exists('is_shop') && is_shop()) { |
| | | $ID = get_option( 'woocommerce_shop_page_id' ); |
| | | $crumbs[] = [ |
| | | 'name' => get_the_title($ID), |
| | | 'url' => get_the_permalink($ID) |
| | | ]; |
| | | |
| | | } else { |
| | | $postTypeObject = get_post_type_object($type); |
| | | $crumbs[] = [ |
| | | 'name' => $postTypeObject->label, |
| | | 'url' => get_post_type_archive_link($type) |
| | | ]; |
| | | } |
| | | |
| | | } |
| | | |
| | | return $crumbs; |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Invalidate breadcrumb cache for specific object |
| | | */ |
| | | public function invalidateCache(?int $objectId = null): void |
| | | { |
| | | if ($objectId) { |
| | | $this->cache->forget($objectId); |
| | | } else { |
| | | $this->cache->flush(); |
| | | } |
| | | } |
| | | |
| | | public function addTaxToCrumbs(array $crumbs, string|array $taxonomy):array |
| | | { |
| | |
| | | */ |
| | | class SEOAdminPage |
| | | { |
| | | use Instance; |
| | | // private SchemaBuilder $registry; |
| | | |
| | | public function __construct() |
| | |
| | | |
| | | public static function renderAdminPageStatic():void |
| | | { |
| | | JVB()->seoAdmin()->renderAdminPage(); |
| | | SEOAdmin::getInstance()->renderAdminPage(); |
| | | } |
| | | |
| | | /** |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | /** |
| | | * Edmonton.ink Configuration |
| | | * |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | //New System |
| | | require(JVB_DIR . '/inc/managers/SEO/render/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/BreadcrumbManager.php'); |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/DataType/Date.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/DataType/DateTime.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/DataType/Time.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\SEO\BreadcrumbManager; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\Service; |
| | | use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness; |
| | | use JVBase\registrar\config\seo\Resolver; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | class SchemaOutput { |
| | | use Instance; |
| | | protected array $types; |
| | | public function __construct() { |
| | | protected function __construct() { |
| | | add_action('wp_head', [$this, 'outputSchema'], 0); |
| | | $this->init(); |
| | | } |
| | |
| | | |
| | | if (!empty($schema)) { |
| | | if (!is_front_page()) { |
| | | $website = JVB()->schemaHelper()::schema('website'); |
| | | $website = SchemaHelper::getInstance()::schema('website'); |
| | | if (!empty($website)) { |
| | | if (JVB_TESTING) { |
| | | Cache::for('websiteSchema')->flush(); |
| | |
| | | |
| | | protected function websiteSchema():array |
| | | { |
| | | $stored = JVB()->schemaHelper()::schema('website'); |
| | | $website = JVB()->schemaHelper()::classFromConfig($stored); |
| | | $stored = SchemaHelper::getInstance()::schema('website'); |
| | | $website = SchemaHelper::getInstance()::classFromConfig($stored); |
| | | |
| | | if (!$website->getUrl() || empty($website->getUrl())){ |
| | | $website->setUrl(get_home_url()); |
| | |
| | | if (JVB_TESTING){ |
| | | Cache::for('websiteSchema')->flush(); |
| | | } |
| | | $storedWebsite = JVB()->schemaHelper()::schema('website'); |
| | | $storedWebsite = SchemaHelper::getInstance()::schema('website'); |
| | | |
| | | if (!$full) { |
| | | return Cache::for('websiteSchema')->remember( |
| | |
| | | return in_array($key, $allowed); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $website = JVB()->schemaHelper()::classFromConfig($storedWebsite); |
| | | $website = SchemaHelper::getInstance()::classFromConfig($storedWebsite); |
| | | if (!$website->getName() || empty($website->getName())) { |
| | | $website->setName(get_bloginfo('name')); |
| | | } |
| | |
| | | return Cache::for('websiteSchema')->remember( |
| | | 'full', |
| | | function () use ($storedWebsite) { |
| | | $website = JVB()->schemaHelper()::classFromConfig($storedWebsite); |
| | | $website = SchemaHelper::getInstance()::classFromConfig($storedWebsite); |
| | | if (!$website->getName() || empty($website->getName())) { |
| | | $website->setName(get_bloginfo('name')); |
| | | } |
| | |
| | | if ($reference) { |
| | | return $this->getOptionSchemaReference('organization'); |
| | | } |
| | | $config = JVB()->schemaHelper()::schema('organization'); |
| | | $config = SchemaHelper::getInstance()::schema('organization'); |
| | | |
| | | return JVB()->schemaHelper()::classFromConfig($config); |
| | | return SchemaHelper::getInstance()::classFromConfig($config); |
| | | } |
| | | |
| | | public function getOptionSchemaReference(string $option):mixed |
| | |
| | | return null; |
| | | } |
| | | |
| | | $stored = JVB()->schemaHelper()::reference($option); |
| | | $stored = SchemaHelper::getInstance()::reference($option); |
| | | |
| | | if (empty($stored)){ |
| | | error_log('Attempted to get schema reference for: '.$option.', but defaults not set.'); |
| | |
| | | |
| | | public function buildOrganizationSchema():array |
| | | { |
| | | $config = JVB()->schemaHelper()::schema('organization'); |
| | | $config = SchemaHelper::getInstance()::schema('organization'); |
| | | |
| | | $class = JVB()->schemaHelper()::classFromConfig($config); |
| | | $class = SchemaHelper::getInstance()::classFromConfig($config); |
| | | return ($class)? $class->outputSchema() : []; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/Action.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/InteractAction.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/CommunicateAction.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | $base = JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Article/'; |
| | | require ($base.'Article.php'); |
| | | require ($base.'Report.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/AudioObject.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/ImageObject.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/CreativeWork.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Article/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Event/Event.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Brand/Brand.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ContactPoint/PostalAddress.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Enumeration/Enumeration.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Enumeration/AdultOrientedEnumeration.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Enumeration/BusinessEntityType.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ItemList/ItemList.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ItemList/BreadcrumbList.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Quantity/Distance.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Quantity/Duration.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Quantity/Mass.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Rating/Rating.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Rating/AggregateRating.php'); |
| | | |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Reservation/Reservation.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Reservation/FoodEstablishmentReservation.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/StructuredValue.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/ContactPoint.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/GeoCoordinates.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Brand/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ContactPoint/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Enumeration/_setup.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | $base = JVB_DIR . '/inc/managers/SEO/render/Thing/MedicalEntity/'; |
| | | require ($base.'MedicalEntity.php'); |
| | | require ($base.'MedicalProcedure.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/FoodEstablishment.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Bakery.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/BarOrPub.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/MedicalBusiness/MedicalBusiness.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/LocalBusiness.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/MedicalBusiness/_setup.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/Organization.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Organization/LocalBusiness/_setup.php'); |
| | | |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Person/Person.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/AdministrativeArea.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/Country.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/Place.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Product/Product.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Product/ProductGroup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Product/ProductModel.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Properties/about.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Properties/alternativeHeadline.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Thing.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/_setup.php'); |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/Organization/OrganizationSchema.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/Place/PlaceSchema.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | |
| | | public static function createImageFromID(int $ID):ImageObject|false |
| | | { |
| | | $cache = Cache::for('schemaImage')->connect('post'); |
| | | $cache = Cache::for('schemaImage', 'post'); |
| | | return $cache->remember( |
| | | $ID, |
| | | function() use($ID) { |
| | |
| | | { |
| | | $this->showID = $showIt; |
| | | } |
| | | |
| | | } |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php'); |
| | |
| | | |
| | | return array_filter(array_map(function($item) { |
| | | if (is_array($item) && array_key_exists('type', $item)) { |
| | | return JVB()->schemaHelper()::classFromConfig($item); |
| | | return SchemaHelper::getInstance()::classFromConfig($item); |
| | | } else { |
| | | return $item; |
| | | } |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/abstractTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/ThingSchema.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/DataType/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/_setup.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\routes\LoginRoutes; |
| | | |
| | | add_action('init', 'jvbRegisterScripts', 5); |
| | |
| | | $strategy |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Scripts |
| | | */ |
| | | add_action('wp_enqueue_scripts', 'jvbScripts', 10); |
| | | |
| | | function jvbScripts():void |
| | | { |
| | | add_action('wp_head', 'jvbInlineNavStyles'); |
| | | |
| | | if (Site::has('dashboard')) { |
| | | wp_enqueue_script('jvb-queue'); |
| | | } |
| | | |
| | | wp_enqueue_script('jvb-auth'); |
| | | wp_enqueue_script('jvb-settings'); |
| | | wp_enqueue_script('jvb-navigation'); |
| | | // wp_enqueue_script('jvb-ui'); |
| | | // wp_enqueue_script('jvb-media'); |
| | | wp_enqueue_script('jvb-gallery'); |
| | | wp_enqueue_script('jvb-cache'); |
| | | |
| | | |
| | | $interactions = []; |
| | | if (Site::has('favourites')) { |
| | | $interactions[] = 'favourites'; |
| | | } |
| | | if (!empty(Registrar::withFeature('karma'))) { |
| | | $interactions[] = 'karma'; |
| | | } |
| | | if (Site::has('notifications')) { |
| | | $interactions[] = 'notifications'; |
| | | } |
| | | |
| | | if (!empty($interactions)) { |
| | | foreach($interactions as $interaction) { |
| | | wp_enqueue_script('jvb-'.$interaction); |
| | | } |
| | | } |
| | | |
| | | |
| | | $queue = [ |
| | | 'api' => rest_url('jvb/v1/'), |
| | | 'redirect' => get_home_url(null, '/login/'), |
| | | 'labels' => Registrar::getLabels(), |
| | | ]; |
| | | |
| | | wp_localize_script('jvb-auth', 'jvbSettings', $queue); |
| | | |
| | | |
| | | $initUserSettings = 'async function initUserItems() { |
| | | if (!jvbSettings.currentUser) return; |
| | | |
| | | window.userFavourites = {}; |
| | | window.userVotes = {}; |
| | | '; |
| | | if (Site::has('favourites')) { |
| | | wp_enqueue_script('jvb-favourites'); |
| | | $initUserSettings .= ' |
| | | //Fetch user favourites |
| | | try { |
| | | const result = await window.jvbCache.fetchWithCache( |
| | | `${jvbSettings.api}favourites?all=true`, |
| | | { |
| | | method: \'GET\', |
| | | headers: {\'X-WP-Nonce\': jvbSettings.nonce} |
| | | } |
| | | ); |
| | | |
| | | if (result && result.favourites) { |
| | | for (const key in result.favourites) { |
| | | window.userFavourites[key] = new Set(); |
| | | result.favourites[key].forEach(id => { |
| | | window.userFavourites[key].add(parseInt(id)); |
| | | }); |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error(\'Failed to fetch user favourites:\', error); |
| | | }'; |
| | | } |
| | | |
| | | if (!empty(Registrar::withFeature('karma'))) { |
| | | wp_enqueue_script('jvb-votes'); |
| | | $initUserSettings .= '// Fetch user votes |
| | | try { |
| | | const result = await window.jvbCache.fetchWithCache( |
| | | `${jvbSettings.api}vote?user=${jvbSettings.currentUser}`, |
| | | { |
| | | method: \'GET\', |
| | | headers: {\'X-WP-Nonce\': jvbSettings.nonce} |
| | | }, |
| | | {forceRefresh: true} |
| | | ); |
| | | |
| | | window.userVotes = {}; |
| | | if (result) { |
| | | for (const key in result) { |
| | | window.userVotes[key] = new Map(); |
| | | for (const [id, vote] of Object.entries(result[key])) { |
| | | window.userVotes[key].set(parseInt(id), vote); |
| | | } |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error(\'Failed to fetch user votes:\', error); |
| | | }'; |
| | | } |
| | | |
| | | $initUserSettings .= '} initUserItems();'; |
| | | |
| | | // wp_add_inline_script('jvb-queue', $initUserSettings, 'after'); |
| | | |
| | | |
| | | if (is_search() || |
| | | is_tax(BASE.'shop') || |
| | | is_singular(BASE.'artist') || |
| | | jvbIsDirectory() |
| | | ) { |
| | | wp_enqueue_script('jvb-page-nav'); |
| | | } |
| | | |
| | | if (has_block('jvb/summaryBlock')) { |
| | | wp_enqueue_script('jvb-page-nav'); |
| | | } |
| | | |
| | | // Only load on single shop pages or other relevant pages |
| | | if (is_tax(BASE.'shop') || |
| | | is_singular(BASE.'partner')) { |
| | | $ID = get_queried_object_id(); |
| | | if (is_tax()) { |
| | | $check = get_term_meta($ID, BASE.'has_map', true); |
| | | } else { |
| | | $check = get_post_meta($ID, BASE.'has_map', true); |
| | | } |
| | | |
| | | // if ($check && $check !== '') { |
| | | //// wp_enqueue_script( |
| | | //// 'google-maps-frontend', |
| | | //// 'https://maps.googleapis.com/maps/api/js?key=' . GOOGLE_API . '&callback=googleMapsLoaded', |
| | | //// [], |
| | | //// null, |
| | | //// ['strategy' => 'async', |
| | | //// 'in_footer' => true] |
| | | //// ); |
| | | // |
| | | // // Add the callback function |
| | | // wp_add_inline_script('google-maps-frontend', ' |
| | | // function googleMapsLoaded() { |
| | | // if (window.googleMapsCallbacks && window.googleMapsCallbacks.length) { |
| | | // window.googleMapsCallbacks.forEach(callback => callback()); |
| | | // } |
| | | // } |
| | | // '); |
| | | // } |
| | | } |
| | | if (is_user_logged_in() && Site::has('notifications')) { |
| | | wp_enqueue_script('jvb-notifications'); |
| | | |
| | | wp_localize_script('jvb-notifications', 'notificationSettings', array( |
| | | 'homeUrl' => home_url(), |
| | | 'strings' => array( |
| | | 'loading' => 'Loading...', |
| | | 'error' => 'Failed to load notifications', |
| | | 'noNotifications' => 'No new notifications', |
| | | 'viewAll' => 'View All Notifications', |
| | | 'markRead' => 'Mark as read', |
| | | 'review' => 'Review' |
| | | ) |
| | | )); |
| | | } |
| | | |
| | | // jvbAddScriptDependency('jvb-feed-view-script', 'jvb-queue'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-selector'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-data-store'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-cache'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-a11y'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-utility'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-gallery'); |
| | | // jvbAddScriptDependency('jvb-feed-view-script', 'jvb-loading'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-queue'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-quill'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-form'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-tabs'); |
| | | |
| | | jvbAddScriptDependency('jvb-timeline-view-script', 'jvb-gallery'); |
| | | } |
| | | |
| | | function jvbAddScriptDependency(string $handle, string $dep):bool |
| | | { |
| | | global $wp_scripts; |
| | | |
| | | $script = $wp_scripts->query($handle); |
| | | if (!$script) { |
| | | return false; |
| | | } |
| | | |
| | | if (!in_array($dep, $script->deps)) { |
| | | $script->deps[] = $dep; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\Instance; |
| | | use WP_Error; |
| | | use WP_Post; |
| | | use WP_Query; |
| | |
| | | |
| | | class TaxonomyRelationships |
| | | { |
| | | use Instance; |
| | | protected Cache $cache; |
| | | protected CustomTable $relationships; |
| | | |
| | | public function __construct() |
| | | private function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | |
| | | $this->cache = Cache::for('term_relationship', WEEK_IN_SECONDS); |
| | | $this->cache->connect('terms'); |
| | | $this->cache = Cache::for('term_relationship', 'term', WEEK_IN_SECONDS); |
| | | |
| | | // Ensure the table exists |
| | | // $this->create_table_if_not_exists(); |
| | |
| | | { |
| | | |
| | | $term_posts = $this->cache->remember( |
| | | $this->cache->generateKey(['term' => $term_id, 'taxonomy' => $taxonomy]), |
| | | $this->cache->generateKey(['id' => $term_id, 'taxonomy' => $taxonomy]), |
| | | function () use ($term_id, $taxonomy) { |
| | | global $wpdb; |
| | | return $wpdb->get_col($wpdb->prepare( |
| | |
| | | |
| | | |
| | | $related_term_posts = $this->cache->remember( |
| | | $this->cache->generateKey(['term' => $related_term_id, 'taxonomy' => $related_taxonomy]), |
| | | $this->cache->generateKey(['id' => $related_term_id, 'taxonomy' => $related_taxonomy]), |
| | | function () use($related_term_id, $related_taxonomy) { |
| | | global $wpdb; |
| | | return $wpdb->get_col($wpdb->prepare( |
| | |
| | | |
| | | |
| | | // Queue the operation |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | |
| | | $queue->queueOperation( |
| | | 'taxonomy_relationships', |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\base\Site; |
| | | use JVBase\Instance; |
| | | use Exception; |
| | | use Imagick; |
| | | use JVBase\managers\queue\executors\UploadExecutor; |
| | | use JVBase\managers\queue\mergers\UploadMerger; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Flexible file upload manager for images, videos, and documents |
| | | * Supports configurable directory structures and processing options |
| | | * The goal here was mainly for membership sites, to organize files by userID, rather than the date structure. |
| | | * This way, if a user deletes their account, or performs a data request, we can simply copy their directory. |
| | | * |
| | | * I started with user name so we could search by name, but issues arose if a user changed their name/display name. So I set it to go to user_id. |
| | | * |
| | | * The general flow is: |
| | | * -> Upload, ensure files are stored in appropriate directory, usually {root}/tmp |
| | | * -> Conversion is queued with Queue, as with the /tmp cleanup |
| | | * |
| | | * Proposed directory structure: |
| | | * Site::has('membership') || Site::has('is_directory') |
| | | * -> defaults to {user_id}/{post_type}/file |
| | | * Default: |
| | | * -> defaults to {post_type}/file |
| | | * Drawback: if a post is uploaded to one post type, then used in another, the organization is slightly flawed. |
| | | * |
| | | * Also, we wanted to ensure images were converted to webp, for speed. |
| | | * -> The front-end javascript attempts to do this in browser |
| | | * -> If this couldn't be handled, it is queued for server processing |
| | | * Of course, some integrations (even google, sillies) only accept jpeg/png, so in some cases we need a jpeg fallback |
| | | * |
| | | * |
| | | */ |
| | | class UploadManager |
| | | { |
| | | use Instance; |
| | | protected array $allowedTypes = [ |
| | | // Images |
| | | 'image/jpeg' => 'image', |
| | |
| | | 'original_retention' => 2592000, // 30 days |
| | | ]; |
| | | |
| | | /****************************************************************** |
| | | * UPLOAD SETTINGS |
| | | ******************************************************************/ |
| | | /** |
| | | * @var array|int[] Max size of file types |
| | | */ |
| | | protected array $maxSize = [ |
| | | 'image' => 5242880, // 5MB |
| | | 'video' => 104857600, // 100MB |
| | | 'document' => 15728640 // 15MB |
| | | ]; |
| | | //Image Specific |
| | | /** |
| | | * @var bool Whether to convert the image |
| | | */ |
| | | protected bool $convertImage = true; |
| | | /** |
| | | * @var string Default image conversion |
| | | */ |
| | | protected string $convertImageTo = 'webp'; |
| | | /** |
| | | * @var int Default image quality |
| | | */ |
| | | protected int $convertQuality = 80; |
| | | /** |
| | | * @var bool Whether to optimize images |
| | | */ |
| | | protected bool $optimizeImages = true; |
| | | /** |
| | | * @var bool Whether to create image thumbnails |
| | | */ |
| | | protected bool $createThumbnails = true; |
| | | //Video specific |
| | | /** |
| | | * @var bool Whether to automatically extract a thumbnail from a video |
| | | */ |
| | | protected bool $extractVideoThumbnail = true; |
| | | /** |
| | | * @var int Time, in microseconds, to grab thumbnail from |
| | | */ |
| | | protected int $videoThumbnailPosition = 0; |
| | | //Document specific |
| | | /** |
| | | * @var bool Whether to extract thumbnail from document. |
| | | */ |
| | | protected bool $extractDocumentThumbnail = false; |
| | | /** |
| | | * @var int How long to keep originals (default 3 months) |
| | | */ |
| | | protected int $keep_originals; |
| | | /****************************************************************** |
| | | * UPLOAD DIRECTORIES |
| | | ******************************************************************/ |
| | | /** |
| | | * @var string WordPress' base upload directory |
| | | */ |
| | | protected string $upload_dir; |
| | | /** |
| | | * @var string WordPress' base upload directory url |
| | | */ |
| | | protected string $upload_url; |
| | | public function __construct() |
| | | protected string $dir_pattern; |
| | | /***************************************************************** |
| | | * FEATURE FLAG |
| | | *****************************************************************/ |
| | | protected bool $has_imagick = false; |
| | | private function __construct() |
| | | { |
| | | $upload = wp_upload_dir(); |
| | | $this->upload_dir = $upload['basedir']; |
| | | $this->upload_url = $upload['baseurl']; |
| | | |
| | | // Check for Imagick |
| | | $this->defaultConfig['use_imagick'] = extension_loaded('imagick'); |
| | | if (Site::has('membership') || Site::has('is_directory')) { |
| | | $this->dir_pattern = '{user_id}/{post_type}'; |
| | | } else { |
| | | $this->dir_pattern = '{post_type}'; |
| | | } |
| | | $this->has_imagick = extension_loaded('imagick'); |
| | | $this->keep_originals = MONTH_IN_SECONDS * 3; |
| | | |
| | | add_action('init', [$this, 'registerUploadExecutors'], 5); |
| | | } |
| | | |
| | | /** |
| | | * Register upload operation types with the queue's TypeRegistry |
| | | */ |
| | | public function registerUploadExecutors(): void |
| | | { |
| | | $registry = Queue::getInstance()->registry(); |
| | | $executor = new UploadExecutor(); |
| | | $merger = new UploadMerger('secured_files'); |
| | | |
| | | // Image uploads - chunked at 5 files |
| | | $registry->register('image_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 3 |
| | | )); |
| | | |
| | | // Video uploads - one at a time (heavy processing) |
| | | $registry->register('video_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 1 |
| | | )); |
| | | |
| | | // Document uploads - chunked at 10 |
| | | $registry->register('document_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 5 |
| | | )); |
| | | |
| | | // Metadata updates |
| | | $registry->register('update_image_meta', new TypeConfig( |
| | | executor: $executor |
| | | )); |
| | | |
| | | // Cleanup - chunked at 5 |
| | | $registry->register('temporary_cleanup', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'files', |
| | | chunkSize: 5 |
| | | )); |
| | | |
| | | // Attach to content (depends on upload completing) |
| | | $registry->register('attach_upload_to_content', new TypeConfig( |
| | | executor: $executor |
| | | )); |
| | | |
| | | // Process upload groups into posts |
| | | $registry->register('process_upload_groups', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'posts', |
| | | chunkSize: 5 |
| | | )); |
| | | } |
| | | |
| | | /** |
| | | * @param string|array $file_path |
| | | * @param array $context |
| | | * @return ?string |
| | | * @throws Exception |
| | | */ |
| | | protected function testFile(string|array $file_path, array $context):?string |
| | | { |
| | | $file_type = null; |
| | | if (is_array($file_path)) { |
| | | if (!isset($file_path['tmp_name']) || $file_path['error'] !== UPLOAD_ERR_OK) { |
| | | throw new Exception('Invalid file upload'); |
| | | } |
| | | $file_path = $file_path['tmp_name']; |
| | | } |
| | | // Validate file upload |
| | | if (!isset($file_path)) { |
| | | throw new Exception('Invalid file upload'); |
| | | } |
| | | |
| | | $mime_type = mime_content_type($file_path); |
| | | $file_type = $this->allowedTypes[$mime_type] ?? 'unknown'; |
| | | if ($file_type === 'unknown') { |
| | | throw new Exception('Unsupported file type: ' . $mime_type); |
| | | } |
| | | |
| | | // Check allowed types if specified |
| | | if (array_key_exists('allowed_types', $context) && !in_array($mime_type, $context['allowed_types'])) { |
| | | throw new Exception('File type not allowed: ' . $mime_type); |
| | | } |
| | | |
| | | return $file_type; |
| | | } |
| | | |
| | | /** |
| | |
| | | public function secureUploadedFile(array $file_data, array $context): array |
| | | { |
| | | try { |
| | | // Validate file upload |
| | | if (!isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) { |
| | | throw new Exception('Invalid file upload'); |
| | | } |
| | | $file_type = $this->testFile($file_data, $context); |
| | | |
| | | $mime_type = mime_content_type($file_data['tmp_name']); |
| | | $file_type = $this->allowedTypes[$mime_type] ?? 'unknown'; |
| | | if ($file_type === 'unknown') { |
| | | throw new Exception('Unsupported file type: ' . $mime_type); |
| | | } |
| | | |
| | | // Check allowed types if specified |
| | | if ($this->defaultConfig['allowed_types'] && !in_array($mime_type, $this->defaultConfig['allowed_types'])) { |
| | | throw new Exception('File type not allowed: ' . $mime_type); |
| | | } |
| | | |
| | | $temp_dir = $this->generateDirectory($file_type, "{user_id}/temp", $context); |
| | | |
| | | // Ensure directories exist |
| | | $this->ensureDirectories($temp_dir); |
| | | $context['pattern'] = $this->dir_pattern.'/tmp'; |
| | | $temp_dir = $this->generateDirectory($file_type, $context); |
| | | |
| | | // Generate a unique filename for temporary storage |
| | | $temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']); |
| | |
| | | |
| | | // Return metadata about the stored file |
| | | return [ |
| | | 'temp_path' => $temp_path, |
| | | 'temp_path' => $temp_path, |
| | | 'original_name' => $file_data['name'], |
| | | 'mime_type' => $file_data['type'], |
| | | 'file_type' => $file_type, |
| | | 'file_size' => $file_data['size'], |
| | | 'stored_at' => current_time('mysql') |
| | | 'mime_type' => $file_data['type'], |
| | | 'file_type' => $file_type, |
| | | 'file_size' => $file_data['size'], |
| | | 'stored_at' => current_time('mysql') |
| | | ]; |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Failed to store uploaded file: ' . print_r($e->getMessage(), true)); |
| | | Errors::getInstance()->log('UploadManager','Failed to store uploaded file: ' . print_r($e->getMessage(), true)); |
| | | return []; |
| | | } |
| | | } |
| | |
| | | public function processUpload(string $temp_path, array $context): array|WP_Error |
| | | { |
| | | try { |
| | | // Validate temp file exists |
| | | if (!file_exists($temp_path)) { |
| | | throw new Exception('Temporary file not found: ' . $temp_path); |
| | | } |
| | | |
| | | // Validate MIME type |
| | | $mime_type = mime_content_type($temp_path); |
| | | if (!$this->validateMimeType($temp_path, $mime_type)) { |
| | | throw new Exception('Invalid or potentially dangerous file type'); |
| | | } |
| | | $file_type = $this->allowedTypes[$mime_type] ?? 'unknown'; |
| | | |
| | | if ($file_type === 'unknown') { |
| | | throw new Exception('Unsupported file type: ' . $mime_type); |
| | | } |
| | | |
| | | // Merge config |
| | | $config = array_merge($this->defaultConfig, $context['config'] ?? [], $context); |
| | | $file_type = $this->testFile($temp_path, $context); |
| | | |
| | | // Extract context |
| | | $user_id = $context['user_id'] ?? get_current_user_id(); |
| | | $post_id = $context['post_id'] ?? 0; |
| | | |
| | | // Check allowed types if specified |
| | | if (!empty($config['allowed_types']) && !in_array($mime_type, $config['allowed_types'])) { |
| | | throw new Exception('File type not allowed: ' . $mime_type); |
| | | } |
| | | |
| | | // Validate file size |
| | | $this->validateFileSize($temp_path, $file_type, $config); |
| | | $this->validateFileSize($temp_path, $file_type, $context); |
| | | |
| | | // Generate directory structure for final storage |
| | | $directory = $this->generateDirectory($file_type, $config['directory_pattern'] ?? '{user_id}/{content}/{file_type}', $context); |
| | | |
| | | // Ensure directories exist |
| | | $this->ensureDirectories($directory); |
| | | $directory = $this->generateDirectory($file_type, $context); |
| | | |
| | | // Generate filename |
| | | $original_name = $context['original_name'] ?? basename($temp_path); |
| | | $filename = $this->generateFilename($original_name, $context); |
| | | |
| | | // Store original from temp |
| | | $original_path = $this->storeOriginalFromTemp($temp_path, $directory, $filename); |
| | | |
| | | // Process based on file type |
| | | return match($file_type) { |
| | | 'image' => $this->processImage($original_path, $directory, $filename, $post_id, $config, $context), |
| | | 'video' => $this->processVideo($original_path, $directory, $filename, $post_id, $config, $context), |
| | | 'document' => $this->processDocument($original_path, $directory, $filename, $post_id, $config, $context), |
| | | 'image' => $this->processImage($temp_path, $directory, $filename, $post_id, $context), |
| | | 'video' => $this->processVideo($temp_path, $directory, $filename, $post_id, $context), |
| | | 'document' => $this->processDocument($temp_path, $directory, $filename, $post_id, $context), |
| | | default => throw new Exception('Unknown file type') |
| | | }; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | '[UploadManager]:processUpload', |
| | | $e->getMessage(), |
| | | ['temp_path' => $temp_path, 'context' => $context] |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Store original file from temp location (not from PHP upload) |
| | | */ |
| | | protected function storeOriginalFromTemp(string $temp_path, string $directory, string $filename): string |
| | | { |
| | | $ext = pathinfo($temp_path, PATHINFO_EXTENSION); |
| | | $original_path = "{$this->upload_dir}/{$directory}/originals/{$filename}.{$ext}"; |
| | | |
| | | // Copy from temp to final location |
| | | if (!copy($temp_path, $original_path)) { |
| | | throw new Exception('Failed to store original file'); |
| | | } |
| | | |
| | | return $original_path; |
| | | } |
| | | |
| | | /** |
| | | * Clean up empty temporary directories |
| | |
| | | @rmdir($temp_base); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Generate directory structure based on pattern |
| | | * @param string $file_type |
| | | * @param array $config |
| | | * @return string |
| | | * @throws Exception |
| | | */ |
| | | protected function generateDirectory(string $file_type, string $pattern, array $context): string |
| | | public function generateDirectory(string $file_type, array $config): string |
| | | { |
| | | $replacements = [ |
| | | '{user_id}' => $context['user_id'] ?? get_current_user_id(), |
| | | '{post_type}' => $context['content'] ?? '', |
| | | '{content}' => $context['content'] ?? '', |
| | | '{file_type}' => $file_type, |
| | | '{year}' => date('Y'), |
| | | '{month}' => date('m'), |
| | | ]; |
| | | $pattern = array_key_exists('pattern', $config) && is_string($config['pattern']) ? $config['pattern'] : $this->dir_pattern; |
| | | |
| | | $directory = str_replace(array_keys($replacements), array_values($replacements), $pattern); |
| | | $pattern = explode('/', $pattern); |
| | | $parts = []; |
| | | foreach ($pattern as $part) { |
| | | $newPart = match($part) { |
| | | '{user_id}' => $context['user_id'] ?? null, |
| | | '{post_type}', '{content}' => $context['content'] ?? null, |
| | | '{tax}', '{taxonomy}' => $context['taxonomy'] ?? null, |
| | | '{term}' => $context['term'] ?? null, |
| | | '{file_type}' => $file_type, |
| | | '{year}' => date('Y'), |
| | | '{month}' => date('m'), |
| | | '{day}' => date('d'), |
| | | default => $part |
| | | }; |
| | | if (is_null($newPart)) { |
| | | error_log('[Uploader] Pattern key was not provided for '.$part); |
| | | } |
| | | if (str_contains($newPart, '{')) { |
| | | error_log('[Uploader] Unhandled pattern found: '. $newPart); |
| | | } |
| | | $newPart = $this->sanitizeFileName($newPart); |
| | | $parts[] = $newPart; |
| | | } |
| | | //Remove any null values |
| | | $parts = array_filter($parts); |
| | | |
| | | $directory = implode('/', $parts); |
| | | |
| | | // Allow filtering |
| | | return apply_filters('jvb_upload_directory', $directory, $file_type, $context); |
| | | $directory = apply_filters('jvb_upload_directory', $directory, $file_type, $config); |
| | | |
| | | // Ensure directories exist |
| | | $this->ensureDirectories($directory); |
| | | return $directory; |
| | | } |
| | | |
| | | /** |
| | |
| | | protected function ensureDirectories(string $rel_path): void |
| | | { |
| | | $dirs = [ |
| | | "{$this->upload_dir}/{$rel_path}", |
| | | "{$this->upload_dir}/{$rel_path}/originals" |
| | | "{$this->upload_dir}/{$rel_path}" |
| | | ]; |
| | | |
| | | foreach ($dirs as $dir) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Store original file |
| | | */ |
| | | protected function storeOriginal(string $tmp_file, string $directory, string $filename): string |
| | | { |
| | | $ext = pathinfo($tmp_file, PATHINFO_EXTENSION); |
| | | $original_path = "{$this->upload_dir}/{$directory}/originals/{$filename}.{$ext}"; |
| | | |
| | | if (!move_uploaded_file($tmp_file, $original_path)) { |
| | | throw new Exception('Failed to store original file'); |
| | | } |
| | | |
| | | return $original_path; |
| | | } |
| | | |
| | | /** |
| | | * Validate file size based on type and config |
| | | * @param string $file_path |
| | | * @param string $file_type |
| | | * @param array $config |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function validateFileSize(string $file_path, string $file_type, array $config): void |
| | | { |
| | | $size = filesize($file_path); |
| | | $max_size = $config['max_size'][$file_type] ?? $this->defaultConfig['max_size'][$file_type]; |
| | | $max_size = $this->maxSize[$file_type]; |
| | | if (array_key_exists('max_size', $config)) { |
| | | if (is_int($config['max_size'])) { |
| | | $max_size = $config['max_size']; |
| | | } else if (is_array($config['max_size']) && array_key_exists($file_type, $config['max_size'])) { |
| | | $max_size = $config['max_size'][$file_type]; |
| | | } |
| | | } |
| | | |
| | | if ($size > $max_size) { |
| | | $max_mb = round($max_size / 1048576, 2); |
| | |
| | | * Process image file |
| | | * @throws Exception |
| | | */ |
| | | protected function processImage(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array |
| | | protected function processImage(string $original_path, string $directory, string $filename, int $post_id, array $context): array |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $original_mime = mime_content_type($original_path); |
| | |
| | | $should_convert = false; |
| | | $target_format = false; |
| | | |
| | | if ($config['convert'] && $config['convert'] !== false) { |
| | | $target_format = strtolower($config['convert']); |
| | | |
| | | // Validate format |
| | | if (!isset($this->supportedFormats[$target_format])) { |
| | | throw new Exception("Unsupported conversion format: {$target_format}"); |
| | | if ((array_key_exists('convert', $context) && $context['convert'] !== false) || $this->convertImage) { |
| | | $target_format = empty($context['convertTo']??'') ? $this->convertImage : $context['convertTo']; |
| | | if (!empty($target_format)) { |
| | | // Validate format |
| | | if (!isset($this->supportedFormats[$target_format])) { |
| | | error_log('[Uploader] Unsupported conversion format: '.$target_format.'... will not convert'); |
| | | } else { |
| | | // Check if conversion is needed |
| | | $target_mime = $this->supportedFormats[$target_format]; |
| | | $should_convert = ($original_mime !== $target_mime); |
| | | } |
| | | } |
| | | |
| | | // Check if conversion is needed |
| | | $target_mime = $this->supportedFormats[$target_format]; |
| | | $should_convert = ($original_mime !== $target_mime); |
| | | } |
| | | |
| | | // Convert or copy based on configuration |
| | |
| | | $final_path = "{$full_dir}/{$filename}.{$target_format}"; |
| | | |
| | | try { |
| | | |
| | | $this->convertImage( |
| | | $original_path, |
| | | $final_path, |
| | | $target_format, |
| | | $config['quality'], |
| | | $config['use_imagick'] |
| | | array_key_exists('quality', $context) && is_int($context['quality']) && $context['quality'] <= 100 ? |
| | | $context['quality'] : |
| | | $this->convertQuality, |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Image conversion failed: ' . print_r($e->getMessage(), true)); |
| | | Errors::getInstance()->log('Uploader', 'Image conversion failed: ' . print_r($e->getMessage(), true)); |
| | | // Fallback to original |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | | $final_path = "{$full_dir}/{$filename}.{$ext}"; |
| | |
| | | } |
| | | |
| | | // Generate thumbnails |
| | | if ($config['create_thumbnails']) { |
| | | $hasOption = array_key_exists('create_thumbnails', $context); |
| | | if (($hasOption && $context['create_thumbnails']) || (!$hasOption && $this->createThumbnails)) { |
| | | $this->generateThumbnails($attachment_id); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'success' => true, |
| | | 'attachment_id' => $attachment_id, |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'image', |
| | | 'format' => $target_format ?: pathinfo($final_path, PATHINFO_EXTENSION) |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'image', |
| | | 'format' => $target_format ?: pathinfo($final_path, PATHINFO_EXTENSION), |
| | | 'context' => $context |
| | | ]; |
| | | } |
| | | |
| | |
| | | * Process video file |
| | | * @throws Exception |
| | | */ |
| | | protected function processVideo(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array |
| | | protected function processVideo(string $original_path, string $directory, string $filename, int $post_id, array $context): array |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | |
| | | try { |
| | | $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Attachment creation failed: '.print_r($e->getMessage(), true)); |
| | | Errors::getInstance()->log('Uploader', 'Attachment creation failed: '.print_r($e->getMessage(), true)); |
| | | return ['success' => false]; |
| | | } |
| | | |
| | | |
| | | // Extract thumbnail if enabled |
| | | if ($config['extract_video_thumbnail']) { |
| | | $this->extractVideoThumbnail($attachment_id, $final_path, $config['video_thumbnail_time']); |
| | | $hasOption = array_key_exists('extract_thumbnail', $context); |
| | | if (($hasOption && $context['extract_thumbnail']) || (!$hasOption && $this->extractVideoThumbnail)) { |
| | | $time = $context['thumbnail_time']??$this->videoThumbnailPosition; |
| | | $this->extractVideoThumbnail($attachment_id, $final_path, $time); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'success' => true, |
| | | 'attachment_id' => $attachment_id, |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'video' |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'video', |
| | | 'context' => $context |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Process document file |
| | | */ |
| | | protected function processDocument(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array |
| | | protected function processDocument(string $original_path, string $directory, string $filename, int $post_id, array $context): array |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | |
| | | try { |
| | | $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Conversion failed: '.print_r($e->getMessage(), true)); |
| | | Errors::getInstance()->log('Uploader','Conversion failed: '.print_r($e->getMessage(), true)); |
| | | return ['success' => false]; |
| | | } |
| | | |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'success' => true, |
| | | 'attachment_id' => $attachment_id, |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'document' |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'document', |
| | | 'context' => $context |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Generate SEO-friendly filename |
| | | */ |
| | | protected function generateFilename(string $original_name, array $context): string |
| | | public function generateFilename(string $original_name, array $context): string |
| | | { |
| | | $name_parts = pathinfo($original_name); |
| | | $base_name = sanitize_title($name_parts['filename']); |
| | | $base_name = $this->sanitizeFileName($name_parts['filename']); |
| | | $user_data = array_key_exists('user_id', $context) ? get_userdata((int)$context['user_id']) : get_current_user(); |
| | | $username = $user_data ? sanitize_title($user_data->display_name) : 'user'; |
| | | $timestamp = time(); |
| | | |
| | | return apply_filters( |
| | | return $this->sanitizeFileName(apply_filters( |
| | | 'jvb_upload_filename', |
| | | "{$base_name}-{$timestamp}", |
| | | $context |
| | | ); |
| | | )); |
| | | } |
| | | |
| | | protected function sanitizeFileName(string $filename): string |
| | |
| | | * @param bool $replace Replace original file (default: true) |
| | | * @return array|WP_Error Result with new attachment details |
| | | */ |
| | | public function convertImageTo(int $attachment_id, string $format, int $quality = 80, bool $replace = true): array|WP_Error |
| | | public function convertImageTo(int $attachment_id, string $format, int $quality = 80, bool $replace = false): array|WP_Error |
| | | { |
| | | try { |
| | | // Validate format |
| | |
| | | $new_path = $path_info['dirname'] . '/' . $new_filename; |
| | | |
| | | // Perform conversion |
| | | $use_imagick = $this->defaultConfig['use_imagick']; |
| | | $this->convertImage($original_path, $new_path, $format, $quality, $use_imagick); |
| | | $this->convertImage($original_path, $new_path, $format, $quality); |
| | | |
| | | if ($replace) { |
| | | // Replace the original file |
| | |
| | | /** |
| | | * Generic image conversion method |
| | | */ |
| | | protected function convertImage(string $source, string $destination, string $format, int $quality, bool $use_imagick): void |
| | | /** |
| | | * @param string $source |
| | | * @param string $destination |
| | | * @param string $format |
| | | * @param int $quality |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function convertImage(string $source, string $destination, string $format, int $quality): void |
| | | { |
| | | if ($use_imagick) { |
| | | if ($this->has_imagick) { |
| | | $this->convertWithImagick($source, $destination, $format, $quality); |
| | | } else { |
| | | $this->convertWithGd($source, $destination, $format, $quality); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Convert image with Imagick (supports multiple formats) |
| | | * Convert image with Imagick |
| | | * @param string $source |
| | | * @param string $destination |
| | | * @param string $format |
| | | * @param int $quality |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function convertWithImagick(string $source, string $destination, string $format, int $quality): void |
| | | { |
| | |
| | | |
| | | $image->writeImage($destination); |
| | | $image->clear(); |
| | | $image->destroy(); |
| | | } catch (Exception $e) { |
| | | throw new Exception("Imagick conversion to {$format} failed: " . $e->getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Convert image with GD (supports multiple formats) |
| | | * Convert image with GD |
| | | * @param string $source |
| | | * @param string $destination |
| | | * @param string $format |
| | | * @param int $quality |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function convertWithGd(string $source, string $destination, string $format, int $quality): void |
| | | { |
| | |
| | | // Placeholder for future implementation |
| | | apply_filters('jvb_extract_video_thumbnail', null, $attachment_id, $video_path, $time); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | /** |
| | | * Phase 1: Validate and process file on disk (no DB writes) |
| | | * Safe to run outside transactions — only does file I/O |
| | | * |
| | | * @return array Prepared file info for registerFile() |
| | | */ |
| | | public function prepareFile(string $temp_path, array $context): array |
| | | { |
| | | if (!file_exists($temp_path)) { |
| | | throw new Exception('Temporary file not found: ' . $temp_path); |
| | | } |
| | | |
| | | $mime_type = mime_content_type($temp_path); |
| | | if (!$this->validateMimeType($temp_path, $mime_type)) { |
| | | throw new Exception('Invalid or potentially dangerous file type'); |
| | | } |
| | | |
| | | $file_type = $this->allowedTypes[$mime_type] ?? 'unknown'; |
| | | if ($file_type === 'unknown') { |
| | | throw new Exception('Unsupported file type: ' . $mime_type); |
| | | } |
| | | |
| | | $config = array_merge($this->defaultConfig, $context['config'] ?? [], $context); |
| | | $post_id = $context['post_id'] ?? 0; |
| | | |
| | | if (!empty($config['allowed_types']) && !in_array($mime_type, $config['allowed_types'])) { |
| | | throw new Exception('File type not allowed: ' . $mime_type); |
| | | } |
| | | |
| | | $this->validateFileSize($temp_path, $file_type, $config); |
| | | |
| | | $directory = $this->generateDirectory( |
| | | $file_type, |
| | | $config['directory_pattern'] ?? '{user_id}/{content}/{file_type}', |
| | | $context |
| | | ); |
| | | $this->ensureDirectories($directory); |
| | | |
| | | $original_name = $context['original_name'] ?? basename($temp_path); |
| | | $filename = $this->generateFilename($original_name, $context); |
| | | $original_path = $this->storeOriginalFromTemp($temp_path, $directory, $filename); |
| | | |
| | | // File-type-specific I/O (conversion, copying) — no DB |
| | | $final_path = match ($file_type) { |
| | | 'image' => $this->prepareImage($original_path, $directory, $filename, $config), |
| | | 'video' => $this->prepareMedia($original_path, $directory, $filename), |
| | | 'document' => $this->prepareMedia($original_path, $directory, $filename), |
| | | default => throw new Exception('Unknown file type'), |
| | | }; |
| | | |
| | | return [ |
| | | 'final_path' => $final_path, |
| | | 'file_type' => $file_type, |
| | | 'filename' => $filename, |
| | | 'post_id' => $post_id, |
| | | 'config' => $config, |
| | | 'context' => $context, |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Phase 2: Register processed file in WordPress (DB writes only) |
| | | * Quick operation — individual WP functions handle their own queries |
| | | * |
| | | * @param array $prepared Output from prepareFile() |
| | | * @return array Standard upload result |
| | | */ |
| | | public function registerFile(array $prepared): array |
| | | { |
| | | $attachment_id = $this->createAttachment( |
| | | $prepared['final_path'], |
| | | $prepared['filename'], |
| | | $prepared['context'], |
| | | $prepared['post_id'] |
| | | ); |
| | | |
| | | // Type-specific post-registration |
| | | if ($prepared['file_type'] === 'image') { |
| | | $alt_text = apply_filters('jvb_upload_alt_text', '', $prepared['context']); |
| | | if ($alt_text !== '') { |
| | | update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text); |
| | | } |
| | | } |
| | | |
| | | if ($prepared['file_type'] === 'video' && ($prepared['config']['extract_video_thumbnail'] ?? false)) { |
| | | $this->extractVideoThumbnail( |
| | | $attachment_id, |
| | | $prepared['final_path'], |
| | | $prepared['config']['video_thumbnail_time'] ?? 0 |
| | | ); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'attachment_id' => $attachment_id, |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $prepared['final_path'], |
| | | 'type' => $prepared['file_type'], |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Image file I/O: convert or copy to final location |
| | | */ |
| | | protected function prepareImage(string $original_path, string $directory, string $filename, array $config): string |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $original_mime = mime_content_type($original_path); |
| | | |
| | | $should_convert = false; |
| | | $target_format = false; |
| | | |
| | | if ($config['convert'] && $config['convert'] !== false) { |
| | | $target_format = strtolower($config['convert']); |
| | | if (!isset($this->supportedFormats[$target_format])) { |
| | | throw new Exception("Unsupported conversion format: {$target_format}"); |
| | | } |
| | | $should_convert = ($original_mime !== $this->supportedFormats[$target_format]); |
| | | } |
| | | |
| | | if ($should_convert) { |
| | | $final_path = "{$full_dir}/{$filename}.{$target_format}"; |
| | | try { |
| | | $this->convertImage($original_path, $final_path, $target_format, $config['quality'], $config['use_imagick']); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Image conversion failed: ' . $e->getMessage()); |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | | $final_path = "{$full_dir}/{$filename}.{$ext}"; |
| | | copy($original_path, $final_path); |
| | | } |
| | | } else { |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | | $final_path = "{$full_dir}/{$filename}.{$ext}"; |
| | | copy($original_path, $final_path); |
| | | } |
| | | |
| | | return $final_path; |
| | | } |
| | | |
| | | /** |
| | | * Video/document file I/O: copy to final location |
| | | */ |
| | | protected function prepareMedia(string $original_path, string $directory, string $filename): string |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | | $final_path = "{$full_dir}/{$filename}.{$ext}"; |
| | | |
| | | copy($original_path, $final_path); |
| | | |
| | | return $final_path; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | use WP_Error; |
| | |
| | | } |
| | | class UserTermsManager |
| | | { |
| | | |
| | | use Instance; |
| | | protected CustomTable $index; |
| | | |
| | | public function __construct() |
| | | private function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | |
| | |
| | | ]; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'rebuild_user_term_index', |
| | | 0, |
| | | [ |
| | |
| | | ]; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'rebuild_user_term_index', |
| | | 0, |
| | | [ |
| | |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\routes\ApprovalRoutes; |
| | | |
| | | class VerifyEntryManager { |
| | | use Instance; |
| | | protected CustomTable $table; |
| | | protected int $max = 3; |
| | | public function __construct() |
| | | protected Rest $route; |
| | | private function __construct() |
| | | { |
| | | $this->route = ApprovalRoutes::getInstance(); |
| | | $this->defineTables(); |
| | | } |
| | | |
| | |
| | | ]); |
| | | |
| | | if ($request && !empty($owners)) { |
| | | JVB()->notification()->notify($owners,'entry_requested',$userID, [ |
| | | NotificationManager::getInstance()->notify($owners,'entry_requested',$userID, [ |
| | | 'target_id' => $termID, |
| | | 'target_type' => $taxonomy |
| | | ]); |
| | |
| | | wp_set_object_terms($request['content_id'], $termID, jvbCheckBase($request['taxonomy'])); |
| | | } |
| | | |
| | | JVB()->notification()?->notify($request['user_id'],$approve ? 'entry_approved' : 'entry_denied',$userID); |
| | | NotificationManager::getInstance()?->notify($request['user_id'],$approve ? 'entry_approved' : 'entry_denied',$userID); |
| | | //TODO: What if there are multiple managers, and one approved the request? Do we just delete the notification? |
| | | JVB()->notification()?->unnotify($owners, 'entry_requested', $request['user_id'], [ |
| | | NotificationManager::getInstance()?->unnotify($owners, 'entry_requested', $request['user_id'], [ |
| | | 'target_id' => $termID, |
| | | 'target_type' => $request['taxonomy'] |
| | | ]); |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\ApprovalManager; |
| | | use JVBase\managers\Approvals; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | require(JVB_DIR . '/inc/managers/ScriptLoader.php'); |
| | | require(JVB_DIR . '/inc/managers/Errors.php'); |
| | | require(JVB_DIR . '/inc/managers/CustomTable.php'); |
| | | //require(JVB_DIR . '/inc/managers/CacheManager.php'); |
| | | require(JVB_DIR . '/inc/managers/Cache.php'); |
| | | |
| | | |
| | | require(JVB_DIR . '/inc/managers/IconsManager.php'); |
| | | add_action('init', 'jvbInit', 0); // Priority 1 - very early |
| | | function jvbInit(): void |
| | | add_action('jvbloaded', 'jvbManagers'); // Priority 1 - very early |
| | | function jvbManagers(): void |
| | | { |
| | | |
| | | Cache::registerHooks(); |
| | | // Cache::registerHooks(); |
| | | // Initialize base sources (this registers hooks and includes defaults) |
| | | IconsManager::for(); |
| | | IconsManager::for('forms'); |
| | |
| | | } |
| | | |
| | | if (Site::has('magic_link')) { |
| | | require(JVB_DIR . '/inc/managers/MagicLinkManager.php'); |
| | | require(JVB_DIR . '/inc/managers/MagicLinks.php'); |
| | | } |
| | | |
| | | if (Site::hasAny(['dashboard', 'feed_block'])) { |
| | |
| | | } |
| | | |
| | | if (!empty(Registrar::withFeature('approve_new'))) { |
| | | require(JVB_DIR . '/inc/managers/ApprovalManager.php'); |
| | | require(JVB_DIR . '/inc/managers/Approvals.php'); |
| | | } |
| | | |
| | | |
| | |
| | | } |
| | | |
| | | if (Site::has('referrals')) { |
| | | require(JVB_DIR . '/inc/managers/ReferralManager.php'); |
| | | require(JVB_DIR . '/inc/managers/Referrals.php'); |
| | | } |
| | | |
| | | if (!empty(Registrar::withFeature('karma'))) { |
| | |
| | | function jvb_load_karma():void |
| | | { |
| | | } |
| | | require(JVB_DIR . '/inc/managers/ErrorHandler.php'); |
| | | |
| | | require(JVB_DIR . '/inc/managers/queue/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/EmailManager.php'); |
| | | require(JVB_DIR . '/inc/managers/LoginManager.php'); |
| | | require(JVB_DIR . '/inc/managers/Emails.php'); |
| | | require(JVB_DIR . '/inc/managers/Login.php'); |
| | | |
| | | |
| | | //require(JVB_DIR . '/inc/managers/SchemaManager.php'); |
| | |
| | | |
| | | require(JVB_DIR . '/inc/managers/ImageGenerator.php'); |
| | | require(JVB_DIR . '/inc/managers/AdminPages.php'); |
| | | require(JVB_DIR . '/inc/managers/RoleManager.php'); |
| | | require(JVB_DIR . '/inc/managers/Roles.php'); |
| | |
| | | $this->operation->processedItems + $count, |
| | | $this->operation->totalItems |
| | | ); |
| | | JVB()->queue()->storage()->saveProgress($this->operation); |
| | | Queue::getInstance()->storage()->saveProgress($this->operation); |
| | | } |
| | | |
| | | public function failItem(mixed $item, string $reason): void |
| | |
| | | 'item' => $item, |
| | | 'reason' => $reason, |
| | | ]; |
| | | JVB()->queue()->storage()->saveProgress($this->operation); |
| | | Queue::getInstance()->storage()->saveProgress($this->operation); |
| | | } |
| | | } |
| | |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\Instance; |
| | | use JVBase\rest\routes\QueueRoutes; |
| | | use WP_Error; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | |
| | | class Queue |
| | | { |
| | | use Instance; |
| | | private Storage $storage; |
| | | private Processor $processor; |
| | | private TypeRegistry $registry; |
| | | public Executor $executor; |
| | | private Locker $locker; |
| | | |
| | | public function __construct() |
| | | private QueueRoutes $routes; |
| | | |
| | | private function __construct() |
| | | { |
| | | //Load routes |
| | | $this->routes = new QueueRoutes(); |
| | | |
| | | $this->storage = new Storage(); |
| | | $this->registry = new TypeRegistry(); |
| | | $this->locker = new Locker(); |
| | |
| | | |
| | | public function registerAdminAction():void |
| | | { |
| | | $admin = JVB()->admin(); |
| | | $admin = AdminPages::getInstance(); |
| | | $admin->registerAction( |
| | | 'Restart Stuck Operations', |
| | | 'restart-stuck-operations', |
| | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | $this->cache = Cache::for('queue', DAY_IN_SECONDS)->user(); |
| | | $this->cache = Cache::for('queue', null, DAY_IN_SECONDS)->user(); |
| | | } |
| | | |
| | | public function hasProcessingOperations(): bool |
| | |
| | | |
| | | public function getExecutor(string $type): ?Executor |
| | | { |
| | | return array_key_exists($type, $this->configs) ? $this->configs[$type]->executor : JVB()->queue()->executor; |
| | | return array_key_exists($type, $this->configs) ? $this->configs[$type]->executor : Queue::getInstance()->executor; |
| | | } |
| | | |
| | | public function getMergeable(string $type): ?Mergeable |
| | |
| | | |
| | | // Send notification |
| | | // if (jvbSiteHasNotifications()) { |
| | | // JVB()->notification()->addNotification( |
| | | // NotificationManager::getInstance()->addNotification( |
| | | // $this->userId, |
| | | // 'content_update_complete', |
| | | // null, |
| | |
| | | { |
| | | error_log('Handling term '.$ID.' with data: '.print_r($data, true)); |
| | | //Existing term update |
| | | if ($registrar->hasFeature('is_ownable') && (!JVB()->roles()->isOwner($this->userId, $ID) && !JVB()->roles()->isManager($this->userId, $ID))) { |
| | | if ($registrar->hasFeature('is_ownable') && (!Roles::getInstance()()->isOwner($this->userId, $ID) && !Roles::getInstance()()->isManager($this->userId, $ID))) { |
| | | error_log('Term is ownable. User does not own this term.'); |
| | | $progress->failItem($ID, 'No permission to modify this term'); |
| | | $results['errors'][$ID] = 'No permission'; |
| | |
| | | use JVBase\managers\queue\Operation; |
| | | use JVBase\managers\queue\Progress; |
| | | use JVBase\managers\queue\Result; |
| | | use JVBase\managers\RoleManager; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use Exception; |
| | |
| | | */ |
| | | class ContentTermExecutor implements Executor |
| | | { |
| | | protected RoleManager $roleManager; |
| | | protected Roles $roleManager; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->roleManager = new RoleManager(); |
| | | $this->roleManager = new Roles(); |
| | | } |
| | | |
| | | public function execute(Operation $operation, Progress $progress): Result |
| | |
| | | $user = get_userdata($userID); |
| | | |
| | | foreach ($managers as $managerID) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $managerID, |
| | | $notificationType, |
| | | [ |
| | |
| | | * {integration}_sync_from, {integration}_sync_customer, etc. |
| | | * |
| | | * Registration (in Integrations::registerAdditionalHooks): |
| | | * $queue = JVB()->queue(); |
| | | * $queue = Queue::getInstance(); |
| | | * $queue->registry()->register('helcim_sync_to', new TypeConfig( |
| | | * executor: new IntegrationExecutor(), |
| | | * chunkKey: 'items', |
| | |
| | | use JVBase\managers\queue\Operation; |
| | | use JVBase\managers\queue\Progress; |
| | | use JVBase\managers\queue\Result; |
| | | use JVBase\managers\RoleManager; |
| | | use JVBase\managers\Roles; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | class InvitationExecutor implements Executor |
| | | { |
| | | protected CustomTable $table; |
| | | protected RoleManager $roleManager; |
| | | protected Roles $roleManager; |
| | | protected int $expiryDays = 14; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->table = CustomTable::for('invitations'); |
| | | $this->roleManager = new RoleManager(); |
| | | $this->roleManager = new Roles(); |
| | | } |
| | | |
| | | public function execute(Operation $operation, Progress $progress): Result |
| | |
| | | <?php |
| | | namespace JVBase\managers\queue\executors; |
| | | |
| | | use JVBase\managers\queue\{Executor, Operation, Progress, Result}; |
| | | use JVBase\managers\queue\{Executor, Operation, Progress, Queue, Result}; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Errors; |
| | | use JVBase\managers\UploadManager; |
| | | use JVBase\meta\Meta; |
| | | use Exception; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | 'process_upload_groups' |
| | | ]; |
| | | |
| | | /** |
| | | * @param Operation $operation |
| | | * @param Progress $progress |
| | | * @return Result |
| | | * @throws Exception |
| | | */ |
| | | public function execute(Operation $operation, Progress $progress): Result |
| | | { |
| | | if (!in_array($operation->type, self::HANDLED_TYPES)) { |
| | | throw new Exception("UploadExecutor cannot handle type: {$operation->type}"); |
| | | throw new Exception("UploadExecutor cannot handle type: $operation->type"); |
| | | } |
| | | |
| | | try { |
| | |
| | | 'temporary_cleanup' => $this->processTemporaryCleanup($operation, $data, $progress), |
| | | 'attach_upload_to_content'=> $this->processAttachToContent($operation, $data, $progress), |
| | | 'process_upload_groups' => $this->processUploadGroups($operation, $data, $progress), |
| | | default => throw new Exception("Unknown type: {$operation->type}") |
| | | default => throw new Exception("Unknown type: $operation->type") |
| | | }; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | '[UploadExecutor]:execute', |
| | | $e->getMessage(), |
| | | [ |
| | |
| | | */ |
| | | private function processFileUpload(Operation $operation, array $data, string $fileType, Progress $progress): Result |
| | | { |
| | | $uploader = new UploadManager(); |
| | | $processedResults = []; |
| | | $uploader = UploadManager::getInstance(); |
| | | $errors = []; |
| | | $uploadIds = []; |
| | | |
| | | $securedFiles = $data['secured_files'] ?? []; |
| | | $args = $data; |
| | | unset($args['secured_files']); |
| | | |
| | | // Phase 1: File I/O — no DB, no transactions |
| | | $prepared = []; |
| | | foreach ($securedFiles as $securedFile) { |
| | | $uploadIds[] = $securedFile['upload_id']; |
| | | try { |
| | | $prepared[$securedFile['upload_id']] = $uploader->prepareFile( |
| | | $prepared[$securedFile['upload_id']] = $uploader->processUpload( |
| | | $securedFile['temp_path'], |
| | | [ |
| | | 'file_type' => $fileType, |
| | | 'user_id' => $operation->userId, |
| | | 'post_id' => (int)($data['post_id'] ?? 0), |
| | | 'term_id' => (int)($data['term_id'] ?? 0), |
| | | 'original_name' => $securedFile['original_name'], |
| | | 'mime_type' => $securedFile['mime_type'], |
| | | 'content' => $data['content'] ?? '', |
| | | ] |
| | | $args |
| | | ); |
| | | $progress->advance(); |
| | | } catch (Exception $e) { |
| | | $progress->failItem($securedFile['upload_id'], $e->getMessage()); |
| | | $errors[] = $e->getMessage(); |
| | | } |
| | | } |
| | | |
| | | // Phase 2: DB registration — quick per-file writes |
| | | foreach ($prepared as $uploadId => $fileInfo) { |
| | | try { |
| | | $result = $uploader->registerFile($fileInfo); |
| | | |
| | | if (!$result['success']) { |
| | | $progress->failItem($uploadId, 'Registration failed'); |
| | | $errors[] = "Failed to register {$uploadId}"; |
| | | continue; |
| | | } |
| | | |
| | | $processedResults[$uploadId] = [ |
| | | 'upload_id' => $uploadId, |
| | | 'attachment_id' => $result['attachment_id'] ?? 0, |
| | | 'url' => $result['url'] ?? '', |
| | | 'sizes' => $result['sizes'] ?? [], |
| | | ]; |
| | | |
| | | // Apply frontend metadata if provided |
| | | $securedFile = $this->findSecuredFile($securedFiles, $uploadId); |
| | | if ($securedFile && !empty($securedFile['metadata'])) { |
| | | $this->applyMeta($result['attachment_id'], $securedFile['metadata']); |
| | | } |
| | | |
| | | $progress->advance(); |
| | | |
| | | } catch (Exception $e) { |
| | | $progress->failItem($uploadId, $e->getMessage()); |
| | | $errors[] = $e->getMessage(); |
| | | } |
| | | } |
| | | |
| | | // Destination + cleanup unchanged |
| | | $this->handleUploadDestination($data, $processedResults); |
| | | $this->cleanupTempFiles($securedFiles, $operation->userId); |
| | | |
| | | $outcome = count($processedResults) > 0 ? 'success' : 'failed'; |
| | | if (count($processedResults) > 0 && !empty($errors)) { |
| | | $outcome = count($prepared) > 0 ? 'success' : 'failed'; |
| | | if (count($prepared) > 0 && !empty($errors)) { |
| | | $outcome = 'partial'; |
| | | } |
| | | |
| | |
| | | outcome: $outcome, |
| | | result: [ |
| | | 'upload_ids' => $uploadIds, |
| | | 'uploads' => $processedResults, |
| | | 'processed_count' => count($processedResults), |
| | | 'uploads' => $prepared, |
| | | 'processed_count' => count($prepared), |
| | | 'total_count' => count($uploadIds), |
| | | 'errors' => $errors, |
| | | ] |
| | |
| | | } |
| | | |
| | | /** |
| | | * Find a secured file entry by upload_id |
| | | */ |
| | | private function findSecuredFile(array $securedFiles, string $uploadId): ?array |
| | | { |
| | | foreach ($securedFiles as $file) { |
| | | if (($file['upload_id'] ?? null) === $uploadId) { |
| | | return $file; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Attach upload results to content |
| | | * @param Operation $operation |
| | | * @param array $data |
| | | * @param Progress $progress |
| | | * @return Result |
| | | * @throws Exception |
| | | */ |
| | | private function processAttachToContent(Operation $operation, array $data, Progress $progress): Result |
| | | { |
| | |
| | | throw new Exception('No upload operation ID provided'); |
| | | } |
| | | |
| | | $uploadOp = JVB()->queue()->get($uploadOpId); |
| | | $uploadOp = Queue::getInstance()->get($uploadOpId); |
| | | if (!$uploadOp || !in_array($uploadOp->outcome, ['success', 'partial'])) { |
| | | throw new Exception("Upload operation {$uploadOpId} not completed successfully"); |
| | | throw new Exception("Upload operation $uploadOpId not completed successfully"); |
| | | } |
| | | |
| | | $uploadResults = $uploadOp->result['uploads'] ?? []; |
| | |
| | | // Resolve post_id from content_update dependency for new posts |
| | | if (empty($data['post_id']) || str_starts_with((string)($data['item_id'] ?? ''), 'new')) { |
| | | foreach ($operation->dependencies as $depId) { |
| | | $dep = JVB()->queue()->get($depId); |
| | | $dep = Queue::getInstance()->get($depId); |
| | | if ($dep && $dep->type === 'content_update' && !empty($dep->result['new_posts'])) { |
| | | $itemId = $data['item_id'] ?? null; |
| | | if ($itemId && isset($dep->result['new_posts'][$itemId])) { |
| | |
| | | if ($data['field_name'] === 'timeline_gallery') { |
| | | $registrar = Registrar::getInstance($data['content']); |
| | | if ($registrar) { |
| | | switch ($registrar->getType()) { |
| | | case 'post': |
| | | $meta = Meta::forPost($data['item_id']); |
| | | break; |
| | | case 'term': |
| | | $meta = Meta::forTerm($data['item_id']); |
| | | break; |
| | | case 'user': |
| | | $meta = Meta::forUser($data['item_id']); |
| | | break; |
| | | default: |
| | | $meta = false; |
| | | $meta = match ($registrar->getType()) { |
| | | 'post' => Meta::forPost($data['item_id']), |
| | | 'term' => Meta::forTerm($data['item_id']), |
| | | 'user' => Meta::forUser($data['item_id']), |
| | | default => false, |
| | | }; |
| | | |
| | | } |
| | | if ($meta) { |
| | | $title = $meta->get('post_title'); |
| | | $current = $meta->get('number'); |
| | |
| | | try { |
| | | if (array_key_exists('depends_on', $info)) { |
| | | // Get the dependency operation to find attachment ID |
| | | $depOp = JVB()->queue()->get($info['depends_on']); |
| | | $depOp = Queue::getInstance()->get($info['depends_on']); |
| | | if (!$depOp || !$depOp->result) { |
| | | $errors[] = "Dependency {$info['depends_on']} not found or has no result"; |
| | | continue; |
| | |
| | | $errors[] = "No attachment found for: ".print_r($info, true); |
| | | continue; |
| | | } |
| | | $postID = wp_get_post_parent_id($attachmentId); |
| | | if ($postID && !in_array($postID, $postsAttachedTo)){ |
| | | Cache::clearPost($postID); |
| | | } |
| | | |
| | | $this->applyMeta($attachmentId, $info); |
| | | $updatedCount++; |
| | |
| | | $progress->failItem($uploadId, $e->getMessage()); |
| | | $errors[] = $e->getMessage(); |
| | | } |
| | | |
| | | if ($success && $attachmentId) { |
| | | $postID = wp_get_post_parent_id($attachmentId); |
| | | if ($postID && !in_array($postID, $postsAttachedTo)){ |
| | | $postsAttachedTo[] = $postID; |
| | | //TODO: is there a better way? |
| | | } |
| | | } |
| | | |
| | | } |
| | | if (!empty ($postsAttachedTo)) { |
| | | foreach ($postsAttachedTo as $postId) { |
| | | Cache::invalidateItem('post', $postId); |
| | | } |
| | | } |
| | | |
| | | $outcome = $updatedCount > 0 ? 'success' : 'failed'; |
| | |
| | | } |
| | | |
| | | /** |
| | | * Cleanup temporary upload files |
| | | * @param Operation $operation |
| | | * @param array $data |
| | | * @param Progress $progress |
| | | * @return Result |
| | | */ |
| | | private function processTemporaryCleanup(Operation $operation, array $data, Progress $progress): Result |
| | | { |
| | | $uploader = new UploadManager(); |
| | | $uploader = UploadManager::getInstance(); |
| | | |
| | | // Cleanup secured files |
| | | if (!empty($data['files'])) { |
| | |
| | | $uploads = []; |
| | | $uploadIds = []; |
| | | foreach ($dependencies as $dependency) { |
| | | $res = JVB()->queue()->getOperationValue($dependency, 'result'); |
| | | $res = Queue::getInstance()->getOperationValue($dependency, 'result'); |
| | | if (empty($res)) { |
| | | continue; |
| | | } |
| | |
| | | // Helper methods |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | /** |
| | | * @param int $attachmentId |
| | | * @param array $metadata |
| | | * @return void |
| | | */ |
| | | private function applyMeta(int $attachmentId, array $metadata): void |
| | | { |
| | | $postUpdates = []; |
| | |
| | | |
| | | } |
| | | |
| | | private function updateFieldValue(array $data, array $results): void |
| | | { |
| | | if (empty($data['field_name'])) { |
| | | return; |
| | | } |
| | | |
| | | $attachmentIds = array_column($results, 'attachment_id'); |
| | | $meta = $this->getMetaManager($data); |
| | | if (!$meta) { |
| | | return; |
| | | } |
| | | |
| | | $existing = $meta->get($data['field_name']); |
| | | $existingIds = !empty($existing) ? explode(',', $existing) : []; |
| | | $allIds = array_unique(array_merge($existingIds, $attachmentIds)); |
| | | |
| | | $meta->set($data['field_name'], implode(',', $allIds)); |
| | | } |
| | | |
| | | private function getMetaManager(array $data): ?Meta |
| | | { |
| | | if (!empty($data['post_id'])) { |
| | |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * @param array $data |
| | | * @param array $results |
| | | * @return array |
| | | */ |
| | | private function createIndividualPosts(array $data, array $results): array |
| | | { |
| | | if (empty($data['content'])) { |
| | |
| | | |
| | | private function cleanupTempFiles(array $securedFiles, int $userId): void |
| | | { |
| | | $uploader = new UploadManager(); |
| | | $uploader = UploadManager::getInstance(); |
| | | |
| | | foreach ($securedFiles as $secured) { |
| | | if (!empty($secured['temp_path']) && file_exists($secured['temp_path'])) { |
| | |
| | | |
| | | use JVBase\managers\queue\Mergeable; |
| | | use JVBase\managers\queue\Operation; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | /** |
| | | * Default merger, uses the chunkKey as the mergeable array |
| | | */ |
| | |
| | | |
| | | use JVBase\managers\queue\Mergeable; |
| | | use JVBase\managers\queue\Operation; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | /** |
| | | * Field-aware merger for upload operations. |
| | | * Ensures uploads only merge when targeting the same field + post/term context. |
| | |
| | | namespace JVBase\meta; |
| | | |
| | | use DateTime; |
| | | use JVBase\base\Site; |
| | | use JVBase\integrations\services\GoogleMaps; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | protected static function renderLocation(string $name, mixed $value, array $config): string |
| | | { |
| | | $googleMaps = JVB()->connect('maps'); |
| | | if (!Site::hasIntegration('maps')) { |
| | | return ''; |
| | | } |
| | | $googleMaps = GoogleMaps::getInstance(); |
| | | if (!$googleMaps->isSetUp()) { |
| | | return '<div class="notice notice-warning"><p>Google Maps not configured. Please configure in Integrations settings.</p></div>'; |
| | | } |
| | |
| | | $meta = []; |
| | | } |
| | | $meta = array_map(fn($value) => maybe_unserialize($value[0]), $meta); |
| | | |
| | | |
| | | $this->fields = []; |
| | | |
| | | if (class_exists('WooCommerce') && $this->type === 'post' && $this->wpObject->post_type??'' === 'product') { |
| | | $fields['post_title'] = [ |
| | | 'type' => 'text', |
| | | 'label' => 'Title', |
| | | ]; |
| | | $fields['post_content'] = [ |
| | | 'type' => 'text', |
| | | 'label' => 'Content', |
| | | ]; |
| | | $fields['post_excerpt'] = [ |
| | | 'type' => 'text', |
| | | 'label' => 'Short Description', |
| | | ]; |
| | | $fields['post_id'] = [ |
| | | 'type' => 'text', |
| | | 'label' => 'ID', |
| | | ]; |
| | | |
| | | |
| | | $fields['_stock'] = [ |
| | | 'type' => 'number', |
| | | 'label' => 'Stock', |
| | | 'default' => null |
| | | ]; |
| | | $fields['_stock_status'] = [ |
| | | 'type' => 'select', |
| | | 'label' => 'Stock Status', |
| | | 'default' => 'instock', |
| | | 'options' => [ |
| | | 'instock' => 'In Stock', |
| | | 'outofstock' => 'Out of Stock', |
| | | 'onbackorder' => 'On Backorder' |
| | | ] |
| | | ]; |
| | | $fields['_price'] = [ |
| | | 'type' => 'text', |
| | | 'subtype' => 'number', |
| | | 'label' => 'Price' |
| | | ]; |
| | | |
| | | |
| | | $product = wc_get_product($id); |
| | | $fields['sku'] = [ |
| | | 'type' => 'text', |
| | | 'label' => 'SKU', |
| | | ]; |
| | | |
| | | $out = []; |
| | | if ($product && $product->is_type('variable')) { |
| | | $var = $product->get_children(); |
| | | $var = array_filter(array_map(function($v) { |
| | | return wc_get_product($v); |
| | | }, $var)); |
| | | |
| | | $title= $product->get_title(); |
| | | foreach ($var as $variation) { |
| | | $out[] = [ |
| | | 'id' => $variation->get_id(), |
| | | 'sku' => $variation->get_sku(), |
| | | 'title' => str_replace($title.' - ', '', $variation->get_name()), |
| | | 'price' => $variation->get_price(), |
| | | 'description'=> $variation->get_short_description(), |
| | | 'sale' => $variation->get_sale_price(), |
| | | 'img' => $variation->get_image_id(), |
| | | 'stock' => $variation->get_stock_quantity(), |
| | | 'stock_status' => $variation->get_stock_status() |
| | | ]; |
| | | } |
| | | } |
| | | if (!empty($out)) { |
| | | $this->fields['variations'] = new Field('variations', $out, [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Variations', |
| | | 'fields' => [ |
| | | 'id' => [ |
| | | 'type' => 'text', |
| | | 'subtype' => 'number', |
| | | 'label' => 'Variation ID', |
| | | ], |
| | | 'sku' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Stock Keeping Unit (SKU)' |
| | | ], |
| | | 'title' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Variation Title' |
| | | ], |
| | | 'price' => [ |
| | | 'type' => 'text', |
| | | 'subtype' => 'number', |
| | | 'label' => 'Variation Price' |
| | | ], |
| | | 'sale' => [ |
| | | 'type' => 'text', |
| | | 'subtype' => 'number', |
| | | 'label' => 'Sale Price', |
| | | ], |
| | | 'image' => [ |
| | | 'type' => 'text', |
| | | 'subtype' => 'number', |
| | | 'label' => 'Variation Image ID' |
| | | ], |
| | | 'stock' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Stock Quantity' |
| | | ], |
| | | 'stock_status' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Stock Status' |
| | | ] |
| | | ] |
| | | ]); |
| | | } |
| | | |
| | | |
| | | } |
| | | foreach ($fields as $fieldName => $config) { |
| | | $fieldName = jvbNoBase($fieldName); |
| | | $value = ''; |
| | | if ($this->type === 'option') { |
| | | $value = get_option(BASE . $fieldName, $config['default'] ?? ''); |
| | | } else if ( |
| | | $this->wpObject && property_exists($this->wpObject, $fieldName) |
| | | || $type === 'user' && $this->wpObject && property_exists($this->wpObject->data, $fieldName)) { |
| | | ($this->wpObject && property_exists($this->wpObject, $fieldName)) |
| | | || ($type === 'user' && $this->wpObject && property_exists($this->wpObject->data, $fieldName))) { |
| | | $config['wp'] = true; |
| | | $value = $this->wpObject->$fieldName; |
| | | |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | //REFACTOR |
| | | require(JVB_DIR . '/inc/meta/Meta.php'); //Main facade |
| | | require(JVB_DIR . '/inc/meta/Item.php'); //Data container for one object |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | //Shared Flags |
| | | 'favouritable', 'karma', 'show_feed', 'show_directory', 'approve_new', 'has_responses', 'invitable', |
| | | //Post Flags |
| | | 'hide_single', 'redirect_to_author', 'is_calendar', 'single_image', 'is_timeline', 'is_gallery', 'is_faq', 'is_glossary', 'rewrite_taxonomy', 'add_image_column', |
| | | 'hide_single', 'redirect_to_author', 'is_calendar', 'single_image', 'is_timeline', 'is_gallery', 'is_faq', 'is_glossary', 'rewrite_taxonomy', 'add_image_column', 'is_profile', |
| | | //Taxonomy Flags |
| | | 'is_content', 'is_ownable', 'verify_entry', 'track_changes', 'associate_user_content', 'prefix_post_type', |
| | | //User Flags |
| | |
| | | * @var bool Whether to generate a profile for this user role |
| | | */ |
| | | public bool $profile_link; |
| | | public bool $is_profile = false; |
| | | /** |
| | | * @var array|string |
| | | */ |
| | |
| | | protected MakeTrackChanges $trackChanges; |
| | | protected MakeVerification $verification; |
| | | |
| | | protected string $umami_event; |
| | | |
| | | private static array $instances = []; |
| | | |
| | | protected string $based; |
| | |
| | | $this->karmaManager = KarmaManager::for($this->slug, $this->type); |
| | | } |
| | | } |
| | | |
| | | public function getKarmaManager():?KarmaManager |
| | | { |
| | | return $this->karmaManager; |
| | | } |
| | | public static function getInstance(string $slug):Registrar|false |
| | | { |
| | | self::ensureInstanced(); |
| | |
| | | |
| | | $this->profile = $slug; |
| | | |
| | | return Registrar::forPost($slug, $singular, $plural); |
| | | $reg = Registrar::forPost($slug, $singular, $plural); |
| | | $reg->setProfile(); |
| | | |
| | | return $reg; |
| | | } |
| | | |
| | | public function setProfile():void |
| | | { |
| | | $this->is_profile = true; |
| | | } |
| | | public function getProfile():self|false |
| | | { |
| | | if (!$this->profile_link) { |
| | |
| | | { |
| | | $this->hasIntegrations = $has; |
| | | } |
| | | |
| | | public function isOwnable():bool |
| | | { |
| | | return $this->is_ownable??false; |
| | | } |
| | | |
| | | public function umamiEvent(string $event):void |
| | | { |
| | | $this->umami_event = str_replace('-', '_', sanitize_title($event)); |
| | | } |
| | | public function getUmamiEvent():string |
| | | { |
| | | return $this->umami_event??$this->getDefaultUmamiType(); |
| | | } |
| | | protected function getDefaultUmamiType():string |
| | | { |
| | | return match($this->type) { |
| | | 'post' => $this->getDefaultPostUmamiType(), |
| | | 'term' => $this->getDefaultTermUmamiType(), |
| | | 'user' => $this->getDefaultUserUmamiType(), |
| | | default => '' |
| | | }; |
| | | } |
| | | protected function getDefaultPostUmamiType():string |
| | | { |
| | | if ($this->is_profile) { |
| | | return 'view_profile'; |
| | | } |
| | | return 'view_content'; |
| | | } |
| | | protected function getDefaultTermUmamiType():string |
| | | { |
| | | if ($this->is_content) { |
| | | return 'view_collective'; |
| | | } |
| | | return 'view_term'; |
| | | } |
| | | protected function getDefaultUserUmamiType():string |
| | | { |
| | | |
| | | return 'view_user'; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | require_once(JVB_DIR . '/inc/registrar/config/Config.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/Breadcrumbs.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/Dashboard.php'); |
| | |
| | | namespace JVBase\registrar\config; |
| | | |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\registrar\config; |
| | | |
| | | use JVBase\integrations\ServiceMap; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | |
| | |
| | | } |
| | | |
| | | final class Integration extends Config{ |
| | | use ServiceMap; |
| | | /** |
| | | * @var string Must match as defined in the JVBase\integrations namespace |
| | | */ |
| | | protected string $service_name; |
| | | protected ?string $content_type = null; |
| | | |
| | | /** |
| | | * @var bool Whether to send to the integration on publish |
| | | */ |
| | |
| | | if (is_null($content)) { |
| | | return $this; |
| | | } |
| | | $connection = JVB()->connect($this->service_name); |
| | | $connection = $this->getService($this->service_name); |
| | | if (!$connection){ |
| | | error_log('[Integration]::setContentType Service is not setup. '.$this->service_name); |
| | | return $this; |
| | |
| | | |
| | | $allowed = $connection->getAllowedContent(); |
| | | if (!in_array($content, $allowed)) { |
| | | error_log($this->service_name.' Connection does not support this content: '.$content); |
| | | //TODO |
| | | // error_log($this->service_name.' Connection does not support this content: '.$content); |
| | | return $this; |
| | | } |
| | | $this->content_type = $content; |
| | |
| | | public function setCustomer(bool $set):self |
| | | { |
| | | $this->isCustomer = $set; |
| | | // JVB()->connect($this->service_name)->addCustomerFields(); |
| | | |
| | | return $this; |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | |
| | | function jvbTSFDoIt(string $slug, ?array $args):bool |
| | | { |
| | | if (!class_exists('THE_SEO_Framework')) { |
| | | return false; |
| | | } |
| | | $tsf = tsf(); |
| | | |
| | | return (null === $args) |
| | |
| | | |
| | | switch ($variable) { |
| | | case 'CREATOR': |
| | | return JVB()->seo()->getCreator(); |
| | | return SchemaOutput::getInstance()->getCreator(); |
| | | case 'name': |
| | | if (!$meta && is_post_type_archive()) { |
| | | $obj = get_queried_object(); |
| | |
| | | //TODO generate from id |
| | | |
| | | } else if (str_contains($value, 'CREATOR')) { |
| | | return JVB()->seo()->getCreator(); |
| | | return SchemaOutput::getInstance()->getCreator(); |
| | | } |
| | | return ''; |
| | | } |
| | |
| | | |
| | | $config = $this->getConfig(); |
| | | |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | $class = SchemaHelper::getInstance()::classFromConfig($config, $meta); |
| | | if (property_exists($class, 'author')) { |
| | | $class->setAuthor(JVB()->seo()->getCreator(true)); |
| | | $class->setAuthor(SchemaOutput::getInstance()->getCreator(true)); |
| | | } |
| | | |
| | | |
| | |
| | | $ID, |
| | | function() use ($ID) { |
| | | $action = BASE.ucfirst($this->slug).'Schema'; |
| | | $config = JVB()->schemaHelper()::schema($action); |
| | | $config = SchemaHelper::getInstance()::schema($action); |
| | | |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; |
| | |
| | | error_log('No class found for archive schema output: '.$config['type']); |
| | | return []; |
| | | } |
| | | $class = JVB()->schemaHelper()::classFromConfig($config); |
| | | $class = SchemaHelper::getInstance()::classFromConfig($config); |
| | | |
| | | $class->setIsPartOf(get_home_url().'/#website'); |
| | | $itemList = new render\Thing\Intangible\ItemList\ItemList(); |
| | |
| | | if (!empty($override)) { |
| | | return $override; |
| | | } |
| | | $config = JVB()->schemaHelper()->archive($this->slug); |
| | | $config = SchemaHelper::getInstance()->archive($this->slug); |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; |
| | | } |
| | |
| | | $obj = get_queried_object(); |
| | | $meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null; |
| | | |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | $class = SchemaHelper::getInstance()::classFromConfig($config, $meta); |
| | | |
| | | $class->setIsPartOf(get_home_url().'/#website'); |
| | | $itemList = new render\Thing\Intangible\ItemList\ItemList(); |
| | |
| | | $meta = null; |
| | | } |
| | | $config = $this->getConfig(); |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | $class = SchemaHelper::getInstance()::classFromConfig($config, $meta); |
| | | if ($class) { |
| | | $class->delete('about'); |
| | | } |
| | |
| | | error_log('[SEO]Schema::getConfig Invalid type: '.$type); |
| | | return []; |
| | | } |
| | | return JVB()->schemaHelper()::getConfig($this->slug, $type); |
| | | return SchemaHelper::getInstance()::getConfig($this->slug, $type); |
| | | } |
| | | |
| | | public function resolveMeta(array $config, Meta $meta):array |
| | |
| | | $question = SchemaHelper::classFromConfig($question); |
| | | $page->setMainEntity($question); |
| | | |
| | | $page->setAuthor(JVB()->seo()->getCreator(true)); |
| | | $page->setAuthor(SchemaOutput::getInstance()->getCreator(true)); |
| | | return $page->outputSchema(); |
| | | |
| | | } |
| | |
| | | } |
| | | |
| | | $page->setImage($images); |
| | | $page->setAuthor(JVB()->seo()->getCreator(true)); |
| | | $page->setAuthor(SchemaOutput::getInstance()->getCreator(true)); |
| | | return $page->outputSchema(); |
| | | |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | require(JVB_DIR . '/inc/registrar/config/seo/Helpers.php'); |
| | | require(JVB_DIR . '/inc/registrar/config/seo/Resolver.php'); |
| | | require(JVB_DIR . '/inc/registrar/config/seo/Archive.php'); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | namespace JVBase\registrar\helpers; |
| | | |
| | | use JVBase\integrations\ServiceMap; |
| | | use JVBase\registrar\config\Integration; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | |
| | | } |
| | | |
| | | class AddIntegrationFields { |
| | | use ServiceMap; |
| | | protected string $service_name; |
| | | private Registrar $registrar; |
| | | protected Integration $config; |
| | |
| | | |
| | | public function getIntegrationFields():array |
| | | { |
| | | if (!$this->getService($this->service_name)) { |
| | | error_log('Attempted to connect to unsupported service: '.$this->service_name); |
| | | return []; |
| | | } |
| | | $fields = [ |
| | | 'share_to_'.$this->service_name => [ |
| | | 'type' => 'true_false', |
| | |
| | | 'default' => 1, |
| | | ]; |
| | | } |
| | | $additional = JVB()->connect($this->service_name)->getAdditionalFields($this->config->getContentType()); |
| | | |
| | | $additional = $this->getService($this->service_name)->getAdditionalFields($this->config->getContentType()); |
| | | if (!empty($additional)) { |
| | | $additional = array_combine( |
| | | array_map(fn($k) => str_starts_with($k, '_'.$this->service_name) ? $k : "_{$this->service_name}_{$k}", array_keys($additional)), |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| | |
| | | 'message' => 'This shop doesn\'t exist?' |
| | | ]); |
| | | } |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | unset($data['user']); |
| | | $operationID = $data['id']; |
| | | unset($data['id']); |
| | |
| | | |
| | | |
| | | //Notify managers |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $owners, |
| | | 'entry_request', |
| | | [ |
| | |
| | | <?php |
| | | namespace JVBase\rest; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Cloudflare; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | protected string $namespace = 'jvb/v1'; |
| | | protected ?Cache $cache = null; |
| | | protected string $cacheName = ''; |
| | | protected int $cacheTtl = 3600; |
| | | protected int $cacheTtl = HOUR_IN_SECONDS; |
| | | protected static ?string $action; |
| | | |
| | | public function __construct() |
| | |
| | | /** |
| | | * Convert MySQL datetime to ISO 8601 timestamp |
| | | */ |
| | | protected function formatTimestamp(?string $mysqlDatetime): ?string |
| | | public function formatTimestamp(?string $mysqlDatetime): ?string |
| | | { |
| | | return Response::formatTimestamp($mysqlDatetime); |
| | | } |
| | |
| | | */ |
| | | protected function verifyTurnstile(string $token): bool |
| | | { |
| | | if (!Site::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) { |
| | | if (!Site::hasIntegration('cloudflare') || !Cloudflare::getInstance()->isSetUp()) { |
| | | return true; |
| | | } |
| | | |
| | | return !empty($token) && JVB()->connect('cloudflare')->verifyTurnstile($token); |
| | | return !empty($token) && Cloudflare::getInstance()->verifyTurnstile($token); |
| | | } |
| | | |
| | | /** |
| | |
| | | $key = $cache->generateKey($key); |
| | | } |
| | | |
| | | // Prefer tag freshness if available |
| | | $tags = $cache->getTags(); |
| | | |
| | | $lastModified = $tags |
| | | ? $cache->getLastModifiedForTags($tags) |
| | | : $cache::lastModified($group); |
| | | |
| | | $lastModified = $cache::lastModified($group); |
| | | |
| | | if (!$lastModified) { |
| | | return false; |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | |
| | |
| | | require(JVB_DIR . '/inc/rest/Rest.php'); |
| | | require(JVB_DIR . '/inc/rest/RateLimits.php'); |
| | | |
| | | //OLD METHOD |
| | | //require(JVB_DIR . '/inc/rest/RateLimiter.php'); |
| | | //require(JVB_DIR . '/inc/rest/RestRouteManager.php'); |
| | | //require(JVB_DIR . '/inc/rest/RegisterRoutes.php'); |
| | | |
| | | //if (Site::has('feed_block')) { |
| | | require(JVB_DIR . '/inc/rest/routes/FeedRoutes.php'); |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\JVB; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\IconsManager; |
| | |
| | | |
| | | class AdminRoutes extends Rest |
| | | { |
| | | protected array $fields; |
| | | protected $meta; |
| | | protected string $metaType; |
| | | protected string $content; |
| | | use Instance; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'itsme'; |
| | | // $this->cacheName = 'itsme'; |
| | | parent::__construct(); |
| | | // $this->cache = Cache::for($this->cacheName, null, $this->cacheTtl); |
| | | } |
| | | public function registerRoutes():void |
| | | { |
| | |
| | | ); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @param array $filters |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function checkFilters(array $filters) |
| | | { |
| | | global $karma; |
| | | $out = []; |
| | | foreach ($filters as $type => $value) { |
| | | if (!array_key_exists($type, $karma)) { |
| | | continue; |
| | | } |
| | | $out[$type] = jvbSanitizeIDList($value); |
| | | } |
| | | return $out; |
| | | } |
| | | |
| | | /** |
| | | * Handle cache-related actions |
| | | */ |
| | |
| | | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Approvals; |
| | | use JVBase\managers\Cache; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | |
| | | use JVBase\base\Site; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use Exception; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | |
| | | |
| | | class ApprovalRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected array $userTypes; |
| | | protected array $termTypes; |
| | | protected array $allTypes; |
| | |
| | | $this->cacheName = 'approvals'; |
| | | $this->hasMemberApproval = Site::membership() && Site::membership()->has('member_verified'); |
| | | parent::__construct(); |
| | | $this->cache = Cache::for('approvals', 'custom', $this->cacheTtl); |
| | | |
| | | $this->initTypes(); |
| | | } |
| | |
| | | if (!in_array($vote, ['approve', 'reject', 'dismiss'])) { |
| | | return false; |
| | | } |
| | | $result = JVB()->approvals()->markApproval($request_id, $user_id, $type, $vote, $notes); |
| | | $result = Approvals::getInstance()->markApproval($request_id, $user_id, $type, $vote, $notes); |
| | | return $result['success']; |
| | | } |
| | | |
| | |
| | | */ |
| | | public function getVerificationDetails(int $requestID, string $type): array|false |
| | | { |
| | | $request = JVB()->approvals()->getRequest($requestID, $type); |
| | | $request = Approvals::getInstance()->getRequest($requestID, $type); |
| | | if (!$request) { |
| | | return false; |
| | | } |
| | | $votes = JVB()->approvals()->getVotes($requestID, $type); |
| | | $votes = Approvals::getInstance()->getVotes($requestID, $type); |
| | | |
| | | // Join with user data for display names |
| | | foreach ($votes as &$vote) { |
| | |
| | | int $required_approvals = 3 |
| | | ): int|false { |
| | | |
| | | $result = JVB()->approvals()->createApproval( |
| | | $result = Approvals::getInstance()->createApproval( |
| | | $user_id, |
| | | $taxonomy, |
| | | $name, |
| | |
| | | return $result['success']; |
| | | } |
| | | |
| | | protected function getTableName(string $type, string $suffix): string |
| | | { |
| | | return match ($type) { |
| | | 'term' => "approval_term_{$suffix}", |
| | | default => "approval_{$type}_{$suffix}", |
| | | }; |
| | | } |
| | | |
| | | public function getApprovals(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $user_id = get_current_user_id(); |
| | | $type = sanitize_text_field($request->get_param('type') ?? 'all'); |
| | | $status = sanitize_text_field($request->get_param('status') ?? 'pending'); |
| | | |
| | | $cacheKey = compact('user_id', 'type', 'status'); |
| | | |
| | | $result = $this->cache->remember($cacheKey, function() use ($type, $status) { |
| | | $data = []; |
| | | |
| | | if ($type === 'user' || $type === 'all') { |
| | | $data['user_approvals'] = $this->getUserApprovals($status); |
| | | } |
| | | |
| | | if ($type === 'term' || $type === 'all') { |
| | | $data['term_approvals'] = $this->getTermApprovals($status); |
| | | } |
| | | |
| | | return $data; |
| | | }); |
| | | $result = Approvals::getInstance()->getApprovalRequests($user_id, $type === 'all' ? null : $type); |
| | | |
| | | return $this->success($result); |
| | | } |
| | | |
| | | private function getUserApprovals(string $status = 'pending'): array |
| | | { |
| | | |
| | | $table = CustomTable::for($this->getTableName('artist', 'requests')); |
| | | |
| | | $query = $table; |
| | | |
| | | if ($status !== 'all') { |
| | | $query = $query->where(['status' => $status]); |
| | | } |
| | | |
| | | return $query->orderBy('created_at', 'DESC')->getResults(ARRAY_A); |
| | | } |
| | | |
| | | private function getTermApprovals(string $status = 'pending'): array |
| | | { |
| | | $table = CustomTable::for($this->getTableName('term', 'requests')); |
| | | |
| | | if ($status === 'all') { |
| | | return $table->orderBy('created_at', 'DESC')->getResults(ARRAY_A); |
| | | } |
| | | |
| | | return $table |
| | | ->where(['status' => $status]) |
| | | ->orderBy('created_at', 'DESC') |
| | | ->getResults(ARRAY_A); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\queue\executors\ContentExecutor; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | |
| | | class ContentRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected array $fields = []; |
| | | protected array $taxonomies = []; |
| | | protected string $post_type = ''; |
| | |
| | | protected array $timelineUniqueFields = []; |
| | | protected static ?string $action = 'dash-'; |
| | | protected Meta $meta; |
| | | protected Cache $termCache; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'user_content_' . get_current_user_id(); |
| | | $this->cacheName = 'user_content'; |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->cacheName, 'post', DAY_IN_SECONDS); |
| | | $this->cache->user(); |
| | | $this->termCache = Cache::for($this->cacheName.'_terms', 'term', DAY_IN_SECONDS); |
| | | $this->termCache->user(); |
| | | |
| | | |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | $this->cache->connect('post', true); |
| | | $this->cache->connect('term', true); |
| | | add_action('init', [$this, 'registerContentExecutors'], 5); |
| | | } |
| | | |
| | |
| | | */ |
| | | public function registerContentExecutors(): void |
| | | { |
| | | $registry = JVB()->queue()->registry(); |
| | | $registry = Queue::getInstance()->registry(); |
| | | $executor = new ContentExecutor(); |
| | | |
| | | // Content updates - chunked at 10 posts |
| | |
| | | unset($data['id']); |
| | | |
| | | error_log('[CONTENT]:'.print_r($data, true)); |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $queue->queueOperation( |
| | | 'content_update', |
| | | $user_id, |
| | |
| | | } |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | $cached = $this->checkCache($key, $request); |
| | | $cached = $this->cache->get($key); |
| | | if ($cached) { |
| | | return $cached; |
| | | } |
| | |
| | | |
| | | $this->cache->set($key, $data); |
| | | |
| | | $response = Response::success($data); |
| | | return $this->addCacheHeaders($response); |
| | | return Response::success($data); |
| | | } |
| | | public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response |
| | | { |
| | |
| | | |
| | | $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_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); |
| | | return Response::success($cache); |
| | | } |
| | | $this->post_type = jvbCheckBase($params['content'] ?? $params['type']); |
| | | // Only expand search to taxonomies if we're actually going to query |
| | |
| | | |
| | | $this->cache->set($key, $data); |
| | | |
| | | $response = Response::success($data); |
| | | return $this->addCacheHeaders($response); |
| | | return Response::success($data); |
| | | } |
| | | |
| | | protected function applySearchFilters(array $args, array $params): array |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\queue\executors\ContentTermExecutor; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | |
| | | */ |
| | | class ContentTermsRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected string $taxonomy; |
| | | protected Registrar $registrar; |
| | | protected ?CustomTable $historyTable = null; |
| | | protected ?CustomTable $requestsTable = null; |
| | | |
| | | public function __construct(string $taxonomy = '') |
| | | //TODO: This needs some work. We need to move most logic into it's own manager class |
| | | protected function __construct(string $taxonomy = '') |
| | | { |
| | | $this->taxonomy = jvbNoBase($taxonomy); |
| | | |
| | |
| | | $this->registrar = Registrar::getInstance($this->taxonomy); |
| | | $this->cacheName = $this->taxonomy; |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->taxonomy, 'term', DAY_IN_SECONDS); |
| | | $this->setupTables(); |
| | | $this->setupCacheConnections(); |
| | | } |
| | |
| | | |
| | | public function registerContentTermsExecutors():void |
| | | { |
| | | $registry = JVB()->queue()->registry(); |
| | | $registry = Queue::getInstance()->registry(); |
| | | $executor = new ContentTermExecutor(); |
| | | $taxonomies = Registrar::withFeature('is_content', 'term'); |
| | | |
| | |
| | | } |
| | | |
| | | return user_can($userID, 'manage_options') || |
| | | JVB()->roles()->isManager($userID, $termID); |
| | | Roles::getInstance()()->isManager($userID, $termID); |
| | | } |
| | | |
| | | public function checkOwnerPermission(WP_REST_Request $request): bool |
| | |
| | | } |
| | | |
| | | return user_can($userID, 'manage_options') || |
| | | JVB()->roles()->isOwner($userID, $termID); |
| | | Roles::getInstance()()->isOwner($userID, $termID); |
| | | } |
| | | |
| | | /** |
| | |
| | | unset($data['user'], $data['term_id']); |
| | | |
| | | // Queue the update |
| | | $op = JVB()->queue()->add( |
| | | $op = Queue::getInstance()->add( |
| | | "{$this->taxonomy}_update", |
| | | $userID, |
| | | array_merge(['term_id' => $termID], $data), |
| | |
| | | $targetUserID = (int)$request->get_param('target_user'); |
| | | |
| | | // Queue the operation |
| | | $op = JVB()->queue()->add( |
| | | $op = Queue::getInstance()->add( |
| | | "{$this->taxonomy}_member_{$action}", |
| | | $userID, |
| | | [ |
| | |
| | | }); |
| | | |
| | | // Notify user |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $requestData['user_id'], |
| | | 'request_' . $status, |
| | | [ |
| | |
| | | return $this->error('Invalid target user'); |
| | | } |
| | | |
| | | $roleManager = JVB()->roles(); |
| | | $roleManager = Roles::getInstance()(); |
| | | |
| | | try { |
| | | $result = match($role) { |
| | |
| | | */ |
| | | protected function notifyTermManagers(int $termID, int $actingUserID, string $notificationType): void |
| | | { |
| | | $roleManager = JVB()->roles(); |
| | | $roleManager = Roles::getInstance()(); |
| | | $managers = $roleManager->getManagedTerms($actingUserID, $this->taxonomy); |
| | | |
| | | $term = get_term($termID, jvbCheckBase($this->taxonomy)); |
| | | |
| | | foreach ($managers as $managerID) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $managerID, |
| | | $notificationType, |
| | | [ |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Errors; |
| | | use JVBase\rest\Response; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | |
| | | } |
| | | class ErrorRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | /** |
| | | * Registers error routes |
| | | * @return void |
| | |
| | | // Determine severity based on error type |
| | | $severity = $this->getSeverityFromType($error_type); |
| | | |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | $contextData['component'] ?? 'client-js', |
| | | $message, |
| | | $contextData, |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\managers\Favourites; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Response; |
| | |
| | | |
| | | class FavouritesRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected array $valid_types; |
| | | protected Cache $listsCache; |
| | | protected Cache $sharedListsCache; |
| | |
| | | $this->cacheName = 'favourites'; |
| | | $this->cacheTtl = HOUR_IN_SECONDS; |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->cacheName)->user(); |
| | | |
| | | // Set up cache connections |
| | | $this->cache->connect('post')->connect('user')->connect('taxonomy')->user(); |
| | | $this->listsCache = Cache::for('lists')->connect('favourites', true)->user(); |
| | | $this->sharedListsCache = Cache::for('sharedLists')->connect('favourites', true)->user(); |
| | | $this->favouritesCache = Cache::for('allFavourites')->connect('favourites', true)->user(); |
| | | $this->listsCache = Cache::for('fav_lists')->user(); |
| | | $this->sharedListsCache = Cache::for('fav_shared_list')->user(); |
| | | $this->favouritesCache = Cache::for('allFavourites')->user(); |
| | | |
| | | $this->valid_types = array_merge(Registrar::getRegistered('post'), Registrar::getRegistered('term')); |
| | | |
| | | // Initialize CustomTable instances |
| | | $this->favourites = CustomTable::for('favourites'); |
| | | $this->lists = CustomTable::for('favourites_lists'); |
| | | $this->listItems = CustomTable::for('favourites_list_items'); |
| | | $this->listShares = CustomTable::for('favourites_list_shares'); |
| | | |
| | | } |
| | | |
| | | public function registerRoutes(): void |
| | |
| | | } |
| | | |
| | | $args = $this->buildParams($request); |
| | | $key = $this->cache->generateKey($args); |
| | | $result = $this->cache->remember( |
| | | $args, |
| | | function() use ($user_id, $args) { |
| | | return Favourites::getInstance()->getFavourites($args); |
| | | } |
| | | ); |
| | | |
| | | // Check cache headers |
| | | $cache_check = $this->checkHeaders($request, $key); |
| | | if ($cache_check) { |
| | | return $cache_check; |
| | | } |
| | | $result = JVB()->favourites()->getFavourites($args); |
| | | $result['items'] = $this->formatItems($result['items']); |
| | | |
| | | return $this->addCacheHeaders(Response::success($result)); |
| | | $response = [ |
| | | 'items' => $this->formatItems($result['items']) |
| | | ]; |
| | | return Response::success($response); |
| | | } |
| | | |
| | | |
| | |
| | | $user_id = get_current_user_id(); |
| | | |
| | | $action = strtolower(sanitize_text_field($params['action'])); |
| | | $action = in_array($action, ['add', 'remove']) ? $action : false; |
| | | $action = in_array($action, ['add', 'remove', 'toggle', 'batch', 'notes']) ? $action : false; |
| | | if (!$action) { |
| | | return $this->error('Invalid favourite action'); |
| | | } |
| | | $target_id = absint($params['target_id']??0); |
| | | if ($target_id === 0) { |
| | | return $this->error('Invalid target id'); |
| | | } |
| | | |
| | | $type = sanitize_text_field($params['type']??''); |
| | | if (empty($type)) { |
| | | return $this->error('No type provided'); |
| | | } |
| | | |
| | | $result = JVB()->favourites()->toggleFavourite( |
| | | $action === 'add', |
| | | $user_id, |
| | | $target_id, |
| | | $type |
| | | ); |
| | | $target_id = absint($params['target_id']??0); |
| | | if ($action !== 'batch' && $target_id === 0) { |
| | | return $this->error('Invalid target id'); |
| | | } |
| | | |
| | | $favourites = Favourites::getInstance(); |
| | | $result = false; |
| | | switch ($action) { |
| | | case 'notes': |
| | | $note = sanitize_text_field($params['note']??''); |
| | | $result = $favourites->addNoteFromTarget($user_id, $target_id, $type, $note); |
| | | break; |
| | | case 'toggle': |
| | | $isFavourited = $favourites->hasFavourited($user_id, $target_id, $type); |
| | | $result = $favourites->toggleFavourite(!$isFavourited, $user_id, $target_id, $type); |
| | | break; |
| | | case 'add': |
| | | $result = $favourites->favourite($user_id, $target_id, $type); |
| | | break; |
| | | case 'remove': |
| | | $result = $favourites->unfavourite($user_id, $target_id, $type); |
| | | break; |
| | | case 'batch': |
| | | $items = $params['items']??[]; |
| | | $items = array_filter($items, function ($item) { |
| | | $keys = ['target_id', 'type', 'action']; |
| | | foreach ($keys as $key) { |
| | | if (!array_key_exists($key, $item)) { |
| | | return false; |
| | | } |
| | | if (empty($item[$key])) { |
| | | return false; |
| | | } |
| | | } |
| | | if (!in_array($item['action'], ['add', 'remove'])) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }); |
| | | if (empty($items)) { |
| | | $result = $this->success(['message' => 'No items to update']); |
| | | } else { |
| | | $result = $favourites->batchToggleFavourites($user_id, $items); |
| | | } |
| | | break; |
| | | } |
| | | |
| | | if ($result) { |
| | | return $this->success(); |
| | | return $this->success(is_bool($result) ? [] : ['result' => $result]); |
| | | } |
| | | return $this->error('Something went wrong'); |
| | | } |
| | |
| | | */ |
| | | public function getLists(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $params = $request->get_params(); |
| | | $user_id = get_current_user_id(); |
| | | |
| | | |
| | | $args = $this->buildParams($request); |
| | | $args['per_page'] = 20; |
| | | $listId = $request->get_param('id'); |
| | |
| | | $args['where']['id'] = sanitize_text_field($listId); |
| | | } |
| | | |
| | | $key = $this->listsCache->generateKey($args); |
| | | |
| | | // Check cache headers |
| | | $cache_check = $this->checkHeaders($request, $key); |
| | | if ($cache_check) { |
| | | return $cache_check; |
| | | } |
| | | |
| | | $includeShares = !empty($request->get_param('include_shares')); |
| | | |
| | | $response = !empty($listId) |
| | | ? JVB()->favourites()->getListDetails($listId, $user_id) |
| | | : JVB()->favourites()->getAvailableLists($args, $includeShares); |
| | | ? Favourites::getInstance()->getListDetails($listId, $user_id) |
| | | : Favourites::getInstance()->getAvailableLists($args, $includeShares); |
| | | |
| | | return $this->addCacheHeaders(Response::success($response)); |
| | | return Response::success($response); |
| | | } |
| | | |
| | | |
| | |
| | | $user_id = get_current_user_id(); |
| | | |
| | | |
| | | $counts = JVB()->favourites()->getFavouriteCounts($user_id); |
| | | $counts = Favourites::getInstance()->getFavouriteCounts($user_id); |
| | | |
| | | return Response::success(['counts' => $counts]); |
| | | } |
| | | |
| | | /** |
| | | * Process favourite operations using transactions |
| | | */ |
| | | public function processOperation($result, object $operation, array $data) |
| | | { |
| | | $action_map = [ |
| | | 'favourite_add' => 'addFavourite', |
| | | 'favourite_remove' => 'removeFavourite', |
| | | 'favourite_batch' => 'batchFavourites', |
| | | 'favourite_note' => 'processNote', |
| | | 'favourite_list_create' => 'createList', |
| | | 'favourite_list_update' => 'updateList', |
| | | 'favourite_list_delete' => 'deleteList', |
| | | 'favourite_list_add_items' => 'addToList', |
| | | 'favourite_list_remove_items' => 'removeFromList', |
| | | 'favourite_list_share' => 'shareList', |
| | | 'favourite_list_unshare' => 'unshareList', |
| | | ]; |
| | | |
| | | if (!isset($action_map[$operation->type])) { |
| | | return $result; |
| | | } |
| | | |
| | | try { |
| | | $method = $action_map[$operation->type]; |
| | | $response = $this->$method($operation->user_id, $data); |
| | | |
| | | // Clear cache on success |
| | | if ($response['success'] ?? false) { |
| | | Cache::invalidateItem('favourites', $operation->user_id); |
| | | $this->listsCache->flush(); |
| | | $this->sharedListsCache->flush(); |
| | | } |
| | | |
| | | return $response; |
| | | } catch (Exception $e) { |
| | | $this->logError('processOperation', [ |
| | | 'error' => $e->getMessage(), |
| | | 'operation_id' => $operation->id, |
| | | 'type' => $operation->type |
| | | ]); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Add a favourite using findOrCreate pattern |
| | | */ |
| | | protected function addFavourite(int $user_id, array $data): array |
| | | { |
| | | $type = $data['type']; |
| | | $target_id = $data['target_id']; |
| | | |
| | | if (!str_starts_with($type, BASE)) { |
| | | $type = BASE . $type; |
| | | } |
| | | |
| | | if (!isset($this->valid_types[$type])) { |
| | | return ['success' => false, 'result' => 'Invalid type']; |
| | | } |
| | | |
| | | // Use findOrCreate pattern |
| | | $result = $this->favourites->findOrCreate([ |
| | | 'user_id' => $user_id, |
| | | 'type' => $type, |
| | | 'target_id' => $target_id |
| | | ]); |
| | | |
| | | if ((bool)$result) { |
| | | $this->updateFavouriteCount($type, $target_id); |
| | | $this->maybeNotifyOwner($type, $target_id, $user_id); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'action' => $result['created'] ? 'added' : 'already_exists', |
| | | 'favourite_id' => $result['id'], |
| | | 'count' => $this->getFavouriteCount($type, $target_id) |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Remove a favourite |
| | | */ |
| | | protected function removeFavourite(int $user_id, array $data): array |
| | | { |
| | | $type = $data['type']; |
| | | $target_id = $data['target_id']; |
| | | |
| | | if (!str_starts_with($type, BASE)) { |
| | | $type = BASE . $type; |
| | | } |
| | | |
| | | $deleted = $this->favourites->where([ |
| | | 'user_id' => $user_id, |
| | | 'type' => $type, |
| | | 'target_id' => $target_id |
| | | ])->deleteResults(); |
| | | |
| | | if ($deleted) { |
| | | $this->updateFavouriteCount($type, $target_id); |
| | | $this->removeRelatedNotifications($type, $target_id, $user_id); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'action' => $deleted ? 'removed' : 'not_found', |
| | | 'count' => $this->getFavouriteCount($type, $target_id) |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Batch favourites using transaction |
| | | */ |
| | | protected function batchFavourites(int $user_id, array $data): array |
| | |
| | | $items = $data['items'] ?? []; |
| | | |
| | | if (empty($items)) { |
| | | return ['success' => false, 'result' => 'No items provided']; |
| | | return $this->success(['result' => 'No items provided']); |
| | | } |
| | | |
| | | return $this->favourites->transaction(function($table) use ($user_id, $items) { |
| | |
| | | public function maybeAcceptListInvite(int $user_id, string $email, array $data):void |
| | | { |
| | | if (array_key_exists('list_token', $data) && !empty($data['list_token'])) { |
| | | JVB()->favourites()->acceptListShare($data['list_token'], $user_id); |
| | | Favourites::getInstance()->acceptListShare($data['list_token'], $user_id); |
| | | } |
| | | } |
| | | |
| | |
| | | protected function getListDetails(int $list_id, int $user_id): array |
| | | { |
| | | // Check access - either owner or has share |
| | | $is_owner = JVB()->favourites()->userOwnsList($list_id, $user_id); |
| | | $is_shared = JVB()->favourites()->userCanViewList($list_id, $user_id); |
| | | $is_owner = Favourites::getInstance()->userOwnsList($list_id, $user_id); |
| | | $is_shared = Favourites::getInstance()->userCanViewList($list_id, $user_id); |
| | | |
| | | |
| | | if (!$is_owner && !$is_shared) { |
| | |
| | | ]; |
| | | } |
| | | |
| | | $list = JVB()->favourites()->getListDetails($list_id, $user_id); |
| | | $list = Favourites::getInstance()->getListDetails($list_id, $user_id); |
| | | |
| | | if (empty($list)) { |
| | | return [ |
| | |
| | | ]); |
| | | |
| | | // Send notification |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $share_user->ID, |
| | | 'list_shared', |
| | | $user_id, |
| | |
| | | $list = $this->lists->where(['id' => $list_id])->first(); |
| | | |
| | | if ($list) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $existing_share->user_id, |
| | | 'list_share_revoked', |
| | | $user_id, |
| | |
| | | $invite_url |
| | | ); |
| | | |
| | | return JVB()->email()->sendEmail($email, $subject, $message); |
| | | return Emails::getInstance()->sendEmail($email, $subject, $message); |
| | | } |
| | | |
| | | /** |
| | |
| | | $user = get_userdata($user_id); |
| | | $display_name = $user ? $user->display_name : $email; |
| | | |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $list->user_id, |
| | | 'list_share_accepted', |
| | | $user_id, |
| | |
| | | return $this->error('Invalid action', 'invalid_action', 400); |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | $operation_type, |
| | | $user_id, |
| | | $data, |
| | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Umami; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | | use Umami; |
| | | use WP_Post; |
| | | use WP_Query; |
| | | use WP_REST_Request; |
| | |
| | | |
| | | class FeedRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected int $per_page = 36; |
| | | protected ?Umami $tracker = null; |
| | | |
| | |
| | | $this->cacheName = 'feed'; |
| | | $this->cacheTtl = 86400; |
| | | parent::__construct(); |
| | | $this->cache |
| | | ->connect('post', true) |
| | | ->connect('taxonomy', true) |
| | | ->connect('user', true); |
| | | $this->cache = Cache::for('feed'); |
| | | |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | |
| | | public function init():void |
| | | { |
| | | if (Site::hasIntegration('umami')) { |
| | | $this->tracker = JVB()->connect('umami'); |
| | | $this->tracker = Umami::getInstance(); |
| | | } |
| | | } |
| | | |
| | |
| | | $post = get_post($postID); |
| | | $type = jvbNoBase($post->post_type); |
| | | $metaType = 'post'; |
| | | $this->cache = Cache::for('feedPosts', 'post'); |
| | | break; |
| | | default: |
| | | $post = get_term($postID, jvbCheckBase($type)); |
| | | $type = jvbNoBase($type); |
| | | $metaType = 'term'; |
| | | $this->cache = Cache::for('feedTerms', 'term'); |
| | | break; |
| | | } |
| | | if (!$post || is_wp_error($post)) { |
| | |
| | | |
| | | protected function formatTaxonomy(WP_Term|int $term, int $postID, string $type) |
| | | { |
| | | $this->cache = Cache::for('feedTaxonomies', 'term'); |
| | | return $this->cache->remember( |
| | | $term->term_id, |
| | | function () use ($term, $postID, $type) { |
| | |
| | | |
| | | protected function getAuthorData(WP_Post $post) |
| | | { |
| | | //Connects to their profile, not user id |
| | | $this->cache = Cache::for('feedUsers', 'post'); |
| | | $author = $post->post_author; |
| | | $userLink = get_user_meta($author, BASE.'profile_link', true); |
| | | return $this->cache->remember( |
| | |
| | | protected function getTaxonomies(int $postID, string $content): array |
| | | { |
| | | $registrar = Registrar::getInstance($content)??false; |
| | | if (!$registrar) { |
| | | return []; |
| | | } |
| | | $this->cache = Cache::for('feedTaxonomyLinks', 'term'); |
| | | $taxonomies = $registrar->registrar->taxonomies; |
| | | $out = []; |
| | | foreach ($taxonomies as $tax) { |
| | |
| | | public function handleFeedRequest(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $args = $this->buildRequestArgs($request); |
| | | $key = $this->cache->generateKey($args); |
| | | |
| | | // Check HTTP cache headers first |
| | | $cache_check = $this->checkHeaders( |
| | | $request, |
| | | $key |
| | | ); |
| | | if ($cache_check) { |
| | | return $cache_check; // Returns 304 Not Modified |
| | | } |
| | | |
| | | $cached = $this->cache->get($key); |
| | | if ($cached) { |
| | | if ($request->get_param('highlight')) { |
| | | $highlight = json_decode($request->get_param('highlight'), true); |
| | | $args['highlight'] = $highlight; |
| | | } |
| | | $cached['items'] = $this->processHighlightedItem($cached['items'], $args); |
| | | $response = $this->success($cached); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | // Fetch and format items |
| | | $items = $this->fetchFeedItems($args); |
| | | |
| | | $ttl = (str_contains($args['orderby'], 'RAND')) ? 300 : $this->cacheTtl; |
| | | $this->cache->set($key, $items, $ttl); |
| | | |
| | | if ($request->get_param('highlight')) { |
| | | $highlight = json_decode($request->get_param('highlight'), true); |
| | | $highlight = $request->get_param('highlight'); |
| | | if (!empty($highlight)) { |
| | | $highlight = json_decode($highlight, true); |
| | | $args['highlight'] = $highlight; |
| | | $items['items'] = $this->processHighlightedItem($items['items'], $args); |
| | | } |
| | | |
| | | $items['items'] = $this->processHighlightedItem($items['items'], $args); |
| | | $response = $this->success($items); |
| | | return $this->addCacheHeaders($response); |
| | | return $this->success($items); |
| | | } |
| | | |
| | | /** |
| | |
| | | $args['fields'] = 'ids'; |
| | | // Get post IDs |
| | | $query = new WP_Query($args); |
| | | |
| | | // Batch prefetch related data |
| | | update_meta_cache('post', $query->posts); |
| | | update_object_term_cache($query->posts, $args['post_type']); |
| | | |
| | | // Format regular items |
| | | $items = array_map(fn($post) => $this->formatItem($post), $query->posts); |
| | | |
| | | wp_reset_postdata(); |
| | | |
| | | return [ |
| | | 'items' => $items, |
| | | 'has_more' => $query->max_num_pages > $args['paged'], |
| | |
| | | */ |
| | | public function getFeedTypes(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | // Check HTTP cache |
| | | $cache_check = $this->checkHeaders($request, 'feed_types'); |
| | | if ($cache_check) { |
| | | return $cache_check; |
| | | } |
| | | |
| | | $feedTypes = $this->buildFeedTypesConfig(); |
| | | |
| | | $response = $this->success($feedTypes); |
| | | return $this->addCacheHeaders($response); |
| | | return $this->success($this->buildFeedTypesConfig()); |
| | | } |
| | | |
| | | public function getFeedTypesConfig():array |
| | |
| | | // Get content types with show_feed |
| | | $contentTypes = Registrar::withFeature('show_feed', 'post'); |
| | | foreach ($contentTypes as $slug) { |
| | | $this->cache->tag('content:'.$slug); |
| | | $registrar = Registrar::getInstance($slug); |
| | | if (!$registrar) continue; |
| | | |
| | |
| | | if (!$registrar || !($registrar->hasFeature('is_content') ?? false)) { |
| | | continue; |
| | | } |
| | | |
| | | $this->cache->tag('taxonomy:'.$slug); |
| | | |
| | | $config[$slug] = [ |
| | | 'type' => 'taxonomy', |
| | | 'singular' => $registrar->getSingular(), |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Cloudflare; |
| | | use JVBase\managers\Emails; |
| | | use JVBase\meta\Sanitizer; |
| | | use JVBase\meta\Validator; |
| | | use JVBase\rest\PermissionHandler; |
| | |
| | | */ |
| | | class FormRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected FormBlock $form_block; |
| | | |
| | | public function __construct() |
| | |
| | | |
| | | protected function verifyTurnstile(string $token): bool |
| | | { |
| | | if (!Site::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) { |
| | | if (!Site::hasIntegration('cloudflare') ) { |
| | | return true; |
| | | } |
| | | |
| | | $cloudflare = Cloudflare::getInstance(); |
| | | if (!$cloudflare->isSetUp()) { |
| | | return true; |
| | | } |
| | | if (empty($token)) { |
| | | return false; |
| | | } |
| | | |
| | | return JVB()->connect('cloudflare')->verifyTurnstile($token); |
| | | return $cloudflare->verifyTurnstile($token); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | // Build email body |
| | | $body = JVB()->email()->h2($form_config['title']); |
| | | $body .= '<p style="font-size:13px;color:' . JVB()->email()->colours['dark-200'] . ';"> |
| | | $body = Emails::getInstance()->h2($form_config['title']); |
| | | $body .= '<p style="font-size:13px;color:' . Emails::getInstance()->colours['dark-200'] . ';"> |
| | | Submitted: ' . current_time('F j, Y \a\t g:i A') . ' |
| | | </p>'; |
| | | |
| | | // Add thread notice if CC'd |
| | | if ($submitter_email) { |
| | | $body .= JVB()->email()->notice( |
| | | $body .= Emails::getInstance()->notice( |
| | | '<strong>Email Thread Created</strong><br>Reply to this email to continue the conversation with ' . |
| | | esc_html($submitter_name ?: 'the submitter') . '.' |
| | | ); |
| | |
| | | ]; |
| | | } |
| | | |
| | | $body .= JVB()->email()->spacer(20); |
| | | $body .= JVB()->email()->table($form); |
| | | $body .= Emails::getInstance()->spacer(20); |
| | | $body .= Emails::getInstance()->table($form); |
| | | |
| | | // Show attachment info |
| | | $attachments = $this->processFileAttachments($files); |
| | | if (!empty($attachments)) { |
| | | $body .= JVB()->email()->spacer(10); |
| | | $body .= JVB()->email()->alert( |
| | | $body .= Emails::getInstance()->spacer(10); |
| | | $body .= Emails::getInstance()->alert( |
| | | sprintf('%d file(s) attached to this email', count($attachments)), |
| | | 'info' |
| | | ); |
| | | } |
| | | |
| | | // Send the email |
| | | return JVB()->email()->sendEmail( |
| | | return Emails::getInstance()->sendEmail( |
| | | $to, |
| | | $subject, |
| | | $body, |
| | |
| | | |
| | | use JVBase\importers\JaneAppClientImporter; |
| | | use JVBase\importers\JaneAppSalesImporter; |
| | | use JVBase\Instance; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | |
| | | */ |
| | | class ImporterRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | /** |
| | | * Register REST routes |
| | | */ |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Helcim; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | |
| | | |
| | | class IntegrationsHelcimRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | public function registerRoutes(): void |
| | | { |
| | | Route::for('helcim/initialize-checkout') |
| | |
| | | { |
| | | $data = $request->get_json_params(); |
| | | $user_id = get_current_user_id(); |
| | | if (!Site::hasIntegration('helcim')) { |
| | | return $this->validationError(['message' => 'Helcim not setup']); |
| | | } |
| | | if (!$user_id) { |
| | | return $this->unauthorized(); |
| | | } |
| | |
| | | } |
| | | |
| | | try { |
| | | $helcim = JVB()->connect('helcim'); |
| | | $helcim = Helcim::getInstance(); |
| | | if (!method_exists($helcim, 'resolveCustomerId') || |
| | | !method_exists($helcim, 'initializeCheckout') |
| | | ) { |
| | | return $this->validationError(['message' => 'Helcim not complete']); |
| | | } |
| | | |
| | | // Auto-resolve customer ID from logged-in user |
| | | if (empty($data['customerId']) && $user_id) { |
| | |
| | | if (!$user_id) { |
| | | return $this->validationError(['message' => 'Not logged in']); |
| | | } |
| | | if (!Site::hasIntegration('helcim')) { |
| | | return $this->validationError(['message' => 'Helcim not setup']); |
| | | } |
| | | |
| | | |
| | | try { |
| | | $helcim = JVB()->connect('helcim'); |
| | | $helcim = Helcim::getInstance(); |
| | | if (!method_exists($helcim, 'handleGetInvoices')) { |
| | | return $this->validationError(['message' => 'Helcim not complete']); |
| | | } |
| | | $user = get_userdata($user_id); |
| | | $result = $helcim->handleGetInvoices([ |
| | | 'email' => $user->user_email, |
| | |
| | | return $this->validationError(['message' => 'Invoice ID required']); |
| | | } |
| | | |
| | | if (!Site::hasIntegration('helcim')) { |
| | | return $this->validationError(['message' => 'Helcim not setup']); |
| | | } |
| | | |
| | | try { |
| | | $helcim = JVB()->connect('helcim'); |
| | | $helcim = Helcim::getInstance(); |
| | | if (!method_exists($helcim, 'handleGetInvoice')) { |
| | | return $this->validationError(['message' => 'Helcim not complete']); |
| | | } |
| | | $result = $helcim->handleGetInvoice(['invoiceId' => $invoiceId]); |
| | | |
| | | return $this->success($result); |
| | |
| | | if (!$user_id) { |
| | | return $this->validationError(['message' => 'Not logged in']); |
| | | } |
| | | if (!Site::hasIntegration('helcim')) { |
| | | return $this->validationError(['message' => 'Helcim not setup']); |
| | | } |
| | | |
| | | try { |
| | | $helcim = JVB()->connect('helcim'); |
| | | $helcim = Helcim::getInstance(); |
| | | if (!method_exists($helcim, 'handleGetCustomerCards')) { |
| | | return $this->validationError(['message' => 'Helcim not complete']); |
| | | } |
| | | $result = $helcim->handleGetCustomerCards([ |
| | | 'email' => get_userdata($user_id)->user_email, |
| | | ]); |
| | |
| | | if (empty($data['secretToken']) || empty($data['transactionData'])) { |
| | | return $this->validationError(['message' => 'Missing secretToken or transactionData']); |
| | | } |
| | | if (!Site::hasIntegration('helcim')) { |
| | | return $this->validationError(['message' => 'Helcim not setup']); |
| | | } |
| | | |
| | | try { |
| | | $helcim = JVB()->connect('helcim'); |
| | | $helcim = Helcim::getInstance(); |
| | | if (!method_exists($helcim, 'validateTransaction')) { |
| | | return $this->validationError(['message' => 'Helcim not complete']); |
| | | } |
| | | $valid = $helcim->validateTransaction( |
| | | $data['secretToken'], |
| | | $data['transactionData'] |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\ServiceMap; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | |
| | | |
| | | class IntegrationsRoutes extends Rest |
| | | { |
| | | |
| | | use Instance; |
| | | use ServiceMap; |
| | | /** |
| | | * Register REST routes |
| | | */ |
| | |
| | | Route::for('integrations') |
| | | ->post([$this, 'handleAction']) |
| | | ->args([ |
| | | 'service' => 'string|required|enum:'.implode(',',JVB()->getAvailableServices()), |
| | | 'service' => 'string|required|enum:'.implode(',',$this->getAvailableServices()), |
| | | 'action' => 'string|required', |
| | | 'user_id' => 'int', |
| | | 'context' => 'string|enum:admin,user', |
| | |
| | | if ($theUserID === false) { |
| | | return $this->validationError(['message' => 'Not logged in']); |
| | | } |
| | | $integration = JVB()->connect($service, $theUserID); |
| | | $integration = $this->getService($service); |
| | | if (!method_exists($integration, 'switchUser')) { |
| | | return $this->validationError(['message' => 'Cannot get user integration']); |
| | | } |
| | | |
| | | if (!$integration) { |
| | | if (!$integration || !method_exists($integration, 'processAction')) { |
| | | return $this->validationError(['message'=>'Invalid service']); |
| | | } |
| | | |
| | | $integration->switchUser($theUserID); |
| | | |
| | | $integration->getCredentials(); |
| | | |
| | | // Handle the action |
| | |
| | | $user_id = $request->get_param('user_id'); |
| | | $return_url = $request->get_param('return_url'); |
| | | |
| | | $integration = JVB()->connect($service, $user_id); |
| | | $theUserID = current_user_can('manage_options') ? null : get_current_user_id(); |
| | | if ($theUserID === false) { |
| | | return $this->validationError(['message' => 'Not logged in']); |
| | | } |
| | | $integration = $this->getService($service); |
| | | if (!method_exists($integration, 'switchUser')) { |
| | | return $this->validationError(['message' => 'Cannot get user integration']); |
| | | } |
| | | |
| | | if (!$integration || !$integration->isOAuthService) { |
| | | if (!$integration || !method_exists($integration, 'processAction')) { |
| | | return $this->validationError(['message'=>'Invalid service']); |
| | | } |
| | | |
| | | $auth_url = $integration->getOAuthUrl($return_url); |
| | | $integration->switchUser($theUserID); |
| | | |
| | | if (!$integration || !method_exists($integration, 'getOAuthBaseURL') || is_null($integration->getOAuthBaseURL())) { |
| | | return $this->validationError(['message'=>'Invalid service']); |
| | | } |
| | | |
| | | $auth_url = $integration->getOAuthBaseURL($return_url); |
| | | |
| | | if ($auth_url) { |
| | | return $this->success($auth_url); |
| | |
| | | |
| | | |
| | | use Exception; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Square; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\rest\Rest; |
| | |
| | | |
| | | class IntegrationsSquareRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | public function registerRoutes():void |
| | | { |
| | | Route::for('square/process-payment') |
| | |
| | | |
| | | try { |
| | | /** @var Square $square */ |
| | | $square = JVB()->connect('square'); |
| | | $square = Square::getInstance(); |
| | | if (!method_exists($square, 'getOrCreateSquareCustomer') || |
| | | !method_exists($square, 'createSquareOrder') || |
| | | !method_exists($square, 'saveOrderToWordPress') || |
| | | !method_exists($square, 'createSquarePayment')) { |
| | | return $this->error('Square not set up'); |
| | | } |
| | | // Step 1: Get or create Square customer |
| | | $customer_id = $square->getOrCreateSquareCustomer($data['customer']); |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\InvitationsManager; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\rest\Route; |
| | |
| | | */ |
| | | class Invitations extends Rest |
| | | { |
| | | use Instance; |
| | | protected array $inviteConfig; |
| | | protected CustomTable $table; |
| | | |
| | |
| | | parent::__construct(); |
| | | |
| | | // Get invitation configuration |
| | | $this->inviteConfig = JVB()->invitations()->getInviteConfig(); |
| | | $this->inviteConfig = InvitationsManager::getInstance()->getInviteConfig(); |
| | | $this->table = CustomTable::for('invitations'); |
| | | |
| | | // Cache connections |
| | |
| | | protected function getTermInvitations(array $args): array |
| | | { |
| | | // Check permission |
| | | if (!JVB()->roles()->isManager($args['user'], $args['to_term'])) { |
| | | if (!Roles::getInstance()()->isManager($args['user'], $args['to_term'])) { |
| | | return $this->forbidden('You cannot view invitations for this ' . $args['taxonomy'])->get_data(); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | // Queue using fluent interface |
| | | $op = JVB()->queue()->add( |
| | | $op = Queue::getInstance()->add( |
| | | 'invitation_create', |
| | | $userID, |
| | | ['invitations' => $validated], |
| | |
| | | */ |
| | | protected function validateInvitations(int $userID, array $invites): array |
| | | { |
| | | return JVB()->invitations()->validateInvitations($userID, $invites); |
| | | return InvitationsManager::getInstance()->validateInvitations($userID, $invites); |
| | | } |
| | | |
| | | |
| | |
| | | return $this->error('Invitation ID required'); |
| | | } |
| | | |
| | | $op = JVB()->queue()->add( |
| | | $op = Queue::getInstance()->add( |
| | | 'invitation_revoke', |
| | | $userID, |
| | | ['invitation_id' => $invitationID], |
| | |
| | | return $this->error('Invitation ID required'); |
| | | } |
| | | |
| | | $op = JVB()->queue()->add( |
| | | $op = Queue::getInstance()->add( |
| | | 'invitation_resend', |
| | | $userID, |
| | | ['invitation_id' => $invitationID], |
| | |
| | | ]; |
| | | |
| | | // Add term information if present |
| | | foreach (JVB()->roles()->getInvitableTaxonomies() as $taxonomy) { |
| | | foreach (Roles::getInstance()()->getInvitableTaxonomies() as $taxonomy) { |
| | | $column = 'to_' . $taxonomy; |
| | | if (isset($invitation->$column) && $invitation->$column) { |
| | | $termID = (int) $invitation->$column; |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Emails; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | |
| | | */ |
| | | class LoginRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected ?string $requestId = null; |
| | | protected bool $hasMagicLink = false; |
| | | |
| | |
| | | |
| | | // Send magic link for email verification |
| | | if ($this->hasMagicLink) { |
| | | JVB()->magicLink()->sendMagicLink($email, 'signup', [ |
| | | MagicLinks::getInstance()->sendMagicLink($email, 'signup', [ |
| | | 'name' => $name, |
| | | 'role' => $role, |
| | | ]); |
| | |
| | | |
| | | // Use magic link if available, otherwise standard WP reset |
| | | if ($this->hasMagicLink) { |
| | | $result = JVB()->magicLink()->sendMagicLink($email, 'reset'); |
| | | $result = MagicLinks::getInstance()->sendMagicLink($email, 'reset'); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $this->logError('Magic link send failed', [ |
| | |
| | | 'error' => $key->get_error_message(), |
| | | ]); |
| | | } else { |
| | | $success = JVB()->email()->sendPasswordResetEmail($user, $key); |
| | | $success = Emails::getInstance()->sendPasswordResetEmail($user, $key); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | // Send magic link |
| | | $result = JVB()->magicLink()->sendMagicLink($email, $type, $context); |
| | | $result = MagicLinks::getInstance()->sendMagicLink($email, $type, $context); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $this->logError('Magic link send failed', [ |
| | |
| | | } |
| | | |
| | | try { |
| | | JVB()->referrals()->processReferralCode($user_id, $referral_code); |
| | | Referrals::getInstance()->processReferralCode($user_id, $referral_code); |
| | | } catch (\Exception $e) { |
| | | $this->logError('Referral processing failed', [ |
| | | 'user_id' => $user_id, |
| | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\managers\MagicLinkManager; |
| | | use JVBase\managers\MagicLinks; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | |
| | | return $this->error('Security verification failed', 'turnstile_failed', 403); |
| | | } |
| | | $email = sanitize_email($request->get_param('email')??$request->get_param('user_email')??''); |
| | | $type = sanitize_text_field($request->get_param('type')) ?? MagicLinkManager::TYPE_LOGIN; |
| | | $type = sanitize_text_field($request->get_param('type')) ?? MagicLinks::TYPE_LOGIN; |
| | | $context = $request->get_param('context') ?? []; |
| | | |
| | | // Validate email |
| | |
| | | |
| | | // Check if email exists |
| | | $exists = email_exists($email); |
| | | if ($type === MagicLinkManager::TYPE_LOGIN && !$exists) { |
| | | if ($type === MagicLinks::TYPE_LOGIN && !$exists) { |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Invalid email address' |
| | | ]); |
| | | } |
| | | |
| | | if ($type === MagicLinkManager::TYPE_SIGNUP && $exists) { |
| | | if ($type === MagicLinks::TYPE_SIGNUP && $exists) { |
| | | // Redirect to login instead |
| | | $type = MagicLinkManager::TYPE_LOGIN; |
| | | $type = MagicLinks::TYPE_LOGIN; |
| | | } |
| | | |
| | | // Send the magic link |
| | | $result = JVB()->magicLink()?->sendMagicLink($email, $type, $context); |
| | | $result = MagicLinks::getInstance()?->sendMagicLink($email, $type, $context); |
| | | |
| | | if (is_wp_error($result)) { |
| | | return new WP_REST_Response([ |
| | |
| | | $email = sanitize_email($request->get_param('email')); |
| | | |
| | | // This returns array|WP_Error - check for error first |
| | | $token_data = JVB()->magicLink()?->verifyToken($token, $email); |
| | | $token_data = MagicLinks::getInstance()?->verifyToken($token, $email); |
| | | |
| | | if (is_wp_error($token_data)) { |
| | | return new WP_REST_Response([ |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\meta\Meta; |
| | |
| | | |
| | | class NewsRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected int $per_page; |
| | | protected bool|object $manager = false; |
| | | public function __construct() |
| | |
| | | */ |
| | | public function handleNewsOperation(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $data = $request->get_params(); |
| | | $user = get_current_user_id(); |
| | | $operationID = sanitize_text_field($data['id']); |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | */ |
| | | class NotificationsRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected int $user_id; |
| | | protected array $notification_types = []; |
| | | protected object $manager; |
| | |
| | | */ |
| | | public function init(): void |
| | | { |
| | | $this->manager = JVB()->notification(); |
| | | $this->manager = NotificationManager::getInstance(); |
| | | $this->notification_types = $this->manager->getNotificationTypes(true); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | if (!empty($queueData)) { |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'notification_' . $action, |
| | | $args['user_id'], |
| | | $queueData |
| | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\base\Options; |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | |
| | | |
| | | class OptionsRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | |
| | | public function __construct() |
| | | { |
| | |
| | | $operationID = $data['id']; |
| | | unset($data['id']); |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'update_options', |
| | | $user, |
| | | $data, |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\queue\Operation; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | | use JVBase\rest\Response; |
| | |
| | | |
| | | class QueueRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'queue'; |
| | | $this->cacheTtl = 300; |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->cacheName, 'custom', $this->cacheTtl)->user(); |
| | | |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | |
| | | if (!$user_id) { |
| | | return $this->unauthorized(); |
| | | } |
| | | $this->cache = Cache::for('queue')->user(); |
| | | $status = sanitize_text_field($params['status']); |
| | | $ids = !empty($params['ids']) |
| | | ? array_map('trim', array_map('sanitize_text_field', explode(',', $params['ids']))) |
| | | : []; |
| | | $limit = absint($params['limit']); |
| | | |
| | | $cacheKey = $this->cache->generateKey(compact('user_id', 'status', 'ids', 'limit')); |
| | | if ($cached = $this->checkHeaders($request, $cacheKey)) { |
| | | return $cached; |
| | | } |
| | | $filters = $this->buildFilters($params); |
| | | $ops = Queue::getInstance()->getUserOperations($user_id, $filters); |
| | | |
| | | $data = $this->cache->tag("user:{$user_id}")->remember($cacheKey, function() use ($user_id, $params) { |
| | | $filters = $this->buildFilters($params); |
| | | $operations = JVB()->queue()->getUserOperations($user_id, $filters); |
| | | |
| | | return [ |
| | | 'items' => array_map([$this, 'formatOperation'], $operations), |
| | | 'total' => count($operations), |
| | | 'queue_stats' => $this->getQueueStats($user_id), |
| | | 'server_time' => date('c'), |
| | | ]; |
| | | }); |
| | | |
| | | $response = Response::success($data); |
| | | return $this->addCacheHeaders($response); |
| | | return Response::success([ |
| | | 'items' => array_map([$this, 'formatOperation'], $ops), |
| | | 'total' => count($ops), |
| | | 'queue_stats' => $this->getQueueStats($user_id), |
| | | 'server_time' => date('c') |
| | | ]); |
| | | } |
| | | |
| | | private function buildFilters(array $params): array |
| | |
| | | } |
| | | |
| | | // Get operations via Queue - verifies ownership |
| | | $operations = JVB()->queue()->getUserOperations($user_id, [ |
| | | $operations = Queue::getInstance()->getUserOperations($user_id, [ |
| | | 'ids' => $ids, |
| | | 'limit' => count($ids), |
| | | ]); |
| | |
| | | $filters['ids'] = array_map('trim', explode(',', $ids)); |
| | | } |
| | | |
| | | $operations = JVB()->queue()->getUserOperations($userId, $filters); |
| | | $operations = Queue::getInstance()->getUserOperations($userId, $filters); |
| | | |
| | | if ($since) { |
| | | $sinceTime = strtotime($since); |
| | |
| | | return $this->unauthorized(); |
| | | } |
| | | $this->cache = Cache::for($user_id.'_queue'); |
| | | $operations = JVB()->queue()->getUserOperations($user_id, [ |
| | | $operations = Queue::getInstance()->getUserOperations($user_id, [ |
| | | 'state' => 'completed', |
| | | 'outcome' => ['failed', 'failed_permanent', 'partial'], |
| | | 'has_errors' => true, |
| | |
| | | } |
| | | $this->cache = Cache::for($userId.'_queue'); |
| | | |
| | | $op = JVB()->queue()->get($id); |
| | | $op = Queue::getInstance()->get($id); |
| | | |
| | | if (!$op || $op->userId !== $userId) { |
| | | return Response::notFound('Operation not found'); |
| | |
| | | { |
| | | return array_merge( |
| | | ['queued' => 0, 'localProcessing' => 0, 'uploading' => 0], |
| | | JVB()->queue()->getUserStats($userId) |
| | | Queue::getInstance()->getUserStats($userId) |
| | | ); |
| | | } |
| | | |
| | |
| | | |
| | | private function processAction(string $action, array $operations, int $userId): array |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $processed = 0; |
| | | $processedIds = []; |
| | | |
| | |
| | | use JVBase\base\Site; |
| | | use JVBase\importers\JaneAppClientImporter; |
| | | use JVBase\importers\JaneAppSalesImporter; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\Referrals; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | */ |
| | | class ReferralRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'referrals'; |
| | | $this->cacheTtl = (int)HOUR_IN_SECONDS; |
| | | parent::__construct(); |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | $this->cache = Cache::for($this->cacheName, 'custom', $this->cacheTtl)->user(); |
| | | } |
| | | |
| | | public function registerRoutes(): void |
| | |
| | | Route::for('referrals') |
| | | ->get([$this, 'getReferrals']) |
| | | ->args([ |
| | | 'status' => 'string|enum:all,pending,consulted,treated,unused,registered,completed|default:all', |
| | | 'status' => 'string|enum:all,invited,pending,consulted,treated,cancelled|default:all', |
| | | 'date_start' => 'string', |
| | | 'date_end' => 'string', |
| | | 'limit' => 'integer|default:50', |
| | |
| | | return $this->getAllReferrals($request); |
| | | } |
| | | |
| | | // Build cache key |
| | | $args = [ |
| | | 'status' => $request->get_param('status') ?? 'all', |
| | | 'limit' => $request->get_param('limit') ?? 50, |
| | | 'offset' => $request->get_param('offset') ?? 0, |
| | | 'date_start' => $request->get_param('date_start'), |
| | | 'date_end' => $request->get_param('date_end'), |
| | | ]; |
| | | $cache_key = "user_{$user_id}_" . $this->cache->generateKey($args); |
| | | |
| | | // Check 304 Not Modified |
| | | $cache_check = $this->checkHeaders($request, $cache_key); |
| | | if ($cache_check instanceof WP_REST_Response) { |
| | | return $cache_check; |
| | | } |
| | | $args = $this->buildArgs($request); |
| | | |
| | | // Get referrals from manager |
| | | $referrals = JVB()->referrals()->getUserReferrals($user_id, $args); |
| | | $referrals = Referrals::getInstance()->getUserReferrals($user_id, $args); |
| | | |
| | | $data = [ |
| | | 'items' => $referrals, |
| | | 'total' => count($referrals) |
| | | ]; |
| | | |
| | | $response = $this->success($data); |
| | | $response = $this->success($referrals); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | |
| | |
| | | return $this->unauthorized(); |
| | | } |
| | | |
| | | //Additional check to not send too many emails in an hour |
| | | $transient_key = "referral_invite_limit_{$user}"; |
| | | $recent_invites = get_transient($transient_key) ?: 0; |
| | | |
| | | if ($recent_invites >= 20) { // Max 5 batch invites per hour |
| | | return $this->error('Too many invitations sent. Please try again later.', 'rate_limit', 429); |
| | | } |
| | | set_transient($transient_key, $recent_invites + 1, HOUR_IN_SECONDS); |
| | | |
| | | $subject = sanitize_text_field($request->get_param('subject')); |
| | | $message = sanitize_textarea_field($request->get_param('message')); |
| | | $subject = sanitize_text_field($request->get_param('subject')??''); |
| | | $message = sanitize_textarea_field($request->get_param('message')??''); |
| | | $invitations = $request->get_param('invite'); |
| | | |
| | | // Validate and sanitize invitations |
| | |
| | | } |
| | | |
| | | $operationID = sanitize_text_field($request->get_param('id')); |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'referral_invite', |
| | | $user, |
| | | [ |
| | |
| | | return $this->error('referral_id required', 'missing_id', 400); |
| | | } |
| | | |
| | | $result = JVB()->referrals()->updateStatus($referral_id, $status); |
| | | [$success, $message] = Referrals::getInstance()->updateStatus($referral_id, $status); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $code = $result->get_error_code(); |
| | | return $code === 'not_found' |
| | | ? $this->notFound($result->get_error_message()) |
| | | : $this->error($result->get_error_message(), $code, 500); |
| | | if (!$success) { |
| | | return $message === 'Referral not found' |
| | | ? $this->notFound($message) |
| | | : $this->error($message); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | |
| | | return $this->error('referral_id required', 'missing_id', 400); |
| | | } |
| | | |
| | | $result = JVB()->referrals()->removeReferral($referral_id, get_current_user_id()); |
| | | $result = Referrals::getInstance()->removeReferral($referral_id, get_current_user_id()); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $code = $result->get_error_code(); |
| | | $status = match($code) { |
| | | 'not_found' => 404, |
| | | 'unauthorized' => 403, |
| | | 'invalid_status' => 400, |
| | | default => 500 |
| | | }; |
| | | return $this->error($result->get_error_message(), $code, $status); |
| | | if (!$result['success']) { |
| | | return $this->error($result['message']); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | return $this->success(['message' => 'Referral removed']); |
| | | } |
| | | |
| | |
| | | return $this->error('referral_id required', 'missing_id', 400); |
| | | } |
| | | |
| | | $result = JVB()->referrals()->resendInvitation($referral_id, get_current_user_id()); |
| | | $result = Referrals::getInstance()->resendInvitation($referral_id, get_current_user_id()); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $code = $result->get_error_code(); |
| | | $status = match($code) { |
| | | 'not_found' => 404, |
| | | 'rate_limit' => 429, |
| | | default => 500 |
| | | }; |
| | | return $this->error($result->get_error_message(), $code, $status); |
| | | if (!$result['success']) { |
| | | return $this->error($result['message']); |
| | | } |
| | | |
| | | return $this->success(['message' => 'Invitation resent']); |
| | |
| | | return $this->unauthorized(); |
| | | } |
| | | |
| | | $code = JVB()->referrals()->getUserReferralCode($user_id); |
| | | $code = Referrals::getInstance()->getUserReferralCode($user_id); |
| | | |
| | | if (is_wp_error($code)) { |
| | | return $this->error($code->get_error_message(), 'code_error', 400); |
| | |
| | | */ |
| | | public function validateCode(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $code = strtoupper(sanitize_text_field($request->get_param('code'))); |
| | | $code = strtoupper(sanitize_text_field($request->get_param('code')??'')); |
| | | |
| | | if (empty($code)) { |
| | | return $this->error('Code required', 'missing_code', 400); |
| | | } |
| | | |
| | | $referrer = JVB()->referrals()->getUserByReferralCode($code); |
| | | $referrer = Referrals::getInstance()->getUserByReferralCode($code); |
| | | |
| | | if (!$referrer) { |
| | | return $this->error('Invalid referral code', 'invalid_code', 404); |
| | |
| | | |
| | | // Check self-referral |
| | | if (is_user_logged_in() && get_current_user_id() === $referrer->ID) { |
| | | return $this->error('Cannot use your own referral code', 'self_referral', 400); |
| | | return $this->error('Cannot use your own referral code', 'self_referral'); |
| | | } |
| | | |
| | | return $this->success([ |
| | |
| | | if (!$user_id) { |
| | | return $this->unauthorized(); |
| | | } |
| | | $cache_key = "stats_{$user_id}"; |
| | | |
| | | // Check 304 Not Modified |
| | | $cache_check = $this->checkHeaders($request, $cache_key); |
| | | if ($cache_check instanceof WP_REST_Response) { |
| | | return $cache_check; |
| | | } |
| | | $stats = Referrals::getInstance()->getUserStats($user_id); |
| | | |
| | | $stats = JVB()->referrals()->getUserStats($user_id); |
| | | |
| | | $response = $this->success(['items' => [$stats]]); |
| | | return $this->addCacheHeaders($response); |
| | | return $this->success(['items' => [$stats]]); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getSettings(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $settings = JVB()->referrals()->getRewardSettings(); |
| | | $settings = Referrals::getInstance()->getRewardSettings(); |
| | | return $this->success(['settings' => $settings]); |
| | | } |
| | | |
| | |
| | | /** |
| | | * Helper: Get all referrals (admin only) |
| | | */ |
| | | protected function buildArgs(WP_REST_Request $request):array |
| | | { |
| | | $params = $request->get_params(); |
| | | $args = [ |
| | | 'limit' => absint($params['limit']??20), |
| | | 'offset' => absint($params['offset']??0) |
| | | ]; |
| | | if (array_key_exists('status', $params) && $params['status'] !== 'all') { |
| | | $args['status'] = sanitize_text_field($params['status']); |
| | | } |
| | | if (array_key_exists('search', $params)) { |
| | | $args['search'] = sanitize_text_field($params['search']); |
| | | } |
| | | if (array_key_exists('date_start', $params) && array_key_exists('date_end', $params)) { |
| | | $args['created_at'] = ['BETWEEN', [sanitize_text_field($params['date_start']), sanitize_text_field($params['date_end'])]]; |
| | | } |
| | | return $args; |
| | | } |
| | | protected function getAllReferrals(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $items = JVB()->referrals()->getAllReferrals([ |
| | | 'status' => $request->get_param('status') ?? 'all', |
| | | 'search' => $request->get_param('search'), |
| | | 'date_start' => $request->get_param('date_start'), |
| | | 'date_end' => $request->get_param('date_end'), |
| | | 'limit' => $request->get_param('limit') ?? 50, |
| | | 'offset' => $request->get_param('offset') ?? 0, |
| | | ]); |
| | | $args = $this->buildArgs($request); |
| | | $items = Referrals::getInstance()->getReferrals($args); |
| | | |
| | | return $this->success(['items' => $items, 'total' => count($items)]); |
| | | return $this->success($items); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Process queued referral operations |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error |
| | | { |
| | | if ($operation->type !== 'referral_invite') { |
| | | return $result; |
| | | } |
| | | |
| | | $result = JVB()->referrals()->sendBatchReferralInvitations( |
| | | $operation->user_id, |
| | | $data['invitations'], |
| | | $data['subject'], |
| | | $data['message'] |
| | | ); |
| | | |
| | | if ($result['success']) { |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'message' => sprintf( |
| | | 'Sent invitations. Success: %d. Failed: %d.', |
| | | count($result['result']['success']), |
| | | count($result['result']['failed']) |
| | | ), |
| | | 'details' => [ |
| | | 'successful' => $result['result']['success'], |
| | | 'failed' => $result['result']['failed'], |
| | | 'total' => count($data['invitations']) |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Handle client CSV upload |
| | | */ |
| | | public function handleClientUpload(WP_REST_Request $request): WP_REST_Response |
| | |
| | | |
| | | // Import using JaneAppClientImporter |
| | | $importer = new JaneAppClientImporter(); |
| | | $default_role = get_option(BASE . 'referral_role', Site::getDefaultReferralRole()); |
| | | $default_role = get_option(BASE . 'referral_role', Site::getReferralRole()); |
| | | |
| | | $options = [ |
| | | 'update_existing' => true, |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\JVB; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\NotificationManager; |
| | | use JVBase\managers\Notifications\Notifications; |
| | | use JVBase\managers\Responses; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\managers\CustomTable; |
| | | use WP_REST_Request; |
| | |
| | | */ |
| | | class ResponseRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected int $perPage = 20; |
| | | protected CustomTable $table; |
| | | protected CustomTable $karmaTable; |
| | |
| | | $this->cacheName = 'responses'; |
| | | $this->cacheTtl = 1800; // 30 minutes |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->cacheName, 'response', $this->cacheTtl); |
| | | |
| | | $this->table = CustomTable::for('responses'); |
| | | $this->karmaTable = CustomTable::for('karma_response'); |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1); |
| | | } |
| | | |
| | | public function registerRoutes(): void |
| | |
| | | */ |
| | | public function getResponses(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $item_id = (int) $request->get_param('item_id'); |
| | | |
| | | if (!$item_id) { |
| | | $params = $request->get_params(); |
| | | if (!array_key_exists('item_id', $params)) { |
| | | return $this->error('Missing item ID', 'missing_item_id'); |
| | | } |
| | | $item_id = absint($params['item_id']); |
| | | |
| | | // Build query args |
| | | $args = $this->buildQueryArgs($request); |
| | | $cacheKey = $this->cache->generateKey(array_merge(['item_id' => $item_id], $args)); |
| | | |
| | | // Check headers for 304 Not Modified |
| | | $headerCheck = $this->checkHeaders($request, $cacheKey); |
| | | if ($headerCheck) { |
| | | return $headerCheck; |
| | | } |
| | | |
| | | // Check cache |
| | | $cached = $this->cache->get($cacheKey); |
| | | if ($cached) { |
| | | return $this->success($cached); |
| | | } |
| | | |
| | | // Get responses |
| | | $responses = $this->getItemResponse($item_id, $args); |
| | | |
| | | if (is_wp_error($responses)) { |
| | | return $this->notFound($responses->get_error_message()); |
| | | } |
| | | |
| | | // Cache and return |
| | | $this->cache->set($cacheKey, $responses); |
| | | return $this->success($responses); |
| | | return $this->success(Responses::getInstance()->getAllItemResponses($item_id, $args)); |
| | | } |
| | | |
| | | /** |
| | |
| | | return $this->unauthorized(); |
| | | } |
| | | |
| | | $operation_id = $data['id'] ?? uniqid('response_'); |
| | | $action = sanitize_text_field($data['action'] ?? ''); |
| | | |
| | | if (!in_array($action, ['create', 'delete', 'update'])) { |
| | |
| | | return $this->error('Missing required fields', 'missing_fields'); |
| | | } |
| | | } |
| | | |
| | | // Prepare data for queue |
| | | $queue_data = match($action) { |
| | | 'create' => [ |
| | | 'item_id' => (int) $data['item_id'], |
| | | 'parent_id' => isset($data['parent_id']) ? (int) $data['parent_id'] : null, |
| | | 'response' => wp_kses_post($data['response']), |
| | | 'content' => sanitize_text_field($data['content']) |
| | | ], |
| | | 'update' => [ |
| | | 'response_id' => (int) $data['response_id'], |
| | | 'response' => isset($data['response']) ? wp_kses_post($data['response']) : null, |
| | | 'status' => isset($data['status']) && in_array($data['status'], ['published', 'hidden', 'flagged']) |
| | | ? $data['status'] |
| | | : null |
| | | ], |
| | | 'delete' => [ |
| | | 'response_id' => (int) $data['response_id'] |
| | | ] |
| | | $responses = Responses::getInstance(); |
| | | $response = match($action) { |
| | | 'create' => $responses->respondTo( |
| | | absint($data['item_id']), |
| | | sanitize_text_field($data['content']), |
| | | $user_id, |
| | | wp_kses_post($data['response']), |
| | | isset($data['parent_id']) ? absint($data['parent_id']) : null |
| | | ), |
| | | 'update' => $responses->editResponse( |
| | | absint($data['response_id']), |
| | | wp_kses_post($data['response']??'') |
| | | ), |
| | | 'delete' => $responses->deleteResponse(absint($data['response_id'])) |
| | | }; |
| | | |
| | | // Queue the operation |
| | | JVB()->queue()->queueOperation( |
| | | $action . '_response', |
| | | $user_id, |
| | | $queue_data, |
| | | [ |
| | | 'operation_id' => 'u' . $user_id . '_' . $operation_id, |
| | | 'priority' => 'high' |
| | | ] |
| | | ); |
| | | |
| | | return $this->queued($operation_id); |
| | | if ($response['success']) { |
| | | return $this->success($response); |
| | | } |
| | | return $this->error($response['message']); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | protected function getItemResponse(int $item_id, array $args = []): array|WP_Error |
| | | { |
| | | $defaults = [ |
| | | 'page' => 1, |
| | | 'per_page' => $this->perPage, |
| | | 'orderby' => 'created_at', |
| | | 'order' => 'DESC', |
| | | 'parent_id' => null |
| | | ]; |
| | | |
| | | $args = wp_parse_args($args, $defaults); |
| | | |
| | | // Verify post exists |
| | | $post = get_post($item_id); |
| | | if (!$post) { |
| | | return new WP_Error('not_found', 'Item not found'); |
| | | } |
| | | |
| | | // Build query with karma calculations |
| | | $query = " |
| | | SELECT r.*, |
| | | COALESCE((SELECT COUNT(*) FROM {$this->table->getFullTableName()} WHERE parent_id = r.id), 0) as reply_count, |
| | | COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'up'), 0) as upvotes, |
| | | COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'down'), 0) as downvotes |
| | | FROM {$this->table->getFullTableName()} r |
| | | WHERE r.item_id = %d |
| | | "; |
| | | |
| | | $query_args = [$item_id]; |
| | | |
| | | // Filter by parent_id |
| | | if (isset($args['parent_id'])) { |
| | | $query .= " AND r.parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d"); |
| | | if ($args['parent_id'] !== null) { |
| | | $query_args[] = $args['parent_id']; |
| | | } |
| | | } |
| | | |
| | | // Get total count for pagination |
| | | $count_query = str_replace( |
| | | ['SELECT r.*,', 'COALESCE((SELECT COUNT(*) FROM', 'as reply_count,', 'as upvotes,', 'as downvotes'], |
| | | ['SELECT COUNT(*)', '', '', '', ''], |
| | | $query |
| | | ); |
| | | $count_query = preg_replace('/COALESCE\([^)]+\)[^,]*,?/', '', $count_query); |
| | | $total_items = (int) $this->table->queryVar($count_query, $query_args); |
| | | |
| | | // Add ordering |
| | | $order_column = in_array($args['orderby'], ['created_at', 'updated_at']) |
| | | ? "r.{$args['orderby']}" |
| | | : $args['orderby']; |
| | | |
| | | $query .= " ORDER BY {$order_column} {$args['order']}"; |
| | | |
| | | // Add pagination |
| | | $query .= " LIMIT %d OFFSET %d"; |
| | | $query_args[] = $args['per_page']; |
| | | $query_args[] = ($args['page'] - 1) * $args['per_page']; |
| | | |
| | | // Get responses |
| | | $responses = $this->table->queryResults($query, $query_args); |
| | | |
| | | $responses = Responses::getInstance()->getAllItemResponses($item_id, $args); |
| | | // Format responses |
| | | $items = array_map([$this, 'formatItem'], $responses); |
| | | $responses['items'] = array_map([$this, 'formatItem'], $responses); |
| | | |
| | | return [ |
| | | 'items' => $items, |
| | | 'has_more' => $args['page'] < ceil($total_items / $args['per_page']), |
| | | 'total_items' => $total_items, |
| | | 'total_pages' => (int) ceil($total_items / $args['per_page']) |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Process queue operations |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | { |
| | | if (!in_array($operation->type, ['create_response', 'update_response', 'delete_response'])) { |
| | | return $result; |
| | | } |
| | | |
| | | return match($operation->type) { |
| | | 'create_response' => $this->createResponse($operation, $data), |
| | | 'update_response' => $this->updateResponse($data), |
| | | 'delete_response' => $this->deleteResponse($data), |
| | | default => $result |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Create a new response |
| | | */ |
| | | protected function createResponse(object $operation, array $data): array |
| | | { |
| | | $response_id = $this->table->insert([ |
| | | 'item_id' => $data['item_id'], |
| | | 'content' => $data['content'], |
| | | 'user_id' => $operation->user_id, |
| | | 'parent_id' => $data['parent_id'], |
| | | 'response' => $data['response'], |
| | | 'status' => 'published' |
| | | ]); |
| | | |
| | | if (!$response_id) { |
| | | $this->logError('Failed to insert response', [ |
| | | 'data' => $data, |
| | | 'error' => $this->table->getLastError() |
| | | ]); |
| | | |
| | | return ['success' => false, 'result' => 'Failed to create response']; |
| | | } |
| | | |
| | | // Send notifications |
| | | $this->sendNotifications($data['item_id'], $data['parent_id'], $response_id, $operation->user_id); |
| | | |
| | | // Clear cache |
| | | $this->cache->flush(); |
| | | |
| | | return ['success' => true, 'result' => $response_id]; |
| | | } |
| | | |
| | | /** |
| | | * Update a response |
| | | */ |
| | | protected function updateResponse(array $data): array |
| | | { |
| | | $update_data = []; |
| | | |
| | | if (isset($data['response'])) { |
| | | $update_data['response'] = $data['response']; |
| | | $update_data['updated_at'] = current_time('mysql'); |
| | | } |
| | | |
| | | if (isset($data['status'])) { |
| | | $update_data['status'] = $data['status']; |
| | | } |
| | | |
| | | if (empty($update_data)) { |
| | | return ['success' => false, 'result' => 'No fields to update']; |
| | | } |
| | | |
| | | $updated = $this->table->update( |
| | | $update_data, |
| | | ['id' => $data['response_id']] |
| | | ); |
| | | |
| | | if ($updated === false) { |
| | | $this->logError('Failed to update response', [ |
| | | 'data' => $data, |
| | | 'error' => $this->table->getLastError() |
| | | ]); |
| | | |
| | | return ['success' => false, 'result' => 'Failed to update response']; |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $updated]; |
| | | } |
| | | |
| | | /** |
| | | * Delete a response (or mark as deleted if it has replies) |
| | | */ |
| | | protected function deleteResponse(array $data): array |
| | | { |
| | | $response = $this->table->get(['id' => $data['response_id']]); |
| | | |
| | | if (!$response) { |
| | | return ['success' => false, 'result' => 'Response not found']; |
| | | } |
| | | |
| | | // Check if it has replies |
| | | $has_replies = $this->table->where(['parent_id' => $data['response_id']])->countResults() > 0; |
| | | |
| | | if ($has_replies) { |
| | | // Don't delete, just mark as deleted |
| | | $updated = $this->table->update( |
| | | [ |
| | | 'response' => '[ deleted ]', |
| | | 'status' => 'deleted', |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['id' => $data['response_id']] |
| | | ); |
| | | |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $updated]; |
| | | } |
| | | |
| | | // No replies, safe to delete |
| | | $deleted = $this->table->delete(['id' => $data['response_id']]); |
| | | |
| | | if ($deleted === false) { |
| | | $this->logError('Failed to delete response', [ |
| | | 'data' => $data, |
| | | 'error' => $this->table->getLastError() |
| | | ]); |
| | | |
| | | return ['success' => false, 'result' => 'Failed to delete response']; |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | return ['success' => true, 'result' => $deleted]; |
| | | } |
| | | |
| | | /** |
| | | * Send notifications for new responses |
| | | */ |
| | | protected function sendNotifications(int $item_id, ?int $parent_id, int $response_id, int $user_id): void |
| | | { |
| | | // Notify post author |
| | | $post = get_post($item_id); |
| | | if ($post && $post->post_author != $user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $post->post_author, |
| | | 'new_response', |
| | | [ |
| | | 'message' => 'Someone responded to your post', |
| | | 'item_id' => $item_id, |
| | | 'response_id' => $response_id |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | // Notify parent response author if this is a reply |
| | | if ($parent_id) { |
| | | $parent = $this->table->get(['id' => $parent_id]); |
| | | |
| | | if ($parent && $parent->user_id != $user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $parent->user_id, |
| | | 'response_reply', |
| | | [ |
| | | 'message' => 'Someone replied to your response', |
| | | 'item_id' => $item_id, |
| | | 'response_id' => $response_id |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | return $responses; |
| | | } |
| | | |
| | | /** |
| | |
| | | // Ordering |
| | | if ($request->has_param('orderby')) { |
| | | $orderby = $request->get_param('orderby'); |
| | | $valid = ['created_at', 'updated_at', 'upvotes', 'downvotes']; |
| | | $valid = ['created_at', 'updated_at', 'upvotes', 'downvotes', 'karmic_score']; |
| | | |
| | | if (in_array($orderby, $valid)) { |
| | | $args['orderby'] = $orderby; |
| | |
| | | } |
| | | |
| | | /** |
| | | * Get child responses recursively |
| | | */ |
| | | protected function getChildren(int $item_id, int $parent_id): array |
| | | { |
| | | return $this->getItemResponse($item_id, ['parent_id' => $parent_id]); |
| | | } |
| | | |
| | | /** |
| | | * Format response for API output |
| | | */ |
| | | protected function formatItem(object $response): array |
| | | protected function formatItem(array $response): array |
| | | { |
| | | $formatted = [ |
| | | 'id' => (int) $response->id, |
| | | 'item_id' => (int) $response->item_id, |
| | | 'parent_id' => $response->parent_id ? (int) $response->parent_id : null, |
| | | 'response' => $response->response, |
| | | 'status' => $response->status, |
| | | 'created_at' => $response->created_at, |
| | | 'updated_at' => $response->updated_at, |
| | | 'children' => $this->getChildren($response->item_id, $response->id), |
| | | 'upvotes' => (int) ($response->upvotes ?? 0), |
| | | 'downvotes' => (int) ($response->downvotes ?? 0), |
| | | 'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0), |
| | | 'reply_count' => (int) ($response->reply_count ?? 0) |
| | | ]; |
| | | |
| | | // Handle deleted users |
| | | if ($response->is_user_deleted) { |
| | | $formatted['user'] = [ |
| | | if ($response['is_user_deleted']) { |
| | | $response['user'] = [ |
| | | 'id' => null, |
| | | 'name' => '[deleted user]', |
| | | 'is_deleted' => true |
| | | ]; |
| | | } else { |
| | | // Add artist info |
| | | $artist = jvbContentFromUser($response->user_id); |
| | | if (!empty($artist)) { |
| | | $formatted['artist'] = [ |
| | | 'name' => $artist['name'], |
| | | 'url' => $artist['url'], |
| | | 'shop' => $artist['shop'] ?? null |
| | | ]; |
| | | } |
| | | $user = get_userdata($response['user_id']); |
| | | $response['user'] = [ |
| | | 'id' => $response['user_id'], |
| | | 'name' => $user->display_name, |
| | | 'url' => jvbUserProfileLink($response['user_id']), |
| | | ]; |
| | | } |
| | | |
| | | return $formatted; |
| | | } |
| | | |
| | | /** |
| | | * Handle user deletion by anonymizing responses |
| | | */ |
| | | public function handleUserDeletion(int $user_id): void |
| | | { |
| | | $updated = $this->table->update( |
| | | [ |
| | | 'is_user_deleted' => 1, |
| | | 'updated_at' => current_time('mysql') |
| | | ], |
| | | ['user_id' => $user_id] |
| | | ); |
| | | |
| | | $this->logError( |
| | | 'Anonymized responses for deleted user', |
| | | ['user_id' => $user_id, 'count' => $updated], |
| | | 'info' |
| | | ); |
| | | |
| | | $this->cache->flush(); |
| | | return $response; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\SEO\render\SchemaOutput; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\SEO\ConfigManager; |
| | | use JVBase\managers\SEO\SchemaBuilder; |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | */ |
| | | class SEORoutes extends Rest |
| | | { |
| | | use Instance; |
| | | // protected SchemaBuilder $registry; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'schema'; |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->cacheName, null, $this->cacheTtl); |
| | | // $this->registry = SchemaBuilder::getInstance(); |
| | | } |
| | | |
| | |
| | | { |
| | | $errors = []; |
| | | |
| | | $config = ConfigManager::for($context); |
| | | $config = SchemaHelper::getInstance()::for($context); |
| | | $result = $config->updateConfig($data); |
| | | |
| | | if (is_wp_error($result)) { |
| | |
| | | */ |
| | | protected function saveTypeSettings(string $type, array $data): WP_REST_Response |
| | | { |
| | | $config = ConfigManager::for($type); |
| | | $config = SchemaHelper::getInstance(); |
| | | |
| | | // Separate meta and schema data if needed |
| | | $meta = $data['meta'] ?? []; |
| | |
| | | |
| | | // Save meta configuration |
| | | if (!empty($meta)) { |
| | | $metaResult = $config->updateMeta($meta); |
| | | if (is_wp_error($metaResult)) { |
| | | $errors['meta'] = [ |
| | | 'message' => $metaResult->get_error_message(), |
| | | 'errors' => $metaResult->get_error_data() |
| | | ]; |
| | | $metaResult = $config->updateMeta($type, $meta); |
| | | if (!$metaResult) { |
| | | $errors[] = 'meta'; |
| | | } |
| | | } |
| | | |
| | | // Save schema configuration |
| | | if (!empty($schema)) { |
| | | $schemaResult = $config->updateConfig($schema); |
| | | if (is_wp_error($schemaResult)) { |
| | | $errors['schema'] = [ |
| | | 'message' => $schemaResult->get_error_message(), |
| | | 'errors' => $schemaResult->get_error_data() |
| | | ]; |
| | | $schemaResult = $config->updateSchema($type, $schema); |
| | | if (!$schemaResult) { |
| | | $errors[] = 'schema'; |
| | | } |
| | | } |
| | | |
| | |
| | | $context = $request->get_param('context'); |
| | | $data = $request->get_json_params(); |
| | | |
| | | $resetMeta = $data['resetMeta'] ?? false; |
| | | $resetSchema = $data['resetSchema'] ?? false; |
| | | |
| | | $config = ConfigManager::for($context); |
| | | |
| | | // Reset based on what was requested |
| | | if ($resetMeta) { |
| | | $config->resetMeta(); |
| | | } |
| | | |
| | | if ($resetSchema) { |
| | | $config->resetConfig(); |
| | | } |
| | | |
| | | if (!$resetMeta && !$resetSchema) { |
| | | // Default: reset everything |
| | | $config->resetAll(); |
| | | $type = $data['type']??false; |
| | | $format = $data['resetMeta'] ? 'meta' : ($data['resetSchema'] ? 'schema' : ($data['resetArchive'] ? 'archive' : false)); |
| | | $hasIt = $type && $format; |
| | | if ($hasIt) { |
| | | SchemaHelper::reset($type, $format); |
| | | } |
| | | |
| | | // Invalidate cache |
| | |
| | | return $this->success([ |
| | | 'status' => 'completed', |
| | | 'message' => 'Reset to defaults successfully', |
| | | 'meta' => $config->meta(), |
| | | 'schema' => $config->schema() |
| | | 'result' => $hasIt ? SchemaHelper::getConfig($type, $format) : [], |
| | | ]); |
| | | } |
| | | |
| | |
| | | protected function previewSchema(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $data = $request->get_json_params(); |
| | | $schemaType = $data['type'] ?? 'Thing'; |
| | | |
| | | // Get field definitions from registry |
| | | $fieldDefinitions = $this->registry->getMetaConfigForType($schemaType); |
| | | //Whether it is the Schema, Meta, Archive, or Reference schema |
| | | $format = $data['format']??false; |
| | | if (!$format) { |
| | | return $this->error('Required field "Format" missing'); |
| | | } |
| | | //WebSite, Organization, or any registered post type/taxonomy |
| | | $type = $data['type']??false; |
| | | if (!$type) { |
| | | return $this->error('Required field "Type" missing'); |
| | | } |
| | | $class = $data['class']??false; |
| | | if (!$class) { |
| | | return $this->error('Required "Schema Class" field missing (Ex: Thing, LocalBusiness, etc)'); |
| | | } |
| | | |
| | | // Build schema with actual form values |
| | | $schema = [ |
| | | '@type' => $schemaType, |
| | | '@id' => get_home_url() . '/#' . strtolower($schemaType), |
| | | '@type' => $class, |
| | | ]; |
| | | |
| | | $theClass = SchemaHelper::classFromConfig($schema); |
| | | |
| | | // Add fields with their actual values |
| | | foreach ($data['fields'] ?? [] as $fieldName => $value) { |
| | | if (empty($value)) { |
| | | continue; |
| | | } |
| | | |
| | | $schema[$fieldName] = $value; |
| | | $method = 'set'.ucfirst($fieldName); |
| | | if (!method_exists($theClass, $method)) { |
| | | error_log('Attempted field not found in class '.$class.': '.$fieldName); |
| | | continue; |
| | | } |
| | | $theClass->$method($value); |
| | | } |
| | | $schema = $theClass->outputSchema(); |
| | | |
| | | return $this->success([ |
| | | 'schema' => $schema |
| | |
| | | /** |
| | | * Get fields for a schema type |
| | | * Used for dynamic type switching |
| | | * TODO |
| | | */ |
| | | public function getFields(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $type = $request->get_param('type'); |
| | | |
| | | $class = SchemaHelper::classFromConfig(['type' => $type]); |
| | | |
| | | // Get Meta field definitions from registry |
| | | $fields = $this->registry->getMetaConfigForType($type); |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\JVB; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | |
| | | } |
| | | class SettingsRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected int $count; |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'user_settings'; |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->cacheName, null, $this->cacheTtl); |
| | | // $this->action = 'dash-'; |
| | | // $this->operation_type = 'user_settings'; |
| | | $this->count = 1; |
| | |
| | | $data['notification_preferences'] = $add; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'user_settings', |
| | | $user_id, |
| | | $data, |
| | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\managers\Emails; |
| | | use JVBase\managers\ImageGenerator; |
| | | use JVBase\managers\UploadManager; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | ]); |
| | | } |
| | | |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | unset($data['user']); |
| | | $operationID = $data['id']; |
| | | unset($data['id']); |
| | |
| | | } |
| | | } |
| | | } |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $queue->queueOperation( |
| | | 'shop_image', |
| | | $user_id, |
| | |
| | | return $result; |
| | | } |
| | | $data['imageType'] = 'shop'; |
| | | $fileGenerator = new UploadManager('shop', $operation->user_id); |
| | | $fileGenerator = UploadManager::getInstance(); //('shop', $operation->user_id); |
| | | $generator = new ImageGenerator($data, $fileGenerator); |
| | | $result = $generator->generate(); |
| | | if ($result['success']) { |
| | |
| | | |
| | | // Notify each manager |
| | | foreach ($owners as $owner) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $owner, |
| | | 'artist_request', |
| | | [ |
| | |
| | | |
| | | // If user exists, send notification through the system |
| | | if ($existing_user_id) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $existing_user_id, |
| | | 'artist_invitation', |
| | | $manager_id, |
| | |
| | | esc_url($invitation_url) |
| | | ); |
| | | |
| | | return JVB()->email()->sendEmail($email, $subject, $message); |
| | | return Emails::getInstance()->sendEmail($email, $subject, $message); |
| | | } |
| | | |
| | | /** |
| | |
| | | // If invitation was for an existing user, notify them |
| | | $user_id = email_exists($invitation->email); |
| | | if ($user_id) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $user_id, |
| | | 'invitation_revoked', |
| | | $manager_id, |
| | |
| | | |
| | | // Notify the artist |
| | | $notification_type = $status === 'accepted' ? 'shop_accepted' : 'shop_rejected'; |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $request->user_id, |
| | | $notification_type, |
| | | [ |
| | |
| | | $inviters = json_decode($invitation['inviters'], true); |
| | | if (!empty($inviters)) { |
| | | foreach ($inviters as $inviter_id) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $inviter_id, |
| | | 'invitation_accepted', |
| | | [ |
| | |
| | | $inviters = json_decode($invitation['inviters'], true); |
| | | if (!empty($inviters)) { |
| | | foreach ($inviters as $inviter_id) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $inviter_id, |
| | | 'invitation_declined', |
| | | [ |
| | |
| | | |
| | | foreach ($owners as $owner_id) { |
| | | if (!empty($owner_id)) { |
| | | JVB()->notification()->addNotification( |
| | | NotificationManager::getInstance()->addNotification( |
| | | $owner_id, |
| | | 'artist_left', |
| | | $user_id, |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\TaxonomyRelationships; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\managers\UserTermsManager; |
| | |
| | | } |
| | | class TermRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | protected object $term_index_manager; |
| | | protected int $per_page; |
| | | |
| | |
| | | { |
| | | $this->cacheName = 'terms'; |
| | | parent::__construct(); |
| | | $this->cache = Cache::for('terms', 'term'); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | $this->cache->connect('taxonomy', true); |
| | | |
| | | $this->per_page = 20; |
| | | |
| | | add_action('edited_term', [$this, 'deleteTermPath']); |
| | | add_action('wp_login', [$this, 'clearUserTaxonomyCache'], 10, 2); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | // Add relationship data if requested |
| | | if ($request->get_param('include_relationships')) { |
| | | $relationship_manager = JVB()->termRelationships(); |
| | | $relationship_manager = TaxonomyRelationships::getInstance(); |
| | | $related_taxonomies = $request->get_param('related_taxonomies') ?: ['jvb_style', 'jvb_theme', 'jvb_city']; |
| | | |
| | | $data['relationships'] = []; |
| | |
| | | public function getTermDetails(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $data = $request->get_params(); |
| | | $taxonomies = array_keys($data); |
| | | |
| | | $cache_check = $this->checkHeaders($request, $this->cache->generateKey(['termDetails' => $data])); |
| | | if ($cache_check) { |
| | | return $cache_check; |
| | | } |
| | | |
| | | $terms = $this->cache->remember( |
| | | $this->cache->generateKey(['termDetails' => $data]), |
| | | ['termDetails' => $data], |
| | | function() use ($data) { |
| | | $result = []; |
| | | foreach ($data as $tax => $IDs) { |
| | |
| | | } |
| | | ); |
| | | |
| | | return $this->addCacheHeaders($this->success(['items' => $terms])); |
| | | return $this->success(['items' => $terms]); |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | $data = $request->get_params(); |
| | | $taxonomy = sanitize_text_field($data['taxonomy'])??''; |
| | | // Check HTTP cache headers |
| | | $cache_check = $this->checkHeaders($request, $taxonomy); |
| | | if ($cache_check) { |
| | | error_log('Header Check failed'); |
| | | return $cache_check; |
| | | if (empty($taxonomy)) { |
| | | return $this->success(['message' => 'No taxonomy selected']); |
| | | } |
| | | |
| | | // Handle batch request (multiple taxonomies) |
| | |
| | | 'include' => $data['termIDs'], |
| | | 'hide_empty' => true, |
| | | ]; |
| | | $key = $this->cache->generateKey($args); |
| | | $cached = $this->cache->get($key); |
| | | if ($cached) { |
| | | $response = $this->success($cached); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | $data = $this->cache->remember( |
| | | $args, |
| | | function() use ($args, $taxonomy) { |
| | | return $this->formatTerms($args, $taxonomy); |
| | | } |
| | | ); |
| | | |
| | | $formatted = $this->formatTerms($args, $taxonomy); |
| | | $response = [ |
| | | 'items' => $formatted |
| | | 'items' => $data |
| | | ]; |
| | | $this->cache->set($key, $response); |
| | | $response = $this->success($response); |
| | | return $this->addCacheHeaders($response); |
| | | |
| | | return $this->success($response); |
| | | } |
| | | if (array_key_exists('content', $data)) { |
| | | // If content_type is provided, use the specialized endpoint |
| | | $content_type = $request->get_param('content'); |
| | | $registrar = Registrar::getInstance($content_type); |
| | | if ($registrar->hasFeature('is_content')) { |
| | | $response = $this->getTermsForContentType($request); |
| | | return $this->addCacheHeaders($response); |
| | | //TODO: Not sure what we're trying for here. Doesn't seem to be used. |
| | | //TODO: Likely for taxonomy content types, like tattoo shops |
| | | return $this->emptyResult(); |
| | | } |
| | | } |
| | | |
| | | $taxonomy = BASE.$request->get_param('taxonomy'); |
| | | $taxonomy = jvbCheckBase($request->get_param('taxonomy')); |
| | | if (!taxonomy_exists($taxonomy)) { |
| | | return $this->emptyResult(); |
| | | } |
| | | $search = $request->get_param('search'); |
| | | |
| | | $parent = (int)$data['parent']??0; |
| | | $page = max(1, (int)($data['page']??1)); |
| | | $per_page = 25; |
| | | |
| | | if (!taxonomy_exists($taxonomy)) { |
| | | return $this->emptyResult(); |
| | | } |
| | | |
| | | $tax_obj = get_taxonomy($taxonomy); |
| | | $is_hierarchical = $tax_obj->hierarchical; |
| | |
| | | // If searching, handle differently |
| | | if (!empty($search)) { |
| | | error_log('Handling search...'); |
| | | $response = $this->handleTermSearch($request); |
| | | return $this->addCacheHeaders($response); |
| | | |
| | | return $this->handleTermSearch($request); |
| | | } |
| | | |
| | | // Get terms for current level with child count |
| | |
| | | |
| | | $main_context = json_decode($request->get_param('main_context'), true); |
| | | $userID = get_post_meta($main_context['id'], BASE.'profile_link', true); |
| | | $manager = new UserTermsManager(); |
| | | $manager = UserTermsManager::getInstance(); |
| | | $related = $manager->fetchUserTerms($userID, $taxonomy); |
| | | |
| | | if (empty($related)) { |
| | | $response = $this->emptyResult(); |
| | | return $this->addCacheHeaders($response); |
| | | return $this->emptyResult(); |
| | | } |
| | | |
| | | $args['include'] = $related; |
| | |
| | | $main_context = json_decode($request->get_param('main_context'), true); |
| | | $thisTaxonomy = str_replace('taxonomy:', '', $main_context['context']); |
| | | $ID = (int)$main_context['id']; |
| | | $manager = JVB()->termRelationships(); |
| | | $manager = TaxonomyRelationships::getInstance(); |
| | | $related = $manager->getRelatedTerms($ID, BASE.$request->get_param('taxonomy')); |
| | | |
| | | if (empty($related)) { |
| | | $response = $this->emptyResult(); |
| | | return $this->addCacheHeaders($response); |
| | | return $this->emptyResult(); |
| | | } |
| | | $args['tax_query'] = [ |
| | | 'taxonomy' => $taxonomy, |
| | |
| | | $match = $request->get_param('match') ?? 'any'; |
| | | $context = json_decode($request['context'], true); |
| | | |
| | | $relationshipManager = JVB()->termRelationships(); |
| | | $relationshipManager = TaxonomyRelationships::getInstance(); |
| | | // Prepare array to collect term IDs that match the context |
| | | $related_term_ids = []; |
| | | |
| | |
| | | $args['include'] = $related_term_ids; |
| | | } else { |
| | | // No related terms found, return empty result |
| | | $response = $this->emptyResult(); |
| | | |
| | | return $this->addCacheHeaders($response); |
| | | return $this->emptyResult(); |
| | | } |
| | | } |
| | | |
| | | $response = $this->cache->remember( |
| | | $args, |
| | | function() use ($args, $taxonomy, $parent, $per_page, $page, $is_hierarchical) { |
| | | $formatted_terms = $this->formatTerms($args, $taxonomy); |
| | | // Get total count for pagination |
| | | $total_terms = wp_count_terms([ |
| | | 'taxonomy' => $taxonomy, |
| | | 'hide_empty' => false, |
| | | 'parent' => $parent |
| | | ]); |
| | | |
| | | // Calculate pagination |
| | | $total_pages = ceil($total_terms / $per_page); |
| | | $has_more = $page < $total_pages; |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | $cache = $this->cache->get($key); |
| | | return [ |
| | | 'items' => $formatted_terms, |
| | | 'is_hierarchical' => $is_hierarchical, |
| | | 'pagination' => [ |
| | | 'page' => $page, |
| | | 'per_page' => $per_page, |
| | | 'total_pages' => $total_pages, |
| | | 'total_terms' => (int)$total_terms |
| | | ], |
| | | 'has_more' => $has_more |
| | | ]; |
| | | } |
| | | ); |
| | | |
| | | if ($cache) { |
| | | $response = $this->success($cache); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | |
| | | $formatted_terms = $this->formatTerms($args, $taxonomy); |
| | | // Get total count for pagination |
| | | $total_terms = wp_count_terms([ |
| | | 'taxonomy' => $taxonomy, |
| | | 'hide_empty' => false, |
| | | 'parent' => $parent |
| | | ]); |
| | | |
| | | // Calculate pagination |
| | | $total_pages = ceil($total_terms / $per_page); |
| | | $has_more = $page < $total_pages; |
| | | |
| | | $response = [ |
| | | 'items' => $formatted_terms, |
| | | 'is_hierarchical' => $is_hierarchical, |
| | | 'pagination' => [ |
| | | 'page' => $page, |
| | | 'per_page' => $per_page, |
| | | 'total_pages' => $total_pages, |
| | | 'total_terms' => (int)$total_terms |
| | | ], |
| | | 'has_more' => $has_more |
| | | ]; |
| | | |
| | | $this->cache->set($key, $response); |
| | | $response = $this->success($response); |
| | | return $this->addCacheHeaders($response); |
| | | return $this->success($response); |
| | | } |
| | | |
| | | protected function handleBatchTermRequest(string $taxonomy, array $data, WP_REST_Request $request):WP_REST_Response |
| | |
| | | 'has_more' => true, |
| | | ]; |
| | | |
| | | $response = $this->success($response); |
| | | return $this->addCacheHeaders($response); |
| | | return $this->success($response); |
| | | } |
| | | |
| | | |
| | |
| | | protected function formatTerms(array $args, string $taxonomy): array |
| | | { |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey($args), |
| | | $args, |
| | | function() use ($args, $taxonomy) { |
| | | $terms = get_terms($args); |
| | | |
| | |
| | | |
| | | $formatted_terms = []; |
| | | foreach ($terms as $term) { |
| | | $formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, true); |
| | | $formatted_terms[] = $this->formatSingleTerm($term, $taxonomy); |
| | | } |
| | | |
| | | return $formatted_terms; |
| | |
| | | ]; |
| | | |
| | | $data = $this->cache->remember( |
| | | $this->cache->generateKey($args), |
| | | $args, |
| | | function() use ($args, $taxonomy, $page, $per_page) { |
| | | $terms = get_terms($args); |
| | | |
| | |
| | | } |
| | | ); |
| | | |
| | | return $this->addCacheHeaders($this->success($data)); |
| | | return $this->success($data); |
| | | } |
| | | |
| | | /** |
| | |
| | | delete_term_meta($termID, BASE.'term_path'); |
| | | } |
| | | |
| | | /** |
| | | * @param string $username |
| | | * @param object $user |
| | | * |
| | | * @return void |
| | | */ |
| | | public function clearUserTaxonomyCache(string $username, object $user):void |
| | | { |
| | | global $wpdb; |
| | | |
| | | // Get all transients for this user |
| | | $like_pattern = $wpdb->esc_like(BASE.'user_' . $user->ID) . '_%'; |
| | | |
| | | $transients = $wpdb->get_col($wpdb->prepare(" |
| | | SELECT option_name |
| | | FROM {$wpdb->options} |
| | | WHERE option_name LIKE %s |
| | | AND option_name LIKE %s |
| | | ", '_transient_' . $like_pattern, '%')); |
| | | |
| | | // Delete all found transients |
| | | foreach ($transients as $transient) { |
| | | delete_transient(str_replace('_transient_', '', $transient)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @param WP_REST_Request $request |
| | | * |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function getTermsForContentType(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $manager = JVB()->termRelationships(); |
| | | $content_type = BASE . $request->get_param('content'); |
| | | $taxonomy = BASE . $request->get_param('taxonomy'); |
| | | $search = $request->get_param('search'); |
| | | $page = max(1, intval($request->get_param('page'))); |
| | | $per_page = $request->get_param('per_page') ?: 20; |
| | | |
| | | // Create cache key |
| | | $cache_key = "terms_for_{$content_type}_{$taxonomy}_" . md5("{$search}_{$page}_{$per_page}"); |
| | | |
| | | $cache_key = $this->cache->generateKey([ |
| | | 'terms_for' => $content_type, |
| | | 'taxonomy' => $taxonomy, |
| | | 'search' => $search, |
| | | 'page' => $page, |
| | | 'per_page' => $per_page |
| | | ]); |
| | | $cache = $this->cache->get($cache_key); |
| | | // Try cache first |
| | | if ($cache !== false) { |
| | | $response = $this->success($cache); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | |
| | | try { |
| | | global $wpdb; |
| | | |
| | | // Starting the query building |
| | | $query_args = []; |
| | | $terms_query = " |
| | | SELECT DISTINCT t.term_id, t.name, t.slug, tt.count, tt.taxonomy, |
| | | (SELECT COUNT(DISTINCT tm.meta_key) FROM {$wpdb->termmeta} tm WHERE tm.term_id = t.term_id) AS meta_count |
| | | FROM {$wpdb->terms} t |
| | | JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id |
| | | JOIN {$manager->getTableName()} tr ON ( |
| | | (tr.primary_taxonomy = %s AND tr.primary_term_id = t.term_id) |
| | | OR (tr.related_taxonomy = %s AND tr.related_term_id = t.term_id) |
| | | ) |
| | | "; |
| | | |
| | | $query_args[] = $taxonomy; |
| | | $query_args[] = $taxonomy; |
| | | |
| | | // Add content type filter through posts |
| | | $query_where = " WHERE tt.taxonomy = %s"; |
| | | $query_args[] = $taxonomy; |
| | | |
| | | // Add search condition if provided |
| | | if ($search) { |
| | | $query_where .= " AND (t.name LIKE %s OR t.slug LIKE %s)"; |
| | | $search_like = '%' . $wpdb->esc_like($search) . '%'; |
| | | $query_args[] = $search_like; |
| | | $query_args[] = $search_like; |
| | | } |
| | | |
| | | // Add relationship strength ordering |
| | | $query_order = " GROUP BY t.term_id"; |
| | | $query_order .= " ORDER BY COUNT(tr.id) DESC, tt.count DESC"; |
| | | |
| | | // Add pagination |
| | | $query_limit = " LIMIT %d OFFSET %d"; |
| | | $query_args[] = $per_page; |
| | | $query_args[] = ($page - 1) * $per_page; |
| | | |
| | | // Combine the query |
| | | $final_query = $query_where . $query_order . $query_limit; |
| | | |
| | | // Get terms |
| | | $terms = $wpdb->get_results( |
| | | $wpdb->prepare( |
| | | $terms_query . $final_query, |
| | | $query_args |
| | | ) |
| | | ); |
| | | // Get total count for pagination |
| | | $count_query = str_replace($query_limit, '', $query_where); |
| | | $count_query = "SELECT COUNT(DISTINCT t.term_id) FROM {$wpdb->terms} t |
| | | JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id |
| | | JOIN {$manager->getTableName()} tr ON ( |
| | | (tr.primary_taxonomy = %s AND tr.primary_term_id = t.term_id) |
| | | OR (tr.related_taxonomy = %s AND tr.related_term_id = t.term_id) |
| | | ) " . $count_query; |
| | | |
| | | // Remove pagination args |
| | | array_pop($query_args); |
| | | array_pop($query_args); |
| | | |
| | | $total = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | $count_query, |
| | | $query_args |
| | | ) |
| | | ); |
| | | |
| | | // Format terms |
| | | $formatted_terms = []; |
| | | $is_hierarchical = is_taxonomy_hierarchical($taxonomy); |
| | | |
| | | foreach ($terms as $term) { |
| | | $formatted = $this->formatSingleTerm($term, $taxonomy, false); |
| | | // Add relationship strength which is unique to this method |
| | | $formatted['relationship_strength'] = $term->relationship_count ?? 0; |
| | | $formatted_terms[] = $formatted; |
| | | } |
| | | |
| | | // Build response |
| | | $total_pages = ceil($total / $per_page); |
| | | $results = [ |
| | | 'items' => $formatted_terms, |
| | | 'pagination' => [ |
| | | 'page' => (int)$page, |
| | | 'per_page' => (int)$per_page, |
| | | 'total_terms'=> $total, |
| | | 'total_pages'=> $total_pages |
| | | ], |
| | | 'has_more' => $page < $total_pages |
| | | ]; |
| | | |
| | | // Cache results |
| | | $this->cache->set($cache_key, $results); |
| | | $response = $this->success($results); |
| | | return $this->addCacheHeaders($response); |
| | | |
| | | } catch (Exception $e) { |
| | | return $this->error('Error getting terms for content: '.$e->getMessage(), 'get_terms_for_content'); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @param WP_REST_Request $request |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\queue\executors\UploadExecutor; |
| | | use JVBase\managers\queue\mergers\UploadMerger; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Errors; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | use Exception; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | } |
| | | class UploadRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | |
| | | public function __construct() |
| | | { |
| | | parent::__construct(); |
| | | |
| | | add_action('init', [$this, 'registerUploadExecutors'], 5); |
| | | } |
| | | |
| | | /** |
| | | * Register upload operation types with the queue's TypeRegistry |
| | | */ |
| | | public function registerUploadExecutors(): void |
| | | { |
| | | $registry = JVB()->queue()->registry(); |
| | | $executor = new UploadExecutor(); |
| | | $merger = new UploadMerger('secured_files'); |
| | | |
| | | // Image uploads - chunked at 5 files |
| | | $registry->register('image_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 3 |
| | | )); |
| | | |
| | | // Video uploads - one at a time (heavy processing) |
| | | $registry->register('video_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 1 |
| | | )); |
| | | |
| | | // Document uploads - chunked at 10 |
| | | $registry->register('document_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 5 |
| | | )); |
| | | |
| | | // Metadata updates |
| | | $registry->register('update_image_meta', new TypeConfig( |
| | | executor: $executor |
| | | )); |
| | | |
| | | // Cleanup - chunked at 5 |
| | | $registry->register('temporary_cleanup', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'files', |
| | | chunkSize: 5 |
| | | )); |
| | | |
| | | // Attach to content (depends on upload completing) |
| | | $registry->register('attach_upload_to_content', new TypeConfig( |
| | | executor: $executor |
| | | )); |
| | | |
| | | // Process upload groups into posts |
| | | $registry->register('process_upload_groups', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'posts', |
| | | chunkSize: 5 |
| | | )); |
| | | } |
| | | |
| | | /** |
| | | * Registers upload routes |
| | |
| | | |
| | | } catch (Exception $e) { |
| | | // Error handling... |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleUploadRequest', |
| | | $e->getMessage(), |
| | | [ |
| | |
| | | */ |
| | | protected function secureFiles(array $files, array $args): array |
| | | { |
| | | $uploader = new UploadManager(); |
| | | $uploader = UploadManager::getInstance(); |
| | | $secured_files = []; |
| | | $errors = []; |
| | | |
| | |
| | | |
| | | error_log('Queueing Operation: '.print_r($operation_type, true)); |
| | | error_log('With ID: '.print_r($args['upload'], true)); |
| | | $queuedProcessing = JVB()->queue()->queueOperation( |
| | | $queuedProcessing = Queue::getInstance()->queueOperation( |
| | | $operation_type, |
| | | get_current_user_id(), |
| | | array_merge( |
| | |
| | | // When merged, the original upload's attach_upload_to_content |
| | | // will handle all files after the merged image_upload completes. |
| | | if (!$queuedProcessing['updated_existing']) { |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'attach_upload_to_content', |
| | | get_current_user_id(), |
| | | $args, |
| | |
| | | } |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'temporary_cleanup', |
| | | get_current_user_id(), |
| | | [ |
| | | 'files' => $secured_data['files'], |
| | | 'context' => $args, |
| | | ], |
| | | [ |
| | | 'priority' => 'low', |
| | | 'chunk_size' => 5, |
| | | 'chunk_key' => 'files', |
| | | 'depends_on' => [$uploadOpId] |
| | | ] |
| | | ); |
| | | |
| | | return $args['id']; |
| | | } |
| | | |
| | |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Step 3: Process operation from queue |
| | | * Called by OperationQueue |
| | | * @param WP_Error|array $result |
| | | * @param object $operation |
| | | * @param array $data |
| | | * |
| | | * @return array|WP_Error |
| | | */ |
| | | public function processOperation(array $result, object $operation, array $data): array |
| | | { |
| | | // Only handle our operation types |
| | | $handled_types = [ |
| | | 'image_upload', |
| | | 'video_upload', |
| | | 'document_upload', |
| | | 'update_metadata', |
| | | 'temporary_cleanup', |
| | | 'attach_upload_to_content', |
| | | 'process_upload_groups' |
| | | ]; |
| | | |
| | | if (!in_array($operation->type, $handled_types)) { |
| | | return $result; // Not our operation, pass through |
| | | } |
| | | |
| | | try { |
| | | // Route to appropriate handler |
| | | $handler_result = match($operation->type) { |
| | | 'image_upload' => $this->processImageUpload($operation, $data), |
| | | 'video_upload' => $this->processVideoUpload($operation, $data), |
| | | 'document_upload' => $this->processDocumentUpload($operation, $data), |
| | | 'update_metadata' => $this->processUploadMeta($result, $operation, $data), |
| | | 'temporary_cleanup' => $this->processTemporaryCleanup($result, $operation, $data), |
| | | 'attach_upload_to_content' => $this->processAttachToContent($operation, $data), |
| | | 'process_upload_groups' => $this->processUploadGroups($result, $operation, $data), |
| | | default => new WP_Error('unknown_type', 'Unknown operation type') |
| | | }; |
| | | |
| | | // Handle WP_Error |
| | | if (is_wp_error($handler_result)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $handler_result->get_error_message() |
| | | ]; |
| | | } |
| | | |
| | | return $handler_result; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processOperation', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | /** |
| | | * Standardize processing result |
| | | */ |
| | | protected function standardizeResult(array $result): array |
| | | { |
| | | return [ |
| | | 'attachment_id' => $result['attachment_id'], |
| | | 'url' => $result['url'], |
| | | 'file' => $result['file'], |
| | | 'upload_id' => $result['upload_id'] ?? null |
| | | ]; |
| | | } |
| | | |
| | | protected function processAttachToContent(object $operation, array $data): array |
| | | { |
| | | try { |
| | | // Get the results from the upload operation |
| | | $upload_results = JVB()->queue()->getOperationValue($data['upload'], 'result', true); |
| | | |
| | | if (empty($upload_results)) { |
| | | throw new Exception('No upload results found for operation: ' . $data['upload']); |
| | | } |
| | | |
| | | if (empty($data['post_id']) || str_starts_with((string)($data['item_id'] ?? ''), 'new')) { |
| | | foreach ($operation->dependencies as $depId) { |
| | | $dep = JVB()->queue()->get($depId); |
| | | if ($dep && $dep->type === 'content_update' && !empty($dep->result['new_posts'])) { |
| | | $itemId = $data['item_id'] ?? null; |
| | | if ($itemId && isset($dep->result['new_posts'][$itemId])) { |
| | | $data['post_id'] = $dep->result['new_posts'][$itemId]; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (empty($data['post_id'])) { |
| | | throw new Exception('Could not resolve post_id from dependencies'); |
| | | } |
| | | } |
| | | |
| | | // Now attach to the specified content |
| | | if (!empty($data['field_name'])) { |
| | | $this->updateFieldValue($data, $upload_results); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => 'Attachments linked to content' |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Cleanup temporary files after processing |
| | | */ |
| | | protected function cleanupTempFiles(array $secured_files, int $user_id): void |
| | | { |
| | | $uploader = new UploadManager(); |
| | | |
| | | foreach ($secured_files as $secured) { |
| | | if (!empty($secured['temp_path']) && file_exists($secured['temp_path'])) { |
| | | @unlink($secured['temp_path']); |
| | | } |
| | | } |
| | | |
| | | // Clean up empty directories |
| | | $uploader->cleanupEmptyTempDirs($user_id); |
| | | } |
| | | |
| | | /** |
| | | * Update field value with new attachment IDs |
| | |
| | | $meta->set($data['field_name'], implode(',', $all_ids)); |
| | | } |
| | | |
| | | /** |
| | | * Generic file upload processor - works for images, videos, documents |
| | | */ |
| | | protected function processFileUpload(object $operation, array $data, string $file_type): WP_Error|array |
| | | { |
| | | try { |
| | | $uploader = new UploadManager(); |
| | | $processed_results = []; |
| | | $config = $this->getFieldConfig($data); |
| | | |
| | | $args = $data; |
| | | unset($args['secured_files']); |
| | | |
| | | foreach ($data['secured_files'] as $secured_file) { |
| | | $result = $uploader->processUpload( |
| | | $secured_file['temp_path'], |
| | | array_merge( |
| | | $config, |
| | | [ |
| | | 'file_type' => $file_type, |
| | | 'user_id' => $operation->user_id, |
| | | 'post_id' => (int)($data['post_id'] ?? 0), |
| | | 'term_id' => (int)($data['term_id'] ?? 0), |
| | | 'original_name' => $secured_file['original_name'], |
| | | 'mime_type' => $secured_file['mime_type'], |
| | | 'content' => $data['content'] ?? '', |
| | | ] |
| | | ) |
| | | ); |
| | | |
| | | if (!is_wp_error($result)) { |
| | | $standardized = $this->standardizeResult($result); |
| | | |
| | | // Use embedded upload_id from the secured file itself |
| | | $standardized['upload_id'] = $secured_file['upload_id'] ?? null; |
| | | |
| | | if ($standardized['upload_id']) { |
| | | $processed_results[$standardized['upload_id']] = $standardized; |
| | | } else { |
| | | $processed_results[] = $standardized; |
| | | } |
| | | |
| | | // Apply frontend metadata if provided |
| | | if (!empty($secured_file['metadata'])) { |
| | | $this->applyMeta($standardized['attachment_id'], $secured_file['metadata']); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $this->handleUploadDestination($data, $processed_results); |
| | | |
| | | // Cleanup temporary files |
| | | $this->cleanupTempFiles($data['secured_files'], $operation->user_id); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => $processed_results |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processFileUpload', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'file_type' => $file_type, |
| | | 'user_id' => $operation->user_id |
| | | ] |
| | | ); |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Process image uploads |
| | | */ |
| | | protected function processImageUpload(object $operation, array $data): WP_Error|array |
| | | { |
| | | return $this->processFileUpload($operation, $data, 'image'); |
| | | } |
| | | |
| | | /** |
| | | * Process video uploads |
| | | */ |
| | | protected function processVideoUpload(object $operation, array $data): WP_Error|array |
| | | { |
| | | return $this->processFileUpload($operation, $data, 'video'); |
| | | } |
| | | |
| | | /** |
| | | * Process document uploads |
| | | */ |
| | | protected function processDocumentUpload(object $operation, array $data): WP_Error|array |
| | | { |
| | | return $this->processFileUpload($operation, $data, 'document'); |
| | | } |
| | | |
| | | /** |
| | | * Process temporary cleanup operation |
| | | * Called by OperationQueue for 'temporary_cleanup' operations |
| | | * |
| | | * @param array $result |
| | | * @param object $operation |
| | | * @param array $data |
| | | * @return array |
| | | */ |
| | | protected function processTemporaryCleanup(array $result, object $operation, array $data): array |
| | | { |
| | | try { |
| | | // Cleanup temporary files if they exist |
| | | if (!empty($data['secured_files'])) { |
| | | $this->cleanupTempFiles($data['secured_files'], $operation->user_id); |
| | | } |
| | | |
| | | // If specific temp paths provided |
| | | if (!empty($data['temp_paths']) && is_array($data['temp_paths'])) { |
| | | foreach ($data['temp_paths'] as $temp_path) { |
| | | if (file_exists($temp_path)) { |
| | | @unlink($temp_path); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Cleanup empty temp directories for this user |
| | | if (!empty($operation->user_id)) { |
| | | $uploader = new UploadManager(); |
| | | $uploader->cleanupEmptyTempDirs($operation->user_id); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => 'Temporary files cleaned up successfully' |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processTemporaryCleanup', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'user_id' => $operation->user_id |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle metadata update requests |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | return $this->sendResponse( |
| | | false, |
| | | ['error_code' => 'missing_identifiers'], |
| | |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleMetadataUpdate', |
| | | $e->getMessage(), |
| | | [ |
| | |
| | | */ |
| | | protected function queueMetaUpdate(array $data, int $user, ?string $operationId = null): WP_REST_Response |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $depends_on = []; |
| | | $errors = []; |
| | | $original = count($data); |
| | |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Process metadata update operation (called by queue processor) |
| | | */ |
| | | public function processUploadMeta(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | { |
| | | try { |
| | | if (!is_array($operation->depends_on)) { |
| | | $operation->depends_on = [$operation->depends_on]; |
| | | } |
| | | $updated_count = 0; |
| | | $errors = []; |
| | | foreach ($operation->depends_on as $dependency) { |
| | | $operationData = JVB()->queue()->getOperation($dependency); |
| | | if (!$operationData || $operationData->state !== 'completed') { |
| | | throw new Exception('Original upload operation not found or not completed'); |
| | | } |
| | | |
| | | $uploadResults = json_decode($operationData->result, true); |
| | | if (!$uploadResults || !$uploadResults['success']) { |
| | | throw new Exception('Original upload operation failed'); |
| | | } |
| | | $attachmentsToUpdate = array_filter($data, function ($item) use ($dependency) { |
| | | return $item['depends_on'] === $dependency; |
| | | }); |
| | | |
| | | $uploadIDToAttachment = $this->buildUploadToAttachmentMapping( |
| | | array_keys($attachmentsToUpdate), |
| | | $uploadResults['data'] |
| | | ); |
| | | |
| | | if (empty($uploadIDToAttachment)) { |
| | | throw new Exception('No valid upload ID to attachment ID mappings found'); |
| | | } |
| | | |
| | | foreach ($uploadIDToAttachment as $uploadId => $attachmentId) { |
| | | try { |
| | | $this->applyMeta($attachmentId, $attachmentsToUpdate[$uploadId]); |
| | | $updated_count++; |
| | | } catch (Exception $e) { |
| | | $errors[] = "Upload {$uploadId} (Attachment {$attachmentId}): " . $e->getMessage(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return [ |
| | | 'success' => $updated_count > 0, |
| | | 'result' => [ |
| | | 'updated_count' => $updated_count, |
| | | 'mappings' => $uploadIDToAttachment, |
| | | 'errors' => $errors, |
| | | 'message' => "Applied metadata to {$updated_count} attachment(s)" |
| | | ] |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processUploadMeta', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'original_operation_id' => $data['original_operation_id'] ?? 'unknown' |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | protected function applyMeta(int $attachment_id, array $metadata): void |
| | | { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Build mapping from frontend upload IDs to WordPress attachment IDs |
| | | */ |
| | | protected function buildUploadToAttachmentMapping(array $upload_ids, array $results): array |
| | | { |
| | | $mapping = []; |
| | | |
| | | foreach ($results as $result) { |
| | | if (!isset($result['upload_id']) || !isset($result['attachment_id'])) { |
| | | continue; |
| | | } |
| | | |
| | | $upload_id = $result['upload_id']; |
| | | $attachment_id = $result['attachment_id']; |
| | | |
| | | // Validate that this upload_id was requested |
| | | if (in_array($upload_id, $upload_ids)) { |
| | | $mapping[$upload_id] = $attachment_id; |
| | | } |
| | | } |
| | | |
| | | return $mapping; |
| | | } |
| | | |
| | | /** |
| | | * Verify user has access to attachment |
| | | * @param int $attachment_id |
| | | * @param int $user_id |
| | | * @return bool |
| | | */ |
| | | protected function verifyAttachmentAccess(int $attachment_id, int $user_id): bool |
| | | { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Get field configuration for upload processing |
| | | */ |
| | | protected function getFieldConfig(array $data): array |
| | | { |
| | | static $config_cache = []; |
| | | |
| | | $cache_key = md5(json_encode([ |
| | | $args['content'] ?? '', |
| | | $args['field_name'] ?? '' |
| | | ])); |
| | | |
| | | if (isset($config_cache[$cache_key])) { |
| | | return $config_cache[$cache_key]; |
| | | } |
| | | |
| | | $config = [ |
| | | 'allowed_types' => null, |
| | | 'max_size' => null, |
| | | 'convert' => 'webp', |
| | | 'quality' => 80, |
| | | 'create_thumbnails' => true, |
| | | ]; |
| | | |
| | | // Get field definition from registry if available |
| | | if (!empty($args['content']) && !empty($args['field_name'])) { |
| | | $content_type = $args['content']; |
| | | $field_name = $args['field_name']; |
| | | $registrar = Registrar::getInstance($content_type); |
| | | if ($registrar) { |
| | | $content_fields = $registrar->getFields(); |
| | | if (array_key_exists($field_name, $content_fields)) { |
| | | $field_def = $content_fields[$field_name]; |
| | | |
| | | // Extract relevant config from field definition |
| | | $config = array_merge($config, [ |
| | | 'allowed_types' => $field_def['accepted_types'] ?? null, |
| | | 'max_size' => $field_def['max_size'] ?? null, |
| | | 'convert' => $field_def['convert'] ?? 'webp', |
| | | 'quality' => $field_def['quality'] ?? 80, |
| | | 'create_thumbnails' => $field_def['create_thumbnails'] ?? true, |
| | | ]); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $config_cache[$cache_key] = $config; |
| | | return $config; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Get files info for error logging |
| | | * @param array $files |
| | | * @return array |
| | | */ |
| | | protected function getFilesInfo(array $files): array |
| | | { |
| | |
| | | $chunkSize = 10; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | $operation_type, |
| | | $args['user'], |
| | | array_merge( |
| | |
| | | ] |
| | | ); |
| | | |
| | | $ID = JVB()->queue()->queueOperation( |
| | | $ID = Queue::getInstance()->queueOperation( |
| | | 'process_upload_groups', |
| | | $args['user'], |
| | | $args, |
| | |
| | | <?php |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | |
| | | */ |
| | | class UserRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'user'; |
| | | $this->cacheTtl = WEEK_IN_SECONDS; |
| | | |
| | | parent::__construct(); |
| | | $this->cache = Cache::for($this->cacheName, 'user', $this->cacheTtl); |
| | | |
| | | } |
| | | |
| | |
| | | try { |
| | | $meta = Meta::forUser($user_id); |
| | | $meta->setAll($data['changes']); |
| | | Cache::clearUser($user_id); |
| | | return $this->success(); |
| | | } catch (Exception $e) { |
| | | return $this->error($e->getMessage()); |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\KarmaManager; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Response; |
| | |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | use Exception; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | class VoteRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | public function __construct() |
| | | { |
| | | $this->cacheName = 'karma'; |
| | |
| | | } |
| | | |
| | | |
| | | $man = KarmaManager::getInstance($type); |
| | | $man = $registrar->getKarmaManager(); |
| | | if (!$man) { |
| | | return Response::validationError(['message' => __('Karma not set up', 'jvb')]); |
| | | } |
| | |
| | | return $this->unauthorized(); |
| | | } |
| | | |
| | | $cache = $this->cache->get($user); |
| | | if ($cache) { |
| | | return Response::success($cache); |
| | | } |
| | | |
| | | $votes = KarmaManager::getUserVotes($user); |
| | | |
| | | $this->cache->set($user, $votes); |
| | | |
| | | return Response::success($votes); |
| | | return Response::success(KarmaManager::getUserVotes($user)); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\ServiceMap; |
| | | use JVBase\meta\Form; |
| | | use JVBase\meta\Meta; |
| | | |
| | |
| | | */ |
| | | class Checkout |
| | | { |
| | | use ServiceMap; |
| | | public static function getProvider():Integrations|false |
| | | { |
| | | $int = ['square','helcim']; |
| | | foreach ($int as $i) { |
| | | if (Site::hasIntegration($i)) { |
| | | error_log('Site has '.$i); |
| | | return JVB()->connect($i); |
| | | $self = new self(); |
| | | return $self->getService($i); |
| | | } |
| | | } |
| | | return false; |
| | |
| | | $tabs->addTab('thelogin') |
| | | ->title('Login') |
| | | ->description('Login to see past orders and access your saved cards') |
| | | ->content(JVB()->magicLink()->renderForm()); |
| | | ->content(MagicLinks::getInstance()->renderForm()); |
| | | } else { |
| | | $tabs->addTab('account') |
| | | ->title('Account') |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | require(JVB_DIR.'/inc/ui/Modal.php'); |
| | | require(JVB_DIR.'/inc/ui/Navigation.php'); |
| | | require(JVB_DIR.'/inc/ui/Tabs.php'); |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| | | // Silence is golden. |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | require(JVB_DIR.'/inc/utility/helpers.php'); |
| | | require(JVB_DIR.'/inc/utility/Image.php'); |
| | | require(JVB_DIR.'/inc/utility/Video.php'); |
| | |
| | | Textdomain: jvb |
| | | */ |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | use JVBase\admin\SEOAdmin; |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\blocks\CustomBlocks; |
| | | use JVBase\JVB; |
| | | use JVBase\managers\AdminPages; |
| | | use JVBase\managers\Approvals; |
| | | use JVBase\managers\Dashboard\Dashboard; |
| | | use JVBase\managers\DirectoryManager; |
| | | use JVBase\managers\Emails; |
| | | use JVBase\managers\Favourites; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\managers\InvitationsManager; |
| | | use JVBase\managers\Login; |
| | | use JVBase\managers\MagicLinks; |
| | | use JVBase\managers\NotificationManager; |
| | | use JVBase\managers\Notifications\Notifications; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\Referrals; |
| | | use JVBase\managers\Responses; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\managers\SEO\render\SchemaOutput; |
| | | use JVBase\managers\TaxonomyRelationships; |
| | | use JVBase\managers\UserTermsManager; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | use JVBase\base\Site; |
| | | |
| | | //security |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | //add_action('admin_enqueue_scripts', function() { |
| | | // global $wp_scripts; |
| | | // add_action('admin_print_scripts', function() use ($wp_scripts) { |
| | |
| | | define('JVB_LOCKOUT_DURATION', 15 * MINUTE_IN_SECONDS); |
| | | |
| | | |
| | | require(JVB_DIR.'/inc/Instance.php'); |
| | | require(JVB_DIR.'/base/_setup.php'); |
| | | |
| | | require(JVB_DIR.'/inc/utility/setup.php'); |
| | | require(JVB_DIR.'/checks.php'); |
| | | |
| | | require(JVB_DIR . '/inc/integrations/ServiceMap.php'); |
| | | require(JVB_DIR . '/inc/registrar/_setup.php'); |
| | | require(JVB_DIR . '/activate.php'); |
| | | |
| | |
| | | 'display' => __('Every minute', 'jvb') |
| | | ]; |
| | | $schedules[ 'monthly' ] = [ |
| | | 'interval' => 604800 * 4, |
| | | 'interval' => MONTH_IN_SECONDS, |
| | | 'display' => __('Once Monthly', 'jvb') |
| | | ]; |
| | | |
| | |
| | | register_deactivation_hook(JVB_DIR.'/jvb.php', 'jvbDeactivatePlugin'); |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | add_action('init', 'jvbUserCheck'); |
| | | function jvbUserCheck():void |
| | | { |
| | | if (is_admin() && isOurPeople()) { |
| | | wp_redirect(get_home_url(null, '/dash')); |
| | | exit; |
| | | } |
| | | } |
| | | |
| | | //require(JVB_DIR . '/inc/users/UserSettings.php'); |
| | | |
| | | |
| | |
| | | |
| | | function jvbLoadBase():void |
| | | { |
| | | JVB::getInstance(); |
| | | do_action('jvbloaded'); |
| | | Emails::getInstance(); |
| | | Queue::getInstance(); |
| | | Roles::getInstance(); |
| | | Login::getInstance(); |
| | | SEOAdmin::getInstance(); |
| | | SchemaOutput::getInstance(); |
| | | SchemaHelper::getInstance(); |
| | | CustomBlocks::getInstance(); |
| | | |
| | | AdminPages::getInstance(); |
| | | |
| | | if (Site::has('magic_link')) { |
| | | MagicLinks::getInstance(); |
| | | } |
| | | if (Site::has('dashboard')) { |
| | | Dashboard::getInstance(); |
| | | } |
| | | if (Site::has('referrals')) { |
| | | Referrals::getInstance(); |
| | | } |
| | | |
| | | |
| | | if (Site::has('is_directory')) { |
| | | DirectoryManager::getInstance(); |
| | | } |
| | | |
| | | if (Site::has('feed_block')) { |
| | | TaxonomyRelationships::getInstance(); |
| | | } |
| | | |
| | | if (Site::hasAll(['dashboard', 'membership', 'feed_block'])) { |
| | | UserTermsManager::getInstance(); |
| | | } |
| | | |
| | | if (Site::has('favourites')) { |
| | | Favourites::getInstance(); |
| | | } |
| | | |
| | | if (!empty(Registrar::withFeature('has_responses'))) { |
| | | Responses::getInstance(); |
| | | } |
| | | |
| | | $membership = Site::membership(); |
| | | if ($membership && $membership->has('notifications')) { |
| | | NotificationManager::getInstance(); |
| | | } |
| | | if ($membership && $membership->has('invitable')) { |
| | | InvitationsManager::getInstance(); |
| | | } |
| | | |
| | | if (!empty(Registrar::withFeature('approve_new'))) { |
| | | Approvals::getInstance(); |
| | | } |
| | | jvbLoadRoutes(); |
| | | } |
| | | |
| | | function jvb_integration_definitions():void |
| | |
| | | Registrar::maybeBuildSections(); |
| | | } |
| | | |
| | | add_action('wp_footer', 'jvbAdditionalActions'); |
| | | function jvbAdditionalActions():void |
| | | { |
| | | if (Login::isLogin()) { |
| | | return; |
| | | } |
| | | $extras = apply_filters('jvbAdditionalActions', []); |
| | | $extras = array_filter($extras, function ($extra) { |
| | | return is_array($extra) && array_key_exists('button', $extra) && array_key_exists('content', $extra); |
| | | }); |
| | | if (empty ($extras)) { |
| | | return; |
| | | } |
| | | $buttons = array_map(function ($extra) { |
| | | return $extra['button']; |
| | | }, $extras); |
| | | $contents = array_map(function($extra) { |
| | | return $extra['content']; |
| | | }, $extras); |
| | | |
| | | if (!empty ($buttons)) { |
| | | ?> |
| | | <section class="main-actions"> |
| | | <div class="buttons col"> |
| | | <?= implode('', $buttons); ?> |
| | | </div> |
| | | <div class="actions"> |
| | | <?= implode('', $contents); ?> |
| | | </div> |
| | | </section> |
| | | <?php |
| | | } |
| | | } |
| | | |
| | | |
| | | function JVB(): JVB |
| | | { |
| | | return JVB::getInstance(); |
| | | } |
| | | |
| | | use JVBase\rest\routes\AdminRoutes; |
| | | use JVBase\rest\routes\ContentRoutes; |
| | | use JVBase\rest\routes\ContentTermsRoutes; |
| | | use JVBase\rest\routes\ErrorRoutes; |
| | | use JVBase\rest\routes\FeedRoutes; |
| | | use JVBase\rest\routes\FormRoutes; |
| | | use JVBase\rest\routes\NewsRoutes; |
| | | use JVBase\rest\routes\OptionsRoutes; |
| | | use JVBase\rest\routes\SEORoutes; |
| | | use JVBase\rest\routes\SettingsRoutes; |
| | | use JVBase\rest\routes\TermRoutes; |
| | | use JVBase\rest\routes\UploadRoutes; |
| | | |
| | | function jvbLoadRoutes():void |
| | | { |
| | | SEORoutes::getInstance(); |
| | | SettingsRoutes::getInstance(); |
| | | UploadRoutes::getInstance(); |
| | | if (!empty(apply_filters('jvb_register_forms', []))) { |
| | | FormRoutes::getInstance(); |
| | | } |
| | | UserRoutes::getInstance(); |
| | | |
| | | if (Site::has('feed_block')) { |
| | | FeedRoutes::getInstance(); |
| | | } |
| | | |
| | | if (Site::has('feed_block') || Site::has('dashboard')) { |
| | | TermRoutes::getInstance(); |
| | | } |
| | | |
| | | if (Site::has('dashboard')) { |
| | | ErrorRoutes::getInstance(); |
| | | AdminRoutes::getInstance(); |
| | | ContentRoutes::getInstance(); |
| | | ContentTermsRoutes::getInstance(); |
| | | OptionsRoutes::getInstance(); |
| | | } |
| | | |
| | | $membership = Site::membership(); |
| | | if ($membership && $membership->has('forum')) |
| | | { |
| | | NewsRoutes::getInstance(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Scripts |
| | | */ |
| | | add_action('wp_enqueue_scripts', 'jvbScripts', 10); |
| | | |
| | | function jvbScripts():void |
| | | { |
| | | add_action('wp_head', 'jvbInlineNavStyles'); |
| | | |
| | | if (Site::has('dashboard')) { |
| | | wp_enqueue_script('jvb-queue'); |
| | | } |
| | | |
| | | wp_enqueue_script('jvb-auth'); |
| | | wp_enqueue_script('jvb-settings'); |
| | | wp_enqueue_script('jvb-navigation'); |
| | | // wp_enqueue_script('jvb-ui'); |
| | | // wp_enqueue_script('jvb-media'); |
| | | wp_enqueue_script('jvb-gallery'); |
| | | wp_enqueue_script('jvb-cache'); |
| | | |
| | | |
| | | $interactions = []; |
| | | if (Site::has('favourites')) { |
| | | $interactions[] = 'favourites'; |
| | | } |
| | | if (!empty(Registrar::withFeature('karma'))) { |
| | | $interactions[] = 'karma'; |
| | | } |
| | | if (Site::has('notifications')) { |
| | | $interactions[] = 'notifications'; |
| | | } |
| | | |
| | | if (!empty($interactions)) { |
| | | foreach($interactions as $interaction) { |
| | | wp_enqueue_script('jvb-'.$interaction); |
| | | } |
| | | } |
| | | |
| | | |
| | | $queue = [ |
| | | 'api' => rest_url('jvb/v1/'), |
| | | 'redirect' => get_home_url(null, '/login/'), |
| | | 'labels' => Registrar::getLabels(), |
| | | ]; |
| | | |
| | | wp_localize_script('jvb-auth', 'jvbSettings', $queue); |
| | | |
| | | |
| | | $initUserSettings = 'async function initUserItems() { |
| | | if (!jvbSettings.currentUser) return; |
| | | |
| | | window.userFavourites = {}; |
| | | window.userVotes = {}; |
| | | '; |
| | | if (Site::has('favourites')) { |
| | | wp_enqueue_script('jvb-favourites'); |
| | | $initUserSettings .= ' |
| | | //Fetch user favourites |
| | | try { |
| | | const result = await window.jvbCache.fetchWithCache( |
| | | `${jvbSettings.api}favourites?all=true`, |
| | | { |
| | | method: \'GET\', |
| | | headers: {\'X-WP-Nonce\': jvbSettings.nonce} |
| | | } |
| | | ); |
| | | |
| | | if (result && result.favourites) { |
| | | for (const key in result.favourites) { |
| | | window.userFavourites[key] = new Set(); |
| | | result.favourites[key].forEach(id => { |
| | | window.userFavourites[key].add(parseInt(id)); |
| | | }); |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error(\'Failed to fetch user favourites:\', error); |
| | | }'; |
| | | } |
| | | |
| | | if (!empty(Registrar::withFeature('karma'))) { |
| | | wp_enqueue_script('jvb-votes'); |
| | | $initUserSettings .= '// Fetch user votes |
| | | try { |
| | | const result = await window.jvbCache.fetchWithCache( |
| | | `${jvbSettings.api}vote?user=${jvbSettings.currentUser}`, |
| | | { |
| | | method: \'GET\', |
| | | headers: {\'X-WP-Nonce\': jvbSettings.nonce} |
| | | }, |
| | | {forceRefresh: true} |
| | | ); |
| | | |
| | | window.userVotes = {}; |
| | | if (result) { |
| | | for (const key in result) { |
| | | window.userVotes[key] = new Map(); |
| | | for (const [id, vote] of Object.entries(result[key])) { |
| | | window.userVotes[key].set(parseInt(id), vote); |
| | | } |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error(\'Failed to fetch user votes:\', error); |
| | | }'; |
| | | } |
| | | |
| | | $initUserSettings .= '} initUserItems();'; |
| | | |
| | | // wp_add_inline_script('jvb-queue', $initUserSettings, 'after'); |
| | | |
| | | |
| | | if (is_search() || |
| | | is_tax(BASE.'shop') || |
| | | is_singular(BASE.'artist') || |
| | | jvbIsDirectory() |
| | | ) { |
| | | wp_enqueue_script('jvb-page-nav'); |
| | | } |
| | | |
| | | if (has_block('jvb/summaryBlock')) { |
| | | wp_enqueue_script('jvb-page-nav'); |
| | | } |
| | | |
| | | // Only load on single shop pages or other relevant pages |
| | | if (is_tax(BASE.'shop') || |
| | | is_singular(BASE.'partner')) { |
| | | $ID = get_queried_object_id(); |
| | | if (is_tax()) { |
| | | $check = get_term_meta($ID, BASE.'has_map', true); |
| | | } else { |
| | | $check = get_post_meta($ID, BASE.'has_map', true); |
| | | } |
| | | |
| | | // if ($check && $check !== '') { |
| | | //// wp_enqueue_script( |
| | | //// 'google-maps-frontend', |
| | | //// 'https://maps.googleapis.com/maps/api/js?key=' . GOOGLE_API . '&callback=googleMapsLoaded', |
| | | //// [], |
| | | //// null, |
| | | //// ['strategy' => 'async', |
| | | //// 'in_footer' => true] |
| | | //// ); |
| | | // |
| | | // // Add the callback function |
| | | // wp_add_inline_script('google-maps-frontend', ' |
| | | // function googleMapsLoaded() { |
| | | // if (window.googleMapsCallbacks && window.googleMapsCallbacks.length) { |
| | | // window.googleMapsCallbacks.forEach(callback => callback()); |
| | | // } |
| | | // } |
| | | // '); |
| | | // } |
| | | } |
| | | if (is_user_logged_in() && Site::has('notifications')) { |
| | | wp_enqueue_script('jvb-notifications'); |
| | | |
| | | wp_localize_script('jvb-notifications', 'notificationSettings', array( |
| | | 'homeUrl' => home_url(), |
| | | 'strings' => array( |
| | | 'loading' => 'Loading...', |
| | | 'error' => 'Failed to load notifications', |
| | | 'noNotifications' => 'No new notifications', |
| | | 'viewAll' => 'View All Notifications', |
| | | 'markRead' => 'Mark as read', |
| | | 'review' => 'Review' |
| | | ) |
| | | )); |
| | | } |
| | | |
| | | // jvbAddScriptDependency('jvb-feed-view-script', 'jvb-queue'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-selector'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-data-store'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-cache'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-a11y'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-utility'); |
| | | jvbAddScriptDependency('jvb-feed-view-script', 'jvb-gallery'); |
| | | // jvbAddScriptDependency('jvb-feed-view-script', 'jvb-loading'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-queue'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-quill'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-form'); |
| | | jvbAddScriptDependency('jvb-forms-view-script', 'jvb-tabs'); |
| | | |
| | | jvbAddScriptDependency('jvb-timeline-view-script', 'jvb-gallery'); |
| | | } |
| | | |
| | | function jvbAdminMap():void |
| | | { |
| | |
| | | // '); |
| | | } |
| | | |
| | | function jvbAddScriptDependency(string $handle, string $dep):bool |
| | | { |
| | | global $wp_scripts; |
| | | |
| | | $script = $wp_scripts->query($handle); |
| | | if (!$script) { |
| | | return false; |
| | | } |
| | | |
| | | if (!in_array($dep, $script->deps)) { |
| | | $script->deps[] = $dep; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | |
| | | add_action('wp_body_open', 'jvbAccessibility'); |
| | |
| | | echo '<div class="screen-reader-text live-region" aria-live="polite" role="status"></div>'; |
| | | } |
| | | |
| | | add_action('wp_head', 'jvbFrontendBase',1); |
| | | function jvbFrontendBase():void |
| | | { |
| | | ?> |
| | | <script type="text/javascript">window.jvbBase = '<?= BASE ?>';</script> |
| | | <?php |
| | | } |
| | | //add_action('wp_head', 'jvbFrontendBase',1); |
| | | //function jvbFrontendBase():void |
| | | //{ |
| | | // ?> |
| | | <!-- <script type="text/javascript">window.jvbBase = '--><?php //= BASE ?>//';</script> |
| | | // <?php |
| | | //} |
| | | |
| | | //add_action('wp_head', 'jvbDumpIt'); |
| | | function jvbDumpIt() |
| | | { |
| | | // jvbDump(Registrar::getInstance('foodie')->getFields()); |
| | | } |
| | | |
| | | |
| | | add_action('after_setup_theme', 'jvbImageSize'); |
| | | function jvbImageSize():void |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render |
| | | */ |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render |
| | | */ |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render |
| | | */ |
| | |
| | | <?php |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\NotificationManager; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | true |
| | | ); |
| | | |
| | | $handler = JVB()->notification(); |
| | | $handler = NotificationManager::getInstance(); |
| | | $user = wp_get_current_user(); |
| | | $role = array_values($user->roles)[0]; |
| | | |