=Updating custom tables to utilize CustomTable.php
1 files renamed
38 files added
76 files modified
2 files deleted
| | |
| | | use JVBase\admin\SEOAdmin; |
| | | use JVBase\managers\AdminPages; |
| | | use JVBase\managers\NotificationManager; |
| | | use JVBase\managers\TaxonomyRelationships; |
| | | use JVBase\managers\UserTermsManager; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\routes\FeedRoutes; |
| | |
| | | // 'uploads' => new UploadManager(), |
| | | 'userTerms' => new UserTermsManager(), |
| | | 'email' => new EmailManager(), |
| | | 'terms' => new TaxonomyRelationships(), |
| | | ]; |
| | | |
| | | $this->routes = [ |
| | |
| | | { |
| | | return $this->managers['schemaHelper']; |
| | | } |
| | | |
| | | public function termRelationships():TaxonomyRelationships |
| | | { |
| | | return $this->managers['terms']; |
| | | } |
| | | } |
| | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\managers\DirectoryManager; |
| | | use JVBase\managers\ErrorHandler; |
| | | use JVBase\managers\InvitationsManager; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\ReferralManager; |
| | | use JVBase\managers\RoleManager; |
| | |
| | | |
| | | function jvbActivatePlugin():void |
| | | { |
| | | |
| | | // $validator = new JVBase\utility\Validator(); |
| | | // $validation = $validator->validateAll(); |
| | | // error_log('Validation result: '.print_r($validation, true)); |
| | |
| | | 'time' => '12:03am tomorrow', |
| | | ], |
| | | //NotificationManager.php |
| | | 'jvb_notification_digest_daily' => |
| | | BASE.'notification_digest_daily' => |
| | | [ |
| | | 'time' => '8:08am tomorrow', |
| | | ], |
| | | 'jvb_notification_digest_weekly' => |
| | | BASE.'notification_digest_weekly' => |
| | | [ |
| | | 'time' => 'monday 6:07am', |
| | | 'recurrence' => 'weekly', |
| | | ], |
| | | 'jvb_notification_digest_monthly' => |
| | | BASE.'notification_digest_monthly' => |
| | | [ |
| | | 'time' => '2025-05-05 9:00am', |
| | | 'recurrence' => 'monthly', |
| | |
| | | * ] |
| | | */ |
| | | |
| | | $membership = apply_filters('jvb_membership', []); |
| | | $membership = apply_filters('jvb_membership', [ |
| | | 'can_invite' => [], |
| | | ]); |
| | | define('JVB_MEMBERSHIP', $membership); |
| | |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebSite', |
| | | 'name' => get_bloginfo('name'), |
| | | 'url' => get_home_url(), |
| | | 'id' => get_home_url() . '#website', |
| | | 'id' => get_home_url() . '/#website', |
| | | 'description' => get_bloginfo('description'), |
| | | 'inLanguage' => 'en-CA' |
| | | ], |
| | | default => [] |
| | | }, |
| | | 'archive' => [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage', |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', |
| | | 'name' => '{{name}}' |
| | | ], |
| | | default => [], |
| | | }; |
| | | return apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults); |
| | | error_log('Getting defaults for: '.BASE.ucfirst($type).ucfirst($format).'Default'); |
| | | $result = apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults); |
| | | if ($format === 'reference' && empty($result)) { |
| | | $full = self::getDefault($type, 'schema'); |
| | | if (!empty($full)) { |
| | | $result = [ |
| | | 'type' => $full['type'], |
| | | 'name' => $full['name'], |
| | | 'description'=> $full['description'] |
| | | ]; |
| | | $check = ['id', 'url']; |
| | | foreach ($check as $ch) { |
| | | if (array_key_exists($ch, $full)) { |
| | | $result[$ch] = $full[$ch]; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return $result; |
| | | } |
| | | |
| | | public static function updateHistory(string $type, string $format, array $newest):bool |
| | |
| | | unset($config['type']); |
| | | $class = new $className(); |
| | | |
| | | |
| | | foreach ($config as $property => $value) { |
| | | if (is_array($value)) { |
| | | if (is_array($value) && array_key_exists('type', $value)) { |
| | | $value = self::classFromConfig($value, $meta); |
| | | } |
| | | $method = 'set'.ucfirst($property); |
| | |
| | | namespace JVBase\admin; |
| | | |
| | | use JVBase\meta\Form; |
| | | use JVBase\meta\Sanitizer; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\ui\Tabs; |
| | | |
| | |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\Webpage' => ' - - WebPage', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\AboutPage' => ' - - - About Page', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CheckoutPage' => ' - - - Checkout Page', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage' => ' - - - Collection Page', |
| | | 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage' => ' - - - Collection Page', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ContactPage' => ' - - - Contact Page', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\FAQPage' => ' - - - FAQ Page', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ItemPage' => ' - - - Item Page', |
| | |
| | | public function render(array $attributes, string $content, WP_Block $block) |
| | | { |
| | | global $post; |
| | | $registrar = Registrar::getInstance($post->post_type)); |
| | | $registrar = Registrar::getInstance($post->post_type); |
| | | if (!$post || !$registrar || !$registrar->hasFeature('is_timeline') ) { |
| | | return ''; |
| | | } |
| | |
| | | new JVBase\blocks\FAQBlock(); |
| | | } |
| | | |
| | | |
| | | if (!empty(Registrar::getFeatured('is_gallery'))) { |
| | | if (!empty(Registrar::getFeatured('is_glossary'))) { |
| | | require(JVB_DIR . '/inc/blocks/GlossaryBlock.php'); |
| | | new JVBase\blocks\GlossaryBlock(); |
| | | } |
| | | |
| | | if (!empty(Registrar::getFeatured('is_timeline'))) { |
| | | require(JVB_DIR . '/inc/blocks/TimelineBlock.php'); |
| | | new JVBase\blocks\TimelineBlock(); |
| | | } |
| | | |
| | | require(JVB_DIR . '/inc/blocks/SummaryBlock.php'); |
| | | new JVBase\blocks\SummaryBlock(); |
| | | |
| | |
| | | update_option(BASE.'do_these_once', $options); |
| | | } |
| | | } |
| | | |
| | | |
| | | function jvbResponse(bool $success, ?string $msg = null):array |
| | | { |
| | | return [ |
| | | 'success' => $success, |
| | | 'message' => is_null($msg) ? ($success ? 'Completed successfully' : 'Something went wrong') : $msg |
| | | ]; |
| | | } |
| | |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | $user->roles) |
| | | ))[0]; |
| | | } |
| | | |
| | | function jvbUserProfileLink(int $userID):string|false |
| | | { |
| | | $cache = Cache::for('userLink')->connect('user'); |
| | | return $cache->remember( |
| | | $userID, |
| | | function() use ($userID) { |
| | | $user = get_userdata($userID); |
| | | if (!$user) { |
| | | return false; |
| | | } |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | if (!$registrar || !$registrar->profile_link) { |
| | | return false; |
| | | } |
| | | $link = get_user_meta($userID, BASE.'profile_link', true); |
| | | //Try to create it |
| | | if (empty($link)) { |
| | | $link = JVB()->roles()->addUserLink($user, $role); |
| | | if (!$link) { |
| | | return false; |
| | | } |
| | | } |
| | | $status = get_post_status($link); |
| | | return ($status === 'publish') ? get_the_permalink($link) : false; |
| | | } |
| | | ); |
| | | } |
| | |
| | | exit; |
| | | } |
| | | |
| | | |
| | | function jvbGetTermOwners(int $termID, bool $includeManagers = true):array |
| | | { |
| | | $owners = get_term_meta($termID, BASE.'owners', true); |
| | | $owners = empty($owners) ? [] : $owners; |
| | | if ($includeManagers) { |
| | | $managers = get_term_meta($termID, BASE.'managers', true); |
| | | $managers = empty($managers) ? [] : $managers; |
| | | $owners = array_merge($owners, $managers); |
| | | } |
| | | return $owners; |
| | | |
| | | } |
| | | /** |
| | | * @param string $term |
| | | * @param int|false $ID |
| | |
| | | if ($users === '') { |
| | | $term = get_term($termID); |
| | | $taxonomy = $term->taxonomy; |
| | | if (taxIsJVBContentTax($taxonomy)) { |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | if ($registrar->hasFeature('is_content')) { |
| | | $posts = new WP_Query([ |
| | | 'post_type' => jvbCheckBase($user), |
| | | 'posts_per_page' => -1, |
| | |
| | | */ |
| | | namespace JVBase\integrations; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | return true; |
| | | } |
| | | |
| | | if (isJVBContentTax() && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) { |
| | | if (!empty(Registrar::getFeatured('is_content', 'term')) && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) { |
| | | return true; |
| | | } |
| | | |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | |
| | | use DateTime; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\managers\CustomTable; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class ApprovalManager { |
| | | protected array $tables =[]; |
| | | protected int $expiresAt = 21; //days for request |
| | | protected int $requiredVotes = 3; //Number of votes before a term is approved |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | if (empty($this->tables)) { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | $types = Registrar::getFeatured('approve_new'); |
| | | foreach ($types as $type) { |
| | | $requests = CustomTable::for("approval_{$type}_requests"); |
| | | |
| | | $requests->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$requests->getUserIDType()} NOT NULL", |
| | | 'name' => 'varchar(255) NOT NULL', |
| | | 'parent_id' => 'bigint(20) unsigned DEFAULT 0', |
| | | 'status' => "ENUM('pending', 'approved', 'rejected', 'appealed', 'expired') DEFAULT 'pending'", |
| | | 'required_approvals'=> 'int unsigned DEFAULT 3', |
| | | 'approvals' => 'int unsigned DEFAULT 0', |
| | | 'rejections'=> 'int unsigned DEFAULT 0', |
| | | 'expires_at' => 'datetime DEFAULT NULL', |
| | | 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | 'approved_by' => 'json DEFAULT NULL', |
| | | 'rejected_by' => 'json DEFAULT NULL', |
| | | 'created_item_id' => 'bigint(20) unsigned DEFAULT NULL', |
| | | ]); |
| | | $requests->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => '`unique_request` (`user_id`, `name`)'], |
| | | '`name` (`name`)', |
| | | '`name_parent` (`name`, `parent_id`)', |
| | | '`status` (`status`)', |
| | | '`expiring_requests` (`status`, `expires_at`)' |
| | | ]); |
| | | $base = BASE; |
| | | $requests->setConstraints([ |
| | | "CONSTRAINTS `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$requests->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINTS `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`) |
| | | REFERENCES `{$requests->getTermTable()}` (`term_id`) ON DELETE CASCADE" |
| | | ]); |
| | | $requests->defineTable(); |
| | | |
| | | $votes = CustomTable::for("approval_{$type}_votes"); |
| | | |
| | | $votes->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'request_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'user_id' => "{$votes->getUserIDType()} NOT NULL", |
| | | 'vote' => "ENUM('approve', 'reject', 'dismiss') NOT NULL", |
| | | 'notes' => 'text DEFAULT NULL', |
| | | 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]); |
| | | |
| | | $votes->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => '`unique_vote` (`request_id`, `user_id`)'], |
| | | '`user_votes` (`user_id`)' |
| | | ]); |
| | | |
| | | $votes->setConstraints([ |
| | | "CONSTRAINT `{$base}{$type}_user_approval_request` FOREIGN KEY (`request_id`) |
| | | REFERENCES `{$requests->getFullTableName()} (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$votes->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $votes->defineTable(); |
| | | |
| | | $this->tables[$type] = [ |
| | | 'requests' => $requests, |
| | | 'votes' => $votes |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | public function canApprove(int $userID, string $type):bool |
| | | { |
| | | $type = jvbNoBase($type); |
| | | if (!array_key_exists($type, $this->tables)) { |
| | | return false; |
| | | } |
| | | |
| | | return user_can($userID, "approve_{$type}"); |
| | | } |
| | | protected function requests(string $type):?CustomTable |
| | | { |
| | | $type = jvbNoBase($type); |
| | | if (!array_key_exists($type, $this->tables)){ |
| | | return null; |
| | | } |
| | | return $this->tables[$type]['requests']??null; |
| | | } |
| | | |
| | | protected function votes(string $type):?CustomTable |
| | | { |
| | | $type = jvbNoBase($type); |
| | | if (!array_key_exists($type, $this->tables)){ |
| | | return null; |
| | | } |
| | | return $this->tables[$type]['votes']??null; |
| | | } |
| | | |
| | | public function response(bool $success, string $msg = ''):array |
| | | { |
| | | $return = ['success' => $success]; |
| | | if ($msg !== '') { |
| | | $return['message'] = $msg; |
| | | } |
| | | return $return; |
| | | } |
| | | |
| | | public function createApproval(int $userID, string $type, string $name, int $parentID = 0):array |
| | | { |
| | | if (!$this->canApprove($userID, $type)) { |
| | | return $this->response(false, 'Cannot create request'); |
| | | } |
| | | $type = jvbNoBase($type); |
| | | if (!array_key_exists($type, $this->tables)) { |
| | | $this->response(false, 'Invalid type'); |
| | | } |
| | | $table = $this->requests($type); |
| | | if (!$table) { |
| | | return $this->response(false, 'Invalid type'); |
| | | } |
| | | |
| | | //Check for existing request |
| | | $existing = $table->get([ |
| | | 'name' => sanitize_text_field($name) |
| | | ]); |
| | | if (!$existing) { |
| | | $this->approve($userID, $existing->id); |
| | | return $this->response(true, 'Existing request - added your vote!'); |
| | | } |
| | | |
| | | if ($parentID > 0) { |
| | | $parent = get_term($parentID, jvbCheckBase($type)); |
| | | if (!$parent || is_wp_error($parent)) { |
| | | $parent = 0; |
| | | } |
| | | } |
| | | |
| | | $date = new DateTime('+' . $this->expiresAt . ' days'); |
| | | $expires = $date->format('Y-m-d H:i:s'); |
| | | |
| | | |
| | | $result = $table->create([ |
| | | 'user_id' => $userID, |
| | | 'name' => sanitize_text_field($name), |
| | | 'parent_id' => $parentID, |
| | | 'expires_at'=> $expires |
| | | ]); |
| | | |
| | | return $this->response((bool)$result, $result?'Request created successfully' : 'Request could not be created'); |
| | | } |
| | | |
| | | public function markApproval(int $request_id, int $userID, string $type, string $vote, ?string $note = null):array |
| | | { |
| | | if (!$this->canApprove($userID, $type)) { |
| | | return $this->response(false, 'Sorry, you cannot do this.'); |
| | | } |
| | | $requests = $this->requests($type); |
| | | if (!$requests) { |
| | | return $this->response(false, 'Invalid type'); |
| | | } |
| | | $request = $requests->get(['request_id' => $request_id]); |
| | | if (!$request) { |
| | | return $this->response(false, 'Invalid request id'); |
| | | } |
| | | |
| | | $votes = $this->votes($type); |
| | | if (!$votes) { |
| | | return $this->response(false, 'Invalid type'); |
| | | } |
| | | |
| | | if (!in_array($vote, ['approve', 'reject', 'dismiss'])) { |
| | | return $this->response(false, 'Invalid vote'); |
| | | } |
| | | |
| | | $response = $votes->findOrCreate([ |
| | | 'request_id' => $request_id, |
| | | 'user_id' => $userID |
| | | ], [ |
| | | 'vote' => 'approve', |
| | | 'notes' => $note ? sanitize_text_field($note) : $note |
| | | ]); |
| | | |
| | | if (!$response) { |
| | | return $this->response(false, 'Could not store vote for some reason.'); |
| | | } |
| | | |
| | | switch ($vote) { |
| | | case 'reject': |
| | | $request['rejections']++; |
| | | break; |
| | | case 'approve': |
| | | $request['approvals']++; |
| | | break; |
| | | default: |
| | | return $this->response(true, 'Successfully dismissed approval request.'); |
| | | } |
| | | |
| | | if ($request['rejections'] === $request['required_approvals']) { |
| | | $this->finalizeDenial($request_id, $type); |
| | | } else if ($request['approvals'] === $request['required_approvals']) { |
| | | $this->finalizeApproval($request_id, $type); |
| | | } |
| | | $updated = $requests->update([ |
| | | 'rejections'=> $request['rejections'], |
| | | 'approvals' => $request['approvals'] |
| | | ], |
| | | [ |
| | | 'request_id' => $request_id |
| | | ]); |
| | | |
| | | if ($updated) { |
| | | return $this->response(true, 'Successfully voted'); |
| | | } |
| | | return $this->response(false, 'Could not finalize vote for some reason'); |
| | | } |
| | | |
| | | public function approveRequest(int $request_id, int $userID, string $type, ?string $note = null):array |
| | | { |
| | | return $this->markApproval($request_id, $userID, $type, 'approve', $note); |
| | | } |
| | | |
| | | public function denyRequest(int $request_id, int $userID, string $type, string $note = ''):array |
| | | { |
| | | return $this->markApproval($request_id, $userID, $type, 'reject', $note); |
| | | } |
| | | |
| | | public function dismissRequest(int $request_id, int $userID, string $type, string $note = ''):array |
| | | { |
| | | return $this->markApproval($request_id, $userID, $type, 'dismiss', $note); |
| | | } |
| | | |
| | | public function isApproved(int $request_id, string $type):bool |
| | | { |
| | | $table = $this->requests($type); |
| | | if (!$table) { |
| | | return false; |
| | | } |
| | | $request = $table->get(['request_id' => $request_id]); |
| | | if (!$request) { |
| | | return false; |
| | | } |
| | | $votes = $this->votes($type); |
| | | if (!$votes) { |
| | | return false; |
| | | } |
| | | $total = $votes->getMany([ |
| | | 'request_id' => $request_id, |
| | | 'status' => ['IN' => ['approve', 'reject']] |
| | | ]); |
| | | $approvals = count(array_filter($total, function($item) { return $item->status === 'approve'; })); |
| | | $denials = count(array_filter($total, function($item) { return $item->status === 'reject'; })); |
| | | if ($denials === $this->requiredVotes) { |
| | | return false; |
| | | } |
| | | return $approvals === $this->requiredVotes; |
| | | } |
| | | |
| | | public function finalizeApproval(int $request_id, string $type):void |
| | | { |
| | | $requests = $this->requests($type); |
| | | $request = $requests->get(['request_id' => $request_id]); |
| | | if (!$request){ |
| | | error_log('Could not finalize denial for request: '.$request_id); |
| | | return; |
| | | } |
| | | |
| | | $updatedID = null; |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar->getType() === 'term') { |
| | | $term = wp_insert_term($request['name'], $registrar->getBased(), ['parent' => $request['parent_id']]); |
| | | if (!is_wp_error($term)) { |
| | | $updatedID = $term['term_id']; |
| | | } |
| | | } elseif ($registrar->getType() === 'user') { |
| | | |
| | | } |
| | | |
| | | $updates = [ |
| | | 'status' => 'approved' |
| | | ]; |
| | | if ($updatedID) { |
| | | $updates['created_item_id'] = $updatedID; |
| | | } |
| | | $updated = $requests->update( |
| | | $updates, |
| | | [ |
| | | 'request_id' => $request_id |
| | | ]); |
| | | |
| | | |
| | | JVB()->notification()->addNotification($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | } |
| | | public function finalizeDenial(int $request_id, string $type):void |
| | | { |
| | | $requests = $this->requests($type); |
| | | $request = $requests->get(['request_id' => $request_id]); |
| | | if (!$request){ |
| | | error_log('Could not finalize denial for request: '.$request_id); |
| | | return; |
| | | } |
| | | $updated = $requests->update([ |
| | | 'status' => 'rejected' |
| | | ], |
| | | [ |
| | | 'request_id' => $request_id |
| | | ]); |
| | | |
| | | JVB()->notification()->addNotification($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | } |
| | | |
| | | public function getApprovalRequests(int $userID, ?string $type = null):array |
| | | { |
| | | $return = []; |
| | | if ($type) { |
| | | if (!array_key_exists($type, $this->tables)) { |
| | | return $return; |
| | | } |
| | | $requests = $this->requests($type); |
| | | $active = $requests->pluck('request_id', [ |
| | | 'status' => 'pending' |
| | | ]); |
| | | $votes = $this->votes($type); |
| | | $voted = $votes->pluck('request_id',[ |
| | | 'user_id' => $userID |
| | | ]); |
| | | |
| | | $all = array_diff($active, $voted); |
| | | $allRequests = $requests->getMany([ |
| | | 'where' => [ |
| | | 'request_id' => ['IN' => $all] |
| | | ] |
| | | ]); |
| | | |
| | | $return = array_map(function ($request) use ($type) { |
| | | return $this->format_for_notification($request, $type); |
| | | }, $allRequests); |
| | | } else { |
| | | foreach ($this->tables as $type => $tables) { |
| | | $requests = $tables['requests'];; |
| | | $active = $requests->pluck('request_id', [ |
| | | 'status' => 'pending', |
| | | ]); |
| | | $votes = $tables['votes']; |
| | | $voted = $votes->pluck('request_id', [ |
| | | 'user_id' => $userID |
| | | ]); |
| | | |
| | | $all = array_diff($active, $voted); |
| | | $allRequests = $requests->getMany([ |
| | | 'where' => [ |
| | | 'request_id' => ['IN' => $all] |
| | | ] |
| | | ]); |
| | | $return = array_merge($return, array_map(function ($request) use ($type) { |
| | | return $this->format_for_notification($request, $type); |
| | | }, $allRequests)); |
| | | } |
| | | } |
| | | |
| | | return $return; |
| | | } |
| | | |
| | | protected function format_for_notification(array $request, string $type): array |
| | | { |
| | | $registrar = Registrar::getInstance($type); |
| | | $message = ''; |
| | | $user = get_userdata($request['user_id']); |
| | | if ($user) { |
| | | $message = $user->first_name; |
| | | } else { |
| | | $message = 'Someone'; |
| | | } |
| | | $message .= ' thinks we need a new '.$registrar->getSingular().'. |
| | | They think: "'.$request['name'].'" |
| | | '; |
| | | if ($request['parent_id'] > 0) { |
| | | $parent = get_term($request['parent_id'], $registrar->getBased()); |
| | | if ($parent && !is_wp_error($parent)) { |
| | | $message .= 'And nest it under: '.$parent->name.' |
| | | '; |
| | | } |
| | | } |
| | | $message .= ' |
| | | What say you?'; |
| | | return [ |
| | | 'id' => $request['id'], |
| | | 'type' => $type.'_new', |
| | | 'message' => $message, |
| | | 'created_at'=> $request['created_at'], |
| | | 'status' => 'pending', |
| | | 'requires_action' => true, |
| | | 'action_taken' => false, |
| | | 'icon' => $registrar->getIcon(), |
| | | 'priority' => 'normal', |
| | | 'target' => [ |
| | | 'id' => $request['id'], |
| | | 'type' => 'approval' |
| | | ], |
| | | 'actions' => [ |
| | | 'approve' => 'Approve', |
| | | 'deny' => 'Deny', |
| | | 'dismiss' => 'Dismiss' |
| | | ] |
| | | ]; |
| | | } |
| | | } |
| | |
| | | |
| | | protected static string $userTable; |
| | | protected static string $userIDType; |
| | | protected static string $termTable; |
| | | protected static string $termIDType; |
| | | protected static string $postIDType; |
| | | protected static string $postTable; |
| | | |
| | | protected Cache $cache; |
| | | |
| | | /** |
| | | * Fluent factory method |
| | |
| | | |
| | | return self::$instances[$tableName]; |
| | | } |
| | | public static function destroyInstance(string $tableName):void |
| | | { |
| | | if (isset(self::$instances[$tableName])) { |
| | | unset(self::$instances[$tableName]); |
| | | } |
| | | } |
| | | public static function getInstance(string $tableName):self|false |
| | | { |
| | | if (!isset(self::$instances[$tableName])) { |
| | | return false; |
| | | } |
| | | return self::$instances[$tableName]; |
| | | } |
| | | |
| | | public function ensureDefined() { |
| | | $this->ensureUserTable(); |
| | |
| | | } |
| | | } |
| | | |
| | | protected function ensureTermTable():void |
| | | { |
| | | if (!isset(static::$termTable)) { |
| | | static::$termTable = $this->wpdb->terms; |
| | | } |
| | | } |
| | | |
| | | protected function ensurePostIDType():void |
| | | { |
| | | if (!isset(static::$postIDType)) { |
| | | static::$postIDType = $this->getColumnType($this->wpdb->posts, 'ID'); |
| | | } |
| | | } |
| | | protected function ensurePostTable():void |
| | | { |
| | | if (!isset(static::$postTable)) { |
| | | static::$postTable = $this->wpdb->posts; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * |
| | |
| | | } |
| | | |
| | | // Constraints |
| | | foreach ($this->constraints as $constraint => $references) { |
| | | $parts[] = "CONSTRAINT {$constraint} REFERENCES {$references}"; |
| | | foreach ($this->constraints as $constraint) { |
| | | $parts[] = $constraint; |
| | | } |
| | | |
| | | $this->definition = "(\n " . implode(",\n ", $parts) . "\n)"; |
| | | $this->createTable(); |
| | | return $this; |
| | | } |
| | | |
| | |
| | | error_log('[CustomTable]No definition set for '.$this->tableName); |
| | | return; |
| | | } |
| | | if ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) { |
| | | return; |
| | | } |
| | | require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); |
| | | error_log('Creating Database Table: '. $this->tableName); |
| | | $charset = self::$charsetCollate; |
| | | $schema = "CREATE TABLE IF NOT EXISTS {$this->fullTableName} {$this->definition} {$charset}"; |
| | | $this->wpdb->flush(); |
| | |
| | | $this->fullTableName = $wpdb->prefix . apply_filters('jvb_base', BASE) . $tableName; |
| | | $this->useTransactions = $useTransactions; |
| | | |
| | | $this->cache = Cache::for($tableName); |
| | | |
| | | $usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->users}'"); |
| | | $parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci'; |
| | | self::$charsetCollate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}"; |
| | |
| | | return false; |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | |
| | | return $this->wpdb->insert_id; |
| | | } |
| | | |
| | |
| | | * ); |
| | | * // Returns: ['id' => 456, 'created' => false, 'record' => object] |
| | | */ |
| | | public function findOrCreate(array $searchData, array $createData = []): array |
| | | public function findOrCreate(array $searchData, array $createData = []):int|false |
| | | { |
| | | $record = $this->get($searchData); |
| | | |
| | | if ($record) { |
| | | return [ |
| | | 'id' => $record->id ?? 0, |
| | | 'created' => false, |
| | | 'record' => $record |
| | | ]; |
| | | if (!empty($createData)) { |
| | | return $this->update($createData, $searchData); |
| | | } |
| | | return $record->id; |
| | | } |
| | | |
| | | $data = array_merge($searchData, $createData); |
| | | $id = $this->insert($data); |
| | | |
| | | return [ |
| | | 'id' => $id, |
| | | 'created' => true, |
| | | 'record' => $this->get(['id' => $id]) |
| | | ]; |
| | | return $this->insert($data); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function get(array $where, string $output = OBJECT): object|array|null |
| | | { |
| | | $query = "SELECT * FROM {$this->fullTableName} WHERE " . $this->buildWhereClause($where); |
| | | $values = array_values($where); |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey(array_merge(['output' => $output], $where)), |
| | | function() use ($where, $output) { |
| | | $query = "SELECT * FROM {$this->fullTableName}"; |
| | | $where = $this->buildWhereClause($where); |
| | | $query .= " WHERE {$where['sql']}"; |
| | | $values = $where['values']; |
| | | |
| | | return $this->wpdb->get_row($this->wpdb->prepare($query, $values), $output); |
| | | return $this->wpdb->get_row($this->wpdb->prepare($query, $values), $output); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getMany(array $args = [], string $output = OBJECT): array |
| | | { |
| | | $query = "SELECT * FROM {$this->fullTableName}"; |
| | | $values = []; |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey(array_merge($args, ['output' => $output])), |
| | | function () use ($args, $output) { |
| | | $query = "SELECT * FROM {$this->fullTableName}"; |
| | | $values = []; |
| | | |
| | | // WHERE clause |
| | | if (!empty($args['where'])) { |
| | | $query .= " WHERE " . $this->buildWhereClause($args['where']); |
| | | $values = array_merge($values, array_values($args['where'])); |
| | | } |
| | | // WHERE clause |
| | | if (!empty($args['where'])) { |
| | | $clause = $this->buildWhereClause($args['where']); |
| | | |
| | | // ORDER BY |
| | | if (!empty($args['orderby'])) { |
| | | $orderby = sanitize_sql_orderby($args['orderby']); |
| | | $order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC'; |
| | | $query .= " ORDER BY {$orderby} {$order}"; |
| | | } |
| | | $query .= " WHERE {$clause['sql']}"; |
| | | $values = array_merge($values, $clause['values']); |
| | | } |
| | | |
| | | // LIMIT |
| | | if (!empty($args['limit'])) { |
| | | $limit = absint($args['limit']); |
| | | $offset = !empty($args['offset']) ? absint($args['offset']) : 0; |
| | | $query .= " LIMIT {$offset}, {$limit}"; |
| | | } |
| | | // ORDER BY |
| | | if (!empty($args['orderby'])) { |
| | | $orderby = sanitize_sql_orderby($args['orderby']); |
| | | $order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC'; |
| | | $query .= " ORDER BY {$orderby} {$order}"; |
| | | } |
| | | |
| | | if (empty($values)) { |
| | | return $this->wpdb->get_results($query, $output); |
| | | } |
| | | // LIMIT |
| | | if (!empty($args['limit'])) { |
| | | $limit = absint($args['limit']); |
| | | $offset = !empty($args['offset']) ? absint($args['offset']) : 0; |
| | | $query .= " LIMIT {$offset}, {$limit}"; |
| | | } |
| | | |
| | | return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output); |
| | | if (empty($values)) { |
| | | return $this->wpdb->get_results($query, $output); |
| | | } |
| | | |
| | | return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Get a specific column value from all matches |
| | | * @param string $column |
| | | * @param array $where |
| | | * @return array |
| | | */ |
| | | public function pluck(string $column, array $where = [], ?string $orderby = null, ?string $order = null, ?int $limit = null): array |
| | | { |
| | | $key = array_merge($where, ['column' => $column, 'orderby' => $orderby, 'order' => $order, 'limit' => $limit]); |
| | | |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey($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"]); |
| | | return []; |
| | | } |
| | | $args = ['where' => $where]; |
| | | if ($orderby) { |
| | | $args['orderby'] = $orderby; |
| | | } |
| | | if ($order) { |
| | | $args['order'] = $order; |
| | | } |
| | | if ($limit) { |
| | | $args['limit'] = $limit; |
| | | } |
| | | |
| | | return array_column($this->getMany($args), $column); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function count(array $where = []): int |
| | | { |
| | | $query = "SELECT COUNT(*) FROM {$this->fullTableName}"; |
| | | $values = []; |
| | | return $this->cache->remember( |
| | | $this->cache->generateKey(array_merge(['source' => 'count'], $where)), |
| | | function() use ($where) { |
| | | $query = "SELECT COUNT(*) FROM {$this->fullTableName}"; |
| | | $values = []; |
| | | |
| | | if (!empty($where)) { |
| | | $query .= " WHERE " . $this->buildWhereClause($where); |
| | | $values = array_values($where); |
| | | } |
| | | if (!empty($where)) { |
| | | $clause = $this->buildWhereClause($where); |
| | | $query .= " WHERE {$clause['sql']}"; |
| | | $values = $clause['values']; |
| | | } |
| | | |
| | | if (empty($values)) { |
| | | return (int) $this->wpdb->get_var($query); |
| | | } |
| | | if (empty($values)) { |
| | | return (int) $this->wpdb->get_var($query); |
| | | } |
| | | |
| | | return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values)); |
| | | return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values)); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | if ($result === false) { |
| | | $this->logError('update', ['data' => $data, 'where' => $where]); |
| | | } |
| | | $this->cache->flush(); |
| | | |
| | | return $result; |
| | | } |
| | |
| | | if ($result === false) { |
| | | $this->logError('delete', ['where' => $where]); |
| | | } |
| | | $this->cache->flush(); |
| | | |
| | | return $result; |
| | | } |
| | |
| | | } catch (Exception $e) { |
| | | $this->rollback(); |
| | | $this->logError('transaction', ['error' => $e->getMessage()]); |
| | | throw $e; |
| | | return false; |
| | | } |
| | | } |
| | | |
| | |
| | | $this->ensureTermIDType(); |
| | | return static::$termIDType; |
| | | } |
| | | public function getTermTable():string |
| | | { |
| | | $this->ensureTermTable(); |
| | | return static::$termTable; |
| | | } |
| | | public function getPostIDType():string |
| | | { |
| | | $this->ensurePostIDType(); |
| | | return static::$postIDType; |
| | | } |
| | | public function getPostTable():string |
| | | { |
| | | $this->ensurePostTable(); |
| | | return static::$postTable; |
| | | } |
| | | |
| | | // ========================================================================= |
| | | // PRIVATE HELPERS |
| | |
| | | /** |
| | | * Build WHERE clause from associative array |
| | | */ |
| | | private function buildWhereClause(array $where): string |
| | | private function buildWhereClause(array $where):array |
| | | { |
| | | $conditions = []; |
| | | $values = []; |
| | | foreach ($where as $column => $value) { |
| | | $column_safe = esc_sql($column); |
| | | |
| | | 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)) { |
| | | $placeholders = implode(',', array_map([$this, 'getPlaceholder'], $operand)); |
| | | $conditions[] = "`{$column_safe}` {$operator} ({$placeholders})"; |
| | | $values = array_merge($values, $operand); |
| | | } else { |
| | | // <, >, <=, >=, !=, LIKE, etc. |
| | | $conditions[] = "`{$column_safe}` {$operator} " . $this->getPlaceholder($operand); |
| | | $values[] = $operand; |
| | | } |
| | | } else { |
| | | $conditions[] = "`{$column_safe}` = " . $this->getPlaceholder($value); |
| | | $values[] = $value; |
| | | } |
| | | } |
| | | return implode(' AND ', $conditions); |
| | | |
| | | return ['sql' => implode(' AND ', $conditions), 'values' => $values]; |
| | | } |
| | | |
| | | /** |
| | |
| | | ); |
| | | } |
| | | |
| | | public function grid(array $items, int $columns = 2):string |
| | | public function grid(array $items, int $columns = 2, string $title = '', string|array $description = '', string $after = ''):string |
| | | { |
| | | $width = floor(100 / $columns) - 2; // 2% gap |
| | | |
| | | $html = '<div style="display:table;width:100%;margin:20px 0;">'; |
| | | $html = ''; |
| | | if (!empty($title) || !empty($description)) { |
| | | $html .= '<div>'; |
| | | if (!empty($title)) { |
| | | $html .= sprintf( |
| | | '<h2>%s</h2>', |
| | | $title |
| | | ); |
| | | } |
| | | if (!empty($description)) { |
| | | if (is_string($description)) { |
| | | if (str_starts_with($description, '<p>')) { |
| | | $html .= $description; |
| | | }else { |
| | | $html .= sprintf( |
| | | '<p>%s</p>', |
| | | $description |
| | | ); |
| | | } |
| | | } else { |
| | | $html .= implode('',array_map(function ($p) { return sprintf('<p>%s</p>', $p); }, $description)); |
| | | } |
| | | } |
| | | } |
| | | $html .= '<div style="display:table;width:100%;margin:20px 0;">'; |
| | | foreach ($items as $index => $item) { |
| | | if ($index > 0 && $index % $columns === 0) { |
| | | $html .= '</div><div style="display:table;width:100%;margin:20px 0;">'; |
| | |
| | | $item |
| | | ); |
| | | } |
| | | $html .= '</div>'; |
| | | $html .= '</div>'.$after; |
| | | |
| | | |
| | | if (!empty($title) || !empty($description)) { |
| | | $html .= '</div>'; |
| | | } |
| | | return $html; |
| | | } |
| | | |
| | | |
| | | public function image(string $src, string $alt = '', int $maxWidth = 600):string |
| | | { |
| | | return sprintf( |
| | |
| | | 'critical' => 4 |
| | | ]; |
| | | |
| | | protected CustomTable $table; |
| | | |
| | | public function __construct() |
| | | { |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->tableName = $wpdb->prefix . BASE . 'error_log'; |
| | | $this->defineTables(); |
| | | // global $wpdb; |
| | | // $this->wpdb = $wpdb; |
| | | // $this->tableName = $wpdb->prefix . BASE . 'error_log'; |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | |
| | |
| | | // add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3); |
| | | } |
| | | |
| | | public function defineTables():void |
| | | { |
| | | $table = CustomTable::for('error_log'); |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'error_type' => 'varchar(50) NOT NULL', |
| | | 'component' => 'varchar(100) NOT NULL', |
| | | 'method' => 'varchar(100) DEFAULT NULL', |
| | | 'page_url' => 'varchar(255) DEFAULT NULL', |
| | | 'message' => 'text NOT NULL', |
| | | 'context' => 'JSON', |
| | | 'severity' => 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'', |
| | | 'user_id' => $table->getUserIDType().' DEFAULT NULL', |
| | | 'user_was_logged_in' => 'tinyint(1) NOT NULL', |
| | | 'source' => 'ENUM(\'frontend\', \'backend\') NOT NULL', |
| | | 'created_at' => 'timestamp DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | '`created_at` (`created_at`)', |
| | | '`component_severity_date` (`component`, `severity`, `created_at`)', |
| | | '`error_type_date` (`error_type`, `created_at`)', |
| | | '`severity_date` (`severity`, `created_at`)' |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->table = $table; |
| | | } |
| | | public function registerAdminAction():void |
| | | { |
| | | $admin = JVB()->admin(); |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | class FavouritesManager |
| | | { |
| | | private Cache $cache; |
| | | |
| | | protected CustomTable $favourites; |
| | | protected CustomTable $lists; |
| | | protected CustomTable $listItems; |
| | | protected CustomTable $listShares; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | } |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | $this->defineFavouriteTable(); |
| | | $this->defineListTable(); |
| | | $this->defineListItemsTable(); |
| | | $this->defineListSharesTable(); |
| | | } |
| | | private function defineFavouriteTable():void |
| | | { |
| | | $table = CustomTable::for('favourites'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'type' => 'varchar(50) NOT NULL', //As in, post type/user/taxonomy |
| | | 'target_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'notes' => 'text DEFAULT NULL', |
| | | 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`unique_favourite` (`user_id`, `type`, `target_id`)'], |
| | | '`user_type` (`user_id`, `type`)', |
| | | '`target_type` (`target_id`, `type`)' |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}favourites_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->favourites = $table; |
| | | } |
| | | private function defineListTable():void |
| | | { |
| | | $table = CustomTable::for('favourites_lists'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'name' => 'varchar(255) NOT NULL', |
| | | 'description'=> 'text', |
| | | 'notes' => 'JSON DEFAULT (\'{}\')', |
| | | 'created_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | '`user_lists` (`user_id`)' |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}favourites_list_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->lists = $table; |
| | | } |
| | | private function defineListItemsTable():void |
| | | { |
| | | $table = CustomTable::for('favourites_list_items'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'list_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'item_type' => 'varchar(50) NOT NULL', |
| | | 'target_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'favourite_id'=> 'bigint(20) unsigned NOT NULL', |
| | | 'notes' => 'JSON DEFAULT (\'{}\')', |
| | | 'added_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`unique_list_item` (`list_id`, `item_type`, `target_id`)'], |
| | | '`list_items` (`list_id`)', |
| | | '`favourite_id` (`favourite_id`)' |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}favourites_list_items_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}favourite_list_items` FOREIGN KEY (`list_id`) |
| | | REFERENCES `{$this->lists->getFullTableName()}` (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}list_favourite` FOREIGN KEY (`favourite_id`) |
| | | REFERENCES `{$this->favourites->getFullTableName()}` (`id`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->listItems = $table; |
| | | } |
| | | private function defineListSharesTable():void |
| | | { |
| | | $table = CustomTable::for('favourites_list_shares'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'list_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'email' => 'varchar(255) NOT NULL', |
| | | 'permission' => "ENUM('view', 'edit') NOT NULL DEFAULT 'view'", |
| | | 'status' => "ENUM('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending'", |
| | | 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`unique_share_user` (`list_id`, `user_id`)'], |
| | | ['key'=> 'UNIQUE', 'value' => '`unique_share_email` (`list_id`, `email`'], |
| | | '`list_shares` (`list_id`)', |
| | | '`list_user` (`list_id`, `user_id`)', |
| | | '`status_index` (`status`)' |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}favourites_share_list` FOREIGN KEY (`list_id`) |
| | | REFERENCES `{$this->lists->getFullTableName()}` (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}favourites_share_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->listShares = $table; |
| | | } |
| | | public function toggleFavourite(bool $favourite, int $user_id, int $target_id, string $type):bool |
| | | { |
| | | if (!get_userdata($user_id)) { |
| | | error_log('[FAVOURITES]::toggleFavourite Invalid user '.$user_id); |
| | | return false; |
| | | } |
| | | $registrar = Registrar::getInstance($type); |
| | | if (!$registrar) { |
| | | error_log('[FAVOURITES]::toggleFavourite Invalid type: '.print_r($type, true)); |
| | | return false; |
| | | } |
| | | $type = jvbCheckBase($type); |
| | | $registerType = $registrar->getType(); |
| | | $test = false; |
| | | switch ($registerType) { |
| | | case 'post': |
| | | $test = get_post($target_id); |
| | | break; |
| | | case 'term': |
| | | $test = get_term($target_id, $type); |
| | | break; |
| | | case 'user': |
| | | $test = get_userdata($target_id); |
| | | break; |
| | | } |
| | | if (!$test || is_wp_error($test)) { |
| | | error_log('[FAVOURITES]::toggleFavourite Could not find target: '.$target_id); |
| | | return false; |
| | | } |
| | | |
| | | $owner = null; |
| | | if ($registrar->getType() === 'post') { |
| | | $post = get_post($target_id); |
| | | $owner = $post->post_author; |
| | | } elseif ($registrar->hasFeature('is_ownable')) { |
| | | $term = get_term($target_id, jvbCheckBase($type)); |
| | | $owner = jvbGetTermOwners($target_id); |
| | | } |
| | | |
| | | if ($favourite) { |
| | | $result = $this->favourites->findOrCreate( |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'target_id' => $target_id, |
| | | 'type' => $type |
| | | ], |
| | | ); |
| | | |
| | | if ($owner) { |
| | | JVB()->notification()->addNotification( |
| | | $owner, |
| | | 'new_favourite', |
| | | $user_id, |
| | | '', |
| | | $target_id, |
| | | $type |
| | | ); |
| | | } |
| | | |
| | | |
| | | } else { |
| | | $result = $this->favourites->delete([ |
| | | 'user_id' => $user_id, |
| | | 'target_id' => $target_id, |
| | | 'type' => $type |
| | | ]); |
| | | |
| | | if (in_array($registerType, ['term', 'user'])) { |
| | | JVB()->notification()->deleteUserPreference($user_id, $target_id, $type); |
| | | } |
| | | if ($owner) { |
| | | JVB()->notification()->removeNotification( |
| | | $owner, |
| | | [ |
| | | 'from_user' => $user_id, |
| | | 'target_id' => $target_id, |
| | | 'target_type' => $type |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | return (bool)$result; |
| | | } |
| | | |
| | | public function favourite(int $user_id, int $target_id, string $type):bool |
| | | { |
| | | return $this->toggleFavourite(true, $user_id, $target_id, $type); |
| | | } |
| | | public function unfavourite(int $user_id, int $target_id, string $type):bool |
| | | { |
| | | return $this->toggleFavourite(false, $user_id, $target_id, $type); |
| | | } |
| | | |
| | | public function addNoteFromTarget(int $user_id, int $target_id, string $type, string $note):bool |
| | | { |
| | | $favourite = $this->favourites->get([ |
| | | 'user_id' => $user_id, |
| | | 'target_id' => $target_id, |
| | | 'type' => $type |
| | | ]); |
| | | if (!$favourite) { |
| | | return false; |
| | | } |
| | | return $this->addNote($user_id, $favourite->id, $note); |
| | | } |
| | | public function addNote(int $user_id, int $favourite_id, string $note):bool |
| | | { |
| | | $favourite = $this->favourites->get(['id' => $favourite_id]); |
| | | if (!$favourite) { |
| | | return false; |
| | | } |
| | | if ($favourite->user_id !== $user_id) { |
| | | return false; |
| | | } |
| | | return (bool) $this->favourites->update([ |
| | | 'note' => sanitize_textarea_field($note) |
| | | ], [ |
| | | 'favourite_id' => $favourite_id |
| | | ]); |
| | | } |
| | | public function deleteNote(int $user_id, int $favourite_id):bool |
| | | { |
| | | if (!$this->userCanEditFavourite($user_id, ['id' => $favourite_id])) { |
| | | return false; |
| | | } |
| | | return (bool) $this->favourites->update([ |
| | | 'note' => '', |
| | | ], [ |
| | | 'favourite_id' => $favourite_id |
| | | ]); |
| | | } |
| | | |
| | | |
| | | public function addListItemNote(int $user_id, int $listItemId, string|array $note):bool |
| | | { |
| | | if (!$this->userCanEditListItem($user_id, $listItemId)) { |
| | | return false; |
| | | } |
| | | $listItem = $this->listItems->get(['id' => $listItemId]); |
| | | $notes = json_decode($listItem->notes, true); |
| | | |
| | | $notes = $this->addNoteToArray($notes, $user_id, $note); |
| | | |
| | | return $this->listItems->update(['id' => $listItemId], ['notes' => json_encode($notes)]); |
| | | } |
| | | public function removeListItemNote(int $user_id, int $listItemId, string $noteID):bool |
| | | { |
| | | if (!$this->userCanEditListItem($user_id, $listItemId)) { |
| | | return false; |
| | | } |
| | | $listItem = $this->listItems->get(['id' => $listItemId]); |
| | | $notes = json_decode($listItem->notes, true); |
| | | $index = array_search($noteID, array_column($notes, 'id')); |
| | | if ($index === false) { |
| | | return false; |
| | | } |
| | | $note = $notes[$index]; |
| | | if ($user_id !== $note['user_id']) { |
| | | return false; |
| | | } |
| | | |
| | | $notes = $this->removeNoteFromArray($notes, $user_id, $noteID); |
| | | |
| | | return $this->listItems->update(['id' => $listItemId], ['notes' => json_encode($notes)]); |
| | | } |
| | | |
| | | public function addListNote(int $user_id, int $listId, string $note):bool |
| | | { |
| | | if (!$this->userCanEditList($user_id, $listId)) { |
| | | return false; |
| | | } |
| | | $list = $this->listItems->get(['id' => $listId]); |
| | | $notes = json_decode($list->notes, true); |
| | | |
| | | $notes = $this->addNoteToArray($notes, $user_id, $note); |
| | | |
| | | return $this->listItems->update(['id' => $listId], ['notes' => json_encode($notes)]); |
| | | } |
| | | |
| | | public function removeListNote(int $user_id, int $listId, string $noteID):bool |
| | | { |
| | | if (!$this->userCanEditList($user_id, $listId)) { |
| | | return false; |
| | | } |
| | | $list = $this->lists->get(['id' => $listId]); |
| | | $notes = json_decode($list->notes, true); |
| | | $index = array_search($noteID, array_column($notes, 'id')); |
| | | if ($index === false) { |
| | | return false; |
| | | } |
| | | $note = $notes[$index]; |
| | | if ($user_id !== $note['user_id']) { |
| | | return false; |
| | | } |
| | | |
| | | $notes = $this->removeNoteFromArray($notes, $user_id, $noteID); |
| | | |
| | | return $this->lists->update(['id' => $listId], ['notes' => json_encode($notes)]); |
| | | } |
| | | |
| | | public function shareList(int $user_id, int $listId, bool $viewOnly = true, ?int $shareWithUser = null, ?array $newUser = null):bool |
| | | { |
| | | $list = $this->lists->get(['id' => $listId]); |
| | | if (!$list) { |
| | | return false; |
| | | } |
| | | if ($list->user_id !== $user_id) { |
| | | return false; |
| | | } |
| | | |
| | | if (!$shareWithUser && (empty($newUser) || !array_key_exists('email', $newUser) || !array_key_exists('name', $newUser))) { |
| | | return false; |
| | | } |
| | | |
| | | $args = []; |
| | | if ($shareWithUser) { |
| | | $args['user_id'] = $shareWithUser; |
| | | JVB()->notification()->addNotification($shareWithUser, 'list_shared', $user_id, '', $listId); |
| | | } elseif ($newUser) { |
| | | $email = sanitize_email($newUser['email']); |
| | | $name = sanitize_text_field($newUser['name']); |
| | | $args['email'] = $email; |
| | | //TODO: Magic Link setup |
| | | } |
| | | if (empty($args)) { |
| | | return false; |
| | | } |
| | | |
| | | return (bool) $this->listShares->findOrCreate(array_merge($args, ['list_id' => $listId, 'permission' => $viewOnly ? 'view' : 'edit'])); |
| | | } |
| | | public function unshareList(int $user_id, int $listId, int|string $userIDorEmail):bool |
| | | { |
| | | $list = $this->lists->get(['id' => $listId]); |
| | | if (!$list) { |
| | | return false; |
| | | } |
| | | if ($list->user_id !== $user_id) { |
| | | return false; |
| | | } |
| | | |
| | | $where = [ |
| | | 'list_id' => $listId |
| | | ]; |
| | | if (is_int($userIDorEmail)) { |
| | | $where['user_id'] = $userIDorEmail; |
| | | |
| | | $notification = JVB()->notification()->table(); |
| | | $n = $notification->get([ |
| | | 'for_user' => $userIDorEmail, |
| | | 'from_user' => $user_id, |
| | | 'type' => 'list_shared', |
| | | 'target_id' => $listId, |
| | | ]); |
| | | if ($n) { |
| | | $notification->delete([ |
| | | 'for_user' => $userIDorEmail, |
| | | 'from_user' => $user_id, |
| | | 'type' => 'list_shared', |
| | | 'target_id' => $listId, |
| | | ]); |
| | | } |
| | | } else if (is_email($userIDorEmail)) { |
| | | $where['email'] = sanitize_email($userIDorEmail); |
| | | } else { |
| | | return false; |
| | | } |
| | | |
| | | return $this->listShares->update($where, ['status' => 'revoked']); |
| | | } |
| | | |
| | | public function acceptListShare(int $listId, int|string $userIDorEmail):bool |
| | | { |
| | | return $this->respondToShare(true, $listId, $userIDorEmail); |
| | | } |
| | | |
| | | public function rejectListShare(int $listId, int|string $userIDorEmail):bool |
| | | { |
| | | return $this->respondToShare(false, $listId, $userIDorEmail); |
| | | } |
| | | public function respondToShare(bool $accept, int $listId, int|string $userIDorEmail):bool |
| | | { |
| | | $args = [ |
| | | 'listId' => $listId |
| | | ]; |
| | | if (is_int($userIDorEmail)) { |
| | | $args['user_id'] = $userIDorEmail; |
| | | |
| | | $list = $this->lists->get(['id' => $listId]); |
| | | if ($list) { |
| | | $user = get_userdata($userIDorEmail); |
| | | |
| | | $message = $accept ? 'accepted' : 'rejected'; |
| | | $message = $user->first_name . ' ' . $message . ' your list share invitation.'; |
| | | JVB()->notification()->addNotification($list->user_id, 'list_share_status', $userIDorEmail, $message); |
| | | } |
| | | }else if (is_email($userIDorEmail)) { |
| | | $args['email'] = sanitize_email($userIDorEmail); |
| | | } else { |
| | | return false; |
| | | } |
| | | $share = $this->listShares->get($args); |
| | | if (!$share) { |
| | | return false; |
| | | } |
| | | |
| | | |
| | | return $this->listShares->update([ |
| | | 'status' => $accept ? 'accepted' : 'rejected', |
| | | ], $args); |
| | | } |
| | | /*************************************************************** |
| | | * UTILITY METHODS |
| | | **************************************************************/ |
| | | /** |
| | | * @param int $user_id |
| | | * @param array $args |
| | | * @return bool |
| | | */ |
| | | public function userCanEditFavourite(int $user_id, array $args):bool |
| | | { |
| | | if (array_key_exists('id', $args)) { |
| | | $where = [ |
| | | 'id' => $args['id'] |
| | | ]; |
| | | } else { |
| | | if (!array_key_exists('target_id', $args) |
| | | || !array_key_exists('type', $args)) { |
| | | error_log('[Favourites]::userCanEditFavourite missing required target_id or type'); |
| | | return false; |
| | | } |
| | | $where = [ |
| | | 'target_id' => absint($args['target_id']), |
| | | 'type' => $args['type'] |
| | | ]; |
| | | } |
| | | |
| | | $get = $this->favourites->get($where); |
| | | return !is_null($get) && $get->user_id === $user_id; |
| | | } |
| | | |
| | | public function userCanViewList(int $user_id, int $list_id):bool |
| | | { |
| | | $list = $this->lists->get(['id' => $list_id]); |
| | | if (!$list) { |
| | | return false; |
| | | } |
| | | if ($list->user_id === $user_id) { |
| | | return true; |
| | | } |
| | | $share = $this->listShares->get(['user_id' => $user_id, 'list_id' => $list_id]); |
| | | return !is_null($share); |
| | | } |
| | | public function userCanEditList(int $user_id, int $list_id):bool |
| | | { |
| | | $list = $this->lists->get(['id' => $list_id]); |
| | | if (!$list) { |
| | | return false; |
| | | } |
| | | if ($list->user_id === $user_id) { |
| | | return true; |
| | | } |
| | | $share = $this->listShares->get(['user_id' => $user_id, 'list_id' => $list_id]); |
| | | if (!$share) return false; |
| | | return $share->permission === 'edit'; |
| | | } |
| | | |
| | | public function userCanEditListItem(int $user_id, int $list_item_id):bool |
| | | { |
| | | $item = $this->listItems->get(['id' => $list_item_id]); |
| | | if (!$item) { |
| | | return false; |
| | | } |
| | | if ($item->user_id === $user_id) { |
| | | return true; |
| | | } |
| | | return $this->userCanEditList($user_id, $item->list_id); |
| | | } |
| | | |
| | | protected function addNoteToArray(array $notes, int $user_id, string|array $note):array |
| | | { |
| | | $updated = date('Y-m-d H:i:s'); |
| | | if (is_string($note)) { |
| | | //It is a new note |
| | | $notes[] = [ |
| | | 'id' => uniqid(), |
| | | 'note' => sanitize_textarea_field($note), |
| | | 'user_id' => $user_id, |
| | | 'updated' => $updated |
| | | ]; |
| | | } else { |
| | | $index = array_search($note['id'], array_column($notes, 'id')); |
| | | if ($index === false) { |
| | | //Shouldn't happen, but just in case: add it to the array |
| | | $notes[] = [ |
| | | 'id' => sanitize_text_field($note['id']), |
| | | 'note' => sanitize_textarea_field($note['note']), |
| | | 'user_id' => $user_id, |
| | | 'updated' => $updated |
| | | ]; |
| | | } else { |
| | | $notes[$index]['note'] = sanitize_textarea_field($note['note']); |
| | | $notes[$index]['updated'] = $updated; |
| | | } |
| | | } |
| | | |
| | | return $notes; |
| | | } |
| | | |
| | | protected function removeNoteFromArray(array $notes, int $user_id, string $noteID):array |
| | | { |
| | | $index = array_search($noteID, array_column($notes, 'id')); |
| | | if ($index === false) { |
| | | return $notes; |
| | | } |
| | | $note = $notes[$index]; |
| | | if ($user_id !== $note['user_id']) { |
| | | return $notes; |
| | | } |
| | | |
| | | unset($notes[$index]); |
| | | return array_values($notes); //reindexed array |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Forum Manager |
| | | * TODO |
| | | * Handles a forum functionality |
| | | */ |
| | | class ForumManager |
| | | { |
| | | protected CustomTable $relationships; |
| | | protected CustomTable $forum; |
| | | protected KarmaManager $karma; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | $this->registerHooks(); |
| | | } |
| | | public function registerHooks():void |
| | | { |
| | | add_action('jvbDefineRegistrar', [$this, 'registerForum']); |
| | | add_action('jvbDefineRegistrarFields', [$this, 'registerForumFields']); |
| | | add_action('plugins_loaded', [$this, 'registerForum'], 1); |
| | | add_action('plugins_loaded', [$this, 'registerForum'], 2); |
| | | } |
| | | public function registerForum():void |
| | | { |
| | | Registrar::forPost('jvbforum', 'Post', 'Posts') |
| | | ->setIcon('note') |
| | | ->make([ |
| | | 'public' => false, |
| | | 'taxonomies' => [ |
| | | 'topic' |
| | | ] |
| | | ]); |
| | | |
| | | Registrar::forTerm('jvbtopic', 'Topic', 'Topics') |
| | | ->setIcon('folder') |
| | | ->make([ |
| | | 'hierarchical' => true, |
| | | 'for' => ['jvbforum'] |
| | | ]); |
| | | } |
| | | public function registerForumFields():void |
| | | { |
| | | $forum = Registrar::getInstance('jvbforum'); |
| | | $fields = $forum->fields(); |
| | | |
| | | $topic = Registrar::getInstance('jvbtopic'); |
| | | $topicFields = $topic->fields(); |
| | | } |
| | | |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | $this->definedRelationshipsTable(); |
| | | $this->karma = KarmaManager::for('forum', 'post'); |
| | | } |
| | | protected function definedRelationshipsTable():void |
| | | { |
| | | $table = CustomTable::for('forum_relationship'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'term_id' => "{$table->getTermIDType()} NOT NULL", |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'profile_id'=> "{$table->getPostIDType()} NOT NULL", |
| | | 'forum_count' => 'int(10) unsigned NOT NULL', |
| | | 'last_post_date'=> 'datetime DEFAULT NULL', |
| | | 'updated_at' => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`term_user` (`term_id`, `user_id`)'], |
| | | '`term_id` (`term_id`)', |
| | | '`user_id` (`user_id`)', |
| | | '`profile_id` (`profile_id`)', |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}fr_term_forum_term` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$table->getTermTable()}` (`term_id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}fr_term_forum_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}fr_term_news_profile` FOREIGN KEY (`profile_id`) |
| | | REFERENCES `{$table->getPostTable()}` (`ID`) ON DELETE CASCADE", |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->relationships = $table; |
| | | } |
| | | } |
| | |
| | | protected Cache $cache; |
| | | public function __construct() |
| | | { |
| | | $this->defineTable(); |
| | | if (!isset($this->table)) { |
| | | return; |
| | | } |
| | | $this->setInviteConfig(); |
| | | $this->cache = Cache::for('invitations'); |
| | | $this->table = CustomTable::for('invitations'); |
| | | add_action('init', [$this, 'registerInvitationExecutors'], 5); |
| | | |
| | | add_action('user_register', [$this, 'checkInvitation']); |
| | |
| | | add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | } |
| | | |
| | | public function defineTable():void |
| | | { |
| | | $terms = Registrar::getFeatured('invitable', 'term'); |
| | | $roles = Features::forMembership()->get('can_invite'); |
| | | if (empty($terms) && empty($roles)) { |
| | | return; |
| | | } |
| | | |
| | | $table = CustomTable::for('invitations'); |
| | | $columns = [ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'name' => 'varchar(255) NOT NULL', |
| | | 'email' => 'varchar(255) NOT NULL', |
| | | 'invitation_token' => 'varchar(255) NOT NULL', |
| | | 'invited_role' => 'varchar(50) NOT NULL COMMENT \'Role being invited to\'', |
| | | 'status' => 'ENUM(\'pending\',\'accepted\',\'rejected\',\'expired\',\'revoked\') DEFAULT \'pending\'', |
| | | 'inviters' => 'JSON NOT NULL COMMENT \'Array of {user_id, invited_at}\'', |
| | | 'new_user_id' => $table->getUserIDType().' DEFAULT NULL', |
| | | 'expires_at' => 'datetime NOT NULL', |
| | | 'accepted_at' => 'datetime DEFAULT NULL', |
| | | 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]; |
| | | |
| | | foreach ($terms as $tax) { |
| | | $columns["to_{$tax}"] = $table->getTermIDType().' DEFAULT NULL'; |
| | | } |
| | | $table->setColumns($columns); |
| | | |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE KEY', 'value' => '`unique_email_role` (`email`, `invited_role`)'], |
| | | '`token_lookup` (`invitation_token`)', |
| | | '`status_expiry` (`status`, `expires_at`)', |
| | | '`role_status` (`invited_role`, `status`)', |
| | | '`email_status` (`email`, `status`)' |
| | | ]); |
| | | $constraints = []; |
| | | $base = BASE; |
| | | global $wpdb; |
| | | foreach ($terms as $tax) { |
| | | $constraints[] = "CONSTRAINT `{$base}invitations_{$tax}_fk` FOREIGN KEY (`to_{$tax}`) REFERENCES `{$wpdb->terms}` (`term_id`) |
| | | ON DELETE SET NULL"; |
| | | } |
| | | $constraints[] = "CONSTRAINT `{$base}invitations_user_fk` FOREIGN KEY (`new_user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) |
| | | ON DELETE SET NULL"; |
| | | |
| | | $table->setConstraints($constraints); |
| | | |
| | | $table->defineTable(); |
| | | $this->table = $table; |
| | | } |
| | | |
| | | protected function setInviteConfig():void |
| | | { |
| | | $this->inviteConfig = get_option(BASE.'invitation_config', [ |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use WP_Error; |
| | | use WP_Post; |
| | | use WP_Term; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Response Manager |
| | | * |
| | | * Handles a responses |
| | | * |
| | | */ |
| | | class KarmaManager |
| | | { |
| | | protected static array $instances; |
| | | protected string $key; |
| | | protected string $references; |
| | | protected CustomTable $table; |
| | | protected static Cache $cache; |
| | | |
| | | private function __construct(string $key, ?string $references = null) |
| | | { |
| | | $this->key = $key; |
| | | if (is_null($references)) { |
| | | $references = match($key){ |
| | | 'post' => 'post', |
| | | 'term' => 'term', |
| | | 'user' => 'user', |
| | | default => false |
| | | }; |
| | | if (!$references) { |
| | | error_log('[KarmaManager]::__construct No references set, and no default found. Could not create instance'); |
| | | unset(self::$instances[$key]); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | $this->references = $references; |
| | | $test = $this->defineTable(); |
| | | if (!$test) { |
| | | unset(self::$instances[$key]); |
| | | } |
| | | |
| | | // $this->cache = Cache::for($this->references.'_karma'); |
| | | // switch ($this->references) { |
| | | // case 'post': |
| | | // $this->cache->connect('post'); |
| | | // break; |
| | | // case 'term': |
| | | // $this->cache->connect('term'); |
| | | // break; |
| | | // case 'user': |
| | | // $this->cache->connect('user'); |
| | | // break; |
| | | // } |
| | | if (!isset(self::$cache)) { |
| | | self::$cache = Cache::for('user_karma')->connect('user'); |
| | | } |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | } |
| | | |
| | | public static function getInstance(string $key):KarmaManager|false |
| | | { |
| | | if (!isset(self::$instances[$key])) { |
| | | return false; |
| | | } |
| | | return self::$instances[$key]; |
| | | } |
| | | |
| | | public static function for(string $key, ?string $references = null): KarmaManager |
| | | { |
| | | $key = sanitize_key($key); |
| | | if (!isset(self::$instances[$key])) { |
| | | self::$instances[$key] = new KarmaManager($key, $references); |
| | | } |
| | | |
| | | return self::$instances[$key]; |
| | | } |
| | | protected function getReferenceTable(CustomTable $table):array |
| | | { |
| | | switch ($this->references) { |
| | | case 'post': |
| | | case 'posts': |
| | | return [ |
| | | $table->getPostIDType(), |
| | | $table->getPostTable(), |
| | | 'ID' |
| | | ]; |
| | | case 'term': |
| | | case 'terms': |
| | | return [ |
| | | $table->getTermIDType(), |
| | | $table->getTermTable(), |
| | | 'term_id' |
| | | ]; |
| | | case 'user': |
| | | case 'users': |
| | | return [ |
| | | $table->getUserIDType(), |
| | | $table->getUserTable(), |
| | | 'ID' |
| | | ]; |
| | | } |
| | | //If we get here, it is a custom table. |
| | | $custom = CustomTable::getInstance($this->references); |
| | | if (!$custom) { |
| | | return [false,false,false]; |
| | | } |
| | | return [ |
| | | 'bigint(20)', |
| | | $custom->getFullTableName(), |
| | | 'id' |
| | | ]; |
| | | } |
| | | protected function defineTable():bool |
| | | { |
| | | $table = CustomTable::for('karma_'.$this->key); |
| | | [$type, $table, $column] = $this->getReferenceTable($table); |
| | | if (!$type) { |
| | | error_log('[KarmaManager]::defineTable Attempted to build reference for invalid table: '.$this->references); |
| | | CustomTable::destroyInstance('karma_'.$this->key); |
| | | return false; |
| | | } |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'item_id' => "{$type} NOT NULL", |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'content' => 'varchar(255) NOT NULL', |
| | | 'vote' => "ENUM('up,'down') NOT NULL", |
| | | 'created_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]); |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => '`user_item` (`user_id`, `item_id`, `content`)'], |
| | | '`item_vote` (`item_id`,`content`,`vote`)', |
| | | '`item_id` (`item_id`)', |
| | | '`user_id` (`user_id`)' |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}kt_{$type}_item_id` FOREIGN KEY (`item_id`) |
| | | REFERENCES `{$table}` (`{$column}`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}kt_{$type}_user_id` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->table = $table; |
| | | return true; |
| | | } |
| | | |
| | | protected function response(?bool $success = null, string $message =''):array |
| | | { |
| | | $response = [ |
| | | 'success' => is_null($success) ? 'partial' : $success, |
| | | ]; |
| | | if (!empty($message)) { |
| | | $response['message'] = $message; |
| | | } |
| | | return $response; |
| | | } |
| | | |
| | | protected function getItem(int $itemID, string $content):object|false |
| | | { |
| | | $based = jvbCheckBase($content); |
| | | return match($this->references) { |
| | | 'post' => get_post($itemID), |
| | | 'term' => get_term($itemID, $based), |
| | | 'user' => get_userdata($itemID), |
| | | 'response' => JVB()->responses()->response->get(['id' => $itemID])??false, |
| | | default => false //TODO if we do have custom tables, we'll have to set that up here |
| | | }; |
| | | } |
| | | |
| | | public function vote(int $userID, int $itemID, string $content, ?bool $vote = null):array |
| | | { |
| | | |
| | | $content = jvbNoBase($content); |
| | | $item = $this->getItem($itemID, $content); |
| | | |
| | | if (!$item || is_wp_error($item)) { |
| | | return $this->response(false, 'Could not find target'); |
| | | } |
| | | |
| | | $message = ''; |
| | | //removing record |
| | | if (is_null($vote)){ |
| | | $success = $this->table->delete([ |
| | | 'user_id' => $userID, |
| | | 'item_id' => $itemID, |
| | | 'content' => $content |
| | | ]); |
| | | if ($success) { |
| | | $message = 'Vote removed successfully'; |
| | | } |
| | | } else { |
| | | |
| | | $success = $this->table->findOrCreate( |
| | | [ |
| | | 'user_id' => $userID, |
| | | 'item_id' => $itemID, |
| | | 'content' => $content |
| | | ], |
| | | [ |
| | | 'vote' => $vote ? 'up' : 'down' |
| | | ] |
| | | ); |
| | | if (!$success) { |
| | | $message = 'Could not cast your vote'; |
| | | } else { |
| | | $message = 'Vote cast successfully'; |
| | | } |
| | | } |
| | | self::$cache->forget($userID); |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'karmic', |
| | | 0, |
| | | [ |
| | | 'items' => [ |
| | | 'itemID' => $itemID, |
| | | 'content' => $content, |
| | | ], |
| | | 'type' => $this->references, |
| | | ], |
| | | [ |
| | | 'chunk_key' => 'items', |
| | | ] |
| | | ); |
| | | return $this->response((bool) $success, $message); |
| | | } |
| | | |
| | | public function getCount(int $itemID, string $content):array |
| | | { |
| | | |
| | | $upvotes = count($this->table->pluck( |
| | | 'id', |
| | | [ |
| | | 'item_id' => $itemID, |
| | | 'content' => $content, |
| | | 'vote' => 'up' |
| | | ])); |
| | | $downvotes = count($this->table->pluck( |
| | | 'id', |
| | | [ |
| | | 'item_id' => $itemID, |
| | | 'content' => $content, |
| | | 'vote' => 'down' |
| | | ] |
| | | )); |
| | | return [ |
| | | 'upvotes' => $upvotes, |
| | | 'downvotes' => $downvotes, |
| | | 'karmic_score' => $upvotes - $downvotes |
| | | ]; |
| | | } |
| | | |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | | { |
| | | if ($operation->type !== 'karmic') { |
| | | return $result; |
| | | } |
| | | if ($data['type'] !== $this->references) { |
| | | return $result; |
| | | } |
| | | $results = []; |
| | | foreach ($data['items'] as $item) { |
| | | [$upvotes, $downvotes, $karma] = $this->getCount($item['itemID'], $item['content']); |
| | | try { |
| | | switch ($this->references) { |
| | | case 'post': |
| | | update_post_meta($item['itemID'], BASE.'upvotes', $upvotes); |
| | | update_post_meta($item['itemID'], BASE.'downvotes', $downvotes); |
| | | update_post_meta($item['itemID'], BASE.'karma', $karma); |
| | | break; |
| | | case 'term': |
| | | update_term_meta($item['itemID'], BASE.'upvotes', $upvotes); |
| | | update_term_meta($item['itemID'], BASE.'downvotes', $downvotes); |
| | | update_term_meta($item['itemID'], BASE.'karma', $karma); |
| | | break; |
| | | case 'user': |
| | | update_user_meta($item['itemID'], BASE.'upvotes', $upvotes); |
| | | update_user_meta($item['itemID'], BASE.'downvotes', $downvotes); |
| | | update_user_meta($item['itemID'], BASE.'karma', $karma); |
| | | break; |
| | | case 'response': |
| | | JVB()->responses()->response->update( |
| | | [ |
| | | 'upvotes' => $upvotes, |
| | | 'downvotes' => $downvotes, |
| | | 'karma' => $karma |
| | | ], |
| | | [ |
| | | 'id' => $item['itemID'] |
| | | ] |
| | | ); |
| | | break; |
| | | } |
| | | $results[$item['itemID']] = true; |
| | | } catch (Exception $e) { |
| | | $results[$item['itemID']] = false; |
| | | } |
| | | |
| | | } |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => $results |
| | | ]; |
| | | } |
| | | |
| | | public static function getUserVotes(int $userID):array |
| | | { |
| | | return self::$cache->remember( |
| | | $userID, |
| | | function() use($userID) { |
| | | $votes = []; |
| | | foreach (self::$instances as $instance) { |
| | | $results = $instance->table->getMany([ |
| | | 'where' => [ |
| | | 'user_id' => $userID |
| | | ] |
| | | ]); |
| | | foreach ($results as $result) { |
| | | if (!array_key_exists($result['content'], $votes)) { |
| | | $votes[$result['content']] = []; |
| | | } |
| | | $votes[$result['content']][$result['item_id']] = $result['vote']; |
| | | } |
| | | } |
| | | return $votes; |
| | | } |
| | | ); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Error; |
| | | use Exception; |
| | | use WP_Post; |
| | | use WP_User; |
| | | use JVBase\managers\Notifications\Content; |
| | | use JVBase\managers\Notifications\EmailDigests; |
| | | use JVBase\managers\Notifications\Notifications; |
| | | use JVBase\managers\Notifications\Preferences; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | //TODO: Ensure this works with the constants setup |
| | | /** |
| | | * NotificationManager - Centralized notification system for edmonton.ink |
| | | * |
| | |
| | | */ |
| | | class NotificationManager |
| | | { |
| | | protected Cache $userCache; //the individual notifications |
| | | protected Cache $contentCache; //the 'shared' notifications on new content that has been created |
| | | protected Cache $artistsCache; |
| | | protected Cache $favouritesCache; |
| | | protected Cache $followerCache; |
| | | protected string $campaign; |
| | | protected string $table = BASE.'notifications'; |
| | | protected string $contentTable = BASE.'notifications_content'; |
| | | protected string $seenTable = BASE.'notifications_user_seen'; |
| | | protected string $preferencesTable = BASE.'notification_preferences'; |
| | | protected array $notification_types = [ |
| | | // System notifications |
| | | 'new_favourite' => [ |
| | | 'icon' => 'heart', |
| | | 'priority' => 'low', |
| | | 'requires_action' => false, |
| | | 'audience' => 'content_owner', |
| | | 'email_digest' => true |
| | | ], |
| | | 'artist_approved' => [ |
| | | 'icon' => 'check', |
| | | 'priority' => 'high', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'artist_joined' => [ |
| | | 'icon' => 'artist', |
| | | 'priority' => 'low', |
| | | 'requires_action' => false, |
| | | 'email_digest' => false, |
| | | ], |
| | | 'artist_rejected' => [ |
| | | 'icon' => 'x', |
| | | 'priority' => 'high', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'artist_invitation' => [ |
| | | 'icon' => 'invite', |
| | | 'priority' => 'high', |
| | | 'requires_action' => true, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'artist_request' => [ |
| | | 'icon' => 'artist', |
| | | 'priority' => 'high', |
| | | 'requires_action' => true, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true, |
| | | ], |
| | | 'shop_accepted' => [ |
| | | 'icon' => 'shop', |
| | | 'priority' => 'high', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true, |
| | | ], |
| | | 'shop_rejected' => [ |
| | | 'icon' => 'shop', |
| | | 'priority' => 'high', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true, |
| | | ], |
| | | 'new_term' => [ |
| | | 'icon' => 'style', |
| | | 'priority' => 'medium', |
| | | 'requires_action' => true, |
| | | 'audience' => 'artists', |
| | | 'email_digest' => false |
| | | ], |
| | | 'term_approved' => [ |
| | | 'icon' => 'check', |
| | | 'priority' => 'high', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'term_rejected' => [ |
| | | 'icon' => 'x', |
| | | 'priority' => 'medium', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'list_shared' => [ |
| | | 'icon' => 'share', |
| | | 'priority' => 'medium', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'list_share_accepted' => [ |
| | | 'icon' => 'check', |
| | | 'priority' => 'low', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'list_share_revoked' => [ |
| | | 'icon' => 'close', |
| | | 'priority' => 'medium', |
| | | 'requires_action' => false, |
| | | 'audience' => 'single', |
| | | 'email_digest' => true |
| | | ], |
| | | 'system_message' => [ |
| | | 'icon' => 'info', |
| | | 'priority' => 'high', |
| | | 'requires_action' => false, |
| | | 'audience' => 'all', |
| | | 'email_digest' => false |
| | | ] |
| | | ]; |
| | | protected Notifications $notifications; |
| | | protected Content $content; |
| | | protected EmailDigests $digest; |
| | | protected Preferences $preferences; |
| | | |
| | | /** |
| | | * Constructor |
| | | */ |
| | | public function __construct() |
| | | { |
| | | $this->userCache = Cache::for('userNotifications', WEEK_IN_SECONDS); |
| | | $this->contentCache = Cache::for('contentNotifications', WEEK_IN_SECONDS)->connect('post', true)->connect('taxonomy', true); |
| | | $this->artistsCache = Cache::for('artist', WEEK_IN_SECONDS)->connect('post'); |
| | | $this->favouritesCache = Cache::for('favouritedUsers', WEEK_IN_SECONDS)->connect('favourites'); |
| | | $this->followerCache = Cache::for('totalFollowers', WEEK_IN_SECONDS)->connect('favourites'); |
| | | public function __construct() |
| | | { |
| | | $this->notifications = new Notifications(); |
| | | $this->content = new Content(); |
| | | $this->digest = new EmailDigests(); |
| | | $this->preferences = new Preferences(); |
| | | } |
| | | |
| | | // Add filter for bulk operation handling |
| | | add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3); |
| | | |
| | | add_action(BASE . 'cleanup_notifications', [$this, 'cleanupOldNotifications']); |
| | | |
| | | // Track content creation for notifications |
| | | add_action('save_post', [ $this, 'trackContentCreation' ], 10, 3); |
| | | add_action('saved_term', [ $this, 'track_shop_creation' ], 10, 3); |
| | | |
| | | // Generate content summaries when users log in |
| | | add_action('wp_login', [ $this, 'generateUserContentNotifications' ], 10, 2); |
| | | |
| | | // Register digest cron jobs |
| | | $this->registerCron(); |
| | | } |
| | | |
| | | /** |
| | | * Registers the digest cron jobs |
| | | * @return void |
| | | */ |
| | | protected function registerCron():void |
| | | { |
| | | add_action(BASE . 'notification_digest_daily', [ $this, 'runDailyDigests' ]); |
| | | add_action(BASE . 'notification_digest_weekly', [ $this, 'runWeeklyDigests' ]); |
| | | add_action(BASE . 'notification_digest_monthly', [ $this, 'runMonthlyDigests' ]); |
| | | } |
| | | |
| | | /************************************************************ |
| | | * Basic Notification Methods |
| | | ************************************************************/ |
| | | /** |
| | | * @return array |
| | | */ |
| | | public function getNotificationTypes():array |
| | | { |
| | | return $this->notification_types; |
| | | } |
| | | /** |
| | | * Add a new notification |
| | | * |
| | | * @param int|array $user_ids Recipient user ID(s) |
| | | * @param string $type Notification type |
| | | * @param int|null $action_user_id User who performed the action (optional) |
| | | * @param string $message Notification message (optional) |
| | | * @param int|null $target_id Related target ID (optional) |
| | | * @param string|null $target_type Related target type (optional) |
| | | * @param array|null $context Additional contextual data (optional) |
| | | * |
| | | * @return bool|WP_Error Success or error |
| | | */ |
| | | public function addNotification( |
| | | mixed $user_ids, |
| | | string $type, |
| | | int|null $action_user_id = null, |
| | | string $message = '', |
| | | int|null $target_id = null, |
| | | string|null $target_type = null, |
| | | array|null $context = null |
| | | ):bool|WP_Error { |
| | | // Validate notification type |
| | | if (!isset($this->notification_types[$type])) { |
| | | $this->logError("Invalid notification type: $type", [ |
| | | 'user_ids' => is_array($user_ids) ? implode(',', $user_ids) : $user_ids, |
| | | 'message' => $message |
| | | ], 'warning'); |
| | | return new WP_Error('invalid_type', 'Invalid notification type'); |
| | | } |
| | | |
| | | // Handle single user or array of users |
| | | $user_ids = is_array($user_ids) ? $user_ids : [$user_ids]; |
| | | |
| | | if (empty($user_ids)) { |
| | | return false; |
| | | } |
| | | // Queue the bulk notification operation |
| | | $queue = JVB()->queue(); |
| | | $queue->queueOperation( |
| | | 'addNotification', |
| | | $action_user_id, |
| | | [ |
| | | 'user_ids' => $user_ids, |
| | | 'type' => $type, |
| | | 'action_user_id' => $action_user_id, |
| | | 'message' => $message, |
| | | 'target_id' => $target_id, |
| | | 'target_type' => $target_type, |
| | | 'context' => $context |
| | | ], |
| | | [ |
| | | 'count' => count($user_ids), |
| | | 'priority' => 'normal', |
| | | 'chunk_key' => 'user_ids', |
| | | 'chunk_size'=> 50, |
| | | 'operation_id' => 'notifications' . uniqid() |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Process notification operation |
| | | * |
| | | * @param int|array $user_ids Recipient user ID(s) |
| | | * @param string $type Notification type |
| | | * @param int|null $action_user_id User who performed the action (optional) |
| | | * @param string $message Notification message (optional) |
| | | * @param int|null $target_id Related target ID (optional) |
| | | * @param string|null $target_type Related target type (optional) |
| | | * @param array|null $context Additional contextual data (optional) |
| | | * |
| | | * @return bool|WP_Error Success or error |
| | | */ |
| | | public function processNotification( |
| | | int|array $user_ids, |
| | | string $type, |
| | | int|null $action_user_id = null, |
| | | string $message = '', |
| | | int|null $target_id = null, |
| | | string|null $target_type = null, |
| | | array|null $context = null |
| | | ):bool|WP_Error { |
| | | $config = $this->notification_types[$type]; |
| | | $notifications = []; |
| | | $errors = []; |
| | | |
| | | $user_ids = is_array($user_ids) ? $user_ids : [$user_ids]; |
| | | |
| | | global $wpdb; |
| | | foreach ($user_ids as $user_id) { |
| | | // Skip invalid users |
| | | if (!$user_id || $user_id <= 0) { |
| | | $errors[] = "Invalid user ID: $user_id"; |
| | | continue; |
| | | } |
| | | |
| | | // Skip sending notifications to self if action_user_id == user_id |
| | | if ($action_user_id && $action_user_id == $user_id && !($config['notify_self'] ?? false)) { |
| | | continue; |
| | | } |
| | | |
| | | try { |
| | | // Prepare context data |
| | | $context_json = null; |
| | | if (!empty($context)) { |
| | | $context_json = json_encode($context); |
| | | } |
| | | |
| | | // Insert new notification |
| | | $result = $wpdb->insert( |
| | | $wpdb->prefix . $this->table, |
| | | [ |
| | | 'owner_id' => $user_id, |
| | | 'action_user_id' => $action_user_id, |
| | | 'type' => $type, |
| | | 'status' => 'unread', |
| | | 'message' => $message, |
| | | 'priority' => $config['priority'] ?? 'normal', |
| | | 'target_id' => $target_id, |
| | | 'target_type' => $target_type, |
| | | 'context' => $context_json, |
| | | 'requires_action' => !empty($config['requires_action']) ? 1 : 0, |
| | | 'created_at' => current_time('mysql'), |
| | | 'updated_at' => current_time('mysql') |
| | | ] |
| | | ); |
| | | |
| | | if ($result) { |
| | | $notification_id = $wpdb->insert_id; |
| | | $notifications[] = $notification_id; |
| | | |
| | | // Clear cache for this user |
| | | $this->clearNotificationCache($user_id); |
| | | } else { |
| | | $errors[] = "Database error for user $user_id: " . $wpdb->last_error; |
| | | $this->logError("Failed to insert notification", [ |
| | | 'user_id' => $user_id, |
| | | 'type' => $type, |
| | | 'db_error' => $wpdb->last_error |
| | | ]); |
| | | } |
| | | } catch (Exception $e) { |
| | | $errors[] = "Exception for user $user_id: " . $e->getMessage(); |
| | | $this->logError("Error adding notification", [ |
| | | 'user_id' => $user_id, |
| | | 'type' => $type, |
| | | 'message' => $message, |
| | | 'error' => $e->getMessage() |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | // Return results |
| | | if (!empty($notifications) && !empty($errors)) { |
| | | $this->logError("Some notifications failed", [ |
| | | 'successful' => count($notifications), |
| | | 'errors' => $errors |
| | | ], 'warning'); |
| | | } |
| | | |
| | | if (empty($notifications) && !empty($errors)) { |
| | | return new WP_Error('notification_failed', 'All notifications failed', [ |
| | | 'errors' => $errors |
| | | ]); |
| | | } |
| | | |
| | | return !empty($notifications); |
| | | } |
| | | |
| | | /** |
| | | * Wrapper for notifying verified artists |
| | | * |
| | | * @param string $type Notification type |
| | | * @param int|null $action_user_id User who performed the action (optional) |
| | | * @param string $message Notification message (optional) |
| | | * @param int|null $target_id Related target ID (optional) |
| | | * @param string|null $target_type Related target type (optional) |
| | | * @param array|null $context Additional contextual data (optional) |
| | | * |
| | | * @return bool|WP_Error Success or error |
| | | */ |
| | | public function notifyVerifiedArtists(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error |
| | | { |
| | | $artists = $this->getVerified('artist'); |
| | | return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context); |
| | | } |
| | | /** |
| | | * Wrapper for notifying verified partners |
| | | * |
| | | * @param string $type Notification type |
| | | * @param int|null $action_user_id User who performed the action (optional) |
| | | * @param string $message Notification message (optional) |
| | | * @param int|null $target_id Related target ID (optional) |
| | | * @param string|null $target_type Related target type (optional) |
| | | * @param array|null $context Additional contextual data (optional) |
| | | * |
| | | * @return bool|WP_Error Success or error |
| | | */ |
| | | public function notifyVerifiedPartners(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error |
| | | { |
| | | $artists = $this->getVerified('partner'); |
| | | return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context); |
| | | } |
| | | /** |
| | | * Wrapper for notifying verified partners |
| | | * |
| | | * @param string $type Notification type |
| | | * @param int|null $action_user_id User who performed the action (optional) |
| | | * @param string $message Notification message (optional) |
| | | * @param int|null $target_id Related target ID (optional) |
| | | * @param string|null $target_type Related target type (optional) |
| | | * @param array|null $context Additional contextual data (optional) |
| | | * |
| | | * @return bool|WP_Error Success or error |
| | | */ |
| | | public function notifyEnthusiasts(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error |
| | | { |
| | | $artists = $this->getUserIDs('enthusiast'); |
| | | return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context); |
| | | } |
| | | /** |
| | | * Wrapper for notifying verified partners |
| | | * |
| | | * @param string $type Notification type |
| | | * @param int|null $action_user_id User who performed the action (optional) |
| | | * @param string $message Notification message (optional) |
| | | * @param int|null $target_id Related target ID (optional) |
| | | * @param string|null $target_type Related target type (optional) |
| | | * @param array|null $context Additional contextual data (optional) |
| | | * |
| | | * @return bool|WP_Error Success or error |
| | | */ |
| | | public function notifyEveryone(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error |
| | | { |
| | | $artists = $this->getUserIDs(Registrar::getRegistered('user')); |
| | | return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context); |
| | | } |
| | | |
| | | |
| | | |
| | | /************************************************************ |
| | | * Content Notification Methods |
| | | ************************************************************/ |
| | | |
| | | /** |
| | | * Track content creation or updates for notification purposes |
| | | * |
| | | * @param int $post_id Post ID |
| | | * @param WP_Post $post Post object |
| | | * @param bool $update Whether this is an update |
| | | * @return void |
| | | */ |
| | | public function trackContentCreation(int $post_id, WP_POST $post, bool $update):void |
| | | { |
| | | // SAFETY: Skip attachments and other non-content post types |
| | | if (in_array($post->post_type, jvbIgnoredPostTypes())) { |
| | | return; |
| | | } |
| | | // Skip if not a published post |
| | | if ($post->post_status !== 'publish') { |
| | | return; |
| | | } |
| | | |
| | | // Check if this is a relevant content type |
| | | $content_types = array_map(function($type) {return jvbCheckBase($type->getSlug()); }, Registrar::getFeatured('show_feed', 'post')); |
| | | if (!in_array($post->post_type, $content_types)) { |
| | | return; |
| | | } |
| | | |
| | | // Check if the artist has any followers before tracking |
| | | $follower_count = $this->getFollowerCount($post->post_author); |
| | | if ($follower_count === 0) { |
| | | return; // No need to track if nobody follows this artist |
| | | } |
| | | |
| | | // Get the clean content type for storing |
| | | $content_type = str_replace(BASE, '', $post->post_type); |
| | | |
| | | // Update the artist's content notification records |
| | | $this->updateContentNotificationRecord($post->post_author, $content_type, $post_id, $update); |
| | | } |
| | | |
| | | /** |
| | | * Update content notification record for an artist |
| | | * |
| | | * @param int $artist_id Artist user ID |
| | | * @param string $content_type Content type (tattoo, artwork, etc) |
| | | * @param int $content_id Content post ID |
| | | * @param bool $is_update Whether this is an update to existing content |
| | | * |
| | | * @return bool Success or failure |
| | | */ |
| | | protected function updateContentNotificationRecord(int $artist_id, string $content_type, int $content_id, bool $is_update = false):bool |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . $this->contentTable; |
| | | |
| | | // Start transaction |
| | | $wpdb->query('START TRANSACTION'); |
| | | $success = false; |
| | | |
| | | try { |
| | | // Get today's date |
| | | $today = date('Y-m-d'); |
| | | $frequency_updates = []; |
| | | |
| | | // Update records for each frequency |
| | | foreach (['daily', 'weekly', 'monthly'] as $frequency) { |
| | | // Find or create a record for this artist, date and frequency |
| | | $record = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT * FROM {$table} |
| | | WHERE user_id = %d AND date = %s AND frequency = %s", |
| | | $artist_id, |
| | | $today, |
| | | $frequency |
| | | )); |
| | | |
| | | if ($record) { |
| | | $frequency_updates[$frequency] = $this->updateExistingContentRecord( |
| | | $record, |
| | | $content_type, |
| | | $content_id, |
| | | $is_update |
| | | ); |
| | | } else { |
| | | $frequency_updates[$frequency] = $this->createNewContentRecord( |
| | | $artist_id, |
| | | $frequency, |
| | | $content_type, |
| | | $content_id, |
| | | $is_update |
| | | ); |
| | | } |
| | | } |
| | | |
| | | // Check if all updates were successful |
| | | $success = !in_array(false, $frequency_updates, true); |
| | | |
| | | if ($success) { |
| | | $wpdb->query('COMMIT'); |
| | | return true; |
| | | } else { |
| | | // If any update failed, roll back |
| | | $wpdb->query('ROLLBACK'); |
| | | $this->logError("Failed to update content notification records", [ |
| | | 'artist_id' => $artist_id, |
| | | 'content_type' => $content_type, |
| | | 'content_id' => $content_id, |
| | | 'updates' => $frequency_updates |
| | | ]); |
| | | return false; |
| | | } |
| | | } catch (Exception $e) { |
| | | $wpdb->query('ROLLBACK'); |
| | | $this->logError("Exception in content notification update", [ |
| | | 'artist_id' => $artist_id, |
| | | 'content_type' => $content_type, |
| | | 'content_id' => $content_id, |
| | | 'error' => $e->getMessage() |
| | | ]); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update an existing content notification record |
| | | * |
| | | * @param object $record Existing database record |
| | | * @param string $content_type Content type |
| | | * @param int $content_id Content ID |
| | | * @param bool $is_update Whether this is an update |
| | | * |
| | | * @return bool Success or failure |
| | | */ |
| | | protected function updateExistingContentRecord(object $record, string $content_type, int $content_id, bool $is_update):bool |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . $this->contentTable; |
| | | |
| | | // Decode existing JSON data |
| | | $new_items = json_decode($record->new_items, true) ?: []; |
| | | $updated_items = json_decode($record->updated_items, true) ?: []; |
| | | |
| | | // Initialize arrays if not present |
| | | if (!isset($new_items[BASE.$content_type])) { |
| | | $new_items[BASE.$content_type] = []; |
| | | } |
| | | if (!isset($updated_items[BASE.$content_type])) { |
| | | $updated_items[BASE.$content_type] = []; |
| | | } |
| | | |
| | | // Add ID to appropriate array |
| | | if ($is_update) { |
| | | // Add to updated array if not already there and not in new items |
| | | if (!in_array($content_id, $updated_items[BASE.$content_type]) && |
| | | !in_array($content_id, $new_items[BASE.$content_type])) { |
| | | $updated_items[BASE.$content_type][] = $content_id; |
| | | } |
| | | } else { |
| | | // Add to new array if not already there |
| | | if (!in_array($content_id, $new_items[BASE.$content_type])) { |
| | | $new_items[BASE.$content_type][] = $content_id; |
| | | } |
| | | } |
| | | |
| | | // Prepare update data |
| | | $update_data = [ |
| | | 'new_items' => json_encode($new_items), |
| | | 'updated_items' => json_encode($updated_items), |
| | | 'updated_at' => current_time('mysql') |
| | | ]; |
| | | |
| | | // Update appropriate count column |
| | | $count_column = "{$content_type}_count"; |
| | | if (property_exists($record, $count_column)) { |
| | | $update_data[ $count_column ] = $record->$count_column + ( $is_update ? 0 : 1 ); |
| | | } |
| | | |
| | | // Update total count |
| | | $update_data['total_items'] = $record->total_items + ( $is_update ? 0 : 1 ); |
| | | |
| | | // Update the record |
| | | return $wpdb->update( |
| | | $table, |
| | | $update_data, |
| | | [ 'id' => $record->id ] |
| | | ) !== false; |
| | | } |
| | | |
| | | /** |
| | | * Create a new content notification record |
| | | * |
| | | * @param int $artist_id Artist user ID |
| | | * @param string $frequency Frequency (daily, weekly, monthly) |
| | | * @param string $content_type Content type |
| | | * @param int $content_id Content ID |
| | | * @param bool $is_update Whether this is an update |
| | | * |
| | | * @return bool Success or failure |
| | | */ |
| | | protected function createNewContentRecord(int $artist_id, string $frequency, string $content_type, int $content_id, bool $is_update):bool |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . $this->contentTable; |
| | | |
| | | // Initialize arrays for new and updated items |
| | | $new_items = []; |
| | | $updated_items = []; |
| | | |
| | | if ($is_update) { |
| | | $updated_items[BASE.$content_type] = [ $content_id ]; |
| | | } else { |
| | | $new_items[BASE.$content_type] = [ $content_id ]; |
| | | } |
| | | |
| | | // Prepare insert data |
| | | $insert_data = [ |
| | | 'user_id' => $artist_id, |
| | | 'date' => date('Y-m-d'), |
| | | 'frequency' => $frequency, |
| | | 'total_items' => 1, |
| | | 'new_items' => json_encode($new_items), |
| | | 'updated_items' => json_encode($updated_items), |
| | | 'created_at' => current_time('mysql'), |
| | | 'updated_at' => current_time('mysql') |
| | | ]; |
| | | |
| | | // Set count column |
| | | $insert_data["{$content_type}_count"] = $is_update ? 0 : 1; |
| | | |
| | | // Insert the record |
| | | return $wpdb->insert($table, $insert_data) !== false; |
| | | } |
| | | |
| | | /** |
| | | * Generate content notifications for a user upon login |
| | | * |
| | | * @param string $username Username |
| | | * @param WP_User $user User object |
| | | * @return void |
| | | */ |
| | | public function generateUserContentNotifications(string $username, WP_User $user):void |
| | | { |
| | | $this->createContentSeenRecords($user->ID); |
| | | } |
| | | |
| | | /** |
| | | * Create content seen records for a user's followed artists |
| | | * |
| | | * @param int $user_id User ID |
| | | * |
| | | * @return int Number of notifications created |
| | | */ |
| | | protected function createContentSeenRecords(int $user_id):int |
| | | { |
| | | global $wpdb; |
| | | |
| | | try { |
| | | // Get followed artists |
| | | $followed_artists = $this->getFollowedArtists($user_id); |
| | | if (empty($followed_artists)) { |
| | | return 0; |
| | | } |
| | | |
| | | // Get the last time notifications were checked |
| | | $last_check = get_user_meta($user_id, BASE . 'last_content_check', true); |
| | | $since_date = $last_check ? date('Y-m-d', strtotime($last_check)) : date('Y-m-d', strtotime('-7 days')); |
| | | |
| | | // Create placeholders for SQL query |
| | | $placeholders = implode(',', array_fill(0, count($followed_artists), '%d')); |
| | | |
| | | // Get content notifications since last check |
| | | $content_records = $wpdb->get_results( |
| | | $wpdb->prepare( |
| | | "SELECT * FROM {$wpdb->prefix}{$this->contentTable} |
| | | WHERE user_id IN ($placeholders) |
| | | AND date >= %s |
| | | AND total_items > 0", |
| | | array_merge($followed_artists, [ $since_date ]) |
| | | ) |
| | | ); |
| | | |
| | | if (empty($content_records)) { |
| | | return 0; |
| | | } |
| | | |
| | | // Add user seen records for each content notification |
| | | $count = 0; |
| | | $user_seen_table = $wpdb->prefix . $this->seenTable; |
| | | |
| | | foreach ($content_records as $record) { |
| | | // Check if record already exists |
| | | $exists = $wpdb->get_var( |
| | | $wpdb->prepare( |
| | | "SELECT id FROM {$user_seen_table} |
| | | WHERE user_id = %d AND content_notification_id = %d", |
| | | $user_id, |
| | | $record->id |
| | | ) |
| | | ); |
| | | |
| | | if (!$exists) { |
| | | // Create new seen record |
| | | $wpdb->insert( |
| | | $user_seen_table, |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'content_notification_id' => $record->id, |
| | | 'status' => 'unread', |
| | | 'created_at' => current_time('mysql') |
| | | ] |
| | | ); |
| | | $count ++; |
| | | } |
| | | } |
| | | |
| | | // Update last check time |
| | | update_user_meta($user_id, BASE . 'last_content_check', current_time('mysql')); |
| | | |
| | | // Clear cache |
| | | $this->clearNotificationCache($user_id); |
| | | |
| | | return $count; |
| | | } catch (Exception $e) { |
| | | $this->logError("Error creating content seen records: " . $e->getMessage(), [ |
| | | 'user_id' => $user_id |
| | | ]); |
| | | |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | |
| | | /************************************************************ |
| | | * Notification Digest Methods |
| | | ************************************************************/ |
| | | |
| | | /** |
| | | * Process daily notification digests |
| | | */ |
| | | public function runDailyDigests():void |
| | | { |
| | | $this->campaign = 'daily_digest_' . date('Y-m-d'); |
| | | $this->processDigest('daily'); |
| | | } |
| | | |
| | | /** |
| | | * Process weekly notification digests |
| | | */ |
| | | public function runWeeklyDigests():void |
| | | { |
| | | $this->campaign = 'weekly_digest_' . date('Y-m-d'); |
| | | $this->processDigest('weekly'); |
| | | } |
| | | |
| | | /** |
| | | * Process monthly notification digests |
| | | */ |
| | | public function runMonthlyDigests():void |
| | | { |
| | | $this->campaign = 'monthly_digest_' . date('Y-m-d'); |
| | | $this->processDigest('monthly'); |
| | | } |
| | | |
| | | /** |
| | | * Process digests for a specific frequency |
| | | * |
| | | * @param string $frequency Digest frequency (daily, weekly, monthly) |
| | | * |
| | | * @return bool Success or failure |
| | | */ |
| | | protected function processDigest(string $frequency):bool |
| | | { |
| | | // Get users with this digest frequency preference |
| | | $users = $this->getUsersForDigest($frequency); |
| | | |
| | | if (empty($users)) { |
| | | return true; // No users to process |
| | | } |
| | | |
| | | // Queue digest generation |
| | | $queue = JVB()->queue(); |
| | | $queue->queueOperation( |
| | | 'email_notification_digest', |
| | | 0, // System user |
| | | [ |
| | | 'frequency' => $frequency, |
| | | 'users' => $users |
| | | ], |
| | | [ |
| | | 'count' => count($users), |
| | | 'chunk_key' => 'users', |
| | | 'chunk_size' => 20, |
| | | 'priority' => 'normal', |
| | | 'operation_id' => 'notification_digest_' . date('Y_m_d') |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Get users who have subscribed to a specific digest frequency |
| | | * |
| | | * @param string $frequency Digest frequency |
| | | * |
| | | * @return array Array of user IDs |
| | | */ |
| | | protected function getUsersForDigest(string $frequency):array |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . $this->preferencesTable; |
| | | |
| | | // Get users with this frequency setting |
| | | $users = $wpdb->get_col( |
| | | $wpdb->prepare( |
| | | "SELECT DISTINCT user_id FROM $table |
| | | WHERE frequency = %s", |
| | | $frequency |
| | | ) |
| | | ); |
| | | |
| | | return $users ?: []; |
| | | } |
| | | |
| | | /** |
| | | * Generate and send a digest email for a user |
| | | * |
| | | * @param int $user_id User ID |
| | | * @param string $frequency Digest frequency |
| | | * |
| | | * @return bool Success status |
| | | */ |
| | | public function generateUserDigest(int $user_id, string $frequency):bool |
| | | { |
| | | try { |
| | | $user = get_userdata($user_id); |
| | | if (!$user || !is_email($user->user_email)) { |
| | | return false; |
| | | } |
| | | |
| | | // Get date range based on frequency |
| | | $today = date('Y-m-d'); |
| | | $since_date = $this->getSinceDate($frequency, $today); |
| | | |
| | | // Get regular notifications |
| | | $notifications = $this->getDigestNotifications($user_id); |
| | | |
| | | // Get content updates from followed artists |
| | | $content_updates = $this->getContentUpdatesForDigest($user_id, $since_date); |
| | | |
| | | if (empty($notifications) && empty($content_updates)) { |
| | | return true; // Nothing to send |
| | | } |
| | | |
| | | // Generate and send email |
| | | $sent = $this->sendDigestEmail($user, $frequency, $notifications, $content_updates); |
| | | |
| | | if ($sent) { |
| | | // Update preferences last_sent timestamp |
| | | $this->updateDigestTimestamps($user_id, $frequency); |
| | | |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | } catch (Exception $e) { |
| | | $this->logError("Error generating digest for user $user_id: " . $e->getMessage()); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get notifications for a digest |
| | | * |
| | | * @param int $user_id User ID |
| | | * |
| | | * @return array Notification objects |
| | | */ |
| | | protected function getDigestNotifications(int $user_id):array |
| | | { |
| | | global $wpdb; |
| | | |
| | | // Get unread, non-emailed notifications |
| | | return $wpdb->get_results( |
| | | $wpdb->prepare( |
| | | "SELECT * FROM {$wpdb->prefix}{$this->table} |
| | | WHERE user_id = %d |
| | | AND status = 'unread' |
| | | AND emailed_at IS NULL |
| | | ORDER BY priority DESC, created_at DESC", |
| | | $user_id |
| | | ) |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Get content updates for digest |
| | | * |
| | | * @param int $user_id User ID |
| | | * @param string $since_date Date string (YYYY-MM-DD) |
| | | * |
| | | * @return array Content update records |
| | | */ |
| | | protected function getContentUpdatesForDigest(int $user_id, string $since_date):array |
| | | { |
| | | // Get followed artists |
| | | $followed_artists = $this->getFollowedArtists($user_id); |
| | | if (empty($followed_artists)) { |
| | | return []; |
| | | } |
| | | |
| | | global $wpdb; |
| | | |
| | | // Create placeholders for SQL IN clause |
| | | $placeholders = implode(',', array_fill(0, count($followed_artists), '%d')); |
| | | |
| | | // Get content records since the date |
| | | return $wpdb->get_results( |
| | | $wpdb->prepare( |
| | | "SELECT * FROM {$wpdb->prefix}{$this->contentTable} |
| | | WHERE user_id IN ($placeholders) |
| | | AND date >= %s |
| | | AND total_items > 0 |
| | | ORDER BY date DESC", |
| | | array_merge($followed_artists, [ $since_date ]) |
| | | ) |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Get since date for a frequency |
| | | * |
| | | * @param string $frequency Digest frequency |
| | | * @param string $today Today's date |
| | | * |
| | | * @return string Date string (YYYY-MM-DD) |
| | | */ |
| | | protected function getSinceDate(string $frequency, string $today):string |
| | | { |
| | | switch ($frequency) { |
| | | case 'daily': |
| | | return date('Y-m-d', strtotime('-1 day', strtotime($today))); |
| | | case 'weekly': |
| | | return date('Y-m-d', strtotime('-1 week', strtotime($today))); |
| | | case 'monthly': |
| | | return date('Y-m-d', strtotime('-1 month', strtotime($today))); |
| | | default: |
| | | return ''; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Send a digest email |
| | | * |
| | | * @param WP_User $user User object |
| | | * @param string $frequency Digest frequency |
| | | * @param array $notifications Regular notifications |
| | | * @param array $content_updates Content update records |
| | | * |
| | | * @return bool Whether the email was sent |
| | | */ |
| | | protected function sendDigestEmail(WP_User $user, string $frequency, array $notifications, array $content_updates):bool |
| | | { |
| | | // Generate subject based on frequency |
| | | $subjects = [ |
| | | 'daily' => [ |
| | | "[edmonton.ink] Your " . date('l') . " Ink Update ♡", |
| | | "[edmonton.ink] Fresh Ink Alert - Your " . date('l') . " Digest", |
| | | "[edmonton.ink] What You Missed in Edmonton's Tattoo Scene Today" |
| | | ], |
| | | 'weekly' => [ |
| | | "[edmonton.ink] Your Weekly Roundup from edmonton.ink ♡", |
| | | "[edmonton.ink] This Week in Edmonton's Tattoo Scene", |
| | | "[edmonton.ink] Weekly Ink Update - Fresh From the Scene" |
| | | ], |
| | | 'monthly' => [ |
| | | "[edmonton.ink] Monthly Ink Roundup ♡", |
| | | "[edmonton.ink] Your Monthly Scene Report: " . date('F') . " Edition", |
| | | "[edmonton.ink] " . date('F') . " in Edmonton Tattoos" |
| | | ] |
| | | ]; |
| | | |
| | | // Randomly select a subject for variety |
| | | $subject_options = $subjects[ $frequency ] ?? [ "Your edmonton.ink Update" ]; |
| | | $subject = $subject_options[ array_rand($subject_options) ]; |
| | | |
| | | // Generate email content |
| | | $content = $this->generateDigestContent($user, $frequency, $notifications, $content_updates); |
| | | |
| | | // Add tracking pixel |
| | | $tracking_code = sprintf( |
| | | '<img src="%s/track-email.php?uid=%s&digest=%s" width="1" height="1" alt="" />', |
| | | site_url(), |
| | | base64_encode($user->ID), |
| | | $frequency |
| | | ); |
| | | $content .= $tracking_code; |
| | | |
| | | // Set header based on frequency |
| | | $header = match ($frequency) { |
| | | 'daily' => "TODAY'S INK DROP", |
| | | 'weekly' => "THIS WEEK'S SCENE REPORT", |
| | | 'monthly' => "MONTHLY INK ROUNDUP", |
| | | default => "YOUR INK UPDATES", |
| | | }; |
| | | |
| | | // Send the email |
| | | return JVB()->email()->sendEmail($user->user_email, $subject, $content, $header); |
| | | } |
| | | |
| | | /** |
| | | * Generate HTML content for digest email |
| | | * |
| | | * @param WP_User $user User object |
| | | * @param string $frequency Digest frequency |
| | | * @param array $notifications Regular notifications |
| | | * @param array $content_updates Content update records |
| | | * |
| | | * @return string HTML email content |
| | | */ |
| | | protected function generateDigestContent(WP_User $user, string $frequency, array $notifications, array $content_updates):string |
| | | /************************************************ |
| | | * PREFERENCES |
| | | ************************************************/ |
| | | public function getUsersByFrequency(string $frequency):array |
| | | { |
| | | $content = sprintf('<p>Hey %s,</p>', $user->first_name ?: $user->display_name); |
| | | |
| | | // Intro text based on frequency |
| | | switch ($frequency) { |
| | | case 'daily': |
| | | $content .= '<p>Here\'s what happened in Edmonton\'s tattoo scene today:</p>'; |
| | | break; |
| | | case 'weekly': |
| | | $content .= '<p>Here\'s what you missed in Edmonton\'s tattoo scene this week:</p>'; |
| | | break; |
| | | case 'monthly': |
| | | $content .= sprintf('<p>Here\'s your monthly roundup of what happened in %s in Edmonton\'s tattoo scene:</p>', date('F')); |
| | | break; |
| | | } |
| | | |
| | | // Process artist content updates - the most visually interesting part |
| | | $content .= $this->generateContentUpdatesSection($content_updates); |
| | | |
| | | // Process regular notifications |
| | | if (!empty($notifications)) { |
| | | $content .= $this->generateNotificationsSection($notifications); |
| | | } |
| | | |
| | | // Add footer content |
| | | $content .= JVB()->email()->divider(); |
| | | $settings_url = add_query_arg([ |
| | | 'utm_source' => 'email', |
| | | 'utm_medium' => 'digest', |
| | | 'utm_campaign' => $this->campaign |
| | | ], site_url('/dash/settings/')); |
| | | |
| | | $content .= sprintf( |
| | | '<p>You\'re receiving this %s digest because you follow artists on edmonton.ink. You can %s at any time.</p>', |
| | | $frequency, |
| | | '<a href="' . esc_url($settings_url) . '">adjust your notification settings</a>' |
| | | ); |
| | | |
| | | return $content; |
| | | return $this->preferences->getUsersByFrequency($frequency); |
| | | } |
| | | |
| | | /** |
| | | * Generate HTML section for content updates |
| | | * |
| | | * @param array $content_updates Content update records |
| | | * |
| | | * @return string HTML content |
| | | * TODO: This needs some work |
| | | */ |
| | | protected function generateContentUpdatesSection(array $content_updates):string |
| | | { |
| | | if (empty($content_updates)) { |
| | | return ''; |
| | | } |
| | | |
| | | $content = ''; |
| | | $cache = Cache::for('digest_content', HOUR_IN_SECONDS * 6); // Cache for 6 hours |
| | | |
| | | // Group updates by artist |
| | | $updates_by_artist = []; |
| | | foreach ($content_updates as $update) { |
| | | if (!isset($updates_by_artist[$update->user_id])) { |
| | | $updates_by_artist[ $update->user_id ] = []; |
| | | } |
| | | $updates_by_artist[ $update->user_id ][] = $update; |
| | | } |
| | | |
| | | // Process each artist's updates |
| | | foreach ($updates_by_artist as $artist_id => $updates) { |
| | | $artist_data = $this->getArtistData($artist_id); |
| | | if (!$artist_data) { |
| | | continue; |
| | | } |
| | | |
| | | // Combine all content updates from this artist |
| | | $combined_new_items = []; |
| | | |
| | | foreach ($updates as $update) { |
| | | $new_items = json_decode($update->new_items, true) ?: []; |
| | | |
| | | foreach ($new_items as $type => $ids) { |
| | | if (!isset($combined_new_items[ $type ])) { |
| | | $combined_new_items[ $type ] = []; |
| | | } |
| | | $combined_new_items[ $type ] = array_merge($combined_new_items[ $type ], $ids); |
| | | // Remove duplicates |
| | | $combined_new_items[ $type ] = array_unique($combined_new_items[ $type ]); |
| | | } |
| | | } |
| | | |
| | | // Skip if no content to show |
| | | if (empty($combined_new_items)) { |
| | | continue; |
| | | } |
| | | |
| | | // Add artist header |
| | | $content .= sprintf( |
| | | '<h3><a href="%s" class="text-link">%s</a></h3>', |
| | | esc_url(add_query_arg([ |
| | | 'utm_source' => 'email', |
| | | 'utm_medium' => 'digest', |
| | | 'utm_campaign' => $this->campaign |
| | | ], $artist_data['url'])), |
| | | esc_html($artist_data['display_name']) |
| | | ); |
| | | |
| | | // Process each content type |
| | | foreach ($combined_new_items as $type => $ids) { |
| | | if (empty($ids)) { |
| | | continue; |
| | | } |
| | | |
| | | $clean_type = str_replace(BASE, '', $type); |
| | | $type_label = $this->getContentTypeLabel($clean_type); |
| | | |
| | | // Add subheading for content type |
| | | $content .= sprintf('<h4>%s</h4>', esc_html($type_label)); |
| | | |
| | | // Display up to 3 items in a row |
| | | $display_ids = array_slice($ids, 0, 3); |
| | | $content .= '<table><tr>'; |
| | | |
| | | foreach ($display_ids as $item_id) { |
| | | // Get cached item data or fetch it |
| | | $cache_key = "digest_{$type}_{$item_id}"; |
| | | $item_data = $cache->get($cache_key); |
| | | |
| | | if ($item_data === false) { |
| | | $item_data = $this->getContentDetails($type, $item_id, $artist_id); |
| | | if ($item_data) { |
| | | $cache->set($cache_key, $item_data); |
| | | } |
| | | } |
| | | |
| | | if (!$item_data) { |
| | | continue; |
| | | } |
| | | |
| | | // Add item to email |
| | | $content .= '<td style="padding: 10px; width: 33.3%; vertical-align: top; text-align: center;">'; |
| | | $content .= '<div style="margin-bottom: 10px;">'; |
| | | $content .= sprintf( |
| | | '<a href="%s"><img src="%s" alt="%s" style="width: 100%%; max-width: 150px; height: auto; border-radius: 4px;"></a>', |
| | | esc_url(add_query_arg([ |
| | | 'utm_source' => 'email', |
| | | 'utm_medium' => 'digest', |
| | | 'utm_campaign' => $this->campaign |
| | | ], $item_data['url'])), |
| | | esc_url($item_data['image']), |
| | | esc_attr($item_data['title']) |
| | | ); |
| | | $content .= '</div>'; |
| | | $content .= sprintf('<p style="margin: 5px 0; font-weight: bold;">%s</p>', esc_html($item_data['title'])); |
| | | $content .= '</td>'; |
| | | } |
| | | |
| | | // Fill empty cells if needed |
| | | for ($i = count($display_ids); $i < 3; $i++) { |
| | | $content .= '<td style="width: 33.3%;"></td>'; |
| | | } |
| | | |
| | | $content .= '</tr></table>'; |
| | | |
| | | // Add "See all" link if there are more items |
| | | if (count($ids) > 3) { |
| | | $content .= sprintf( |
| | | '<p style="text-align: right;"><a href="%s" class="text-link">See all %d %s →</a></p>', |
| | | esc_url(add_query_arg([ |
| | | 'utm_source' => 'email', |
| | | 'utm_medium' => 'digest', |
| | | 'utm_campaign' => $this->campaign |
| | | ], site_url('/dash/feed/?type=' . $clean_type . '&artist=' . $artist_id))), |
| | | count($ids), |
| | | $this->pluralize($clean_type) |
| | | ); |
| | | } |
| | | } |
| | | |
| | | $content .= '<div class="divider"></div>'; |
| | | } |
| | | |
| | | return $content; |
| | | } |
| | | |
| | | /** |
| | | * Generate HTML section for regular notifications |
| | | * |
| | | * @param array $notifications Notification objects |
| | | * |
| | | * @return string HTML content |
| | | */ |
| | | protected function generateNotificationsSection(array $notifications):string |
| | | public function getUserSubscriptions(int $userID, string $frequency):array |
| | | { |
| | | if (empty($notifications)) { |
| | | return ''; |
| | | } |
| | | |
| | | $items = []; |
| | | |
| | | // Group notifications by type |
| | | $by_type = []; |
| | | foreach ($notifications as $notification) { |
| | | if (!isset($by_type[$notification->type])) { |
| | | $by_type[$notification->type] = []; |
| | | } |
| | | $by_type[$notification->type][] = $notification; |
| | | } |
| | | |
| | | // Process each type |
| | | foreach ($by_type as $type => $type_notifications) { |
| | | foreach ($type_notifications as $notification) { |
| | | $message = $notification->message; |
| | | if (empty($message)) { |
| | | $message = $this->generateNotificationMessage($notification); |
| | | } |
| | | |
| | | if (!empty($message)) { |
| | | $items[] = ['label' => '', 'value' => $message]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return JVB()->email()->table($items, 'Other Updates'); |
| | | return $this->preferences->getUserSubscriptions($userID, $frequency); |
| | | } |
| | | |
| | | /** |
| | | * Generate a message for a notification when none is provided |
| | | * |
| | | * @param object $notification Notification object |
| | | * |
| | | * @return string Formatted message |
| | | */ |
| | | protected function generateNotificationMessage(object $notification):string |
| | | { |
| | | switch ($notification->type) { |
| | | case 'new_favourite': |
| | | return 'Someone favourited your content'; |
| | | |
| | | case 'artist_approved': |
| | | return 'Your artist profile has been approved'; |
| | | |
| | | case 'artist_invitation': |
| | | return 'You have a new invitation to join a shop'; |
| | | |
| | | case 'new_term': |
| | | return 'New term suggestion requires your approval'; |
| | | |
| | | case 'term_approved': |
| | | return 'Your term suggestion was approved'; |
| | | |
| | | case 'term_rejected': |
| | | return 'Your term suggestion was not approved'; |
| | | |
| | | case 'list_shared': |
| | | return 'Someone shared a list with you'; |
| | | |
| | | default: |
| | | return 'You have a new notification'; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get content details for digest |
| | | * |
| | | * @param string $type Content type (with jvb_ prefix) |
| | | * @param int $content_id Content ID |
| | | * @param int $artist_id Artist ID |
| | | * |
| | | * @return array|false Content details or false if not found |
| | | */ |
| | | protected function getContentDetails(string $type, int $content_id, int $artist_id):array|false |
| | | { |
| | | // Build post type from content type |
| | | $post_type = $type; // Already has jvb_ prefix |
| | | |
| | | // Get the post |
| | | $post = get_post($content_id); |
| | | if (!$post || $post->post_type !== $post_type || $post->post_status !== 'publish') { |
| | | return false; |
| | | } |
| | | |
| | | // Get artist data |
| | | $artist_data = $this->getArtistData($artist_id); |
| | | if (!$artist_data) { |
| | | return false; |
| | | } |
| | | |
| | | // Get featured image if available |
| | | $image_url = get_the_post_thumbnail_url($content_id, 'medium'); |
| | | if (!$image_url) { |
| | | // Try meta fields for image |
| | | $meta_image_id = get_post_meta($content_id, BASE . 'image', true); |
| | | if ($meta_image_id !== '') { |
| | | $image_url = wp_get_attachment_image_url($meta_image_id, 'medium'); |
| | | } |
| | | } |
| | | |
| | | if (!$image_url) { |
| | | return false; // Skip items without images for digest |
| | | } |
| | | |
| | | return [ |
| | | 'id' => $content_id, |
| | | 'title' => $post->post_title, |
| | | 'url' => get_permalink($content_id), |
| | | 'image' => $image_url, |
| | | 'date' => $post->post_date, |
| | | 'artist_id' => $artist_id, |
| | | 'artist_name' => $artist_data['display_name'], |
| | | 'artist_url' => $artist_data['url'], |
| | | 'type' => str_replace(BASE, '', $type) |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Get human-readable label for content type |
| | | * |
| | | * @param string $type Content type (without jvb_ prefix) |
| | | * |
| | | * @return string Label |
| | | */ |
| | | protected function getContentTypeLabel(string $type):string |
| | | { |
| | | $labels = [ |
| | | 'tattoo' => 'New Tattoos', |
| | | 'artwork' => 'New Artwork', |
| | | 'piercing' => 'New Piercings', |
| | | 'event' => 'Upcoming Events', |
| | | 'news' => 'News & Updates', |
| | | 'offer' => 'Special Offers' |
| | | ]; |
| | | |
| | | return $labels[$type] ?? ucfirst($type . 's'); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Update digest timestamps for notification preferences |
| | | * |
| | | * @param int $user_id User ID |
| | | * @param string $frequency Digest frequency |
| | | * |
| | | * @return bool Success or failure |
| | | */ |
| | | protected function updateDigestTimestamps(int $user_id, string $frequency):bool |
| | | { |
| | | global $wpdb; |
| | | $table = $wpdb->prefix . $this->preferencesTable; |
| | | |
| | | $result = $wpdb->update( |
| | | $table, |
| | | [ |
| | | 'last_sent' => current_time('mysql') |
| | | ], |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'frequency' => $frequency |
| | | ] |
| | | ); |
| | | |
| | | return $result !== false; |
| | | } |
| | | |
| | | /************************************************************ |
| | | * Bulk Operation Handlers |
| | | ************************************************************/ |
| | | |
| | | /** |
| | | * Handle bulk operations for notifications |
| | | * |
| | | * @param WP_Error|array $result Default result |
| | | * @param object $operation Operation object |
| | | * @param array $data Current item |
| | | * |
| | | * @return WP_Error|array Operation result |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | | { |
| | | switch ($operation->type) { |
| | | case 'email_notification_digest': |
| | | return $this->handleDigestBatch($operation, $data); |
| | | |
| | | case 'addNotification': |
| | | return $this->handleAddNotificationOperation($operation, $data); |
| | | default: |
| | | return $result; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle digest batch operation |
| | | * |
| | | * @param object $operation Operation object |
| | | * @param array $data |
| | | * |
| | | * @return WP_Error|array|bool Operation result |
| | | */ |
| | | protected function handleDigestBatch(object $operation, array $data):WP_Error|array|bool |
| | | { |
| | | try { |
| | | |
| | | // Process one user at a time |
| | | $user_id = $data['users'][ $operation->progress_count ] ?? null; |
| | | |
| | | if (!$user_id) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Invalid user ID' |
| | | ]; |
| | | } |
| | | |
| | | $result = $this->generateUserDigest($user_id, $data['frequency']); |
| | | |
| | | return [ |
| | | 'success' => $result, |
| | | 'result' => $result ? 'Digest sent successfully' : 'Failed to send digest', |
| | | ]; |
| | | } catch (Exception $e) { |
| | | $this->logError("Error processing digest batch: " . $e->getMessage(), [ |
| | | 'operation_id' => $operation->id |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @param object $operation |
| | | * @param array $data |
| | | * |
| | | * @return WP_Error|array|bool |
| | | */ |
| | | protected function handleAddNotificationOperation(object $operation, array $data):WP_Error|array|bool |
| | | { |
| | | try { |
| | | $user_ids = $data['user_ids'] ?? []; |
| | | $type = $data['type'] ?? ''; |
| | | $message = $data['message'] ?? ''; |
| | | $target_id = $data['target_id'] ?? null; |
| | | $target_type = $data['target_type'] ?? null; |
| | | |
| | | if (empty($user_ids) || empty($type)) { |
| | | return new WP_Error('invalid_data', 'Missing required notification data'); |
| | | } |
| | | |
| | | foreach ($user_ids as $key => $user_id) { |
| | | if (!$this->checkUser($user_id)) { |
| | | unset($user_ids[$key]); |
| | | } |
| | | } |
| | | |
| | | // Add notification for this user |
| | | $result = $this->processNotification($user_ids, $type, $message, $target_id, $target_type); |
| | | |
| | | return [ |
| | | 'success' => !is_wp_error($result), |
| | | 'result' => is_wp_error($result) ? $result->get_error_message() : $result, |
| | | ]; |
| | | } catch (Exception $e) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * |
| | | * @return array|false|mixed |
| | | */ |
| | | protected function getArtistData(int $user_id):array|false |
| | | { |
| | | // Try to get from cache |
| | | $artist_id = get_user_meta($user_id, BASE . 'link', true); |
| | | if (!$artist_id || $artist_id === '') { |
| | | return false; |
| | | } |
| | | $cached = $this->artistsCache->get($artist_id); |
| | | |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | // Get basic artist data |
| | | $artist_post = get_post($artist_id); |
| | | if (!$artist_post) { |
| | | return false; |
| | | } |
| | | |
| | | $data = [ |
| | | 'id' => $artist_id, |
| | | 'user_id' => $user_id, |
| | | 'display_name' => get_the_title($artist_id), |
| | | 'first_name' => get_post_meta($artist_id, BASE . 'first_name', true), |
| | | 'url' => get_permalink($artist_id), |
| | | 'image' => get_post_thumbnail_id($artist_id) |
| | | ]; |
| | | |
| | | // Cache the result |
| | | $this->artistsCache->set($artist_id, $data); |
| | | |
| | | return $data; |
| | | } |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function getFollowedArtists(int $user_id):array |
| | | { |
| | | return $this->favouritesCache->remember( |
| | | $user_id, |
| | | function() use ($user_id) { |
| | | global $wpdb; |
| | | $favourites_table = $wpdb->prefix . BASE . 'favourites'; |
| | | |
| | | // Get artists this user has favourited |
| | | return $wpdb->get_col($wpdb->prepare( |
| | | "SELECT f.target_id |
| | | FROM {$favourites_table} f |
| | | JOIN {$wpdb->posts} p ON f.target_id = p.ID |
| | | WHERE f.user_id = %d |
| | | AND f.type = 'artist' |
| | | AND p.post_status = 'publish'", |
| | | $user_id |
| | | )); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * @param int $artist_id |
| | | * |
| | | * @return int |
| | | */ |
| | | protected function getFollowerCount(int $artist_id):int |
| | | { |
| | | return $this->followerCache->remember( |
| | | $artist_id, |
| | | function() use ($artist_id) { |
| | | global $wpdb; |
| | | $favourites_table = $wpdb->prefix . BASE . 'favourites'; |
| | | |
| | | return $wpdb->get_var($wpdb->prepare( |
| | | "SELECT COUNT(DISTINCT user_id) |
| | | FROM {$favourites_table} |
| | | WHERE target_id = %d AND type = 'artist'", |
| | | $artist_id |
| | | )); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * @param string $word |
| | | * |
| | | * @return string |
| | | */ |
| | | protected function pluralize(string $content):string |
| | | { |
| | | $registrar = Registrar::getInstance($content); |
| | | return ($registrar) ? $registrar->getPlural() |
| | | : str_replace('_', ' ', $content.'s'); |
| | | } |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * |
| | | * @return void |
| | | */ |
| | | protected function clearNotificationCache(int $user_id):void |
| | | { |
| | | $this->userCache->forget($user_id); |
| | | $this->contentCache->forget($user_id); |
| | | } |
| | | |
| | | /** |
| | | * Log errors with proper context using the error handler |
| | | * |
| | | * @param string $message Error message |
| | | * @param array $context Additional context data |
| | | * @param string $severity Error severity (debug, info, warning, error, critical) |
| | | * @return void |
| | | */ |
| | | protected function logError(string $message, array $context = [], string $severity = 'error'):void |
| | | { |
| | | try { |
| | | // Use the ErrorHandler through the JVB singleton |
| | | JVB()->error()->log( |
| | | 'notifications', // component |
| | | $message, |
| | | $context, |
| | | $severity |
| | | ); |
| | | } catch (Exception $e) { |
| | | // Fallback if error handler fails |
| | | error_log("NotificationManager Error: $message - " . json_encode($context)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @return void |
| | | */ |
| | | public function cleanupOldNotifications():void |
| | | { |
| | | global $wpdb; |
| | | $notifications_table = $wpdb->prefix . $this->table; |
| | | $seen_table = $wpdb->prefix . $this->seenTable; |
| | | |
| | | // Keep track of current time |
| | | $current_time = current_time('mysql'); |
| | | |
| | | // Delete regular notifications older than 3 months |
| | | $wpdb->query($wpdb->prepare( |
| | | "DELETE FROM {$notifications_table} |
| | | WHERE created_at < DATE_SUB(%s, INTERVAL 3 MONTH) |
| | | AND status IN ('read', 'actioned', 'dismissed')", |
| | | $current_time |
| | | )); |
| | | |
| | | // Delete dismissed content notifications older than 1 month |
| | | $wpdb->query($wpdb->prepare( |
| | | "DELETE FROM {$seen_table} |
| | | WHERE status = 'dismissed' |
| | | AND created_at < DATE_SUB(%s, INTERVAL 1 MONTH)", |
| | | $current_time |
| | | )); |
| | | |
| | | // Delete read content notifications older than 3 months |
| | | $wpdb->query($wpdb->prepare( |
| | | "DELETE FROM {$seen_table} |
| | | WHERE status = 'read' |
| | | AND created_at < DATE_SUB(%s, INTERVAL 3 MONTH)", |
| | | $current_time |
| | | )); |
| | | } |
| | | |
| | | /** |
| | | * @return array |
| | | */ |
| | | protected function getVerified(string|array $userRoles):array |
| | | { |
| | | $userRoles = $this->checkRoles($userRoles); |
| | | |
| | | if (empty($userRoles)) { |
| | | return []; |
| | | } |
| | | $cache = Cache::for('verifiedUsers', DAY_IN_SECONDS)->connect('user',true); |
| | | return $cache->remember( |
| | | 'verified', |
| | | function() use ($userRoles) { |
| | | return get_users([ |
| | | 'role' => $userRoles, |
| | | 'capability' => 'skip_moderation', |
| | | 'fields' => 'ID' |
| | | ]); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | protected function getUserIDs(array|string $roles):array |
| | | public function addUserPreference(int $userID, int $item_id, string $item_type, string $frequency):bool |
| | | { |
| | | $roles = $this->checkRoles($roles); |
| | | if (empty($roles)) { |
| | | return []; |
| | | } |
| | | $cache = Cache::for('everyone', DAY_IN_SECONDS)->connect('user', true); |
| | | return $cache->remember( |
| | | $cache->generateKey($roles), |
| | | function() use ($roles) { |
| | | return get_users([ |
| | | 'role' => $roles, |
| | | 'fields' => 'ID' |
| | | ]); |
| | | } |
| | | ); |
| | | return $this->preferences->addUserPreference($userID, $item_id, $item_type, $frequency); |
| | | } |
| | | |
| | | protected function checkRoles(string|array $roles):array |
| | | public function deleteUserPreference(int $userID, int $item_id, string $item_type):bool |
| | | { |
| | | if (!is_array($roles)) { |
| | | $roles = explode(',',$roles); |
| | | } |
| | | |
| | | return array_map(function ($r) { |
| | | return jvbCheckBase(trim($r)); |
| | | }, array_filter($roles, function ($r) { |
| | | return Registrar::getInstance(trim($r))!== false; |
| | | })); |
| | | return $this->preferences->deleteUserPreference($userID, $item_id, $item_type); |
| | | } |
| | | /************************************************ |
| | | * NOTIFICATIONS |
| | | ************************************************/ |
| | | public function notify(int|array $user_ids, string $notification_type, int $fromUser = 0, array $args = []):bool |
| | | { |
| | | return $this->notifications->notify($user_ids, $notification_type, $fromUser, $args); |
| | | } |
| | | public function unnotify(int|array $user_ids, string $notification_type, int $fromUser = 0, array $args = []):bool |
| | | { |
| | | return $this->notifications->unnotify($user_ids, $notification_type, $fromUser, $args); |
| | | } |
| | | public function getUserNotifications(int $user_id, array $args = []):array |
| | | { |
| | | return $this->notifications->getUserNotifications($user_id, $args); |
| | | } |
| | | |
| | | /** |
| | | * @param int $userID |
| | | * |
| | | * @return bool|mixed |
| | | */ |
| | | protected function checkUser(int $userID):bool |
| | | { |
| | | $cache = Cache::for('checked_users', DAY_IN_SECONDS)->connect('user', true); |
| | | return $cache->remember( |
| | | $userID, |
| | | function() use ($userID) { |
| | | return (bool)get_userdata($userID)?:null; |
| | | } |
| | | ); |
| | | } |
| | | public function markRead(int $userID, int|array $notification_id):bool |
| | | { |
| | | return $this->notifications->markRead($userID, $notification_id); |
| | | } |
| | | public function markDismissed(int $userID, int|array $notification_id):bool |
| | | { |
| | | return $this->notifications->markDismissed($userID, $notification_id); |
| | | } |
| | | public function markActioned(int $userID, int|array $notification_id, array $result = []):bool |
| | | { |
| | | return $this->notifications->markActioned($userID, $notification_id, $result); |
| | | } |
| | | |
| | | /************************************************ |
| | | * CONTENT NOTIFICATIONS |
| | | * These are pooled notifications of new content for: |
| | | * - a particular artist |
| | | * - a particular term |
| | | ************************************************/ |
| | | /************************************************ |
| | | * EMAIL DIGESTS |
| | | ************************************************/ |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\Notifications; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * ContentNotifications |
| | | * Gets notifications of |
| | | **/ |
| | | class Content |
| | | { |
| | | protected static Cache $cache; |
| | | public function __construct() |
| | | { |
| | | self::$cache = Cache::for('content_notifications')->connect('post'); |
| | | } |
| | | public static function getSinceDate(string $date):string |
| | | { |
| | | return match ($date) { |
| | | 'week' => '-1 week', |
| | | 'month' => '-1 month', |
| | | default => '-1 day', |
| | | }; |
| | | } |
| | | public static function forUser(int $userID, ?string $sinceDate = null, bool $modified = false):array |
| | | { |
| | | $date = self::getSinceDate($sinceDate); |
| | | |
| | | $posts = new WP_Query([ |
| | | 'posts_per_page' => -1, |
| | | 'post_status' => 'publish', |
| | | 'post_author' => $userID, |
| | | 'date_query' => [ |
| | | [ |
| | | 'after' => $date, |
| | | 'inclusive' => true, |
| | | 'column' => $modified ? 'post_modified' : 'post_date' |
| | | ] |
| | | ], |
| | | 'fields' => 'ids', |
| | | ]); |
| | | wp_reset_postdata(); |
| | | return $posts->posts; |
| | | } |
| | | |
| | | public static function forTerm(int $termID, string $taxonomy, ?string $sinceDate = null, bool $modified = false):array |
| | | { |
| | | if (!in_array($taxonomy, ['category', 'tag'])){ |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | } |
| | | $date = self::getSinceDate($sinceDate); |
| | | $posts = new WP_Query([ |
| | | 'posts_per_page' => -1, |
| | | 'post_status' => 'publish', |
| | | 'date_query' => [ |
| | | [ |
| | | 'after' => $date, |
| | | 'inclusive' => true, |
| | | 'column' => $modified ? 'post_modified' : 'post_date' |
| | | ] |
| | | ], |
| | | 'tax_query' => [ |
| | | [ |
| | | 'taxonomy' => $taxonomy, |
| | | 'terms' => $termID, |
| | | ] |
| | | ], |
| | | 'fields' => 'ids', |
| | | ]); |
| | | wp_reset_postdata(); |
| | | return $posts->posts; |
| | | } |
| | | |
| | | public static function formatForNotification(array $IDs, string $type):array |
| | | { |
| | | $posts = []; |
| | | foreach ($IDs as $ID) { |
| | | $posts[] = self::$cache->remember( |
| | | $ID, |
| | | function() use ($ID) { |
| | | $meta = Meta::forPost($ID); |
| | | $img = $meta->get('post_thumbnail'); |
| | | $registrar = Registrar::getInstance(get_post_type($ID)); |
| | | return array_merge([ |
| | | 'url' => get_the_permalink($ID), |
| | | 'content' => $registrar->getType(), |
| | | 'icon' => $registrar->getIcon(), |
| | | 'image' => (!empty($img) ? jvbImageData($img) : []) |
| | | ], |
| | | $meta->getAll([ |
| | | 'post_date', |
| | | 'post_modified', |
| | | 'post_title', |
| | | 'post_excerpt', |
| | | ]) |
| | | ); |
| | | } |
| | | ); |
| | | } |
| | | return $posts; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\Notifications; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Error; |
| | | use WP_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * EmailDigests |
| | | * Prepares the email digest for daily/weekly/monthly email summaries |
| | | **/ |
| | | class EmailDigests |
| | | { |
| | | protected string $campaign; |
| | | protected Cache $terms; |
| | | protected Cache $users; |
| | | protected CustomTable $userIndex; |
| | | protected CustomTable $termIndex; |
| | | public function __construct() |
| | | { |
| | | $this->registerCron(); |
| | | $this->registerTable(); |
| | | |
| | | add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3); |
| | | } |
| | | |
| | | protected function registerCron():void |
| | | { |
| | | add_action(BASE . 'notification_digest_daily', [ $this, 'runDailyDigests' ]); |
| | | add_action(BASE . 'notification_digest_weekly', [ $this, 'runWeeklyDigests' ]); |
| | | add_action(BASE . 'notification_digest_monthly', [ $this, 'runMonthlyDigests' ]); |
| | | } |
| | | |
| | | protected function registerTable():void |
| | | { |
| | | $this->registerUserIndex(); |
| | | $this->registerTermIndex(); |
| | | } |
| | | protected function registerUserIndex():void |
| | | { |
| | | $table = CustomTable::for('user_notification_email_digest'); |
| | | // $types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::getFeatured('favouritable'))); |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'frequency' => "ENUM('daily', 'weekly', 'monthly') NOT NULL", |
| | | 'content_ids' => 'JSON DEFAULT NULL', |
| | | 'output' => 'TEXT DEFAULT NULL', |
| | | 'total_sent' => 'int unsigned DEFAULT 0', |
| | | 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`user_frequency` (`user_id`, `frequency`)'], |
| | | 'user_id (`user_id`)' |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}digest_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->userIndex = $table; |
| | | } |
| | | protected function registerTermIndex():void |
| | | { |
| | | $table = CustomTable::for('user_notification_email_digest'); |
| | | $types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::getFeatured('favouritable', 'term'))); |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'term_id' => "{$table->getTermIDType()} NOT NULL", |
| | | 'type' => "ENUM({$types}) NOT NULL", |
| | | 'frequency' => "ENUM('daily', 'weekly', 'monthly') NOT NULL", |
| | | 'content_ids' => 'JSON DEFAULT NULL', |
| | | 'output' => 'TEXT DEFAULT NULL', |
| | | 'total_sent' => 'int unsigned DEFAULT 0', |
| | | 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`term_frequency` (`term_id`, `type`, `frequency`)'], |
| | | 'term_id (`term_id`)' |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}digest_term` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$table->getTermTable()}` (`term_id`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->termIndex = $table; |
| | | } |
| | | |
| | | public function runDailyDigests():void |
| | | { |
| | | $this->campaign = 'daily_digest_' .date('Y-m-d'); |
| | | $this->processDigest('daily'); |
| | | } |
| | | public function runWeeklyDigests():void |
| | | { |
| | | $this->campaign = 'weekly_digest_'.date('Y-m-d'); |
| | | $this->processDigest('weekly'); |
| | | } |
| | | |
| | | public function runMonthlyDigests():void |
| | | { |
| | | $this->campaign = 'monthly_digest_'.date('Y-m-d'); |
| | | $this->processDigest('monthly'); |
| | | } |
| | | |
| | | protected function processDigest(string $frequency):void |
| | | { |
| | | $users = $this->getUsers($frequency); |
| | | if (empty($users)){ |
| | | return; |
| | | } |
| | | |
| | | JVB()->queue()->add( |
| | | 'email_notification_digest', |
| | | 0, |
| | | [ |
| | | 'frequency' => $frequency, |
| | | 'users' => $users, |
| | | ], |
| | | [ |
| | | 'chunk_key' => 'users', |
| | | 'chunk_size'=> 20, |
| | | 'operation_id'=> 'notification_digest_'.date('Y_m_d_His') |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | public function getUsers(string $frequency):array |
| | | { |
| | | if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) { |
| | | return []; |
| | | } |
| | | return JVB()->notification()->getUsersByFrequency($frequency); |
| | | } |
| | | |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | | { |
| | | if ($operation->type !== 'email_notification_digest') { |
| | | return $result; |
| | | } |
| | | |
| | | $results = []; |
| | | foreach ($data['user'] as $userID) { |
| | | $result = $this->generateUserDigest($userID, $data['frequency']); |
| | | if ($result) { |
| | | $results[$userID] = $result; |
| | | } |
| | | usleep(50); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => $results |
| | | ]; |
| | | } |
| | | protected function getSinceDate(string $frequency):string |
| | | { |
| | | return match ($frequency) { |
| | | 'weekly' => '-1 week', |
| | | 'monthly' => '-1 month', |
| | | default => '-1 day', |
| | | }; |
| | | } |
| | | |
| | | protected function generateUserDigest(int $userID, string $frequency):string|false |
| | | { |
| | | $subscription = JVB()->notification()->getUserSubscriptions($userID, $frequency); |
| | | |
| | | //TODO |
| | | $notifications = JVB()->notification()->getUserNotifications($userID); |
| | | |
| | | if (empty($subscription) && empty($notifications)) { |
| | | return false; |
| | | } |
| | | $content = ''; |
| | | foreach ($subscription as $item) { |
| | | $temp = match ($item['item_type']) { |
| | | array_merge(['user'], Registrar::getFeatured('favouritable', 'user')) => $this->getUserUpdates($item['item_id'], $frequency), |
| | | Registrar::getFeatured('favouritable', 'term') => $this->getTermUpdates($item['item_id'], $item['item_type'], $frequency), |
| | | default => false, |
| | | }; |
| | | if ($temp) { |
| | | $content .= $temp; |
| | | } |
| | | } |
| | | if (empty($content)) { |
| | | return false; |
| | | } |
| | | return $content; |
| | | } |
| | | |
| | | protected function getUserUpdates(int $userID, string $frequency):string|false |
| | | { |
| | | $frequency = strtolower($frequency); |
| | | $frequency = in_array($frequency, ['daily', 'weekly', 'monthly']) ? $frequency : 'daily'; |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return false; |
| | | } |
| | | $since = $this->getSinceDate($frequency); |
| | | |
| | | $entry = $this->userIndex->get([ |
| | | 'user_id' => $userID, |
| | | 'frequency' => $frequency |
| | | ]); |
| | | if ($entry) { |
| | | return $entry->output; |
| | | } |
| | | $new = [ |
| | | 'user_id' => $userID, |
| | | 'frequency' => $frequency, |
| | | ]; |
| | | //Didn't do it yet, create it here |
| | | $IDs = Content::forUser($userID, $since); |
| | | $new['content_ids'] = json_encode($IDs); |
| | | $new['output'] = $this->contentForUser($userID, $IDs); |
| | | |
| | | $this->userIndex->insert($new); |
| | | return $new['output']; |
| | | } |
| | | |
| | | protected function contentForUser(int $userID, array $IDs):string |
| | | { |
| | | if (empty($IDs)) { |
| | | return ''; |
| | | } |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return ''; |
| | | } |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | if (!$registrar) { |
| | | return ''; |
| | | } |
| | | if ($registrar->profile_link) { |
| | | $meta = Meta::forPost(jvbUserProfileLink($userID)); |
| | | $title = $meta->get('post_title'); |
| | | $goTo = get_the_permalink(jvbUserProfileLink($userID)); |
| | | }else { |
| | | $meta = Meta::forUser($userID); |
| | | $title = $meta->get('display_name'); |
| | | $goTo = get_author_posts_url($userID); |
| | | } |
| | | |
| | | $output = []; |
| | | |
| | | $count = count($IDs); |
| | | $max = (min($count, 5)); |
| | | $canIncrease = $count > $max; |
| | | for ($i = 0; $i <=$max; $i++) { |
| | | $ID = $IDs[$i]; |
| | | $m = Meta::forPost($ID); |
| | | $img = $m->get('post_thumbnail'); |
| | | if (empty($img)) { |
| | | if ($canIncrease){ |
| | | $max++; |
| | | } |
| | | continue; |
| | | } |
| | | $image = wp_get_attachment_image_src($img, 'medium'); |
| | | if (!$image) { |
| | | if ($canIncrease){ |
| | | $max++; |
| | | } |
| | | continue; |
| | | } |
| | | $url = get_the_permalink($ID); |
| | | $image = sprintf( |
| | | '<a href="%s">%s</a>', |
| | | $url, |
| | | JVB()->email()->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); |
| | | } |
| | | |
| | | $final = JVB()->email()->button($goTo, 'Want to See More?'); |
| | | return JVB()->email()->grid($output, 3, 'New from '.$title,'',$final); |
| | | } |
| | | |
| | | protected function getTermUpdates(int $termID, string $type, string $frequency):string|false |
| | | { |
| | | $frequency = strtolower($frequency); |
| | | $frequency = in_array($frequency, ['daily', 'weekly', 'monthly']) ? $frequency : 'daily'; |
| | | $taxonomy = jvbCheckBase($type); |
| | | $term = get_term($termID, $taxonomy); |
| | | if (!$term || is_wp_error($term)) { |
| | | return false; |
| | | } |
| | | $since = $this->getSinceDate($frequency); |
| | | |
| | | $entry = $this->termIndex->get([ |
| | | 'term_id' => $termID, |
| | | 'type' => $type, |
| | | 'frequency' => $frequency |
| | | ]); |
| | | if ($entry) { |
| | | return $entry->output; |
| | | } |
| | | $new = [ |
| | | 'term_id' => $termID, |
| | | 'type' => $type, |
| | | 'frequency' => $frequency |
| | | ]; |
| | | //Didn't do it yet, create it here |
| | | $IDs = Content::forTerm($termID, $taxonomy, $since); |
| | | $new['content_ids'] = json_encode($IDs); |
| | | $new['output'] = $this->contentForTerm($termID, $type, $IDs); |
| | | |
| | | $this->termIndex->insert($new); |
| | | return $new['output']; |
| | | } |
| | | |
| | | protected function contentForTerm(int $termID, string $tax, array $IDs):string |
| | | { |
| | | if (empty($IDs)) { |
| | | return ''; |
| | | } |
| | | $taxonomy = jvbCheckBase($tax); |
| | | $term = get_term($termID, $taxonomy); |
| | | if (!$term || is_wp_error($term)) { |
| | | return ''; |
| | | } |
| | | $registrar = Registrar::getInstance($tax); |
| | | if (!$registrar) { |
| | | return ''; |
| | | } |
| | | $meta = Meta::forTerm($termID); |
| | | $title = $meta->get('name'); |
| | | |
| | | $goTo = get_term_link($termID,$taxonomy); |
| | | |
| | | $output = []; |
| | | |
| | | $count = count($IDs); |
| | | $max = (min($count, 5)); |
| | | $canIncrease = $count > $max; |
| | | for ($i = 0; $i <=$max; $i++) { |
| | | $ID = $IDs[$i]; |
| | | $m = Meta::forPost($ID); |
| | | $img = $m->get('post_thumbnail'); |
| | | if (empty($img)) { |
| | | if ($canIncrease){ |
| | | $max++; |
| | | } |
| | | continue; |
| | | } |
| | | $image = wp_get_attachment_image_src($img, 'medium'); |
| | | if (!$image) { |
| | | if ($canIncrease){ |
| | | $max++; |
| | | } |
| | | continue; |
| | | } |
| | | $url = get_the_permalink($ID); |
| | | $image = sprintf( |
| | | '<a href="%s">%s</a>', |
| | | $url, |
| | | JVB()->email()->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); |
| | | } |
| | | |
| | | $final = JVB()->email()->button($goTo, 'See More'); |
| | | return JVB()->email()->grid($output, 3, 'New in '.$title, '', $final); |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\Notifications; |
| | | |
| | | use Exception; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\utility\Features; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Preferences |
| | | * Manages preferences for a user's favourites |
| | | **/ |
| | | class Notifications |
| | | { |
| | | protected array $types; |
| | | protected CustomTable $notifications; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTypes(); |
| | | $this->defineTable(); |
| | | } |
| | | |
| | | private function defineTypes():void |
| | | { |
| | | $types = ['system_message' => [ |
| | | 'icon' => 'info', |
| | | 'digest'=> true |
| | | ]]; |
| | | if (Features::forSite()->has('favourites')) { |
| | | $types = array_merge($types, [ |
| | | 'new_favourite' => [ |
| | | 'icon' => 'heart', |
| | | 'digest'=> true |
| | | ], |
| | | 'list_shared' => [ |
| | | 'icon' => 'list-heart', |
| | | 'digest'=> false |
| | | ], |
| | | 'list_share_status' => [ |
| | | 'icon' => 'list-heart', |
| | | 'digest'=> false |
| | | ] |
| | | ]); |
| | | } |
| | | $contentTax = Registrar::getFeatured('is_content', 'term'); |
| | | $verifyEntry = Registrar::getFeatured('verify_entry', 'term'); |
| | | if (!empty(array_intersect($contentTax, $verifyEntry))) { |
| | | $types = array_merge($types, [ |
| | | 'entry_requested' => [ |
| | | 'icon' => 'user-circle-plus', |
| | | 'digest'=> true, |
| | | 'action'=> ['approve', 'reject', 'dismiss'] |
| | | ], |
| | | 'entry_approved' => [ |
| | | 'icon' => 'user-circle-check', |
| | | 'digest'=> false |
| | | ], |
| | | 'entry_denied' => [ |
| | | 'icon' => 'prohibit', |
| | | 'digest'=> false |
| | | ], |
| | | 'entry_revoked' => [ |
| | | 'icon' => 'warning-circle', |
| | | 'digest'=> false |
| | | ] |
| | | ]); |
| | | } |
| | | $invitable = Registrar::getFeatured('invitable'); |
| | | if (!empty($invitable)) { |
| | | $types = array_merge($types, [ |
| | | 'invitation_requested' => [ |
| | | 'icon' => 'plus-circle', |
| | | 'digest'=> true, |
| | | 'note' => true, |
| | | 'action'=> ['approve','reject','dismiss'] |
| | | ], |
| | | 'invitation_granted' => [ |
| | | 'icon' => 'check-circle', |
| | | 'digest'=> false |
| | | ], |
| | | 'invitation_revoked' => [ |
| | | 'icon' => 'x-circle', |
| | | 'digest'=> false |
| | | ], |
| | | 'invitation_accepted' => [ |
| | | 'icon' => 'check-circle', |
| | | 'digest'=> false |
| | | ], |
| | | 'invitation_refused' => [ |
| | | 'icon' => 'x-circle', |
| | | 'digest'=> false |
| | | ] |
| | | ]); |
| | | } |
| | | |
| | | $approvals = Registrar::getFeatured('approve_new'); |
| | | if (!empty($approvals)) { |
| | | $tmp = ['user', 'term', 'post']; |
| | | $app = []; |
| | | foreach ($tmp as $t) { |
| | | $approvals = Registrar::getFeatured('approve_new', $t); |
| | | if (!empty($approvals)) { |
| | | $app = array_merge($app, [ |
| | | $t.'_new' => [ |
| | | 'icon' => 'plus-circle', |
| | | 'digest'=> true |
| | | ], //For newly created items |
| | | $t.'_approved' => [ |
| | | 'icon' => 'check-circle', |
| | | 'digest'=> false |
| | | ], //For notices of acceptance |
| | | $t.'_denied' => [ |
| | | 'icon' => 'x-circle', |
| | | 'digest'=> false |
| | | ], //For notices of denial |
| | | $t.'_status' => [ |
| | | 'icon' => 'info', |
| | | 'digest'=> true |
| | | ], //For final status |
| | | ]); |
| | | } |
| | | } |
| | | $types = array_merge($types, $app); |
| | | } |
| | | |
| | | $this->types = $types; |
| | | } |
| | | |
| | | private function defineTable():void |
| | | { |
| | | $table = CustomTable::for('notifications_main'); |
| | | |
| | | $typeEnum = implode(',',array_map(function($type) { return '`'.$type.'`';}, array_keys($this->types))); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'for_user' => $table->getUserIDType().' NOT NULL', |
| | | 'from_user' => $table->getUserIDType().' NOT NULL', |
| | | 'target_id' => 'bigint(20) DEFAULT NULL', |
| | | 'target_type' => 'varchar(30) DEFAULT NULL', |
| | | 'type' => "ENUM({$typeEnum}) NOT NULL DEFAULT 'system_message'", |
| | | 'status' => "ENUM('unread', 'read', 'actioned', 'dismissed') NOT NULL DEFAULT 'unread'", |
| | | 'priority' => "ENUM('low','normal','high') NOT NULL DEFAULT 'normal'", |
| | | 'message' => 'varchar(255) DEFAULT NULL', |
| | | 'action_taken' => 'tinyint(1) NOT NULL DEFAULT 0', |
| | | 'result' => 'JSON DEFAULT NULL', |
| | | 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'read_at' => 'datetime DEFAULT NULL', |
| | | 'actioned_at' => 'datetime DEFAULT NULL', |
| | | 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | '`user_status` (`for_user`, `status`)', |
| | | '`target_lookup` (`target_id`, `target_type`)', |
| | | '`unread_notifications` (`for_user`, `status`, `created_at`)', |
| | | '`requires_action` (`for_user`, `requires_action`, `action_taken`)', |
| | | '`from_user_lookup` (`for_user`, `from_user`, `target_id`, `target_type`, `type`)' |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}notify_owner` FOREIGN KEY (`for_user`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}from_user` FOREIGN KEY (`from_user`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->notifications = $table; |
| | | } |
| | | |
| | | protected function checkArgs(array $args):array |
| | | { |
| | | if (empty($args)) { |
| | | return $args; |
| | | } |
| | | $allowedKeys = ['target_id', 'target_type','priority','message']; |
| | | $allowed = array_filter($args, function($key) use ($allowedKeys) { |
| | | return !in_array($key, $allowedKeys); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | $notAllowed = array_diff($allowedKeys, $allowed); |
| | | if (!empty($notAllowed)) { |
| | | error_log('[Notifications]::checkArgs Attempted keys not allowed:'.print_r($notAllowed, true)); |
| | | } |
| | | $out = []; |
| | | foreach ($allowed as $key => $value) { |
| | | switch ($key) { |
| | | case 'target_id': |
| | | $value = absint($value); |
| | | if ($value > 0) { |
| | | $out[$key] = $value; |
| | | } |
| | | break; |
| | | case 'target_type': |
| | | $registrar = Registrar::getInstance($value); |
| | | if ($registrar) { |
| | | $out[$key] = $value; |
| | | } |
| | | break; |
| | | case 'priority': |
| | | $value = strtolower($value); |
| | | $value = in_array($value, ['low','normal','high']) ? $value : false; |
| | | if ($value){ |
| | | $out[$key] = $value; |
| | | } |
| | | break; |
| | | case 'message': |
| | | $value = sanitize_text_field($value); |
| | | $out[$key] = $value; |
| | | break; |
| | | } |
| | | } |
| | | return $out; |
| | | } |
| | | public function notify(int|array $user_ids, string $n_type, int $fromUser = 0, array $args = []):bool |
| | | { |
| | | $args = $this->checkArgs($args); |
| | | $n_type = strtolower($n_type); |
| | | if (!in_array($n_type, array_keys($this->types))) { |
| | | error_log('[Notifications]::notify Invalid notification type: '.$n_type); |
| | | return false; |
| | | } |
| | | |
| | | if (is_array($user_ids)) { |
| | | return $this->notifications->transaction( |
| | | function() use ($user_ids, $n_type, $fromUser, $args) { |
| | | $results = []; |
| | | foreach ($user_ids as $user_id) { |
| | | $results[$user_id] = $this->notifications->findOrCreate([ |
| | | 'for_user' => $user_id, |
| | | 'from_user' => $fromUser, |
| | | 'target_id' => $args['target_id']??null, |
| | | 'target_type'=> $args['target_type']??null, |
| | | 'type' => $n_type |
| | | ], [ |
| | | 'message' => $args['message']??null, |
| | | ]); |
| | | } |
| | | return true; |
| | | } |
| | | ); |
| | | } |
| | | return $this->notifications->findOrCreate([ |
| | | 'for_user' => $user_ids, |
| | | 'from_user' => $fromUser, |
| | | 'target_id' => $args['target_id']??null, |
| | | 'target_type'=> $args['target_type']??null, |
| | | 'type' => $n_type |
| | | ], [ |
| | | 'message' => $args['message']??null, |
| | | ]); |
| | | } |
| | | |
| | | public function unnotify(int|array $user_ids, string $n_type, int $fromUser = 0, array $args = []):bool |
| | | { |
| | | $args = $this->checkArgs($args); |
| | | $n_type = strtolower($n_type); |
| | | if (!in_array($n_type, array_keys($this->types))) { |
| | | error_log('[Notifications]::notify Invalid notification type: '.$n_type); |
| | | return false; |
| | | } |
| | | |
| | | if (is_array($user_ids)) { |
| | | return $this->notifications->transaction( |
| | | function() use ($user_ids, $n_type, $fromUser, $args) { |
| | | foreach ($user_ids as $user_id) { |
| | | $item = $this->notifications->get([ |
| | | 'for_user' => $user_id, |
| | | 'from_user' => $fromUser, |
| | | 'target_id' => $args['target_id']??null, |
| | | 'target_type'=> $args['target_type']??null, |
| | | 'type' => $n_type |
| | | ]); |
| | | if ($item) { |
| | | $this->notifications->delete([ |
| | | 'for_user' => $user_id, |
| | | 'from_user' => $fromUser, |
| | | 'target_id' => $args['target_id']??null, |
| | | 'target_type'=> $args['target_type']??null, |
| | | 'type' => $n_type |
| | | ]); |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | ); |
| | | } |
| | | $item = $this->notifications->get([ |
| | | 'for_user' => $user_ids, |
| | | 'from_user' => $fromUser, |
| | | 'target_id' => $args['target_id']??null, |
| | | 'target_type'=> $args['target_type']??null, |
| | | 'type' => $n_type |
| | | ]); |
| | | if ($item) { |
| | | return $this->notifications->delete([ |
| | | 'for_user' => $user_ids, |
| | | 'from_user' => $fromUser, |
| | | 'target_id' => $args['target_id']??null, |
| | | 'target_type'=> $args['target_type']??null, |
| | | 'type' => $n_type |
| | | ]); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | public function getUserNotifications(int $user_id, array $where = []):array |
| | | { |
| | | $gotWhere = $where; |
| | | $allowedKeys = ['from_user','target_id', 'target_type','type','status','priority','action_taken','created_at','read_at','actioned_at','updated_at']; |
| | | $where = array_filter($gotWhere, function($key) use ($allowedKeys) { |
| | | return in_array($key, $allowedKeys); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | $notAllowed = array_diff($gotWhere, $allowedKeys); |
| | | if (!empty($notAllowed)){ |
| | | error_log('[Notifications]::getUserNotifications Invalid where arguments, removed from request: '.print_r($notAllowed, true)); |
| | | } |
| | | |
| | | return $this->notifications->getMany(array_merge([ |
| | | 'for_user' => $user_id, |
| | | 'status' => 'unread', //default to unread, but the merge will override if different |
| | | ], $where)); |
| | | } |
| | | |
| | | public function markRead(int $userID, int|array $notification_id):bool |
| | | { |
| | | if (is_int($notification_id)) { |
| | | $notification = $this->notifications->get(['id' => $notification_id]); |
| | | if (!$notification) { |
| | | return false; |
| | | } |
| | | if ($notification['for_user'] !== $userID) { |
| | | return false; |
| | | } |
| | | |
| | | return (bool) $this->notifications->update([ |
| | | 'status' => 'read' |
| | | ], [ |
| | | 'id' => $notification_id |
| | | ]); |
| | | } |
| | | try { |
| | | $this->notifications->transaction( |
| | | function () use ($notification_id, $userID) { |
| | | foreach ($notification_id as $id) { |
| | | $notification = $this->notifications->get(['id' => $notification_id]); |
| | | if (!$notification) { |
| | | continue; |
| | | } |
| | | if ($notification['for_user'] !== $userID) { |
| | | continue; |
| | | } |
| | | |
| | | $this->notifications->update([ |
| | | 'status' => 'read' |
| | | ], [ |
| | | 'id' => $id |
| | | ]); |
| | | } |
| | | } |
| | | ); |
| | | return true; |
| | | } catch (Exception $e) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | public function markDismissed(int $userID, int|array $notification_id):bool |
| | | { |
| | | if (is_int($notification_id)) { |
| | | $notification = $this->notifications->get(['id' => $notification_id]); |
| | | if (!$notification) { |
| | | return false; |
| | | } |
| | | if ($notification['for_user'] !== $userID) { |
| | | return false; |
| | | } |
| | | |
| | | return (bool) $this->notifications->update([ |
| | | 'status' => 'dismissed' |
| | | ], [ |
| | | 'id' => $notification_id |
| | | ]); |
| | | } |
| | | |
| | | try { |
| | | $this->notifications->transaction( |
| | | function () use ($notification_id, $userID) { |
| | | foreach ($notification_id as $id) { |
| | | $notification = $this->notifications->get(['id' => $notification_id]); |
| | | if (!$notification) { |
| | | continue; |
| | | } |
| | | if ($notification['for_user'] !== $userID) { |
| | | continue; |
| | | } |
| | | |
| | | $this->notifications->update([ |
| | | 'status' => 'dismissed' |
| | | ], [ |
| | | 'id' => $id |
| | | ]); |
| | | } |
| | | } |
| | | ); |
| | | return true; |
| | | } catch (Exception $e) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | public function markActioned(int $userID, int|array $notification_id, array $result = []):bool |
| | | { |
| | | $update = [ |
| | | 'status' => 'actioned', |
| | | 'action_taken'=> 1, |
| | | ]; |
| | | if (!empty($result)) { |
| | | $update['result'] = json_encode($result); |
| | | } |
| | | if (is_int($notification_id)) { |
| | | $notification = $this->notifications->get(['id' => $notification_id]); |
| | | if (!$notification) { |
| | | return false; |
| | | } |
| | | if ($notification['for_user'] !== $userID) { |
| | | return false; |
| | | } |
| | | return (bool) $this->notifications->update( |
| | | $update, |
| | | [ |
| | | 'id' => $notification_id |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | try { |
| | | $this->notifications->transaction( |
| | | function () use ($notification_id, $userID, $update) { |
| | | foreach ($notification_id as $id) { |
| | | $notification = $this->notifications->get(['id' => $notification_id]); |
| | | if (!$notification) { |
| | | continue; |
| | | } |
| | | if ($notification['for_user'] !== $userID) { |
| | | continue; |
| | | } |
| | | $this->notifications->update( |
| | | $update, |
| | | [ |
| | | 'id' => $notification_id |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | ); |
| | | return true; |
| | | } catch (Exception $e) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\Notifications; |
| | | |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Preferences |
| | | * Manages preferences for a user's favourites |
| | | **/ |
| | | class Preferences |
| | | { |
| | | protected CustomTable $preferences; |
| | | public function __construct() |
| | | { |
| | | $this->defineTable(); |
| | | } |
| | | |
| | | private function defineTable():void |
| | | { |
| | | $table = CustomTable::for('notifications_preferences'); |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'item_id' => 'bigint(20) NOT NULL', |
| | | 'item_type' => 'varchar(50) NOT NULL', |
| | | 'frequency' => "ENUM('never', 'daily', 'weekly', 'monthly') DEFAULT 'never'", |
| | | 'last_sent' => 'datetime DEFAULT NULL', |
| | | 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`user_type` (`user_id`, `item_id`)'], |
| | | '`user_frequency` (`user_id`, `frequency`)', |
| | | '`frequency_lookup` (`frequency`, `last_sent`)' |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}notification_pref_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->preferences = $table; |
| | | } |
| | | |
| | | public function getUsersByFrequency(string $frequency):array |
| | | { |
| | | if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) { |
| | | return []; |
| | | } |
| | | return array_unique($this->preferences->pluck('user_id', ['frequency' => $frequency])); |
| | | } |
| | | |
| | | public function getUserSubscriptions(int $userID, string $frequency):array |
| | | { |
| | | if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) { |
| | | return []; |
| | | } |
| | | return $this->preferences->getMany([ |
| | | 'user_id' => $userID, |
| | | 'frequency' => $frequency |
| | | ]); |
| | | } |
| | | |
| | | public function addUserPreference(int $userID, int $item_id, string $item_type, string $frequency):bool |
| | | { |
| | | if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) { |
| | | return false; |
| | | } |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return false; |
| | | } |
| | | $registrar = Registrar::getInstance($item_type); |
| | | if (!$registrar) { |
| | | return false; |
| | | } |
| | | $type = $registrar->getType(); |
| | | switch ($type) { |
| | | case 'term': |
| | | $term = get_term($item_id, $registrar->getBased()); |
| | | if (!$term || is_wp_error($term)) { |
| | | return false; |
| | | } |
| | | break; |
| | | case 'user': |
| | | $user = get_userdata($item_id); |
| | | if (!$user || is_wp_error($user)) { |
| | | return false; |
| | | } |
| | | break; |
| | | } |
| | | return $this->preferences->findOrCreate( |
| | | [ |
| | | 'user_id' => $userID, |
| | | 'item_id' => $item_id, |
| | | 'item_type' => $item_type |
| | | ], |
| | | [ |
| | | 'frequency' => $frequency |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | public function deleteUserPreference(int $userID, int $item_id, string $item_type):bool |
| | | { |
| | | $hasRecord = $this->preferences->get(['user_id' => $userID, 'item_id' => $item_id, 'item_type' => $item_type]); |
| | | if (!$hasRecord) { |
| | | return false; |
| | | } |
| | | return $this->preferences->delete([ |
| | | 'user_id' => $userID, |
| | | 'item_id' => $item_id, |
| | | 'item_type' => $item_type |
| | | ]); |
| | | } |
| | | } |
| | |
| | | protected ?int $referralPage = null; |
| | | protected string $rewards_table; |
| | | |
| | | protected CustomTable $referrals; |
| | | protected CustomTable $codes; |
| | | protected CustomTable $janeClients; |
| | | protected CustomTable $rewards; |
| | | protected CustomTable $treatments; |
| | | |
| | | // Default reward settings |
| | | protected array $default_settings = [ |
| | | 'referrer_reward_applies_to' => 'per_user', // 'per_user' or 'flat_total' |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->cache = Cache::for('referrals', WEEK_IN_SECONDS); |
| | |
| | | |
| | | add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3); |
| | | add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1); |
| | | add_action('jvbUserRegistered', [$this, 'processReferral'], 10, 1); |
| | | add_action('user_register', [$this, 'processReferral'], 10, 1); |
| | | |
| | | // Add meta boxes for admin to manage referrals |
| | | add_action('show_user_profile', [$this, 'displayUserReferralInfo']); |
| | |
| | | add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3); |
| | | } |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | $this->defineReferralsTable(); |
| | | $this->defineCodeTable(); |
| | | $this->defineJaneClientsTable(); |
| | | $this->defineRewardsTable(); |
| | | $this->defineTreatmentsTable(); |
| | | } |
| | | protected function defineReferralsTable():void |
| | | { |
| | | $table = CustomTable::for('referrals'); |
| | | |
| | | $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', |
| | | 'referral_code'=> 'varchar(50) NOT NULL', |
| | | 'status' => "ENUM('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'", |
| | | 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'consulted_at'=> 'datetime DEFAULT NULL', |
| | | 'treated_at'=> 'datetime DEFAULT NULL', |
| | | 'treatment_count' => 'int DEFAULT 0', |
| | | 'notes' => 'text DEFAULT NULL', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => 'to_user (`to_user`)'], |
| | | 'from_user (`from_user`)', |
| | | 'status (`status`)', |
| | | 'code (`referral_code`)', |
| | | 'date (`created_at`)', |
| | | 'consult (`consulted_at`)' |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}referral_from_user_fk` FOREIGN KEY (`from_user`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}referral_to_user_fk` FOREIGN KEY (`to_user`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->referrals = $table; |
| | | } |
| | | |
| | | protected function defineCodeTable():void |
| | | { |
| | | $table = CustomTable::for('referrals_codes'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'code' => 'varchar(50) NOT NULL', |
| | | 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'UNIQUE', 'value' => 'code (`code`)'], |
| | | 'user (`user_id`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}referral_code_user_fk` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->codes = $table; |
| | | } |
| | | protected function defineJaneClientsTable():void |
| | | { |
| | | $table = CustomTable::for('referrals_jane_clients'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'patient_guid' => 'varchar(50) NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'first_name' => 'varchar(100) NOT NULL', |
| | | 'last_name' => 'varchar(100) NOT NULL', |
| | | 'email' => 'varchar(255) NOT NULL', |
| | | 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => 'patient_guid (`patient_guid`)'], |
| | | 'user (`user_id`)', |
| | | 'email (`email`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}jane_clients_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->janeClients = $table; |
| | | } |
| | | protected function defineRewardsTable():void |
| | | { |
| | | $table = CustomTable::for('referrals_rewards'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'referral_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'reward_type' => "ENUM('referrer', 'referee') NOT NULL", |
| | | 'amount' => 'decimal(10,2) NOT NULL', |
| | | 'reward_calculation'=> "ENUM('percentage', 'fixed')", |
| | | 'status' => "ENUM('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available'", |
| | | 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | 'redeemed_at' => 'datetime DEFAULT NULL', |
| | | 'expires_at' => 'datetime DEFAULT NULL', |
| | | 'notes' => 'text DEFAULT NULL', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | 'referral (`referral_id`)', |
| | | 'user (`user_id`)', |
| | | 'status (`status`)', |
| | | 'type (`reward_type`)' |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}reward_referral` FOREIGN KEY (`referral_id`) |
| | | REFERENCES {$this->referrals->getFullTableName()} (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}reward_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES {$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->rewards = $table; |
| | | } |
| | | protected function defineTreatmentsTable():void |
| | | { |
| | | $table = CustomTable::for('referral_treatments'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'referral_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'treatment_type'=> 'varchar(100) NOT NULL', //Tier 1-6, Brows, etc |
| | | 'treatment_date'=> 'datetime NOT NULL', |
| | | 'invoice_number'=> 'varchar(50) DEFAULT NULL', |
| | | 'amount' => 'decimal(10,2) DEFAULT NULL', |
| | | 'status' => "ENUM('completed', 'no_show', 'cancelled') DEFAULT 'completed'", |
| | | 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | 'referral (`referral_id`)', |
| | | 'user (`user_id`)', |
| | | 'date (`treatment_date`)', |
| | | 'type (`treatment_type`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}treatment_referral` FOREIGN KEY (`referral_id`) |
| | | REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}treatment_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | | $this->treatments = $table; |
| | | } |
| | | |
| | | public function getSettings():array |
| | | { |
| | | return $this->settings; |
| | |
| | | } |
| | | } |
| | | |
| | | public function createCode(int $user_id, string $code):string|false |
| | | { |
| | | $code = sanitize_title($code); |
| | | $existing = $this->codes->get(['code' => $code]); |
| | | if ($existing) { |
| | | if ($existing['user_id'] !== $user_id) { |
| | | return false; |
| | | } |
| | | return $code; |
| | | } |
| | | $success = $this->codes->insert([ |
| | | 'user_id' => $user_id, |
| | | 'code' => $code |
| | | ]); |
| | | if ($success) { |
| | | return $code; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Generate or get existing referral code for a user |
| | | * |
| | |
| | | * @param string|null $custom_code Optional custom code |
| | | * @return string|WP_Error |
| | | */ |
| | | public function getUserReferralCode(int $user_id, ?string $custom_code = null) |
| | | public function getUserReferralCode(int $user_id, ?string $custom_code = null):array|wp_error |
| | | { |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return new WP_Error('invalid_user', 'User not found'); |
| | | } |
| | | |
| | | // Check if user already has a code |
| | | $existing_code = get_user_meta($user_id, BASE . 'referral_code', true); |
| | | |
| | | if ($existing_code && !$custom_code) { |
| | | return $existing_code; |
| | | $existing = $this->codes->pluck('code', ['user_id' => $user_id],'created_at', 'DESC'); |
| | | if ($existing && !$custom_code) { |
| | | return $existing; |
| | | } |
| | | if ($custom_code && !in_array($custom_code, $existing)) { |
| | | $test = $this->createCode($user_id, $custom_code); |
| | | if ($test) { |
| | | return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC'); |
| | | } |
| | | } else { |
| | | return $existing; |
| | | } |
| | | |
| | | // Generate new code if custom provided or none exists |
| | | $code = $custom_code ?: $this->generateReferralCode($user); |
| | | $code = $this->generateReferralCode($user); |
| | | |
| | | // Validate uniqueness |
| | | if ($this->isCodeTaken($code, $user_id)) { |
| | | return new WP_Error('code_taken', 'This referral code is already in use'); |
| | | $success = $this->createCode($user_id, $code); |
| | | if ($success) { |
| | | return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC'); |
| | | } |
| | | |
| | | // Save the code |
| | | update_user_meta($user_id, BASE . 'referral_code', $code); |
| | | |
| | | return $code; |
| | | } |
| | | |
| | |
| | | * Check if a referral code is already taken |
| | | * |
| | | * @param string $code |
| | | * @param int|null $exclude_user_id |
| | | * @return bool |
| | | */ |
| | | protected function isCodeTaken(string $code, ?int $exclude_user_id = null): bool |
| | | protected function isCodeTaken(string $code): bool |
| | | { |
| | | $args = [ |
| | | 'meta_key' => BASE . 'referral_code', |
| | | 'meta_value' => $code, |
| | | 'fields' => 'ID', |
| | | 'number' => 1 |
| | | ]; |
| | | |
| | | if ($exclude_user_id) { |
| | | $args['exclude'] = [$exclude_user_id]; |
| | | } |
| | | |
| | | $users = get_users($args); |
| | | return !empty($users); |
| | | return (bool) $this->codes->get(['code' => $code]); |
| | | } |
| | | |
| | | public function processRegistrationToken(int $user_id, string $email, array $data): void |
| | |
| | | * Track a new referral when user registers |
| | | * |
| | | * @param int $user_id |
| | | * @param array $userData |
| | | * @return bool; |
| | | */ |
| | | public function processReferral(int $user_id): bool |
| | | public function processReferral(int $user_id, array $userData): bool |
| | | { |
| | | // Try to get code from user meta first (set during registration) |
| | | $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true); |
| | | $referral = $this->referrals->get(['to_user' => $user_id]); |
| | | |
| | | if (empty($referral_code)) { |
| | | if (empty($referral)) { |
| | | $referral = $this->referrals->get(['to_email' => $userData['email']]); |
| | | } |
| | | 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'] |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | if (empty($referral_code)) { |
| | | if (empty($referral)) { |
| | | return false; // No referral code - regular registration |
| | | } |
| | | |
| | | // Find the referrer |
| | | $referrer = $this->getUserByReferralCode($referral_code); |
| | | if (!$referrer) { |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | | $referrer = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]); |
| | | if (empty($referrer)) { |
| | | //This should not happen, but whatever |
| | | return false; |
| | | } |
| | | $referrer = $referrer[0]; |
| | | $record = $this->referrals->findOrCreate([ |
| | | 'to_user' => $user_id, |
| | | 'referral_code' => $referral['referral_code'], |
| | | ], [ |
| | | 'from_user' => $referrer, |
| | | '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; |
| | | } |
| | | |
| | | $user = get_userdata($user_id); |
| | | |
| | | // Check if referral already exists for this user |
| | | $existing = $this->wpdb->get_row($this->wpdb->prepare( |
| | | "SELECT * FROM {$this->referrals_table} |
| | | WHERE referrer_id = %d AND (referee_email = %s OR referee_id = %d)", |
| | | $referrer->ID, |
| | | $user->user_email, |
| | | $user_id |
| | | )); |
| | | |
| | | if (!$existing) { |
| | | // Create new referral record - referred_at captures registration time |
| | | $this->wpdb->insert( |
| | | $this->referrals_table, |
| | | [ |
| | | 'referrer_id' => $referrer->ID, |
| | | 'referee_id' => $user_id, |
| | | 'referee_name' => $user->display_name, |
| | | 'referee_email' => $user->user_email, |
| | | 'referee_phone' => get_user_meta($user_id, BASE . 'phone', true) ?: '', |
| | | 'referral_code' => $referral_code, |
| | | 'status' => 'pending', // pending first treatment |
| | | 'referred_at' => current_time('mysql') // When they registered |
| | | ], |
| | | ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s'] |
| | | ); |
| | | } |
| | | |
| | | // Clean up temp data |
| | | delete_user_meta($user_id, BASE . 'pending_referral_code'); |
| | |
| | | $this->cache->flush(); |
| | | |
| | | // Fire action for tracking |
| | | do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code); |
| | | do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']); |
| | | |
| | | // Send notification to referrer |
| | | $this->sendReferrerNotification($referrer->ID, $user->display_name); |
| | | $this->sendReferrerNotification($referrer->ID, $userData['display_name']); |
| | | return true; |
| | | } |
| | | |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Response Manager |
| | | * |
| | | * Handles a responses |
| | | * TODO |
| | | */ |
| | | class ResponseManager |
| | | { |
| | | protected CustomTable $response; |
| | | protected KarmaManager $karma; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | } |
| | | 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', |
| | | 'karma' => 'int NOT NULL DEFAULT 0', |
| | | '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` (`karma`)', |
| | | '`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->response = $table; |
| | | |
| | | $this->karma = KarmaManager::for('responses', 'responses'); |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->roles = array_keys(array_map(function ($instance) { |
| | | return $instance->slug; |
| | | }, Registrar::getRegistered('user'))); |
| | | $this->roles = Registrar::getRegistered('user'); |
| | | |
| | | $this->content = array_map(function($content) { |
| | | $registrar = Registrar::getInstance($content); |
| | |
| | | $user->add_cap(BASE . 'can_own_' . $termID); |
| | | $user->add_cap(BASE . 'can_manage_' . $termID); |
| | | |
| | | $owners = get_term_meta($termID, BASE.'owners', true); |
| | | if (empty($owners)) { |
| | | $owners = []; |
| | | } |
| | | $owners[] = $userID; |
| | | $owners = array_unique($owners); |
| | | update_term_meta($termID, BASE.'owners', $owners); |
| | | |
| | | |
| | | do_action(BASE . 'granted_ownership', $userID, $termID, $taxonomy); |
| | | |
| | | return true; |
| | |
| | | return false; |
| | | } |
| | | |
| | | $owners = get_term_meta($termID, BASE.'owners', true); |
| | | if (empty($owners)) { |
| | | $owners = []; |
| | | } |
| | | if (in_array($userID, $owners)) { |
| | | unset($owners[array_search($userID, $owners)]); |
| | | } |
| | | $owners = array_unique($owners); |
| | | update_term_meta($termID, BASE.'owners', $owners); |
| | | |
| | | $user->remove_cap(BASE . 'can_own_' . $termID); |
| | | |
| | | do_action(BASE . 'revoked_ownership', $userID, $termID, $taxonomy); |
| | |
| | | |
| | | $user->add_cap(BASE . 'can_manage_' . $termID); |
| | | |
| | | $managers = get_term_meta($termID, BASE.'managers', true); |
| | | if (empty($managers)) { |
| | | $managers = []; |
| | | } |
| | | $managers[] = $userID; |
| | | $managers = array_unique($managers); |
| | | update_term_meta($termID, BASE.'managers', $managers); |
| | | |
| | | do_action(BASE . 'granted_management', $userID, $termID, $taxonomy); |
| | | |
| | | return true; |
| | |
| | | |
| | | $user->remove_cap(BASE . 'can_manage_' . $termID); |
| | | |
| | | $managers = get_term_meta($termID, BASE.'managers', true); |
| | | if (empty($managers)) { |
| | | $managers = []; |
| | | } |
| | | if (in_array($userID, $managers)) { |
| | | unset($managers[array_search($userID, $managers)]); |
| | | } |
| | | $managers = array_unique($managers); |
| | | update_term_meta($termID, BASE.'managers', $managers); |
| | | |
| | | do_action(BASE . 'revoked_management', $userID, $termID, $taxonomy); |
| | | |
| | | return true; |
| | |
| | | 'name' => $contentRegistrar->getConfig('breadcrumbs')['title']??$contentRegistrar->getPlural(), |
| | | 'url' => get_post_type_archive_link(jvbCheckBase($content)), |
| | | ]; |
| | | $crumbs[] = [ |
| | | 'name' => 'By ' . $registrar->getSingular(), |
| | | 'url' => false, |
| | | ]; |
| | | // $crumbs[] = [ |
| | | // 'name' => 'By ' . $registrar->getSingular(), |
| | | // 'url' => false, |
| | | // ]; |
| | | } |
| | | } |
| | | |
| | |
| | | // Add directory if exists |
| | | $content = jvbNoBase($post->post_type); |
| | | $registrar = Registrar::getInstance($content); |
| | | $crumbConfig = false; |
| | | if ($registrar){ |
| | | $crumbConfig = $registrar->getConfig('breadcrumbs'); |
| | | } |
| | | |
| | | if($registrar && $registrar->hasFeature('show_directory')) { |
| | | $directory = JVB()->directories()?->directories($content)??[]; |
| | | if (!empty($directory)) { |
| | |
| | | } elseif (is_post_type_archive() && $registrar && $registrar->hasFeature('show_directory')) { |
| | | |
| | | $crumbs[] = [ |
| | | 'name' => $registrar->getConfig('breadcrumb')['title'] ?? $registrar->getPlural(), |
| | | 'name' => $registrar->getConfig('breadcrumbs')['title'] ?? $registrar->getPlural(), |
| | | 'url' => get_post_type_archive_link($type) |
| | | ]; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | public function addTaxToCrumbs(array $crumbs, string $taxonomy):array |
| | | public function addTaxToCrumbs(array $crumbs, string|array $taxonomy):array |
| | | { |
| | | $ID = get_the_ID(); |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $terms = get_the_terms($ID, $taxonomy); |
| | | if ($terms && !is_wp_error($terms)) { |
| | | $term = $terms[0]; |
| | | $ancestors = get_ancestors($term->term_id, $taxonomy, 'taxonomy'); |
| | | $ancestors = array_reverse($ancestors); |
| | | foreach ($ancestors as $ancestor) { |
| | | $aTerm = get_term($ancestor, $taxonomy); |
| | | if ($aTerm && !is_wp_error($aTerm)) { |
| | | $crumbs[] = [ |
| | | 'name' => $aTerm->name, |
| | | 'url' => get_term_link($ancestor, $taxonomy) |
| | | ]; |
| | | $taxonomies = is_string($taxonomy) ? [$taxonomy] : $taxonomy; |
| | | foreach ($taxonomies as $tax) { |
| | | $taxonomy = jvbCheckBase($tax); |
| | | $terms = get_the_terms($ID, $taxonomy); |
| | | if ($terms && !is_wp_error($terms)) { |
| | | $term = $terms[0]; |
| | | $ancestors = get_ancestors($term->term_id, $taxonomy, 'taxonomy'); |
| | | $ancestors = array_reverse($ancestors); |
| | | foreach ($ancestors as $ancestor) { |
| | | $aTerm = get_term($ancestor, $taxonomy); |
| | | if ($aTerm && !is_wp_error($aTerm)) { |
| | | $crumbs[] = [ |
| | | 'name' => $aTerm->name, |
| | | 'url' => get_term_link($ancestor, $taxonomy) |
| | | ]; |
| | | } |
| | | } |
| | | $crumbs[] = [ |
| | | 'name' => html_entity_decode($term->name), |
| | | 'url' => get_term_link($term, $taxonomy) |
| | | ]; |
| | | } |
| | | $crumbs[] = [ |
| | | 'name' => html_entity_decode($term->name), |
| | | 'url' => get_term_link($term, $taxonomy) |
| | | ]; |
| | | } |
| | | |
| | | return $crumbs; |
| | | } |
| | | } |
| | |
| | | return $this->date; |
| | | } |
| | | |
| | | /** |
| | | * @throws \DateMalformedStringException |
| | | */ |
| | | public function setDate(string $date):void |
| | | { |
| | | $time = new \DateTime(strtotime($date)); |
| | | $time = $time->format('c'); |
| | | $time = date('c', strtotime($date)); |
| | | if ($time){ |
| | | $this->date = $time; |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\DataType; |
| | | use DateMalformedStringException; |
| | | use DateTime; |
| | | use JVBase\meta\Sanitizer; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | return $this->time; |
| | | } |
| | | |
| | | /** |
| | | * @throws DateMalformedStringException |
| | | */ |
| | | public function setTime(string $time):void { |
| | | $time = new DateTime(strtotime($time)); |
| | | $time = $time->format('H:i:s'); |
| | | if ($time){ |
| | | $this->time = $time; |
| | | } |
| | | $this->time = Sanitizer::sanitize($time, ['type'=> 'time']); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\SEO\BreadcrumbManager; |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\WebSite; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\OfferCatalog; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\Service; |
| | | use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\config\seo\Resolver; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | |
| | | { |
| | | $schema = []; |
| | | |
| | | $schema[] = $this->buildWebsiteSchema(false); |
| | | |
| | | if (is_front_page()) { |
| | | $schema[] = $this->buildWebsiteSchema(true); |
| | | $test = $this->buildOrganizationSchema(); |
| | | if (!empty($test)) { |
| | | $schema[] = $test; |
| | |
| | | if (is_singular($this->types)) { |
| | | $type = get_post_type(); |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) { |
| | | if ($registrar) { |
| | | $seo = $registrar->getSEO(); |
| | | $schema[] = $seo->schema()->outputSingularSchema(); |
| | | } |
| | | }elseif (is_post_type_archive($this->types)) { |
| | | |
| | | error_log('It is a post type archive'); |
| | | $type = get_queried_object(); |
| | | $type = $type->name; |
| | | $registrar = Registrar::getInstance($type); |
| | | |
| | | if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) { |
| | | if ($registrar) { |
| | | $seo = $registrar->getSEO(); |
| | | $schema[] =$seo->schema()->outputArchiveSchema(); |
| | | } |
| | |
| | | $type = get_queried_object(); |
| | | $type = jvbNoBase($type->taxonomy); |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) { |
| | | if ($registrar ) { |
| | | $seo = $registrar->getSEO(); |
| | | error_log('SEO: '.print_r($seo->schema(), true)); |
| | | $schema[] = $seo->schema()->outputArchiveSchema(); |
| | |
| | | if (is_page($isContent)){ |
| | | $type = get_post_meta(get_the_id(), BASE.'for_type', true); |
| | | error_log('Type: '.print_r($type, true)); |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar) { |
| | | $schema[] = $registrar->getSEO()->schema()->outputContentTaxArchiveSchema(); |
| | | if (!empty($type)) { |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar) { |
| | | $schema[] = $registrar->getSEO()->schema()->outputContentTaxArchiveSchema(); |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | |
| | | } |
| | | |
| | | if (!empty($schema)) { |
| | | $website = JVB()->schemaHelper()::schema('website'); |
| | | if (!empty($website)) { |
| | | if (JVB_TESTING) { |
| | | Cache::for('websiteSchema')->flush(); |
| | | } |
| | | $website = Cache::for('websiteSchema')->remember( |
| | | 'schema', |
| | | function () { |
| | | return $this->websiteSchema(); |
| | | if (!is_front_page()) { |
| | | $website = JVB()->schemaHelper()::schema('website'); |
| | | if (!empty($website)) { |
| | | if (JVB_TESTING) { |
| | | Cache::for('websiteSchema')->flush(); |
| | | } |
| | | ); |
| | | array_unshift($schema, $website); |
| | | $website = Cache::for('websiteSchema')->remember( |
| | | 'schema', |
| | | function () { |
| | | return $this->websiteSchema(); |
| | | } |
| | | ); |
| | | array_unshift($schema, $website); |
| | | } |
| | | } |
| | | |
| | | $schema = [ |
| | | '@context' => 'https://schema.org', |
| | | '@graph' => $schema |
| | |
| | | if (!$website->getName() || empty($website->getName())){ |
| | | $website->setName(get_bloginfo('name')); |
| | | } |
| | | if(!$website->getCreator() || empty($website->getCreator())) { |
| | | $website->setCreator($this->getCreator()); |
| | | if(!$website->getCreator() || empty($website->getCreator(true))) { |
| | | $website->setCreator($this->getCreator(true)); |
| | | } |
| | | return $website->outputSchema(); |
| | | } |
| | |
| | | if (empty($schema)) { |
| | | return; |
| | | } |
| | | // $encoded = wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); |
| | | // $encoded = wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT, 1024); |
| | | $encoded = wp_json_encode($schema, JSON_UNESCAPED_SLASHES, 512); |
| | | if ($encoded === false) { |
| | | error_log('wp_json_encode failed: ' . json_last_error_msg()); |
| | | return; |
| | | } |
| | | echo "\n<!-- SEO Schema by JakeVan -->\n"; |
| | | echo '<script type="application/ld+json">' . "\n"; |
| | | echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); |
| | | |
| | | echo $encoded; |
| | | echo "\n" . '</script>' . "\n"; |
| | | echo "\n" . '<!-- / SEO Schema by JakeVan -->'."\n"; |
| | | } |
| | |
| | | return Cache::for('websiteSchema')->remember( |
| | | 'full', |
| | | function () use ($storedWebsite) { |
| | | error_log('StoredWebsite: '.print_r($storedWebsite, true)); |
| | | $website = JVB()->schemaHelper()::classFromConfig($storedWebsite); |
| | | if (!$website->getName() || empty($website->getName())) { |
| | | $website->setName(get_bloginfo('name')); |
| | |
| | | } |
| | | |
| | | |
| | | public function getCreator(bool $reference = false):LocalBusiness |
| | | public function getCreator(bool $reference = false, bool $jakeVan = false):LocalBusiness |
| | | { |
| | | if (JVB_TESTING){ |
| | | Cache::for('JakeVanCreator')->flush(); |
| | | } |
| | | |
| | | if ($reference) { |
| | | if ($reference && $jakeVan) { |
| | | return Cache::for('JakeVanCreator')->remember( |
| | | 'reference', |
| | | function () { |
| | | $creator = new LocalBusiness(); |
| | | $creator->setId('jakevan'); |
| | | $creator->setName('JakeVan'); |
| | | $creator->setAlternateName('Jake Vanderwerf'); |
| | | $creator->setUrl('https://jakevan.ca/'); |
| | | $creator->setDescription('Let\'s bring your idea to life.'); |
| | | return $creator; |
| | | } |
| | | ); |
| | | } |
| | | if ($jakeVan) { |
| | | return Cache::for('JakeVanCreator')->remember( |
| | | 'full', |
| | | function () { |
| | | $creator = new LocalBusiness(); |
| | | $creator->setName('JakeVan'); |
| | | $creator->setId('https://jakevan.ca/#localbusiness'); |
| | | $creator->setAlternateName(['Jake Vanderwerf', 'Rooted Romantic', 'Jacob Vanderwerf']); |
| | | $creator->setUrl('https://jakevan.ca'); |
| | | $creator->setSameAs([ |
| | | 'https://bsky.app/profile/jakevan.ca', |
| | | ]); |
| | | $creator->setAreaServed(['Edmonton, Alberta', 'Alberta', 'Canada']); |
| | | $offers = new OfferCatalog(); |
| | | $offers->setName('Services'); |
| | | $offers->setUrl('https://jakevan.ca/services'); |
| | | |
| | | return Cache::for('JakeVanCreator')->remember( |
| | | 'full', |
| | | function () { |
| | | $creator = new LocalBusiness(); |
| | | $creator->setName('JakeVan'); |
| | | $creator->setId('https://jakevan.ca/#localbusiness'); |
| | | $creator->setAlternateName(['Jake Vanderwerf', 'Rooted Romantic', 'Jacob Vanderwerf']); |
| | | $creator->setUrl('https://jakevan.ca'); |
| | | $creator->setSameAs([ |
| | | 'https://bsky.app/profile/jakevan.ca', |
| | | ]); |
| | | $creator->setAreaServed(['Edmonton, Alberta', 'Alberta', 'Canada']); |
| | | $offers = new OfferCatalog(); |
| | | $offers->setName('Services'); |
| | | $offers->setUrl('https://jakevan.ca/services'); |
| | | $graphicDesign = new Service(); |
| | | $graphicDesign->setName('Graphic Design'); |
| | | $graphicDesign->setUrl('https://jakevan.ca/services/design/'); |
| | | $graphicDesign->setDescription('From print to digital design.'); |
| | | $websiteDesign = new Service(); |
| | | $websiteDesign->setName('Development'); |
| | | $websiteDesign->setUrl('https://jakevan.ca/services/development/'); |
| | | $websiteDesign->setDescription('From basic websites to custom functionality.'); |
| | | $strategy = new Service(); |
| | | $strategy->setName('Strategy'); |
| | | $strategy->setUrl('https://jakevan.ca/services/strategy/'); |
| | | $strategy->setDescription('From developing your business plan to SEO.'); |
| | | $art = new Service(); |
| | | $art->setName('Art'); |
| | | $art->setUrl('https://jakevan.ca/services/art/'); |
| | | $art->setDescription('From unique, custom, handmade pieces to small-scale wholesale.'); |
| | | |
| | | $graphicDesign = new Service(); |
| | | $graphicDesign->setName('Graphic Design'); |
| | | $graphicDesign->setUrl('https://jakevan.ca/services/design/'); |
| | | $graphicDesign->setDescription('From print to digital design.'); |
| | | $websiteDesign = new Service(); |
| | | $websiteDesign->setName('Development'); |
| | | $websiteDesign->setUrl('https://jakevan.ca/services/development/'); |
| | | $websiteDesign->setDescription('From basic websites to custom functionality.'); |
| | | $strategy = new Service(); |
| | | $strategy->setName('Strategy'); |
| | | $strategy->setUrl('https://jakevan.ca/services/strategy/'); |
| | | $strategy->setDescription('From developing your business plan to SEO.'); |
| | | $art = new Service(); |
| | | $art->setName('Art'); |
| | | $art->setUrl('https://jakevan.ca/services/art/'); |
| | | $art->setDescription('From unique, custom, handmade pieces to small-scale wholesale.'); |
| | | |
| | | $offers->setItemListElement([ |
| | | $graphicDesign, |
| | | $websiteDesign, |
| | | $strategy, |
| | | $art |
| | | ]); |
| | | $offers->setItemListElement([ |
| | | $graphicDesign, |
| | | $websiteDesign, |
| | | $strategy, |
| | | $art |
| | | ]); |
| | | |
| | | |
| | | $creator->setHasOfferCatalog($offers); |
| | | return $creator; |
| | | } |
| | | ); |
| | | $creator->setHasOfferCatalog($offers); |
| | | return $creator; |
| | | } |
| | | ); |
| | | } |
| | | if ($reference) { |
| | | return $this->getOptionSchemaReference('organization'); |
| | | } |
| | | $config = JVB()->schemaHelper()::schema('organization'); |
| | | |
| | | return JVB()->schemaHelper()::classFromConfig($config); |
| | | } |
| | | |
| | | public function getOptionSchemaReference(string $option):mixed |
| | |
| | | error_log('Attempted to get schema reference for: '.$option.', but it does not exist'); |
| | | return null; |
| | | } |
| | | $action = BASE.ucfirst($option).'Schema'; |
| | | $stored = JVB()->schemaHelper()::reference($action); |
| | | |
| | | $stored = JVB()->schemaHelper()::reference($option); |
| | | |
| | | if (empty($stored)){ |
| | | error_log('Attempted to get schema reference for: '.$option.', but defaults not set.'); |
| | |
| | | } |
| | | $class = new $type(); |
| | | unset($stored['type']); |
| | | $minimal = apply_filters($action.'Reference', ['name', 'url', 'sameAs', 'logo']); |
| | | $minimal = apply_filters(BASE.'OrganizationReference', ['name', 'url', 'sameAs', 'logo']); |
| | | |
| | | foreach ($minimal as $property) { |
| | | $method = 'set'.ucfirst($property); |
| | |
| | | if (!$value) {continue;} |
| | | |
| | | if (str_contains($value, '{{')) { |
| | | $meta = null; |
| | | $value = Resolver::resolve($property, $value); |
| | | } |
| | | $class->$method($value); |
| | |
| | | public function buildOrganizationSchema():array |
| | | { |
| | | $config = JVB()->schemaHelper()::schema('organization'); |
| | | |
| | | $class = JVB()->schemaHelper()::classFromConfig($config); |
| | | return ($class)? $class->outputSchema() : []; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Action; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Thing; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\actionProcessTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\actionStatusTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\agentTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\descriptionTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\errorTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\instrumentTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\locationTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\nameTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\objectTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\participantTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\providerTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\resultTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\startTimeTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\targetTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class Action extends Thing { |
| | | use actionProcessTrait, actionStatusTrait, agentTrait, |
| | | endTimeTrait, errorTrait, instrumentTrait, locationTrait, |
| | | objectTrait, participantTrait, providerTrait, resultTrait, |
| | | startTimeTrait, targetTrait; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Action; |
| | | |
| | | use JVBase\managers\SEO\render\Traits\_Properties\aboutTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\inLanguageTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class CommunicateAction extends InteractAction { |
| | | use aboutTrait, inLanguageTrait; |
| | | //also has "recipient" |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Action; |
| | | |
| | | use JVBase\managers\SEO\render\Traits\_Properties\actionProcessTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\actionStatusTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\agentTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\errorTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\instrumentTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\locationTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\objectTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\participantTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\providerTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\resultTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\startTimeTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\targetTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class InteractAction extends Action { |
| | | |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Action; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class OrganizeAction extends Action { |
| | | |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Action; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\scheduledTimeTrait; |
| | | |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class PlanAction extends OrganizeAction { |
| | | use scheduledTimeTrait; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Action; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class ScheduleAction extends PlanAction { |
| | | |
| | | } |
| New file |
| | |
| | | <?php |
| | | 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'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/OrganizeAction.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/PlanAction.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/ScheduleAction.php'); |
| | | |
| | | |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\answerExplanationTrait; |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\parentItemTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class Answer extends Comment |
| | | { |
| | | use parentItemTrait, answerExplanationTrait; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\downvoteCountTrait; |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\parentItemTrait; |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\sharedContentTrait; |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\upvoteCountTrait; |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class Comment extends CreativeWork |
| | | { |
| | | use downvoteCountTrait, parentItemTrait, sharedContentTrait, upvoteCountTrait; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\acceptedAnswerTrait; |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\answerCountTrait; |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\parentItemTrait; |
| | | use JVBase\inc\managers\SEO\render\Traits\_Properties\suggestedAnswerTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class Question extends Comment |
| | | { |
| | | use acceptedAnswerTrait, answerCountTrait, parentItemTrait, suggestedAnswerTrait; |
| | | } |
| New file |
| | |
| | | <?php |
| | | 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'); |
| | |
| | | * @var Date|DateTime Date (including time if available) when this media object was uploaded to this site. |
| | | */ |
| | | protected Date|DateTime $uploadDate; |
| | | public function setUploadDate(string|DateTime $date):void |
| | | { |
| | | if (is_string($date)) { |
| | | $date = new DateTime($date); |
| | | } |
| | | $this->uploadDate = $date; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | |
| | | namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\WebPage; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | |
| | | class CollectionPage extends WebPage |
| | | { |
| | | |
| | | } |
| New file |
| | |
| | | <?php |
| | | |
| | | namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | |
| | | class ImageGallery extends MediaGallery |
| | | { |
| | | |
| | | } |
| New file |
| | |
| | | <?php |
| | | |
| | | namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage; |
| | | |
| | | use JVBase\managers\SEO\render\Traits\_Properties\additionalPropertyTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | |
| | | class MediaGallery extends CollectionPage |
| | | { |
| | | use additionalPropertyTrait; |
| | | } |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/WebPage.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/AboutPage.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CheckoutPage.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/ContactPage.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/FAQPage.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/ItemPage.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebSite.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php'); |
| | |
| | | } |
| | | |
| | | class DayOfWeek extends Enumeration { |
| | | protected string $day; |
| | | protected string|array $day; |
| | | protected array $allowedDay = [ |
| | | 'monday' => 'https://schema.org/Monday', |
| | | 'tuesday' => 'https://schema.org/Tuesday', |
| | |
| | | 'sunday' => 'https://schema.org/Sunday', |
| | | 'publicHolidays' => 'https://schema.org/PublicHolidays' |
| | | ]; |
| | | public function setDay(array|string $day) { |
| | | if (!is_array($day)) { |
| | | $day = [$day]; |
| | | } |
| | | $day = array_filter($day, function($d) { |
| | | if (!array_key_exists(strtolower($d), $this->allowedDay)) { |
| | | error_log('Invalid day attempted: '.$d); |
| | | return false; |
| | | } |
| | | return true; |
| | | }); |
| | | if (empty($day)) { |
| | | return; |
| | | } |
| | | $this->day = count($day) === 1 ? $day[0] : $day; |
| | | } |
| | | |
| | | } |
| File was renamed from inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Thing\Intangible; |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Intangible\ItemList\ItemList; |
| | | |
| | |
| | | <?php |
| | | 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'); |
| | |
| | | |
| | | namespace JVBase\managers\SEO\render\Thing\Intangible\StructuredValue; |
| | | |
| | | use JVBase\managers\SEO\render\DataType\DateTime; |
| | | use JVBase\managers\SEO\render\DataType\Time; |
| | | use JVBase\managers\SEO\render\Thing\Action; |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\WebSite; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\VirtualLocation; |
| | | use JVBase\managers\SEO\render\Thing\Place\Place; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\interactionServiceTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\interactionTypeTrait; |
| | |
| | | use JVBase\managers\SEO\render\Traits\_Properties\minPriceTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\priceCurrencyTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\priceTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\unitTextTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\validFromTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\validThroughTrait; |
| | | |
| | |
| | | class PriceSpecification extends StructuredValue { |
| | | use eligibleQuantityTrait, eligibleTransactionVolumeTrait, maxPriceTrait, |
| | | membershipPointsEarnedTrait, minPriceTrait, priceTrait, priceCurrencyTrait, |
| | | validFromTrait, validThroughTrait; |
| | | validFromTrait, validThroughTrait, unitTextTrait; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Thing\Intangible\StructuredValue; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PriceSpecification; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\unitTextTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class UnitPriceSpecification extends PriceSpecification { |
| | | use unitTextTrait; |
| | | //TODO And others. |
| | | } |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/QuantitativeValue.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/ServicePeriod.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/TypeAndQuantityNode.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/WarrantyPromise.php'); |
| | | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/LocationFeatureSpecification.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Offer.php'); |
| | | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/AggregateOffer.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/PaymentMethod.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Schedule.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Service.php'); |
| | |
| | | <?php |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action.php'); |
| | | |
| | | 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'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Event/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/_setup.php'); |
| | |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\SEO\render\DataType\Date; |
| | | use JVBase\managers\SEO\render\DataType\DateTime; |
| | | use JVBase\managers\SEO\render\DataType\Time; |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue; |
| | | use JVBase\managers\SEO\render\Thing\Thing; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\additionalTypeTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\alternateNameTrait; |
| | |
| | | { |
| | | global $wp; |
| | | $current = home_url( add_query_arg( $_GET, $wp->request ) ); |
| | | $id = (isset($this->id)) ? $this->id : $current.'#'.strtolower($this->getTypeName()); |
| | | $id = (isset($this->id)) ? $this->id : $current.'/#'.strtolower($this->getTypeName()); |
| | | $elements = array_map( |
| | | function ($value) { |
| | | |
| | |
| | | } else if (is_a($value, Time::class)) { |
| | | $value = $value->getTime(); |
| | | }else if (!is_string($value)) { |
| | | |
| | | error_log('Normal value? '.print_r($value, true)); |
| | | if (JVB_TESTING && !is_numeric($value)) { |
| | | error_log('Normal value? '.print_r($value, true)); |
| | | } |
| | | } |
| | | |
| | | return $value; |
| | |
| | | public function setId(string $id):void |
| | | { |
| | | if (!filter_var($id, FILTER_VALIDATE_URL)) { |
| | | error_log('[SEO]Could not set id: '.$id.'. Should be a valid URL'); |
| | | return; |
| | | global $wp; |
| | | $id = home_url( add_query_arg( $_GET, $wp->request ) ).'/#'.sanitize_title($id); |
| | | } |
| | | $this->id = $id; |
| | | } |
| | |
| | | unset($this->$property); |
| | | } |
| | | } |
| | | |
| | | public static function createImageFromID(int $ID):ImageObject|false |
| | | { |
| | | $cache = Cache::for('schemaImage')->connect('post'); |
| | | return $cache->remember( |
| | | $ID, |
| | | function() use($ID) { |
| | | $imagePost = get_post($ID); |
| | | if (!$imagePost) { |
| | | return false; |
| | | } |
| | | $image = wp_get_attachment_image_src($ID,'full'); |
| | | if (empty($image)) { |
| | | return false; |
| | | } |
| | | $imageObject = new ImageObject(); |
| | | $imageObject->setUploadDate($imagePost->post_date); |
| | | $imageObject->setWidth($image[1]); |
| | | $imageObject->setHeight($image[2]); |
| | | $imageObject->setContentUrl($image[0]); |
| | | |
| | | $caption = wp_get_attachment_caption($ID); |
| | | if (!empty($caption)) { |
| | | $imageObject->setCaption($caption); |
| | | } |
| | | |
| | | return $imageObject; |
| | | } |
| | | ); |
| | | |
| | | } |
| | | } |
| | |
| | | <?php |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php'); |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Helpers; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait arrayHelper { |
| | | protected function classArray(string $property, array $array, string $className):array |
| | | { |
| | | return array_filter($array, function ($item) use ($property, $className) { |
| | | return array_filter(array_map(function ($item) { |
| | | if (is_array($item) && array_key_exists('type', $item)) { |
| | | $item = SchemaHelper::classFromConfig($item); |
| | | } |
| | | return $item; |
| | | },$array), function ($item) use ($property, $className) { |
| | | $test = class_exists($className) && is_a($item, $className); |
| | | if (!$test) { |
| | | error_log('[SEO]Item property '.$property.' is not an instance of '.$className); |
| | |
| | | } |
| | | protected function mixedArray(string $property, array $array, array $allowedTypes):array |
| | | { |
| | | return array_filter($array, function($item) use ($property, $allowedTypes) { |
| | | |
| | | return array_filter(array_map(function($item) { |
| | | if (is_array($item) && array_key_exists('type', $item)) { |
| | | return JVB()->schemaHelper()::classFromConfig($item); |
| | | } else { |
| | | return $item; |
| | | } |
| | | },$array), function($item) use ($property, $allowedTypes) { |
| | | $test = in_array(gettype($item), $allowedTypes) || $this->testClasses($item, $allowedTypes); |
| | | if (!$test) { |
| | | error_log('[SEO]Item property '.$property.' is not an allowed type: '.print_r($item, true)); |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Helpers; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait enumerationHelper { |
| | | public function checkEnumArray(array $toCheck, string $property, mixed $class):array |
| | | { |
| | | if (!property_exists($class, $property)) { |
| | | error_log('[enumHelper]::checkEnumArray - invalid property of class: '.$property.', '.print_r($class, true)); |
| | | return []; |
| | | } |
| | | $toCheck = array_map(function($check) use ($property, $class){ |
| | | |
| | | }, $toCheck); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | 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'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/acceptedPaymentMethodTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/acceptsReservationsTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/accountablePersonTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/alternativeHeadlineTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/amenityFeatureTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/amountOfThisGoodTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/applicableCountryTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/areaServedTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/displayLocationTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/dissolutionDateTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/doorTimeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/dunsTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/durationOfWarrantyTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/durationTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/ownsTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/parentOrganizationTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/parentTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/participantTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/partySizeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/patternTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/reviewTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sameAsTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/saturatedFatContentTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/scheduleTimezoneTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sdDatePublishedTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sdLicenseTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/serviceTypeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/serviceUrlTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/servingSizeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/siblingTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/significantLinkTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sizeTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/subOrganizationTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/successorOfTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sugarContentTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/suitableForDietTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/superEventTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/targetTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/unitCodeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/unitTextTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/unsaturatedFatContentTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/urlTemplateTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/urlTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/userInteractionCountTrait.php'); |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\ItemList\ItemList; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait acceptedAnswerTrait { |
| | | /** |
| | | * @var Answer|ItemList The answer(s) that has been accepted as best, typically on a Question/Answer site. Sites vary in their selection mechanisms, e.g. drawing on community opinion and/or the view of the Question author. |
| | | */ |
| | | protected Answer|ItemList $acceptedAnswer; |
| | | |
| | | public function getAcceptedAnswer():Answer|ItemList|null |
| | | { |
| | | return $this->acceptedAnswer??null; |
| | | } |
| | | public function setAcceptedAnswer(Answer|ItemList $acceptedAnswer):void |
| | | { |
| | | $this->acceptedAnswer = $acceptedAnswer; |
| | | } |
| | | } |
| | |
| | | public function setAdditionalProperty(PropertyValue|array $additionalProperty):void |
| | | { |
| | | if (is_array($additionalProperty)) { |
| | | if (!is_numeric(array_key_first($additionalProperty))) { |
| | | $additionalProperty = [$additionalProperty]; |
| | | } |
| | | $additionalProperty = array_map(function($property) { |
| | | if (!array_key_exists('type', $property)) { |
| | | $property['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue'; |
| | | } |
| | | return $property; |
| | | }, $additionalProperty); |
| | | $additionalProperty = $this->classArray('additionalProperty', $additionalProperty, 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue'); |
| | | |
| | | $additionalProperty = array_filter($additionalProperty, function ($property) { |
| | | return property_exists($property, 'value') && !empty($property->value); |
| | | }); |
| | | } |
| | | $this->additionalProperty = $additionalProperty; |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | public function setAddress(PostalAddress|array|string $address):void |
| | | { |
| | | if (is_array($address)){ |
| | | $address = PostalAddress::fromArray($address); |
| | | if (!array_key_exists('type', $address)) { |
| | | $address['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress'; |
| | | } |
| | | $address = SchemaHelper::classFromConfig($address); |
| | | } |
| | | $this->address = $address; |
| | | } |
| | |
| | | 'addressLocality' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Address Locality', |
| | | 'hint' => 'The locality in which the street address is, and which is in the region. For example, "Park Allen".' |
| | | 'hint' => 'The locality in which the street address is, and which is in the region. For example, "Edmonton".' |
| | | ], |
| | | 'addressRegion' => [ |
| | | 'type' => 'text', |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | { |
| | | return $this->aggregateRating??null; |
| | | } |
| | | |
| | | public function setAggregateRating(array|AggregateRating $aggregateRating):void |
| | | { |
| | | if (is_array($aggregateRating)){ |
| | | $aggregateRating =AggregateRating::fromArray($aggregateRating); |
| | | if (!array_key_exists('type', $aggregateRating)) { |
| | | $aggregateRating['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating'; |
| | | } |
| | | $aggregateRating = SchemaHelper::classFromConfig($aggregateRating); |
| | | } |
| | | $this->aggregateRating = $aggregateRating; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait answerCountTrait { |
| | | /** |
| | | * @var int The number of answers this question, answer or comment has received from the community. |
| | | */ |
| | | protected int $answerCount; |
| | | |
| | | public function getAnswerCount():?int |
| | | { |
| | | return $this->answerCount??null; |
| | | } |
| | | public function setAnswerCount(int $answerCount):void |
| | | { |
| | | $this->answerCount = $answerCount; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Comment; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait answerExplanationTrait { |
| | | /** |
| | | * @var Comment A step-by-step or full explanation about Answer. Can outline how this Answer was achieved or contain more broad clarification or statement about it. |
| | | * TODO Can also be "WebContent", not included yet |
| | | */ |
| | | protected Comment $answerExplanation; |
| | | |
| | | public function getAnswerExplanation():?Comment |
| | | { |
| | | return $this->answerExplanation??null; |
| | | } |
| | | public function setAnswerExplanation(Comment $answerExplanation):void |
| | | { |
| | | $this->answerExplanation = $answerExplanation; |
| | | } |
| | | } |
| | |
| | | { |
| | | return $this->closes??null; |
| | | } |
| | | public function setCloses(Time $closes):void |
| | | public function setCloses(Time|string $closes):void |
| | | { |
| | | if (is_string($closes)) { |
| | | $closes = new Time($closes); |
| | | } |
| | | $this->closes = $closes; |
| | | } |
| | | } |
| | |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\DayOfWeek; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\enumerationHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait dayOfWeekTrait { |
| | | use enumerationHelper; |
| | | /** |
| | | * @var DayOfWeek The day of the week from which these opening hours are valid |
| | | * @var DayOfWeek|array The day of the week from which these opening hours are valid |
| | | */ |
| | | protected DayOfWeek $dayOfWeek; |
| | | protected DayOfWeek|array $dayOfWeek; |
| | | |
| | | public function getDayOfWeek():?DayOfWeek |
| | | public function getDayOfWeek():DayOfWeek|array|null |
| | | { |
| | | return $this->dayOfWeek??null; |
| | | } |
| | | public function setDayOfWeek(DayOfWeek $dayOfWeek):void |
| | | public function setDayOfWeek(DayOfWeek|array $dayOfWeek):void |
| | | { |
| | | if (is_array($dayOfWeek)) { |
| | | |
| | | } |
| | | $this->dayOfWeek = $dayOfWeek; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait downvoteCountTrait { |
| | | /** |
| | | * @var int The number of downvotes this question, answer or comment has received from the community. |
| | | */ |
| | | protected int $downvoteCount; |
| | | |
| | | public function getDownvoteCount():?int |
| | | { |
| | | return $this->downvoteCount??null; |
| | | } |
| | | public function setDownvoteCount(int $downvoteCount):void |
| | | { |
| | | $this->downvoteCount = $downvoteCount; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Organization\Organization; |
| | | use JVBase\managers\SEO\render\Thing\Person\Person; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\GeoCoordinates; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\GeoShape; |
| | | |
| | |
| | | { |
| | | return $this->geo??null; |
| | | } |
| | | public function setGeo(GeoCoordinates|GeoShape $geo):void |
| | | |
| | | public function setGeo(GeoCoordinates|GeoShape|array $geo):void |
| | | { |
| | | if (is_array($geo)) { |
| | | if(!array_key_exists('type', $geo)) { |
| | | $geo['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\GeoCoordinates'; |
| | | } |
| | | $geo = SchemaHelper::classFromConfig($geo); |
| | | } |
| | | $this->geo = $geo; |
| | | } |
| | | public function getGeoFieldConfig():array |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Intangible\OfferCatalog; |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | { |
| | | return $this->hasOfferCatalog??null; |
| | | } |
| | | public function setHasOfferCatalog(OfferCatalog $hasOfferCatalog):void |
| | | public function setHasOfferCatalog(OfferCatalog|array $hasOfferCatalog):void |
| | | { |
| | | if (is_array($hasOfferCatalog)){ |
| | | if (array_key_exists('generate', $hasOfferCatalog) && $hasOfferCatalog['generate']) { |
| | | $this->generateOfferCatalog($hasOfferCatalog); |
| | | } else { |
| | | if (!array_key_exists('type', $hasOfferCatalog)) { |
| | | $hasOfferCatalog['type'] = 'JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog'; |
| | | } |
| | | $items = []; |
| | | if (array_key_exists('items', $hasOfferCatalog)) { |
| | | $items = $hasOfferCatalog['items']; |
| | | unset($hasOfferCatalog['items']); |
| | | } |
| | | $hasOfferCatalog = SchemaHelper::classFromConfig($hasOfferCatalog); |
| | | if (!empty($items)) { |
| | | $hasOfferCatalog->setItemListElement($items); |
| | | } |
| | | } |
| | | } |
| | | $this->hasOfferCatalog = $hasOfferCatalog; |
| | | } |
| | | |
| | |
| | | 'fields' => [ |
| | | 'generate' => [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Generate from Content type?' |
| | | 'label' => 'Generate from Content type?', |
| | | 'default' => false, |
| | | ], |
| | | 'content_type' => [ |
| | | 'type' => 'checkbox', |
| | |
| | | 'description' => [ |
| | | 'type' => 'textarea', |
| | | 'label' => 'Description' |
| | | ], |
| | | 'price' => [ |
| | | 'type' => 'number', |
| | | 'label' => 'Price' |
| | | ] |
| | | ] |
| | | ] |
| | |
| | | } |
| | | trait heightTrait { |
| | | /** |
| | | * @var Distance|QuantitativeValue The height of the item. |
| | | * @var Distance|QuantitativeValue|string The height of the item. |
| | | */ |
| | | protected Distance|QuantitativeValue $height; |
| | | protected Distance|QuantitativeValue|string $height; |
| | | |
| | | public function getHeight():Distance|QuantitativeValue|null |
| | | public function getHeight():Distance|QuantitativeValue|string|null |
| | | { |
| | | return $this->height??null; |
| | | } |
| | | public function setHeight(Distance|QuantitativeValue $height):void |
| | | public function setHeight(Distance|QuantitativeValue|string $height):void |
| | | { |
| | | $this->height = $height; |
| | | } |
| | |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait imageTrait { |
| | | use arrayHelper; |
| | | /** |
| | | * @var string|ImageObject Can be a URL, or a fully described ImageObject |
| | | * @var string|ImageObject|array Can be a URL, or a fully described ImageObject |
| | | */ |
| | | protected string|ImageObject $image; |
| | | protected string|ImageObject|array $image; |
| | | |
| | | public function getImage():?string |
| | | { |
| | | return $this->image??null; |
| | | } |
| | | public function setImage(string|ImageObject $image):void |
| | | public function setImage(string|ImageObject|array $image):void |
| | | { |
| | | if (is_array($image)) { |
| | | $image = $this->classArray('image', $image, 'JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject'); |
| | | } |
| | | $this->image = $image; |
| | | } |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Action; |
| | | use JVBase\inc\managers\SEO\render\Thing\Action\Action; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | } |
| | | public function setLogo(string|ImageObject $logo):void |
| | | { |
| | | if (is_string($logo) && is_numeric($logo)) { |
| | | $logo = self::createImageFromID($logo); |
| | | } |
| | | $this->logo = $logo; |
| | | } |
| | | |
| | |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Thing; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait mainEntityTrait { |
| | | use arrayHelper; |
| | | /** |
| | | * @var Thing Indicates the primary entity described in some page or other CreativeWork. |
| | | * @var Thing|array Indicates the primary entity described in some page or other CreativeWork. |
| | | * Inverse property: mainEntityOfPage |
| | | */ |
| | | protected Thing $mainEntity; |
| | | protected Thing|array $mainEntity; |
| | | |
| | | public function getMainEntity():?Thing |
| | | public function getMainEntity():Thing|array|null |
| | | { |
| | | return $this->mainEntity??null; |
| | | } |
| | | public function setMainEntity(Thing $mainEntity):void |
| | | public function setMainEntity(Thing|array $mainEntity):void |
| | | { |
| | | if (is_array($mainEntity)) { |
| | | $mainEntity = $this->classArray('mainEntity', $mainEntity, 'JVBase\managers\SEO\render\Thing\Thing'); |
| | | } |
| | | $this->mainEntity = $mainEntity; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait openingHoursSpecificationTrait { |
| | | use arrayHelper; |
| | | /** |
| | | * @var OpeningHoursSpecification The opening hours of a certain place. |
| | | * @var OpeningHoursSpecification|array The opening hours of a certain place. |
| | | */ |
| | | protected OpeningHoursSpecification $openingHoursSpecification; |
| | | protected OpeningHoursSpecification|array $openingHoursSpecification; |
| | | |
| | | public function getOpeningHoursSpecification():?OpeningHoursSpecification |
| | | public function getOpeningHoursSpecification():OpeningHoursSpecification|array|null |
| | | { |
| | | return $this->openingHoursSpecification??null; |
| | | } |
| | | public function setOpeningHoursSpecification(OpeningHoursSpecification $openingHoursSpecification):void |
| | | public function setOpeningHoursSpecification(OpeningHoursSpecification|array $openingHoursSpecification):void |
| | | { |
| | | if (is_array($openingHoursSpecification)) { |
| | | if (array_key_exists('dayOfWeek', $openingHoursSpecification)) { |
| | | if (!array_key_exists('type', $openingHoursSpecification)) { |
| | | $openingHoursSpecification['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification'; |
| | | } |
| | | $openingHoursSpecification = SchemaHelper::classFromConfig($openingHoursSpecification); |
| | | } else { |
| | | $out = []; |
| | | foreach ($openingHoursSpecification as $hours) { |
| | | if (!array_key_exists('type', $hours)){ |
| | | $hours['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification'; |
| | | } |
| | | $out[] = SchemaHelper::classFromConfig($hours); |
| | | } |
| | | $openingHoursSpecification = $out; |
| | | } |
| | | } |
| | | $this->openingHoursSpecification = $openingHoursSpecification; |
| | | } |
| | | |
| | |
| | | { |
| | | return $this->opens??null; |
| | | } |
| | | public function setOpens(Time $opens):void |
| | | public function setOpens(Time|string $opens):void |
| | | { |
| | | if (is_string($opens)) { |
| | | $opens = new Time($opens); |
| | | } |
| | | $this->opens = $opens; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Comment; |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait parentItemTrait { |
| | | use arrayHelper; |
| | | /** |
| | | * @var Comment|CreativeWork|array The parent of a question, answer or item in general. Typically used for Q/A discussion threads e.g. a chain of comments with the first comment being an Article or other CreativeWork. See also comment which points from something to a comment about it. |
| | | */ |
| | | protected Comment|CreativeWork|array $parentItem; |
| | | |
| | | public function getParent():Comment|CreativeWork|array|null |
| | | { |
| | | return $this->parentItem??null; |
| | | } |
| | | public function setParent(Comment|CreativeWork|array $parentItem):void |
| | | { |
| | | if (is_array($parentItem)) { |
| | | $parentItem = $this->mixedArray('parentItem', $parentItem, [ |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork', |
| | | 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Comment' |
| | | ]); |
| | | } |
| | | $this->parentItem = $parentItem; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Action; |
| | | use JVBase\inc\managers\SEO\render\Thing\Action\Action; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | public function setPotentialAction(Action|array $potentialAction):void |
| | | { |
| | | if (is_array($potentialAction)) { |
| | | $potentialAction = $this->classArray('potentialAction', $potentialAction, 'JVBase\managers\SEO\render\Thing\Action'); |
| | | $potentialAction = $this->classArray('potentialAction', $potentialAction, 'JVBase\inc\managers\SEO\render\Thing\Action\Action'); |
| | | $potentialAction = array_map( |
| | | function ($item) { |
| | | $target = $item->getTarget(); |
| | | $url = $target->getUrlTemplate(); |
| | | $target->setId($url.'#entrypoint'); |
| | | |
| | | return $item; |
| | | }, |
| | | $potentialAction |
| | | ); |
| | | if (empty($potentialAction)) { |
| | | return; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\DataType\Time; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait scheduledTimeTrait { |
| | | /** |
| | | * @var Time The opening hour of the place or service on the given day(s) of the week |
| | | */ |
| | | protected Time $scheduledTime; |
| | | |
| | | public function getScheduledTime():?Time |
| | | { |
| | | return $this->scheduledTime??null; |
| | | } |
| | | public function setScheduledTime(Time|string $scheduledTime):void |
| | | { |
| | | if (is_string($scheduledTime)) { |
| | | $scheduledTime = new Time($scheduledTime); |
| | | } |
| | | $this->scheduledTime = $scheduledTime; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork; |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait sharedContentTrait { |
| | | use arrayHelper; |
| | | /** |
| | | * @var CreativeWork|array A CreativeWork such as an image, video, or audio clip shared as part of this posting. |
| | | */ |
| | | protected CreativeWork|array $shared_content; |
| | | |
| | | public function getSharedContent():CreativeWork|array|null |
| | | { |
| | | return $this->shared_content??null; |
| | | } |
| | | public function setSharedContent(CreativeWork|array $sharedContent):void |
| | | { |
| | | if (is_array($sharedContent)) { |
| | | $sharedContent = $this->classArray('sharedContent', $sharedContent, 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork'); |
| | | } |
| | | $this->shared_content = $sharedContent; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\ItemList\ItemList; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait suggestedAnswerTrait { |
| | | /** |
| | | * @var Answer|ItemList The answer(s) that has been suggested as best, typically on a Question/Answer site. Sites vary in their selection mechanisms, e.g. drawing on community opinion and/or the view of the Question author. |
| | | */ |
| | | protected string $suggestedAnswer; |
| | | |
| | | public function getSuggestedAnswer():?string |
| | | { |
| | | return $this->suggestedAnswer??null; |
| | | } |
| | | public function setSuggestedAnswer(string $suggestedAnswer):void |
| | | { |
| | | $this->suggestedAnswer = $suggestedAnswer; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\EntryPoint; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | } |
| | | trait targetTrait { |
| | | /** |
| | | * @var EntryPoint|string Indicates a target EntryPoint, or url, for an Action. |
| | | * @var EntryPoint Indicates a target EntryPoint, or url, for an Action. |
| | | */ |
| | | protected EntryPoint|string $description; |
| | | protected EntryPoint $target; |
| | | |
| | | public function getDescription():EntryPoint|string|null |
| | | public function getTarget():EntryPoint|null |
| | | { |
| | | return $this->description??null; |
| | | return $this->target??null; |
| | | } |
| | | public function setDescription(EntryPoint|string $description):void |
| | | public function setTarget(EntryPoint|string $target):void |
| | | { |
| | | $this->description = $description; |
| | | if (is_string($target)) { |
| | | if (str_starts_with($target, '/')) { |
| | | $target = home_url().$target; |
| | | } |
| | | $temp = [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\Intangible\EntryPoint', |
| | | 'urlTemplate' => $target |
| | | ]; |
| | | $target = SchemaHelper::classFromConfig($temp); |
| | | } |
| | | $this->target = $target; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\managers\SEO\render\Traits\_Properties; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait upvoteCountTrait { |
| | | /** |
| | | * @var int The number of upvotes this question, answer or comment has received from the community. |
| | | */ |
| | | protected int $upvoteCount; |
| | | |
| | | public function getUpvoteCount():?int |
| | | { |
| | | return $this->upvoteCount??null; |
| | | } |
| | | public function setUpvoteCount(int $upvoteCount):void |
| | | { |
| | | $this->upvoteCount = $upvoteCount; |
| | | } |
| | | } |
| | |
| | | } |
| | | trait widthTrait { |
| | | /** |
| | | * @var Distance|QuantitativeValue The width of the item. |
| | | * @var Distance|QuantitativeValue|string The width of the item. |
| | | */ |
| | | protected Distance|QuantitativeValue $width; |
| | | protected Distance|QuantitativeValue|string $width; |
| | | |
| | | public function getWidth():Distance|QuantitativeValue|null |
| | | public function getWidth():Distance|QuantitativeValue|string|null |
| | | { |
| | | return $this->width??null; |
| | | } |
| | | public function setWidth(Distance|QuantitativeValue $width):void |
| | | public function setWidth(Distance|QuantitativeValue|string $width):void |
| | | { |
| | | $this->width = $width; |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use Exception; |
| | | use WP_Error; |
| | | use WP_Post; |
| | | use WP_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | |
| | | |
| | | class TaxonomyRelationships |
| | | { |
| | | private string $table_name; |
| | | protected object $cache; |
| | | |
| | | protected Cache $cache; |
| | | protected CustomTable $relationships; |
| | | |
| | | public function __construct() |
| | | { |
| | | global $wpdb; |
| | | $this->table_name = $wpdb->prefix . BASE.'taxonomy_relationships'; |
| | | $this->defineTables(); |
| | | |
| | | $this->cache = Cache::for('term_relationship', WEEK_IN_SECONDS); |
| | | $this->cache->connect('terms'); |
| | | |
| | | // Ensure the table exists |
| | | // $this->create_table_if_not_exists(); |
| | |
| | | add_action('init', [$this, 'init']); |
| | | } |
| | | |
| | | /** |
| | | * @return string |
| | | */ |
| | | public function getTableName():string |
| | | { |
| | | return $this->table_name; |
| | | } |
| | | protected function defineTables():void |
| | | { |
| | | $relationships = CustomTable::for('taxonomy_relationships'); |
| | | |
| | | $relationships->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'term_id' => "{$relationships->getTermIDType()} NOT NULL", |
| | | 'related_term_id' => "{$relationships->getTermIDType()} NOT NULL", |
| | | 'taxonomy' => 'varchar(32) NOT NULL', |
| | | 'related_taxonomy' => 'varchar(32) NOT NULL', |
| | | 'post_count' => 'int(11) NOT NULL DEFAULT 0', |
| | | 'is_direct' => 'tinyint(1) NOT NULL DEFAULT 1', |
| | | 'is_hierarchical' => 'tinyint(1) NOT NULL DEFAULT 0', |
| | | 'last_updated' => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]); |
| | | |
| | | $relationships->setKeys([ |
| | | ['key'=>'PRIMARY', 'value' => '(`id`)'], |
| | | ['key'=>'UNIQUE', 'value' => '`relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`)'], |
| | | '`term_id` (`term_id`)', |
| | | '`related_term_id` (`related_term_id`)', |
| | | '`taxonomy` (`taxonomy`)', |
| | | '`related_taxonomy` (`related_taxonomy`)' |
| | | ]); |
| | | $base = BASE; |
| | | $relationships->setConstraints([ |
| | | "CONSTRAINT {$base}tax_rel_term_id FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$relationships->getTermTable()}` (`term_id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}tax_rel_related_id` FOREIGN KEY (`related_term_id`) |
| | | REFERENCES `{$relationships->getTermTable()}` (`term_id`) ON DELETE CASCADE" |
| | | ]); |
| | | $relationships->defineTable(); |
| | | |
| | | $this->relationships = $relationships; |
| | | } |
| | | |
| | | /** |
| | | * Hook into term and post saves |
| | |
| | | */ |
| | | public function updatePostRelationships(int $post_id, WP_Post $post):void |
| | | { |
| | | // Skip autosaves and revisions |
| | | if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) { |
| | | return; |
| | | } |
| | | $post_type = $post->post_type; |
| | | if (in_array($post_type, jvbIgnoredPostTypes())) { |
| | | return; |
| | |
| | | */ |
| | | public function updateRelationship(int $term_id, int $related_term_id, string $taxonomy, string $related_taxonomy):bool |
| | | { |
| | | global $wpdb; |
| | | |
| | | // Make sure both terms exist |
| | | $term = get_term($term_id, $taxonomy); |
| | | $related_term = get_term($related_term_id, $related_taxonomy); |
| | |
| | | return false; |
| | | } |
| | | |
| | | // Ensure term_id and related_term_id are integers |
| | | $term_id = (int)$term_id; |
| | | $related_term_id = (int)$related_term_id; |
| | | // Check if term is parent of related term |
| | | $is_hierarchical = 0; |
| | | if ($taxonomy === $related_taxonomy) { |
| | | $ancestors = get_ancestors($related_term_id, $taxonomy, 'taxonomy'); |
| | | if (in_array($term_id, $ancestors)) { |
| | | $is_hierarchical = 1; |
| | | } |
| | | } |
| | | // Calculate number of shared posts |
| | | $shared_posts_count = $this->countSharedPosts($term_id, $related_term_id, $taxonomy, $related_taxonomy); |
| | | $updated = $this->relationships->findOrCreate([ |
| | | 'term_id' => $term_id, |
| | | 'related_term_id' => $related_term_id, |
| | | 'taxonomy' => $taxonomy, |
| | | 'related_taxonomy' => $related_taxonomy |
| | | ],[ |
| | | 'is_hierarchical' => $is_hierarchical, |
| | | 'post_count' => $shared_posts_count |
| | | ]); |
| | | |
| | | // Check if relationship exists |
| | | $existing = $wpdb->get_row($wpdb->prepare( |
| | | "SELECT id, post_count FROM {$this->table_name} |
| | | WHERE term_id = %d AND related_term_id = %d |
| | | AND taxonomy = %s AND related_taxonomy = %s", |
| | | $term_id, |
| | | $related_term_id, |
| | | $taxonomy, |
| | | $related_taxonomy |
| | | )); |
| | | |
| | | // Calculate number of shared posts |
| | | $shared_posts_count = $this->countSharedPosts($term_id, $related_term_id, $taxonomy, $related_taxonomy); |
| | | |
| | | // Check if term is parent of related term |
| | | $is_hierarchical = 0; |
| | | if ($taxonomy === $related_taxonomy) { |
| | | $ancestors = get_ancestors($related_term_id, $taxonomy, 'taxonomy'); |
| | | if (in_array($term_id, $ancestors)) { |
| | | $is_hierarchical = 1; |
| | | } |
| | | } |
| | | |
| | | if ($existing) { |
| | | // Update existing relationship |
| | | $wpdb->update( |
| | | $this->table_name, |
| | | [ |
| | | 'post_count' => $shared_posts_count, |
| | | 'is_hierarchical' => $is_hierarchical |
| | | ], |
| | | [ |
| | | 'id' => $existing->id |
| | | ] |
| | | ); |
| | | } else { |
| | | // Insert new relationship |
| | | $wpdb->insert( |
| | | $this->table_name, |
| | | [ |
| | | 'term_id' => $term_id, |
| | | 'related_term_id' => $related_term_id, |
| | | 'taxonomy' => $taxonomy, |
| | | 'related_taxonomy' => $related_taxonomy, |
| | | 'post_count' => $shared_posts_count, |
| | | 'is_hierarchical' => $is_hierarchical |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | return true; |
| | | return (bool)$updated; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | private function countSharedPosts(int $term_id, int $related_term_id, string $taxonomy, string $related_taxonomy):int |
| | | { |
| | | global $wpdb; |
| | | |
| | | $term_posts = $wpdb->get_col($wpdb->prepare( |
| | | "SELECT object_id FROM {$wpdb->term_relationships} tr |
| | | $term_posts = $this->cache->remember( |
| | | $this->cache->generateKey(['term' => $term_id, 'taxonomy' => $taxonomy]), |
| | | function () use ($term_id, $taxonomy) { |
| | | global $wpdb; |
| | | return $wpdb->get_col($wpdb->prepare( |
| | | "SELECT object_id FROM {$wpdb->term_relationships} tr |
| | | JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id |
| | | WHERE tt.term_id = %d AND tt.taxonomy = %s", |
| | | $term_id, |
| | | $taxonomy |
| | | )); |
| | | $term_id, |
| | | $taxonomy |
| | | )); |
| | | } |
| | | ); |
| | | |
| | | $related_term_posts = $wpdb->get_col($wpdb->prepare( |
| | | "SELECT object_id FROM {$wpdb->term_relationships} tr |
| | | |
| | | $related_term_posts = $this->cache->remember( |
| | | $this->cache->generateKey(['term' => $related_term_id, 'taxonomy' => $related_taxonomy]), |
| | | function () use($related_term_id, $related_taxonomy) { |
| | | global $wpdb; |
| | | return $wpdb->get_col($wpdb->prepare( |
| | | "SELECT object_id FROM {$wpdb->term_relationships} tr |
| | | JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id |
| | | WHERE tt.term_id = %d AND tt.taxonomy = %s", |
| | | $related_term_id, |
| | | $related_taxonomy |
| | | )); |
| | | $related_term_id, |
| | | $related_taxonomy |
| | | )); |
| | | }); |
| | | |
| | | |
| | | return count(array_intersect($term_posts, $related_term_posts)); |
| | | } |
| | |
| | | public function getRelatedTerms(int $term_id, string $desired_taxonomy, bool $include_hierarchical = true):array |
| | | { |
| | | |
| | | $key = sprintf( |
| | | '%d_to_%s', |
| | | $term_id, |
| | | $desired_taxonomy |
| | | ); |
| | | if ($include_hierarchical) { |
| | | $key.='_hierarchy'; |
| | | } |
| | | $cache = $this->cache->get($key); |
| | | if ($cache) { |
| | | return $cache; |
| | | } |
| | | |
| | | global $wpdb; |
| | | |
| | | $term = get_term($term_id); |
| | | if (is_wp_error($term) || empty($term)) { |
| | | return []; |
| | | } |
| | | |
| | | $query = $wpdb->prepare( |
| | | "SELECT related_term_id |
| | | FROM {$this->table_name} |
| | | WHERE term_id = %d |
| | | AND related_taxonomy = %s", |
| | | $term_id, |
| | | $desired_taxonomy |
| | | ); |
| | | |
| | | // Optionally include hierarchical relationships |
| | | if (!$include_hierarchical) { |
| | | $query .= " AND is_hierarchical = 0"; |
| | | } |
| | | |
| | | // Order by post count for popularity |
| | | $query .= " ORDER BY post_count DESC"; |
| | | |
| | | $related_term_ids = $wpdb->get_col($query); |
| | | |
| | | $this->cache->set($key, $related_term_ids); |
| | | |
| | | return $related_term_ids; |
| | | $where = [ |
| | | 'term_id' => $term_id, |
| | | 'related_taxonomy' => $desired_taxonomy |
| | | ]; |
| | | if (!$include_hierarchical) { |
| | | $where['is_hierarchical'] = 0; |
| | | } |
| | | return $this->relationships->pluck('related_term_id', $where, 'post_count', 'DESC'); |
| | | } |
| | | |
| | | // Get all related terms based on multiple source terms (for "any" match) |
| | |
| | | */ |
| | | public function getAnyRelatedTerms(array $term_ids, string $taxonomy):array |
| | | { |
| | | if (empty($term_ids)) { |
| | | return []; |
| | | } |
| | | $term_ids = array_filter(array_map('absint', $term_ids)); |
| | | return array_unique($this->relationships->pluck('related_term_id', ['term_id' => ['IN' => $term_ids], 'related_taxonomy' => $taxonomy], 'post_count', 'DESC')); |
| | | |
| | | $related_term_ids = []; |
| | | foreach ($term_ids as $term_id) { |
| | | $related = $this->getRelatedTerms($term_id, $taxonomy); |
| | | $related_term_ids = array_merge($related_term_ids, $related); |
| | | } |
| | | |
| | | return array_unique($related_term_ids); |
| | | } |
| | | |
| | | // Get related terms that are common to all source terms (for "all" match) |
| | | |
| | | /** |
| | | * @param array $term_ids |
| | | * @param string $taxonomy |
| | | * |
| | | * @return array |
| | | */ |
| | | public function getAllRelatedTerms(array $term_ids, string $taxonomy):array |
| | | { |
| | | if (empty($term_ids)) { |
| | | return []; |
| | | } |
| | | |
| | | $related_sets = []; |
| | | foreach ($term_ids as $term_id) { |
| | | $related = $this->getRelatedTerms($term_id, $taxonomy); |
| | | if (!empty($related)) { |
| | | $related_sets[] = $related; |
| | | } |
| | | } |
| | | |
| | | if (count($related_sets) === 1) { |
| | | return $related_sets[0]; |
| | | } elseif (count($related_sets) > 1) { |
| | | // Find intersection of all sets |
| | | $result = array_intersect(...$related_sets); |
| | | return array_values($result); |
| | | } |
| | | |
| | | return []; |
| | | } |
| | | |
| | | // Rebuild all relationships (useful for initial setup) |
| | |
| | | global $wpdb; |
| | | |
| | | // Clear existing relationships |
| | | $wpdb->query("TRUNCATE TABLE {$this->table_name}"); |
| | | $total_posts = $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish'"); |
| | | $wpdb->query("TRUNCATE TABLE {$this->relationships->getFullTableName()}"); |
| | | |
| | | // Calculate number of batches needed (50 posts per batch) |
| | | $batch_size = 50; |
| | | $total_batches = ceil($total_posts / $batch_size); |
| | | $posts = new WP_Query([ |
| | | 'post_status' => 'publish', |
| | | 'posts_per_page'=> -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | if (!$posts->have_posts()) { |
| | | wp_reset_postdata(); |
| | | return true; |
| | | } |
| | | |
| | | $ids = $posts->posts; |
| | | wp_reset_postdata(); |
| | | |
| | | |
| | | // Queue the operation |
| | | $queue = JVB()->queue(); |
| | | $operation_id = 'taxonomy_rebuild_' . uniqid(); |
| | | |
| | | $queue->queueOperation( |
| | | 'taxonomy_relationships', |
| | | 1, |
| | | [ |
| | | 'action' => 'rebuild_all', |
| | | 'offset' => 0, |
| | | 'limit' => $batch_size, |
| | | 'total_posts' => $total_posts, |
| | | 'total_batches' => $total_batches |
| | | 'posts' => $ids, |
| | | ], |
| | | [ |
| | | 'operation_id' => $operation_id, |
| | | 'count' => $total_batches, |
| | | 'priority' => 'normal', |
| | | 'chunk_key' => 'posts', |
| | | 'chunk_size' => 50 |
| | | ] |
| | | ); |
| | | |
| | |
| | | return $result; |
| | | } |
| | | |
| | | if (isset($data['action']) && $data['action'] === 'rebuild_all') { |
| | | // Clear existing relationships |
| | | global $wpdb; |
| | | |
| | | // Get batch of posts to process |
| | | $offset = isset($data['offset']) ? $data['offset'] : 0; |
| | | $limit = isset($data['limit']) ? $data['limit'] : 50; // Process in batches |
| | | |
| | | $posts = $wpdb->get_col($wpdb->prepare( |
| | | "SELECT ID FROM {$wpdb->posts} |
| | | WHERE post_status = 'publish' |
| | | ORDER BY ID |
| | | LIMIT %d OFFSET %d", |
| | | $limit, |
| | | $offset |
| | | )); |
| | | |
| | | // Process each post |
| | | foreach ($posts as $post_id) { |
| | | $this->updatePostRelationships($post_id); |
| | | } |
| | | |
| | | // Get total number of posts |
| | | $total_posts = $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish'"); |
| | | |
| | | // Return progress information |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'processed' => count($posts), |
| | | 'offset' => $offset, |
| | | 'next_offset' => $offset + $limit, |
| | | 'total' => $total_posts, |
| | | 'completed' => ($offset + $limit >= $total_posts) |
| | | ] |
| | | ]; |
| | | } |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => __('Hmmm.', 'jvb') |
| | | ]; |
| | | try { |
| | | foreach ($data['posts'] as $postID) { |
| | | $post = get_post($postID); |
| | | $this->updatePostRelationships($postID, $post); |
| | | } |
| | | } catch (Exception $e) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => $e->getMessage() |
| | | ]; |
| | | } |
| | | return [ |
| | | 'success' => true |
| | | ]; |
| | | } |
| | | |
| | | // Hook this to term deletion |
| | |
| | | */ |
| | | public function deleteTermRelationships(int $term_id):void |
| | | { |
| | | global $wpdb; |
| | | |
| | | $wpdb->query($wpdb->prepare( |
| | | "DELETE FROM {$this->table_name} |
| | | WHERE term_id = %d OR related_term_id = %d", |
| | | $term_id, |
| | | $term_id |
| | | )); |
| | | $termIDs = $this->relationships->delete(['term_id' => $term_id]); |
| | | $related = $this->relationships->delete(['related_term_id' => $term_id]); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | use WP_Error; |
| | | use Exception; |
| | | use WP_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | class UserTermsManager |
| | | { |
| | | private string $table_name; |
| | | private Cache $cache; |
| | | private string $cacheGroup = 'user_terms_'; |
| | | private int $ttl = DAY_IN_SECONDS; // 1 day default |
| | | protected \wpdb $wpdb; |
| | | |
| | | protected CustomTable $index; |
| | | |
| | | public function __construct() |
| | | { |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->table_name = $this->wpdb->prefix . BASE . 'user_term_index'; |
| | | $this->defineTables(); |
| | | $this->cache = Cache::for('term_ids')->connect('user'); |
| | | |
| | | // Register hooks |
| | | add_action('save_post', [$this, 'updatePostUserTerms'], 10, 3); |
| | | add_action('before_delete_post', [$this, 'removePostUserTerms']); |
| | | add_action('set_object_terms', [$this, 'handleTermAssignment'], 10, 6); |
| | | |
| | | // Add filter for bulk operation handling |
| | | add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3); |
| | | } |
| | | |
| | | public function defineTables():void |
| | | { |
| | | $userIndex = CustomTable::for('user_term_index'); |
| | | $userIndex->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$userIndex->getUserIDType()} NOT NULL", |
| | | 'term_id' => "{$userIndex->getTermIDType()} NOT NULL", |
| | | 'taxonomy' => 'varchar(32) NOT NULL', |
| | | 'post_count' => 'int(11) NOT NULL DEFAULT 1', |
| | | 'parent_id' => "{$userIndex->getTermIDType()} NOT NULL DEFAULT 0", |
| | | 'last_used' => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | $userIndex->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`user_term` (`user_id`, `term_id`, `taxonomy`)'], |
| | | '`user_taxonomy` (`user_id`, `taxonomy`)', |
| | | '`taxonomy` (`taxonomy`)', |
| | | '`user_id` (`user_id`)', |
| | | '`term_id` (`term_id`)', |
| | | '`parent_id` (`parent_id`)' |
| | | ]); |
| | | $base = BASE; |
| | | $userIndex->setConstraints([ |
| | | "CONSTRAINT `{$base}user_term_user_fk` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$userIndex->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}user_term_term_fk` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$userIndex->getTermTable()}` (`term_id`) ON DELETE CASCADE" |
| | | ]); |
| | | $userIndex->defineTable(); |
| | | $this->index = $userIndex; |
| | | } |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * @param string|null $taxonomy |
| | |
| | | $cache->flush(); |
| | | } |
| | | |
| | | // Update term usage when a post is saved |
| | | |
| | | /** |
| | | * @param int $post_id |
| | | * @param WP_Post $post |
| | | * @param bool $update |
| | | * |
| | | * @return void |
| | | */ |
| | | public function updatePostUserTerms(int $post_id, WP_Post $post, bool $update):void |
| | | { |
| | | // Skip autosaves and revisions |
| | | if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) { |
| | | return; |
| | | } |
| | | // SAFETY: Skip attachments and other non-content post types |
| | | if (in_array($post->post_type, jvbIgnoredPostTypes())) { |
| | | return; |
| | | } |
| | | |
| | | // Skip non-custom post types |
| | | $post_type = get_post_type($post); |
| | | if (!str_starts_with($post_type, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $user_id = $post->post_author; |
| | | |
| | | // Get all taxonomies for this post type |
| | | $taxonomies = get_object_taxonomies($post_type); |
| | | |
| | | foreach ($taxonomies as $taxonomy) { |
| | | $terms = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']); |
| | | |
| | | if (!is_wp_error($terms) && !empty($terms)) { |
| | | // Add the direct terms |
| | | foreach ($terms as $term_id) { |
| | | $this->updateUserTerm($user_id, $term_id, $taxonomy, false); |
| | | |
| | | // Check if taxonomy is hierarchical and add parent terms |
| | | if (is_taxonomy_hierarchical($taxonomy)) { |
| | | $this->addParentTerms($user_id, $term_id, $taxonomy); |
| | | } |
| | | } |
| | | } |
| | | $this->clearUserCache($user_id, $taxonomy); |
| | | } |
| | | } |
| | | |
| | | // Add all parent terms for a given term |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * @param int $term_id |
| | | * @param string $taxonomy |
| | | * |
| | | * @return void |
| | | */ |
| | | public function addParentTerms(int $user_id, int $term_id, string $taxonomy):void |
| | | { |
| | | // Get all ancestors (parent terms) |
| | | $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy'); |
| | | |
| | | if (!empty($ancestors)) { |
| | | foreach ($ancestors as $ancestor_id) { |
| | | $this->updateUserTerm($user_id, $ancestor_id, $taxonomy, true); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Handle term assignment changes |
| | | |
| | | /** |
| | | * Handle term assignment changes |
| | | * @param int $object_id |
| | | * @param array $terms |
| | | * @param array $tt_ids |
| | |
| | | } |
| | | |
| | | $user_id = $post->post_author; |
| | | $is_hierarchical = is_taxonomy_hierarchical($taxonomy); |
| | | |
| | | // Get added terms |
| | | $added_tt_ids = array_diff($tt_ids, $old_tt_ids); |
| | | $added_terms = []; |
| | | |
| | | if (!empty($added_tt_ids)) { |
| | | foreach ($added_tt_ids as $tt_id) { |
| | | $term_id = $this->getTermIDFromTTID($tt_id); |
| | | if ($term_id) { |
| | | $added_terms[] = $term_id; |
| | | } |
| | | } |
| | | |
| | | // Add or increment the new terms |
| | | foreach ($added_terms as $term_id) { |
| | | $this->updateUserTerm($user_id, $term_id, $taxonomy, false); |
| | | |
| | | // Add parent terms for hierarchical taxonomies |
| | | if ($is_hierarchical) { |
| | | $this->addParentTerms($user_id, $term_id, $taxonomy); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Handle removed terms |
| | | $removed_tt_ids = array_diff($old_tt_ids, $tt_ids); |
| | | $removed_terms = []; |
| | | |
| | | if (!empty($removed_tt_ids)) { |
| | | foreach ($removed_tt_ids as $tt_id) { |
| | | $term_id = $this->getTermIDFromTTID($tt_id); |
| | | if ($term_id) { |
| | | $removed_terms[] = $term_id; |
| | | } |
| | | } |
| | | |
| | | // Decrement removed terms |
| | | foreach ($removed_terms as $term_id) { |
| | | $this->decreaseUserTerm($user_id, $term_id, $taxonomy, false); |
| | | |
| | | // Handle parent terms for hierarchical taxonomies |
| | | if ($is_hierarchical) { |
| | | $this->handleParentTermRemoval($user_id, $term_id, $taxonomy); |
| | | } |
| | | } |
| | | } |
| | | $termIDs = array_unique(array_merge($tt_ids, $old_tt_ids)); |
| | | if (!empty($termIDs)) { |
| | | foreach ($termIDs as $termID) { |
| | | $term_id = $this->getTermIDFromTTID($termID); |
| | | $this->updateUserTerm($user_id, $term_id, $taxonomy); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Handles parent term removal logic |
| | | |
| | | /** |
| | | * Updates the entry for the user's term relationship |
| | | * @param int $user_id |
| | | * @param int $term_id |
| | | * @param string $taxonomy |
| | | * |
| | | * @return void |
| | | */ |
| | | public function handleParentTermRemoval(int $user_id, int $term_id, string $taxonomy):void |
| | | { |
| | | // Get parent terms |
| | | $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy'); |
| | | |
| | | if (!empty($ancestors)) { |
| | | foreach ($ancestors as $ancestor_id) { |
| | | // Check if this parent is still used by other terms |
| | | $still_needed = $this->isParentNeeded($user_id, $ancestor_id, $taxonomy); |
| | | |
| | | if (!$still_needed) { |
| | | $this->decreaseUserTerm($user_id, $ancestor_id, $taxonomy, true); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Check if a parent term is still needed by other terms |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * @param int $parent_term_id |
| | | * @param string $taxonomy |
| | | * |
| | | * @return bool |
| | | */ |
| | | private function isParentNeeded(int $user_id, int $parent_term_id, string $taxonomy):bool |
| | | public function updateUserTerm(int $user_id, int $term_id, string $taxonomy):bool |
| | | { |
| | | |
| | | |
| | | // Get all direct terms the user has |
| | | $direct_terms = $this->wpdb->get_col($this->wpdb->prepare( |
| | | "SELECT term_id FROM {$this->table_name} |
| | | WHERE user_id = %d |
| | | AND taxonomy = %s |
| | | AND is_parent = 0 |
| | | AND post_count > 0", |
| | | $user_id, |
| | | $taxonomy |
| | | )); |
| | | |
| | | if (empty($direct_terms)) { |
| | | return false; |
| | | } |
| | | |
| | | // Check if any of these terms have the parent as an ancestor |
| | | foreach ($direct_terms as $term_id) { |
| | | if ($term_id == $parent_term_id) { |
| | | continue; // Skip the term itself |
| | | } |
| | | |
| | | $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy'); |
| | | |
| | | if (in_array($parent_term_id, $ancestors)) { |
| | | return true; // This parent is needed |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | // Remove all term usage records for a post |
| | | |
| | | /** |
| | | * @param int $post_id |
| | | * |
| | | * @return void |
| | | */ |
| | | public function removePostUserTerms(int $post_id):void |
| | | { |
| | | $post = get_post($post_id); |
| | | |
| | | // Skip if not a valid post |
| | | if (!$post || !str_starts_with($post->post_type, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $user_id = $post->post_author; |
| | | $taxonomies = get_object_taxonomies($post->post_type); |
| | | |
| | | foreach ($taxonomies as $taxonomy) { |
| | | $terms = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']); |
| | | $is_hierarchical = is_taxonomy_hierarchical($taxonomy); |
| | | |
| | | if (!is_wp_error($terms) && !empty($terms)) { |
| | | foreach ($terms as $term_id) { |
| | | $this->decreaseUserTerm($user_id, $term_id, $taxonomy, false); |
| | | |
| | | // Handle parent terms for hierarchical taxonomies |
| | | if ($is_hierarchical) { |
| | | $this->handleParentTermRemoval($user_id, $term_id, $taxonomy); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Add or increment a user-term relationship |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * @param int $term_id |
| | | * @param string $taxonomy |
| | | * @param bool $is_parent |
| | | * |
| | | * @return bool |
| | | */ |
| | | public function updateUserTerm(int $user_id, int $term_id, string $taxonomy, bool $is_parent = false):bool |
| | | { |
| | | |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | |
| | | // Ensure the term exists |
| | | $term = get_term($term_id, $taxonomy); |
| | |
| | | return false; |
| | | } |
| | | |
| | | // Insert or update the record |
| | | $result = $this->wpdb->query($this->wpdb->prepare( |
| | | "INSERT INTO {$this->table_name} |
| | | (user_id, term_id, taxonomy, post_count, is_parent, last_used) |
| | | VALUES (%d, %d, %s, 1, %d, NOW()) |
| | | ON DUPLICATE KEY UPDATE |
| | | post_count = post_count + 1, |
| | | is_parent = IF(%d = 1, 1, is_parent), |
| | | last_used = NOW()", |
| | | $user_id, |
| | | $term_id, |
| | | $taxonomy, |
| | | $is_parent ? 1 : 0, |
| | | $is_parent ? 1 : 0 |
| | | )); |
| | | $posts = new WP_Query([ |
| | | 'post_author' => $user_id, |
| | | 'tax_query' => [ |
| | | [ |
| | | 'taxonomy' => $taxonomy, |
| | | 'terms' => $term_id, |
| | | ] |
| | | ], |
| | | 'post_status' => 'publish', |
| | | 'posts_per_page'=> -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | $userPosts = count($posts->posts); |
| | | |
| | | // Clear the cache for this user and taxonomy |
| | | $this->clearUserCache($user_id, $taxonomy); |
| | | $result = $this->index->findOrCreate([ |
| | | 'user_id' => $user_id, |
| | | 'term_id' => $term_id, |
| | | 'taxonomy' => $taxonomy |
| | | ],[ |
| | | 'parent_id' => $term->parent, |
| | | 'post_count'=> $userPosts |
| | | ]); |
| | | |
| | | return ($result !== false); |
| | | if ($term->parent > 0) { |
| | | $this->updateUserTerm($user_id, $term->parent, $taxonomy); |
| | | } |
| | | |
| | | return (bool) $result; |
| | | } |
| | | |
| | | // Decrement a user-term relationship |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * @param int $term_id |
| | | * @param string $taxonomy |
| | | * @param bool $is_parent |
| | | * |
| | | * @return bool |
| | | */ |
| | | public function decreaseUserTerm(int $user_id, int $term_id, string $taxonomy, bool $is_parent = false):bool |
| | | { |
| | | |
| | | |
| | | // Update the record, decrementing the counter |
| | | $this->wpdb->query($this->wpdb->prepare( |
| | | "UPDATE {$this->table_name} |
| | | SET post_count = GREATEST(post_count - 1, 0), |
| | | last_used = NOW() |
| | | WHERE user_id = %d |
| | | AND term_id = %d |
| | | AND taxonomy = %s", |
| | | $user_id, |
| | | $term_id, |
| | | $taxonomy |
| | | )); |
| | | |
| | | // Clean up zero-count records |
| | | $this->wpdb->query($this->wpdb->prepare( |
| | | "DELETE FROM {$this->table_name} |
| | | WHERE user_id = %d |
| | | AND term_id = %d |
| | | AND taxonomy = %s |
| | | AND post_count = 0", |
| | | $user_id, |
| | | $term_id, |
| | | $taxonomy |
| | | )); |
| | | |
| | | // Clear the cache for this user and taxonomy |
| | | $this->clearUserCache($user_id, $taxonomy); |
| | | |
| | | return true; |
| | | } |
| | | |
| | | // Helper function to get term_id from term_taxonomy_id |
| | | |
| | | /** |
| | | * Helper function to get term_id from term_taxonomy_id |
| | | * @param int $tt_id |
| | | * |
| | | * @return int |
| | | */ |
| | | private function getTermIDFromTTID(int $tt_id):int |
| | | { |
| | | |
| | | |
| | | return $this->wpdb->get_var($this->wpdb->prepare( |
| | | "SELECT term_id FROM {$this->wpdb->term_taxonomy} WHERE term_taxonomy_id = %d", |
| | | $tt_id |
| | | )); |
| | | global $wpdb; |
| | | return $wpdb->get_var($wpdb->prepare( |
| | | "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d", |
| | | $tt_id |
| | | )); |
| | | } |
| | | |
| | | // Rebuild the user terms index for all users |
| | | |
| | | /** |
| | | * Rebuild the user terms index for all users |
| | | * @return array |
| | | */ |
| | | public function rebuildAllUserIndex():array |
| | |
| | | |
| | | |
| | | // Clear existing index |
| | | $this->wpdb->query("TRUNCATE TABLE {$this->table_name}"); |
| | | global $wpdb; |
| | | $wpdb->query("TRUNCATE TABLE {$this->index->getFullTableName()}"); |
| | | |
| | | // Get all users with posts |
| | | $users = $this->wpdb->get_col($this->wpdb->prepare( |
| | | "SELECT DISTINCT post_author |
| | | FROM {$this->wpdb->posts} |
| | | WHERE post_status = 'publish' |
| | | AND post_type LIKE %s", |
| | | $this->wpdb->esc_like(BASE) . '%' |
| | | )); |
| | | $users = get_users([ |
| | | 'has_published_posts' => [array_map('jvbCheckBase', Registrar::getRegistered('post'))], |
| | | 'fields' => 'ID' |
| | | ]); |
| | | if (empty($users)) { |
| | | return [ |
| | | 'success' => true, |
| | | 'message' => 'No users found to update' |
| | | ]; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'rebuild_user_term_index', |
| | |
| | | 'users' => $users |
| | | ], |
| | | [ |
| | | 'count' => count($users), |
| | | 'chunk_key' => 'users', |
| | | 'chunk_size' => 5, |
| | | 'operation_id' => 'rebuild_user_terms_' . date('Y_m_d') |
| | |
| | | ]; |
| | | } |
| | | |
| | | // Rebuild the index for a specific user |
| | | |
| | | /** |
| | | * Rebuild the index for a specific user |
| | | * @param int $user_id |
| | | * |
| | | * @return array |
| | | */ |
| | | public function rebuildUserIndex(int $user_id):array |
| | | { |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'User does not exist' |
| | | ]; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'rebuild_user_term_index', |
| | | 0, |
| | |
| | | ], |
| | | [ |
| | | 'count' => 1, |
| | | 'operation_id' => 'rebuild_user_terms_' . date('Y_m_d') |
| | | 'operation_id' => 'rebuild_user_'.$user_id.'_terms_' . date('Y_m_d') |
| | | ] |
| | | ); |
| | | |
| | |
| | | } |
| | | try { |
| | | |
| | | $results = []; |
| | | foreach ($data['users'] as $user_id) { |
| | | // Clear existing user records |
| | | $this->wpdb->query($this->wpdb->prepare( |
| | | "DELETE FROM {$this->table_name} WHERE user_id = %d", |
| | | $user_id |
| | | )); |
| | | $this->index->delete(['user_id' => $user_id]); |
| | | |
| | | // Clear all caches for this user |
| | | $this->clearUserCache($user_id); |
| | | |
| | | // Get all the user's published posts |
| | | $posts = $this->wpdb->get_col($this->wpdb->prepare( |
| | | "SELECT ID FROM {$this->wpdb->posts} |
| | | WHERE post_author = %d |
| | | AND post_status = 'publish' |
| | | AND post_type LIKE %s", |
| | | $user_id, |
| | | $this->wpdb->esc_like(BASE) . '%' |
| | | )); |
| | | $posts = new WP_Query([ |
| | | 'post_status' => 'publish', |
| | | 'post_type' => array_map('jvbCheckBase', Registrar::getRegistered('post')), |
| | | 'posts_per_page'=> -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | |
| | | $processed_terms = 0; |
| | | if (empty($posts->posts)) { |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'user_id' => $user_id, |
| | | 'processed_posts'=> 0, |
| | | 'processed_terms'=> 0, |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | foreach ($posts as $post_id) { |
| | | $post_type = get_post_type($post_id); |
| | | $taxonomies = get_object_taxonomies($post_type); |
| | | $terms = []; |
| | | |
| | | foreach ($taxonomies as $taxonomy) { |
| | | $terms = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']); |
| | | $is_hierarchical = is_taxonomy_hierarchical($taxonomy); |
| | | $taxonomies = []; |
| | | $result=[ |
| | | 'user_id' => $user_id, |
| | | 'posts' => count($posts->posts) |
| | | ]; |
| | | foreach ($posts->posts as $postID) { |
| | | $postType = get_post_type($postID); |
| | | if (!array_key_exists($postType, $taxonomies)) { |
| | | $taxonomies[$postType] = get_object_taxonomies($postType); |
| | | } |
| | | $tax = $taxonomies[$postType]; |
| | | $terms = array_unique(array_merge($terms, wp_get_object_terms($postID, $tax, ['fields' => 'ids']))); |
| | | } |
| | | |
| | | if (!is_wp_error($terms) && !empty($terms)) { |
| | | foreach ($terms as $term_id) { |
| | | // Add direct term |
| | | $this->updateUserTerm($user_id, $term_id, $taxonomy, false); |
| | | $processed_terms++; |
| | | $result['terms'] = count($terms); |
| | | |
| | | // Add parent terms for hierarchical taxonomies |
| | | if ($is_hierarchical) { |
| | | $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy'); |
| | | foreach ($ancestors as $ancestor_id) { |
| | | $this->updateUserTerm($user_id, $ancestor_id, $taxonomy, true); |
| | | $processed_terms++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | foreach ($terms as $termID) { |
| | | $taxonomy = get_term($termID)->taxonomy??false; |
| | | if (!$taxonomy) { |
| | | continue; |
| | | } |
| | | $this->updateUserTerm($user_id, $termID, $taxonomy); |
| | | } |
| | | |
| | | $results[] = $result; |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'user_id' => $user_id, |
| | | 'processed_posts' => count($posts), |
| | | 'processed_terms' => $processed_terms |
| | | ] |
| | | 'result' => $results |
| | | ]; |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | |
| | | * |
| | | * @return array |
| | | */ |
| | | public function getUserTerms(int $user_id, string $taxonomy, array $args = []):array |
| | | public function fetchUserTerms(int $user_id, string $taxonomy, array $args = []):array |
| | | { |
| | | // Default arguments |
| | | $defaults = [ |
| | | 'orderby' => 'count', // 'count' or 'name' or 'last_used' |
| | | 'order' => 'DESC', // 'DESC' or 'ASC' |
| | | 'limit' => 0, // 0 for no limit |
| | | 'min_count' => 1, // Minimum usage count |
| | | 'include_parents' => true, // Whether to include parent terms |
| | | 'only_direct' => false, // If true, only returns directly-used terms |
| | | 'skip_cache' => false // Whether to skip cache lookup |
| | | ]; |
| | | $args = wp_parse_args($args, $defaults); |
| | | |
| | | // Skip cache and fetch directly |
| | | return $this->fetchUserTerms($user_id, $taxonomy, $args); |
| | | } |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * @param string $taxonomy |
| | | * @param array $args |
| | | * |
| | | * @return array |
| | | */ |
| | | private function fetchUserTerms(int $user_id, string $taxonomy, array $args):array |
| | | { |
| | | $taxonomy = jvbNoBase($taxonomy); |
| | | if (!in_array($taxonomy, Registrar::getRegistered('term'))){ |
| | | return []; |
| | | } |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $cache = Cache::for($user_id.'_term_relationships', DAY_IN_SECONDS)->connect('post', true)->connect('taxonomy', true); |
| | | $key = $cache->generateKey(array_merge( |
| | | [ |
| | | 'taxonomy' => $taxonomy, |
| | | ], |
| | | $args |
| | | )); |
| | | if (!$args['skip_cache']) { |
| | | $cached = $cache->get($key); |
| | | if ($cached) { |
| | | return $cached; |
| | | } |
| | | } |
| | | |
| | | // Build query |
| | | $query = " |
| | | SELECT ut.term_id, ut.post_count, ut.last_used, ut.is_parent, t.name, t.slug |
| | | FROM {$this->table_name} ut |
| | | JOIN {$this->wpdb->terms} t ON ut.term_id = t.term_id |
| | | WHERE ut.user_id = %d |
| | | AND ut.taxonomy = %s |
| | | AND ut.post_count >= %d |
| | | "; |
| | | $order = array_key_exists('order', $args) ? (!in_array(strtoupper($args['order']), ['ASC', 'DESC']) ? 'DESC' : $args['order']) : 'DESC'; |
| | | $orderby = array_key_exists('orderby', $args) ? (!in_array(strtolower($args['orderby']), ['post_count', 'last_used']) ? 'post_count' : $args['orderby']) : 'post_count'; |
| | | $limit = array_key_exists('limit', $args) ? absint($args['limit']) : 0; |
| | | $limit = $limit === 0 ? null : $limit; |
| | | |
| | | $query_args = [ |
| | | $user_id, |
| | | $taxonomy, |
| | | $args['min_count'] |
| | | ]; |
| | | |
| | | // Add parent term filter if needed |
| | | if ($args['only_direct']) { |
| | | $query .= " AND ut.is_parent = 0"; |
| | | } elseif (!$args['include_parents']) { |
| | | $query .= " AND ut.is_parent = 0"; |
| | | } |
| | | |
| | | // Add ordering |
| | | switch ($args['orderby']) { |
| | | case 'name': |
| | | $query .= " ORDER BY t.name " . ($args['order'] === 'DESC' ? 'DESC' : 'ASC'); |
| | | break; |
| | | case 'last_used': |
| | | $query .= " ORDER BY ut.last_used " . ($args['order'] === 'DESC' ? 'DESC' : 'ASC'); |
| | | break; |
| | | case 'count': |
| | | default: |
| | | $query .= " ORDER BY ut.post_count " . ($args['order'] === 'DESC' ? 'DESC' : 'ASC'); |
| | | break; |
| | | } |
| | | |
| | | // Add limit if specified |
| | | if ($args['limit'] > 0) { |
| | | $query .= " LIMIT %d"; |
| | | $query_args[] = $args['limit']; |
| | | } |
| | | |
| | | // Execute query |
| | | $results = $this->wpdb->get_results( |
| | | $this->wpdb->prepare($query, $query_args), |
| | | ARRAY_A |
| | | ); |
| | | $cache->set($key, $results); |
| | | |
| | | return $results; |
| | | } |
| | | |
| | | // Simple function to get just term IDs for a user |
| | | |
| | | /** |
| | | * @param int $user_id |
| | | * @param string $taxonomy |
| | | * @param array $args |
| | | * |
| | | * @return array |
| | | */ |
| | | public function getUserTermIDs(int $user_id, string $taxonomy, array $args = []):array |
| | | { |
| | | // Skip cache |
| | | $terms = $this->getUserTerms($user_id, $taxonomy, array_merge($args, ['skip_cache' => true])); |
| | | |
| | | if (empty($terms)) { |
| | | return []; |
| | | } |
| | | |
| | | return array_map(function ($term) { |
| | | return (int)$term['term_id']; |
| | | }, $terms); |
| | | return $this->index->pluck( |
| | | 'term_id', |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'taxonomy' => $taxonomy |
| | | ], |
| | | $orderby, |
| | | $order, |
| | | $limit |
| | | ); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\managers\CustomTable; |
| | | class VerifyEntryManager { |
| | | protected CustomTable $table; |
| | | protected int $max = 3; |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | } |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | $types = implode(',', array_map(function($item) { return "`{$item}`"; },Registrar::getFeatured('verify_entry'))); |
| | | |
| | | $table = CustomTable::for('verify_entry'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'content_id' => "{$table->getPostIDType()} NOT NULL", |
| | | 'content_type' => 'varchar(255) NOT NULL', |
| | | 'term_id' => "{$table->getTermIDType()} NOT NULL", |
| | | 'taxonomy' => "ENUM({$types}) NOT NULL", |
| | | 'status' => "ENUM('requested', 'rejected','accepted') NOT NULL DEFAULT 'requested'", |
| | | 'result' => 'JSON DEFAULT NULL', |
| | | 'notes' => 'text DEFAULT NULL', |
| | | 'created_date' => 'timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_date' => 'timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => 'content_term (`content_id`, `term_id`)'], |
| | | 'user_id (`user_id`)', |
| | | 'term_id (`term_id`)', |
| | | 'status (`term_id`, `status`)' |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}verify_entry_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}verify_entry_term` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$table->getTermTable()}` (`term_id`) ON DELETE CASCADE", |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | |
| | | $this->table = $table; |
| | | } |
| | | protected function response(?bool $success = null, string $message = ''):array |
| | | { |
| | | $response = [ |
| | | 'success' => is_null($success) ? 'partial' : $success, |
| | | ]; |
| | | if (!empty($message)) { |
| | | $response['message'] = $message; |
| | | } |
| | | return $response; |
| | | } |
| | | |
| | | public function requestEntry(int $userID, int $termID, string $taxonomy):array |
| | | { |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return $this->response(false, 'User does not exist'); |
| | | } |
| | | $term = get_term($termID, jvbCheckBase($taxonomy)); |
| | | if (!$term || is_wp_error($term)) { |
| | | return $this->response(false, 'Term does not exist'); |
| | | } |
| | | |
| | | $profile = get_user_meta($userID, BASE.'link', true); |
| | | if (empty($profile)) { |
| | | return $this->response(false, 'No Profile found'); |
| | | } |
| | | |
| | | $managers = jvbGetTermOwners($termID); |
| | | if (in_array($userID, $managers)) { |
| | | wp_set_object_terms($profile, $termID, jvbCheckBase($taxonomy)); |
| | | return $this->response(true, 'User is manager'); |
| | | } |
| | | |
| | | $total = $this->table->pluck('user_id', [ |
| | | 'user_id' => $userID, |
| | | 'status' => 'requested' |
| | | ]); |
| | | if (count($total) >= $this->max) { |
| | | return $this->response(false, 'User already has maximum requests'); |
| | | } |
| | | |
| | | $existing = $this->table->get([ |
| | | 'user_id' => $userID, |
| | | 'term_id' => $termID |
| | | ]); |
| | | if ($existing) { |
| | | return $this->response(false, 'Existing pending request found. Please wait for response.'); |
| | | } |
| | | |
| | | $request = $this->table->insert([ |
| | | 'user_id' => $userID, |
| | | 'content_id'=> (int)$profile, |
| | | 'content_type'=> jvbNoBase(get_post_type($profile)), |
| | | 'term_id' => $termID, |
| | | 'taxonomy' => jvbNoBase($taxonomy) |
| | | ]); |
| | | |
| | | if ($request && !empty($owners)) { |
| | | JVB()->notification()->notify($owners,'entry_requested',$userID, [ |
| | | 'target_id' => $termID, |
| | | 'target_type' => $taxonomy |
| | | ]); |
| | | } |
| | | return $this->response(true, 'Request has been sent'); |
| | | } |
| | | |
| | | protected function actionEntry(bool $approve, int $userID, int $requestID, ?string $notes = null):array |
| | | { |
| | | $request = $this->table->get(['id' => $requestID]); |
| | | if (!$request) { |
| | | return $this->response(false, 'Request does not exist'); |
| | | } |
| | | |
| | | $termID = $request['term_id']; |
| | | $owners = jvbGetTermOwners($termID); |
| | | if (!user_can($userID, 'can_manage_'.$termID) || !in_array($userID, $owners)) { |
| | | return $this->response(false, 'User does not exist'); |
| | | } |
| | | $result = $this->table->update([ |
| | | 'status' => ($approve ? 'accepted' : 'rejected'), |
| | | 'notes' => $notes |
| | | ], [ |
| | | 'id' => $requestID |
| | | ]); |
| | | if ($approve) { |
| | | wp_set_object_terms($request['content_id'], $termID, jvbCheckBase($request['taxonomy'])); |
| | | } |
| | | |
| | | JVB()->notification()?->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'], [ |
| | | 'target_id' => $termID, |
| | | 'target_type' => $request['taxonomy'] |
| | | ]); |
| | | return $this->response(true, 'Request has been '. $approve ? 'approved' : 'denied'); |
| | | } |
| | | public function approveEntry(int $userID, int $requestID, ?string $notes = null):array |
| | | { |
| | | return $this->actionEntry(true, $userID, $requestID, $notes); |
| | | } |
| | | |
| | | public function denyEntry(int $userID, int $requestID, ?string $notes = null):array |
| | | { |
| | | return $this->actionEntry(false, $userID, $requestID, $notes); |
| | | } |
| | | } |
| | |
| | | //} |
| | | |
| | | if (Features::forSite()->has('notifications')) { |
| | | require(JVB_DIR . '/inc/managers/Notifications/Content.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/EmailDigests.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/Notifications.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/Preferences.php'); |
| | | require(JVB_DIR . '/inc/managers/NotificationManager.php'); |
| | | |
| | | } |
| | | |
| | | if (Features::forMembership()->has('forum') && !empty(Registrar::getFeatured('is_content', 'term'))) { |
| | |
| | | ]); |
| | | |
| | | $queue->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | '`idx_run_queue` (`state`, `priority`, `scheduled_at`)', |
| | | '`idx_user_ops` (`user_id`, `state`)', |
| | | '`idx_user_type_pending` (`user_id`, `type`, `state`)', |
| | |
| | | ]); |
| | | |
| | | $stats->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => 'id'], |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '(`date`, `type`)'], |
| | | '`date_idx` (`date`)', |
| | | '`type_idx` (`type`)' |
| | |
| | | |
| | | public function modifyField(string $name, string $property, mixed $value):void |
| | | { |
| | | $property = 'set'.implode('',array_map('ucfirst',explode('_', $property))); |
| | | $field = $this->fields[$name]; |
| | | $field->$property = $value; |
| | | $field->$property($value); |
| | | } |
| | | |
| | | public function getFields():array |
| | |
| | | <?php |
| | | namespace JVBase\registrar; |
| | | |
| | | use JVBase\forms\TaxonomySelector; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | * @var string|false |
| | | */ |
| | | public string|false $template_lock; |
| | | protected string $unbased; |
| | | protected ?string $taxonomyRewrite = null; |
| | | |
| | | public function __construct(string $postType, string $singular, string $plural) |
| | | { |
| | | $this->unbased = jvbNoBase($postType); |
| | | $this->postType = jvbCheckBase($postType); |
| | | $this->labels = $this->buildLabels($singular, $plural); |
| | | } |
| | |
| | | unset ($args['postType']); |
| | | |
| | | // error_log('Registering PostType '.$this->postType.', with args: '.print_r($args, true)); |
| | | $registrar = Registrar::getInstance($this->postType); |
| | | $rewrite = $args['rewrite']['slug']??''; |
| | | if ($registrar) { |
| | | |
| | | $hasSlug = array_key_exists('rewrite', $args) && array_key_exists('slug', $args['rewrite']); |
| | | |
| | | if ($registrar->rewrite_taxonomy && !str_contains($rewrite, '%')) { |
| | | if (!$hasSlug && !array_key_exists('rewrite', $args)) { |
| | | $args['rewrite'] = []; |
| | | } |
| | | $tax = is_array($registrar->rewrite_taxonomy) ? $registrar->rewrite_taxonomy[array_key_first($registrar->rewrite_taxonomy)] : $registrar->rewrite_taxonomy; |
| | | $args['rewrite']['slug'] = $hasSlug ? $rewrite."/%{$tax}%" : $this->unbased."/%{$tax}%"; |
| | | } |
| | | |
| | | if ($registrar->hasFeature('is_calendar')) { |
| | | if (!$hasSlug && !array_key_exists('rewrite', $args)) { |
| | | $args['rewrite'] = []; |
| | | } |
| | | $args['rewrite']['slug'] = $hasSlug ? $rewrite."/%eyear%/%emonth%/%eday%" : $this->unbased."/%eyear%/%emonth%/%eday%"; |
| | | } |
| | | } |
| | | |
| | | |
| | | $registered = register_post_type($this->postType, $args); |
| | | if (is_wp_error($registered)) { |
| | | JVB()->error()->log('JVBase\registrar\Posts', 'Could not register post type', $registered->get_error_messages()); |
| | |
| | | 'not_found_in_trash' => "No {$plural} found in Trash.", |
| | | ]; |
| | | } |
| | | |
| | | public function addTaxonomyRewrite(string $taxonomy):void |
| | | { |
| | | $exists = Registrar::getInstance($taxonomy); |
| | | if (!$exists) return; |
| | | |
| | | $this->taxonomyRewrite = $taxonomy; |
| | | |
| | | if (!isset($this->rewrite)) { |
| | | $this->rewrite = []; |
| | | } |
| | | if (array_key_exists('slug', $this->rewrite)) { |
| | | if (str_contains($this->rewrite['slug'], '%')) { |
| | | return; |
| | | } |
| | | $this->rewrite['slug'] = $this->rewrite['slug'].'/%'.$taxonomy.'%'; |
| | | } else { |
| | | $this->rewrite['slug'] = $this->unbased.'/%'.$taxonomy.'%'; |
| | | } |
| | | |
| | | $this->addTaxonomyRewriteRules(); |
| | | add_action('post_type_link', [$this, 'rewriteTaxonomySingle'], 15, 2); |
| | | add_action('post_type_archive_link', [$this, 'rewriteTaxonomyArchive'], 15, 2); |
| | | } |
| | | public function addTaxonomyRewriteRules(): void |
| | | { |
| | | if (!$this->taxonomyRewrite) return; |
| | | $tax = jvbCheckBase($this->taxonomyRewrite); |
| | | |
| | | // Rule 1: Post type archive - /faq/ |
| | | add_rewrite_rule( |
| | | "{$this->unbased}/?$", |
| | | "index.php?post_type={$this->postType}", |
| | | 'top' |
| | | ); |
| | | |
| | | // Rule 2: Single posts with taxonomy - /faq/section/post/ |
| | | add_rewrite_rule( |
| | | "{$this->unbased}/([^/]+)/([^/]+)/?$", |
| | | "index.php?post_type={$this->postType}&name=\$matches[2]&{$tax}=\$matches[1]", |
| | | 'top' |
| | | ); |
| | | |
| | | // Rule 3: Un-sectioned posts - /faq/post/ |
| | | // Use 'bottom' priority so taxonomy rules match first |
| | | add_rewrite_rule( |
| | | "{$this->unbased}/([^/]+)/?$", |
| | | "index.php?post_type={$this->postType}&name=\$matches[1]", |
| | | 'bottom' |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Set $this->rewrite_taxonomy to a valid taxonomy |
| | | * @param string $url |
| | | * @param \WP_Post $post |
| | | * @return string |
| | | */ |
| | | public function rewriteTaxonomySingle(string $url, \WP_Post $post): string |
| | | { |
| | | if ($post->post_type === $this->postType && !is_null($this->taxonomyRewrite)) { |
| | | $type = $this->taxonomyRewrite; |
| | | $taxonomy = jvbCheckBase($type); |
| | | $terms = wp_get_post_terms($post->ID, $taxonomy); |
| | | if (!empty($terms) && !is_wp_error($terms)) { |
| | | $path = TaxonomySelector::getTermPath($terms[0], true); |
| | | $path = implode('/', array_map(function($term) { |
| | | return sanitize_title($term); |
| | | }, $path)); |
| | | return str_replace("%{$type}%", $path, $url); |
| | | } |
| | | return str_replace("/%{$type}%", '', $url); |
| | | } |
| | | return $url; |
| | | } |
| | | |
| | | /** |
| | | * Set $this->rewrite_taxonomy to a valid taxonomy |
| | | * @param string $url |
| | | * @param string $post_type |
| | | * @return string |
| | | */ |
| | | public function rewriteTaxonomyArchive(string $url, string $post_type):string |
| | | { |
| | | if ($post_type === $this->postType && !is_null($this->taxonomyRewrite)) { |
| | | $url = get_home_url(null, "/{$this->postType}/"); |
| | | } |
| | | return $url; |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\registrar; |
| | | |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\inc\registrar\helpers\HideSingle; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CRUD; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\managers\KarmaManager; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\config\Breadcrumbs; |
| | | use JVBase\registrar\config\Dashboard; |
| | |
| | | use JVBase\registrar\config\SEO; |
| | | use JVBase\registrar\helpers\AddIntegrationFields; |
| | | use JVBase\registrar\helpers\MakeCalendarType; |
| | | use JVBase\registrar\helpers\MakeTimelineType; |
| | | use JVBase\registrar\helpers\MakeTrackChanges; |
| | | use JVBase\registrar\helpers\MakeVerification; |
| | | use JVBase\utility\Features; |
| | | use WP_Post; |
| | | use WP_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | protected int|false $page = false; |
| | | |
| | | public ?string $rewrite_taxonomy = null; |
| | | |
| | | protected static array $allFlags = [ |
| | | //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', |
| | | 'hide_single', 'redirect_to_author', 'is_calendar', 'single_image', 'is_timeline', 'is_gallery', 'is_faq', 'is_glossary', 'rewrite_taxonomy', |
| | | //Taxonomy Flags |
| | | 'is_content', 'is_ownable', 'verify_entry', 'track_changes', 'associate_user_content', |
| | | //User Flags |
| | |
| | | * @var bool Whether to setup karma for this content |
| | | */ |
| | | protected bool $karma = false; |
| | | protected ?KarmaManager $karmaManager = null; |
| | | /** |
| | | * @var bool Whether this should be available in the feed block |
| | | */ |
| | |
| | | * @var bool Whether single items of this content should be hidden |
| | | */ |
| | | protected bool $hide_single = false; |
| | | protected ?HideSingle $hideSingleHandler = null; |
| | | |
| | | /** |
| | | * @var bool Whether single items should just go to the author's page |
| | |
| | | /** |
| | | * @var bool Whether to make this a calendar type (example: events) |
| | | */ |
| | | protected bool $is_calendar; |
| | | protected bool $is_calendar = false; |
| | | protected ?MakeCalendarType $isCalendarHandler = null; |
| | | /** |
| | | * @var bool Whether this is a before/after post type |
| | | */ |
| | | protected bool $is_timeline = false; |
| | | protected ?MakeTimelineType $isTimelineHandler = null; |
| | | /** |
| | | * @var bool Whether this is a defined term post type |
| | | */ |
| | | protected bool $is_glossary = false; |
| | | /** |
| | | * @var bool Whether this is a faq post type |
| | | */ |
| | | protected bool $is_faq = false; |
| | | /** |
| | | * @var bool Whether the uploader can group images prior to upload |
| | | */ |
| | |
| | | /** |
| | | * @var bool For taxonomy types only. Treats the taxonomy as a content (ie: tattoo shops)) |
| | | */ |
| | | protected bool $is_content; |
| | | protected bool $is_content = false; |
| | | /** |
| | | * @var bool Whether this taxonomy can be owned/managed by specific people only |
| | | */ |
| | |
| | | * @var bool Whether users/content need to request the owner for admission |
| | | */ |
| | | protected bool $verify_entry; |
| | | protected ?MakeVerification $verifyEntryHandler = null; |
| | | /** |
| | | * @var bool Whether we should track post movements from term to term (ie. artists in tattoo shops) |
| | | */ |
| | | protected bool $track_changes; |
| | | protected ?MakeTrackChanges $trackChangesHandler = null; |
| | | |
| | | /** |
| | | * @var bool Whether any content by members in this taxonomy should show up in this taxonomy |
| | |
| | | /** |
| | | * @var bool Whether to generate a profile for this user role |
| | | */ |
| | | protected bool $profile_link; |
| | | public bool $profile_link; |
| | | /** |
| | | * @var array|string |
| | | */ |
| | |
| | | return $this; |
| | | } |
| | | $this->args = $args; |
| | | |
| | | foreach ($args as $property => $value) { |
| | | $this->registrar->$property = $value; |
| | | if (property_exists($this->registrar, $property)) { |
| | | $this->registrar->$property = $value; |
| | | } |
| | | |
| | | if (property_exists($this, $property)) { |
| | | $this->$property = $value; |
| | | } |
| | | |
| | | } |
| | | if (isset($this->icon) && !str_contains($this->icon, 'dashicons')){ |
| | | $this->registrar->menu_icon = IconsManager::for()->getCSSIcon($this->icon); |
| | |
| | | return $this->based; |
| | | } |
| | | |
| | | public function setGlossary(bool $set):self |
| | | { |
| | | $this->is_glossary = $set; |
| | | // if ($set) { |
| | | // $this->timeline = new MakeTimelineType($this->slug, $this); |
| | | // } |
| | | return $this; |
| | | } |
| | | public function isGlossary():bool |
| | | { |
| | | return $this->is_glossary; |
| | | } |
| | | public function setFaq(bool $set):self |
| | | { |
| | | $this->is_faq = $set; |
| | | // if ($set) { |
| | | // $this->timeline = new MakeTimelineType($this->slug, $this); |
| | | // } |
| | | return $this; |
| | | } |
| | | public function isFaq():bool |
| | | { |
| | | return $this->is_faq; |
| | | } |
| | | public function setTimeline(bool $set):self |
| | | { |
| | | $this->is_timeline = $set; |
| | |
| | | case 'is_content': |
| | | add_action('init', [$this, 'setupContent'], 20); |
| | | break; |
| | | case 'is_glossary': |
| | | $this->hide_single = true; |
| | | break; |
| | | } |
| | | } |
| | | return $this; |
| | |
| | | } |
| | | public static function getFeatured(string $feature, ?string $type = null):array |
| | | { |
| | | self::ensureInstanced(); |
| | | |
| | | if (!in_array($feature, static::$allFlags)) { |
| | | error_log('Feature requested not found: '.$feature); |
| | | return []; |
| | | } |
| | | |
| | | return array_map(function($inst) { return $inst->slug; },array_filter(self::$instances, function ($inst) use ($feature, $type){ |
| | | return isset($inst->$feature) && $inst->$feature === true && (is_null($type) || $inst->type === $type); |
| | | if (!is_null($type) && $inst->type !== $type) { |
| | | return false; |
| | | } |
| | | return property_exists($inst, $feature) && isset($inst->$feature) && $inst->$feature === true; |
| | | })); |
| | | } |
| | | |
| | |
| | | } |
| | | protected function getBreadcrumbs():Breadcrumbs |
| | | { |
| | | $this->breadcrumbs = new Breadcrumbs($this->slug, $this); |
| | | if (!isset($this->breadcrumbs)) { |
| | | $this->breadcrumbs = new Breadcrumbs($this->slug, $this); |
| | | } |
| | | |
| | | return $this->breadcrumbs; |
| | | } |
| | | protected function getCalendar():MakeCalendarType|false |
| | | { |
| | | if ($this->is_calendar){ |
| | | if ($this->is_calendar && !isset($this->calendar)){ |
| | | $this->calendar = new MakeCalendarType($this->slug, $this); |
| | | } else { |
| | | $this->calendar = false; |
| | |
| | | |
| | | protected function getDashboard():Dashboard |
| | | { |
| | | $this->dashboard = new Dashboard($this->plural, $this); |
| | | if (!isset($this->dashboard)) { |
| | | $this->dashboard = new Dashboard($this->plural, $this); |
| | | } |
| | | |
| | | return $this->dashboard; |
| | | } |
| | | |
| | | protected function getDirectory():Directory|false |
| | | { |
| | | if ($this->show_directory) { |
| | | $this->directory = new Directory($this->singular, $this); |
| | | } else { |
| | | $this->directory = false; |
| | | if (!isset($this->directory)) { |
| | | $this->directory = new Directory($this->singular); |
| | | } |
| | | return $this->directory; |
| | | } |
| | | |
| | | protected function getFeed():Feed|false |
| | | { |
| | | if ($this->show_feed) { |
| | | $this->feed = new Feed($this->slug, $this); |
| | | } else { |
| | | $this->feed = false; |
| | | if (!isset($this->feed)) { |
| | | $this->feed = new Feed($this->slug); |
| | | } |
| | | return $this->feed; |
| | | } |
| | |
| | | public function getSEO():SEO |
| | | { |
| | | if (!isset($this->seo)){ |
| | | $this->seo = new SEO($this->slug, $this); |
| | | $this->seo = new SEO($this->slug); |
| | | } |
| | | return $this->seo; |
| | | } |
| | |
| | | |
| | | public function register():void |
| | | { |
| | | if ($this->registrar) { |
| | | $this->registrar->register(); |
| | | if ($this->type === 'post') { |
| | | if ($this->hide_single) { |
| | | $this->hideSingleHandler = new HideSingle($this->slug, $this); |
| | | } |
| | | if ($this->is_timeline) { |
| | | $this->isTimelineHandler = new MakeTimelineType($this->slug, $this); |
| | | $this->registrar->hierarchical = true; |
| | | } |
| | | if ($this->is_calendar) { |
| | | $this->isCalendarHandler = new MakeCalendarType($this->slug, $this); |
| | | } |
| | | |
| | | if (!is_null($this->rewrite_taxonomy)) { |
| | | $this->registrar->addTaxonomyRewrite($this->rewrite_taxonomy); |
| | | } |
| | | |
| | | if ($this->registrar) { |
| | | $this->registrar->register(); |
| | | } |
| | | } elseif ($this->type === 'term') { |
| | | if ($this->is_content) { |
| | | if ($this->verify_entry) { |
| | | $this->verifyEntryHandler = new MakeVerification(); |
| | | } |
| | | if ($this->track_changes) { |
| | | $this->trackChangesHandler = new MakeTrackChanges($this->slug); |
| | | } |
| | | } |
| | | |
| | | if ($this->registrar) { |
| | | $this->registrar->register(); |
| | | } |
| | | } |
| | | |
| | | if ($this->karma) { |
| | | $this->karmaManager = KarmaManager::for($this->slug); |
| | | } |
| | | } |
| | | public static function getInstance(string $slug):Registrar|false |
| | | { |
| | | self::ensureInstanced(); |
| | | |
| | | $slug = jvbNoBase($slug); |
| | | if (array_key_exists($slug, static::$instances)) { |
| | | return static::$instances[$slug]; |
| | |
| | | |
| | | public static function getFieldsFor(string $slug):array |
| | | { |
| | | self::ensureInstanced(); |
| | | if (!array_key_exists($slug, static::$instances)) { |
| | | return []; |
| | | } |
| | |
| | | |
| | | public static function getRegistered(?string $type = null):array |
| | | { |
| | | self::ensureInstanced(); |
| | | $instances = ($type) ? array_filter(static::$instances, function($instance) use ($type) { |
| | | return $instance->type === $type; |
| | | }) : static::$instances; |
| | |
| | | |
| | | public static function getLabels():array |
| | | { |
| | | self::ensureInstanced(); |
| | | return array_map(function ($instance) { |
| | | return ['singular' => $instance->getSingular(), |
| | | 'plural' => $instance->getPlural()]; |
| | |
| | | error_log('Built the '.$this->slug.' page content.'); |
| | | return $content . $out; |
| | | } |
| | | |
| | | public static function ensureInstanced():void |
| | | { |
| | | if (empty(self::$instances)) { |
| | | do_action('jvbDefineRegistrar'); |
| | | do_action('jvbDefineRegistrarFields'); |
| | | } |
| | | } |
| | | |
| | | /***************************************************************** |
| | | * FLAGGED FEATURES |
| | | *****************************************************************/ |
| | | |
| | | |
| | | |
| | | |
| | | } |
| | |
| | | require(JVB_DIR . '/inc/registrar/fields/SelectorField.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/UploadField.php'); |
| | | |
| | | require(JVB_DIR . '/inc/registrar/helpers/AddIntegrationFields.php'); |
| | | require(JVB_DIR . '/inc/registrar/helpers/HideSingle.php'); |
| | | require(JVB_DIR . '/inc/registrar/helpers/MakeCalendarType.php'); |
| | | require(JVB_DIR . '/inc/registrar/helpers/MakeTimelineType.php'); |
| | | require(JVB_DIR . '/inc/registrar/helpers/MakeTrackChanges.php'); |
| | | require(JVB_DIR . '/inc/registrar/helpers/MakeVerification.php'); |
| | | |
| | |
| | | $config = [ |
| | | 'title' => $this->title, |
| | | ]; |
| | | if (isset($this->addCrumb)) { |
| | | if (!empty($this->addCrumb)) { |
| | | $config['addCrumb'] = $this->addCrumb; |
| | | } |
| | | return $config; |
| | |
| | | if ($registrar) { |
| | | switch ($registrar->getType()) { |
| | | case 'term': |
| | | $this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | $this->config['schema']['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; |
| | | break; |
| | | case 'post': |
| | | $this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork'; |
| | |
| | | |
| | | public static function resolve(string $template, ?Meta $meta): string |
| | | { |
| | | if (!$meta) { |
| | | if (is_singular()){ |
| | | $meta = Meta::forPost(get_the_ID()); |
| | | }else if (is_tax()) { |
| | | $meta = Meta::forTerm(get_queried_object()->term_id); |
| | | } |
| | | } |
| | | self::$meta = $meta; |
| | | return preg_replace_callback( |
| | | '/\{\{([^}]+)}}/', |
| | |
| | | error_log('[SEO]Meta Resolver. Could not find meta configuration for variable: '.$variable); |
| | | return ''; |
| | | } |
| | | if ($variable === 'post_content') { |
| | | return self::resolvePostContent($variable, $meta); |
| | | } |
| | | return match($config['type']) { |
| | | 'upload', 'image', 'gallery' => self::resolveImage($variable, $meta), |
| | | 'selector', 'taxonomy', 'user', 'post' => self::resolveObject($variable, $meta), |
| | |
| | | } |
| | | |
| | | } |
| | | error_log('resolveRelation: '.print_r($path, true)); |
| | | |
| | | $ID = $meta->get($relation); |
| | | if (!$ID || $ID === '') { |
| | | return ''; |
| | |
| | | if (!$config) { |
| | | return ''; |
| | | } |
| | | |
| | | $type = false; |
| | | if ($config['type'] === 'taxonomy' || array_key_exists('taxonomy', $config)) { |
| | | $type = 'taxonomy'; |
| | | } elseif ($config['type'] === 'user'){ |
| | | $type = 'term'; |
| | | } elseif ($config['type'] === 'user' || array_key_exists('user', $config)){ |
| | | $type = 'user'; |
| | | } elseif ($config['type'] === 'post'){ |
| | | } elseif ($config['type'] === 'post' || array_key_exists('post', $config)){ |
| | | $type = 'post'; |
| | | } |
| | | |
| | | if (!$type) { |
| | | error_log('[SEO]Meta Resolver. Could not find type for relation: '.$relation.': '.$field); |
| | | return ''; |
| | |
| | | return self::resolve($field, $newMeta); |
| | | } |
| | | |
| | | protected static function resolvePostContent(string $variable, ?Meta $meta):string{ |
| | | return wp_strip_all_tags(str_replace("\n", '', $meta->get('post_content'))); |
| | | } |
| | | protected static function resolveImage(string $variable, ?Meta $meta, bool $returnID = false): string |
| | | { |
| | | $imgID = $meta->get($variable); |
| | |
| | | return $checkType; |
| | | } |
| | | |
| | | error_log('[SEO]Resolver - No method found for '.$property.' with value: '.print_r($value, true).'. Defaulting to base Resolver'); |
| | | $ignore = ['description', 'name']; |
| | | if (!in_array($property, $ignore)) { |
| | | error_log('[SEO]Resolver - No method found for '.$property.' with value: '.print_r($value, true).'. Defaulting to base Resolver'); |
| | | } |
| | | |
| | | |
| | | return self::resolve($value, $meta); |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\registrar\config\seo; |
| | | |
| | | use JVBase\base\SchemaHelper; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\SEO\render; |
| | | use JVBase\meta\Meta; |
| | |
| | | ]; |
| | | |
| | | protected array $defaultArchive = [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage', |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', |
| | | 'title' => '{{registrar.plural}}' |
| | | ]; |
| | | |
| | |
| | | |
| | | switch ($registrar->getType()) { |
| | | case 'term': |
| | | $this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | $this->defaultSchema['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; |
| | | break; |
| | | case 'user': |
| | | $this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ProfilePage'; |
| | |
| | | |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | $this->defaultArchive = [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage', |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', |
| | | 'name' => $registrar->getPlural(), |
| | | 'description' => $registrar->getDescription() |
| | | ]; |
| | |
| | | public function outputSchema():void |
| | | { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | |
| | | if (is_singular()) { |
| | | $this->outputSingularSchema(); |
| | | } elseif (is_post_type_archive(jvbCheckBase($this->slug) || is_tax(jvbCheckBase($this->slug)))) { |
| | |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | |
| | | return $this->cache->remember( |
| | | $ID, |
| | | function () use ($ID) { |
| | | function () use ($ID, $registrar) { |
| | | $meta = Meta::forPost($ID); |
| | | if ($registrar->hasFeature('is_faq')) { |
| | | return $this->outputQASchema($ID, $meta); |
| | | } |
| | | if ($registrar->hasFeature('is_timeline')) { |
| | | return $this->outputTimelineSchema($ID, $meta); |
| | | } |
| | | |
| | | $config = $this->getConfig(); |
| | | |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | |
| | | $config = JVB()->schemaHelper()::schema($action); |
| | | |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; |
| | | } |
| | | if (!class_exists($config['type'])) { |
| | | error_log('No class found for archive schema output: '.$config['type']); |
| | |
| | | $action = BASE.ucfirst($this->slug).'Archive'; |
| | | $config = JVB()->schemaHelper()->archive($this->slug); |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; |
| | | } |
| | | if (!class_exists($config['type'])) { |
| | | error_log('No class found for archive schema output: '.$config['type']); |
| | | return []; |
| | | } |
| | | |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | if ($registrar->hasFeature('is_glossary')) { |
| | | return $this->outputGlossarySchema(); |
| | | } else if ($registrar->hasFeature('is_faq')) { |
| | | return $this->outputFAQSchema(); |
| | | } |
| | | if ($registrar->hasFeature('is_timeline')) { |
| | | return $this->outputTimelineArchiveSchema(); |
| | | } |
| | | $obj = get_queried_object(); |
| | | $meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null; |
| | | |
| | |
| | | foreach ($items->posts as $ID) { |
| | | $item = $this->outputReferenceSchema($ID, 'post',false); |
| | | $listItem = new render\Thing\Intangible\ListItem(); |
| | | $listItem->setId('listitem-'.$ID); |
| | | $listItem->setPosition($pos); |
| | | $listItem->setItem($item); |
| | | $itemListItems[] = $listItem; |
| | |
| | | error_log('Invalid type used for reference: '.print_r($type, true)); |
| | | $meta = null; |
| | | } |
| | | $config = $this->getConfig('archive'); |
| | | $config = $this->getConfig(); |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | $class->delete('about'); |
| | | |
| | |
| | | return JVB()->schemaHelper()::getConfig($this->slug, $type); |
| | | } |
| | | |
| | | public function resolveMeta(array $config, Meta $meta):array |
| | | { |
| | | foreach ($config as $property => $value) { |
| | | if (is_array($value)) { |
| | | $config[$property] = $this->resolveMeta($value, $meta); |
| | | if ($property === 'additionalProperty' && (!array_key_exists('value', $config[$property]) || empty($config[$property]['value']))) { |
| | | unset($config[$property]); |
| | | } |
| | | } |
| | | if (is_string($value) && str_contains($value, '{{')) { |
| | | $value = Resolver::resolve($value, $meta); |
| | | if (empty($value)){ |
| | | unset($config[$property]); |
| | | } else { |
| | | $config[$property] = $value; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return $config; |
| | | } |
| | | |
| | | public function define(string $property, string $value):void |
| | | { |
| | | $class = $this->getConfig('schema')['type']; |
| | |
| | | if (is_singular($based)){ |
| | | $config = $this->getConfig('meta'); |
| | | $meta = Meta::forPost(get_the_ID()); |
| | | $title = Resolver::resolve($config['name'], $meta); |
| | | $title = Resolver::resolve($config['name']??$config['title'], $meta); |
| | | } elseif (is_post_type_archive($based) ) { |
| | | $config = $this->getConfig('archive'); |
| | | $title = $config['name']; |
| | |
| | | } |
| | | return $title; |
| | | } |
| | | public function outputQASchema(int $ID, Meta $meta):array |
| | | { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | global $wp; |
| | | $current = get_home_url(null, $wp->request).'/'; |
| | | $config = $this->getConfig(); |
| | | |
| | | $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\QAPage'; |
| | | $page = SchemaHelper::classFromConfig($config, $meta); |
| | | |
| | | $post = get_queried_object(); |
| | | $question = [ |
| | | 'id' => $current.'#question-'.$post->post_name, |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question', |
| | | 'name' => $meta->get('post_title'), |
| | | 'acceptedAnswer' => [ |
| | | 'id' => $current.'#answer-'.$post->post_name, |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer', |
| | | 'text' => wp_strip_all_tags(str_replace("\n", '', $meta->get('post_content'))), |
| | | ], |
| | | ]; |
| | | $question = SchemaHelper::classFromConfig($question); |
| | | $page->setMainEntity($question); |
| | | $page->setAuthor(JVB()->seo()->getCreator(true)); |
| | | return $page->outputSchema(); |
| | | |
| | | } |
| | | public function outputTimelineSchema(int $ID, Meta $meta):array |
| | | { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | global $wp; |
| | | $current = get_home_url(null, $wp->request).'/'; |
| | | $config = $this->getConfig(); |
| | | |
| | | $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\ImageGallery'; |
| | | $this->resolveMeta($config, $meta); |
| | | $page = SchemaHelper::classFromConfig($config, $meta); |
| | | |
| | | $post = get_queried_object(); |
| | | $images = []; |
| | | $children = new WP_Query([ |
| | | 'post_type'=> jvbCheckBase($this->slug), |
| | | 'post_status' => 'publish', |
| | | 'post_parent' => $post->ID, |
| | | ]); |
| | | $children = $children->posts; |
| | | array_unshift($children, $post); |
| | | |
| | | foreach ($children as $index => $child) { |
| | | $meta = Meta::forPost($child->ID); |
| | | |
| | | $image = render\Thing\Thing::createImageFromID($meta->get('post_thumbnail')); |
| | | $image->setId(($index === 0) ? '#before-treatment' : '#treatment-'.$index); |
| | | $image->setPosition($index+1); |
| | | $image->setName(($index === 0) ? 'Before Laser Tattoo Removal' : 'After '.$index.' Laser Tattoo Removal Treatments'); |
| | | if (!empty ($meta->get('post_excerpt'))){ |
| | | $image->setDescription($meta->get('post_excerpt')); |
| | | } |
| | | |
| | | $images[] = $image; |
| | | } |
| | | |
| | | $page->setImage($images); |
| | | $page->setAuthor(JVB()->seo()->getCreator(true)); |
| | | return $page->outputSchema(); |
| | | |
| | | } |
| | | |
| | | /********************************************* |
| | | * Archive Presets |
| | | *********************************************/ |
| | | public function outputFAQSchema():array |
| | | { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | global $wp; |
| | | $current = get_home_url(null, $wp->request).'/'; |
| | | $config = $this->getConfig('archive'); |
| | | $page = [ |
| | | 'id' => $current.'#'.$registrar->getSlug(), |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\FAQPage', |
| | | 'name' => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(), |
| | | 'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(), |
| | | 'url' => $current, |
| | | ]; |
| | | |
| | | $page = SchemaHelper::classFromConfig($page); |
| | | |
| | | $args = [ |
| | | 'post_type' => $registrar->getBased(), |
| | | 'posts_per_page'=> -1, |
| | | 'post_status' => 'publish', |
| | | ]; |
| | | $obj = get_queried_object(); |
| | | |
| | | if (property_exists($obj, 'taxonomy')) { |
| | | $page->setName('FAQ on '.$obj->name); |
| | | |
| | | $args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for); |
| | | |
| | | $args['tax_query'] = [ |
| | | [ |
| | | 'taxonomy' => $obj->taxonomy, |
| | | 'terms' => $obj->term_id, |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | $questions = []; |
| | | $posts = new WP_Query($args); |
| | | foreach ($posts->posts as $post) { |
| | | $meta = Meta::forPost($post->ID); |
| | | $question = [ |
| | | 'id' => $current.'#question-'.$post->post_name, |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question', |
| | | 'name' => $meta->get('post_title'), |
| | | 'acceptedAnswer' => [ |
| | | 'id' => $current.'#answer-'.$post->post_name, |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer', |
| | | 'text' => $meta->get('post_excerpt'), |
| | | ], |
| | | 'url' => get_the_permalink($post->ID), |
| | | ]; |
| | | $questions[] = SchemaHelper::classFromConfig($question); |
| | | } |
| | | wp_reset_postdata(); |
| | | $page->setMainEntity($questions); |
| | | |
| | | return $page->outputSchema(); |
| | | |
| | | } |
| | | public function outputTimelineArchiveSchema():array |
| | | { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | global $wp; |
| | | $current = get_home_url(null, $wp->request).'/'; |
| | | $config = $this->getConfig('archive'); |
| | | $page = array_merge($config, [ |
| | | 'id' => $current.'#'.$registrar->getSlug(), |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', |
| | | 'name' => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(), |
| | | 'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(), |
| | | 'url' => $current, |
| | | ]); |
| | | $page = SchemaHelper::classFromConfig($page); |
| | | |
| | | $parts = []; |
| | | $timelines = new WP_Query([ |
| | | 'post_type' => $registrar->getBased(), |
| | | 'posts_per_page'=> 50, |
| | | 'post_status' => 'publish', |
| | | 'post_parent' => 0, |
| | | ]); |
| | | foreach ($timelines->posts as $post) { |
| | | $item = $this->outputReferenceSchema($post->ID, 'post', false); |
| | | $item->setId($current.'#'.$post->post_name); |
| | | $item->setName($post->post_title); |
| | | $item->setUrl(get_the_permalink($post->ID)); |
| | | $parts[] = $item; |
| | | } |
| | | |
| | | $page->setHasPart($parts); |
| | | return $page->outputSchema(); |
| | | } |
| | | public function outputGlossarySchema():array |
| | | { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | global $wp; |
| | | $current = get_home_url(null, $wp->request).'/'; |
| | | $config = $this->getConfig('archive'); |
| | | $page = [ |
| | | 'id' => $current.'#'.$registrar->getSlug(), |
| | | 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', |
| | | 'name' => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(), |
| | | 'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(), |
| | | 'url' => $current, |
| | | ]; |
| | | $page = SchemaHelper::classFromConfig($page); |
| | | |
| | | |
| | | //Defined Termset |
| | | $termset = [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\DefinedTermSet', |
| | | 'id' => $current.'#definedtermset', |
| | | 'name' => $registrar->getPlural(), |
| | | 'description' => $registrar->getDescription(), |
| | | ]; |
| | | $termset = SchemaHelper::classFromConfig($termset); |
| | | |
| | | $terms = new WP_Query([ |
| | | 'post_type' => $registrar->getBased(), |
| | | 'posts_per_page' => -1, |
| | | 'post_status' => 'publish', |
| | | ]); |
| | | |
| | | $outputTerms = []; |
| | | foreach ($terms->posts as $post) { |
| | | $item = $this->outputReferenceSchema($post->ID, 'post', false); |
| | | $item->setId($current.'#'.$post->post_name); |
| | | $item->setName($post->post_title); |
| | | $outputTerms[] = $item; |
| | | } |
| | | $termset->setHasDefinedTerm($outputTerms); |
| | | |
| | | $page->setMainEntity($termset); |
| | | return $page->outputSchema(); |
| | | } |
| | | } |
| | |
| | | protected bool $quickEdit = true; // whether to show in quick edit table |
| | | protected bool $quill; // whether to use quill |
| | | protected int $maxLength; // of characters |
| | | protected string $subtype; |
| | | protected array $allowedSubtype = ['text', 'url','number','tel','email','number']; |
| | | /** |
| | | * @var ?bool For timeline post types. Indicates whether all posts get this field, or just the parent |
| | | */ |
| | |
| | | |
| | | foreach ($config as $key => $value) { |
| | | if (property_exists($class, $key)) { |
| | | $method = 'set' . ucfirst($key); |
| | | $method = 'set'.implode('',array_map('ucfirst',explode('_', $key)));; |
| | | $class->$method($value); |
| | | } else { |
| | | error_log('Instance: '.print_r($class, true)); |
| | |
| | | } |
| | | }, $config); |
| | | } |
| | | public function setSubtype(string $subtype):void |
| | | { |
| | | if (!in_array($subtype, $this->allowedSubtype)) { |
| | | error_log('[SelectorField]Attempted subtype not allowed: '.$subtype); |
| | | return; |
| | | } |
| | | $this->subtype = $subtype; |
| | | } |
| | | public function getSubtype():string |
| | | { |
| | | return $this->subtype; |
| | | } |
| | | } |
| | |
| | | |
| | | protected int $maxUploads; |
| | | |
| | | protected function setMultiple(bool $set):void |
| | | public function setMultiple(bool $set):void |
| | | { |
| | | $this->multiple = $set; |
| | | } |
| | | protected function getMultiple():bool |
| | | public function getMultiple():bool |
| | | { |
| | | return $this->multiple; |
| | | } |
| | | |
| | | protected function setMaxUploads(int $maxUploads):void |
| | | public function setMaxUploads(int $maxUploads):void |
| | | { |
| | | $max = 20; |
| | | $this->maxUploads = min($maxUploads, $max); |
| | | } |
| | | protected function getMaxUploads():int |
| | | public function getMaxUploads():int |
| | | { |
| | | return $this->maxUploads; |
| | | } |
| | | |
| | | protected function setSubtype(string $subtype):void |
| | | public function setSubtype(string $subtype):void |
| | | { |
| | | $allowed = ['document', 'video', 'image', 'all']; |
| | | if (!in_array($subtype, $allowed)) { |
| | |
| | | } |
| | | $this->subtype = $subtype; |
| | | } |
| | | protected function getSubtype():string |
| | | public function getSubtype():string |
| | | { |
| | | return $this->subtype; |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\inc\registrar\helpers; |
| | | |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class HideSingle { |
| | | protected string $slug; |
| | | protected string $postType; |
| | | protected Registrar $registrar; |
| | | public function __construct(string $slug, Registrar $registrar) { |
| | | $this->slug = $slug; |
| | | $this->postType = jvbCheckBase($slug); |
| | | $this->registrar = $registrar; |
| | | |
| | | if ($this->registrar->hasFeature('hide_single')) { |
| | | add_filter('is_post_type_viewable', [$this, 'hideFromPublic']); |
| | | if ($this->registrar->hasFeature('redirect_to_author')) { |
| | | add_filter('post_type_link', [$this, 'redirectSingleToAuthor'], 15, 2); |
| | | add_action('template_redirect', [$this, 'actuallyRedirectToAuthor']); |
| | | } else { |
| | | add_filter('post_type_link', [$this, 'redirectSingleToArchive'], 15, 2); |
| | | add_action('template_redirect', [$this, 'actuallyRedirectToArchive']); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Use $this->hide_single |
| | | * @param bool $is_viewable |
| | | * @return bool |
| | | */ |
| | | public function hideFromPublic(bool $is_viewable):bool |
| | | { |
| | | if (!is_admin() && is_singular($this->postType)) { |
| | | return false; |
| | | } |
| | | return $is_viewable; |
| | | } |
| | | |
| | | /** |
| | | * Use $this->redirect_to_author for this method |
| | | * @param string $url |
| | | * @param \WP_Post $post |
| | | * @return string |
| | | */ |
| | | public function redirectSingleToAuthor(string $url, \WP_Post $post): string |
| | | { |
| | | if ($post->post_type !== $this->postType) { |
| | | return $url; |
| | | } |
| | | |
| | | // Redirect to author page or archive |
| | | $user_link = jvbUserProfileLink($post->post_author); |
| | | if ($user_link) { |
| | | $query_var = str_replace(BASE, '', $post->post_type); |
| | | return add_query_arg($query_var, $post->ID, get_permalink($user_link)); |
| | | } |
| | | |
| | | return get_post_type_archive_link($post->post_type); |
| | | } |
| | | public function actuallyRedirectToAuthor():void |
| | | { |
| | | if (is_singular($this->postType)) { |
| | | global $post; |
| | | $url = $this->redirectSingleToAuthor('', $post); |
| | | $url = add_query_arg($this->slug, $post->ID, $url); |
| | | wp_redirect($url, 301); |
| | | exit; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Use $this->hide_single |
| | | * @param string $url |
| | | * @param \WP_Post $post |
| | | * @return string |
| | | */ |
| | | public function redirectSingleToArchive(string $url, \WP_Post $post): string |
| | | { |
| | | if ($post->post_type !== $this->postType) { |
| | | return $url; |
| | | } |
| | | |
| | | return get_post_type_archive_link($post->post_type).'#'.$post->post_name; |
| | | } |
| | | |
| | | |
| | | public function actuallyRedirectToArchive():void |
| | | { |
| | | if (is_singular($this->postType)) { |
| | | global $post; |
| | | $url = get_post_type_archive_link($this->postType).'#'.$post->post_name; |
| | | wp_redirect($url, 301); |
| | | exit; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\registrar\helpers; |
| | | |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class MakeTimelineType { |
| | | protected string $slug; |
| | | protected string $postType; |
| | | protected Registrar $registrar; |
| | | public function __construct(string $slug, Registrar $registrar) { |
| | | $this->slug = $slug; |
| | | $this->postType = jvbCheckBase($slug); |
| | | $this->registrar = $registrar; |
| | | |
| | | add_action('template_redirect', [$this, 'redirectChildToParent']); |
| | | } |
| | | |
| | | /** |
| | | * Set $this->is_timeline |
| | | * @return void |
| | | */ |
| | | public function redirectChildToParent(): void |
| | | { |
| | | if (!is_singular($this->postType)) { |
| | | return; |
| | | } |
| | | |
| | | global $post; |
| | | |
| | | // If this post has a parent, redirect to parent |
| | | if ($post->post_parent) { |
| | | $parent_url = get_permalink($post->post_parent); |
| | | |
| | | // Add anchor or query param to indicate which child was accessed |
| | | $redirect_url = add_query_arg('update', $post->ID, $parent_url); |
| | | |
| | | wp_redirect($redirect_url, 301); |
| | | exit; |
| | | } |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\registrar\helpers; |
| | | |
| | | use JVBase\managers\CustomTable; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class MakeTrackChanges { |
| | | protected string $slug; |
| | | protected string $based; |
| | | protected CustomTable $table; |
| | | public function __construct(string $slug) { |
| | | $this->slug = $slug; |
| | | $this->based = jvbCheckBase($slug); |
| | | $this->defineTables(); |
| | | |
| | | add_action('set_object_terms', [$this, 'trackHistory'], 10, 6); |
| | | } |
| | | public function defineTables():void |
| | | { |
| | | $table = CustomTable::for('history_'.$this->slug); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'profile_id' => "{$table->getPostIDType()} NOT NULL", |
| | | 'role' => 'varchar(255) NOT NULL', |
| | | 'term_id' => "{$table->getTermIDType()} NOT NULL", |
| | | 'start_date' => 'date DEFAULT NULL', |
| | | 'end_date' => 'date DEFAULT NULL', |
| | | 'locked' => 'tinyint(1) NOT NULL DEFAULT 0', |
| | | 'created_at' => 'timestamp DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at' => 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', |
| | | ]); |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '(`profile_id`, `term_id`, `start_date`)'], |
| | | 'content_role (`term_id`, `role`)', |
| | | 'user_id (`user_id`)', |
| | | 'profile_id (`profile_id`)', |
| | | 'term_id (`term_id`)', |
| | | ]); |
| | | |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}{$this->slug}_history_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}{$this->slug}_history_profile` FOREIGN KEY (`profile_id`) |
| | | REFERENCES `{$table->getPostTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | |
| | | $table->defineTable(); |
| | | $this->table = $table; |
| | | } |
| | | |
| | | public function trackHistory(int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids):void |
| | | { |
| | | if ($taxonomy !== $this->based) { |
| | | return; |
| | | } |
| | | |
| | | $user = get_post_meta($object_id, BASE.'link', true); |
| | | if (empty($author)) { |
| | | $user = get_post($object_id)->post_author??false; |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | } |
| | | $userObj = get_userdata($user); |
| | | if (!$userObj) {return;} |
| | | |
| | | $role = jvbUserRole($user); |
| | | |
| | | $new = array_diff($tt_ids, $old_tt_ids); |
| | | $old = array_diff($old_tt_ids, $new); |
| | | |
| | | $this->table->transaction( |
| | | function() use ($new, $old, $role, $user, $object_id) { |
| | | foreach ($new as $newTerm) { |
| | | $termID = $this->getTermIDFromTTID($newTerm); |
| | | |
| | | $this->table->findOrCreate([ |
| | | 'user_id' => $user, |
| | | 'profile_id'=> $object_id, |
| | | 'role' => $role, |
| | | 'term_id' => $termID, |
| | | 'locked' => 0 |
| | | ], [ |
| | | 'start_date' => date('Y-m-d') |
| | | ]); |
| | | } |
| | | |
| | | foreach ($old as $oldTerm) { |
| | | $termID = $this->getTermIDFromTTID($oldTerm); |
| | | |
| | | $this->table->update( |
| | | [ |
| | | 'end_date' => date('Y-m-d'), |
| | | 'locked' => 1 |
| | | ], |
| | | [ |
| | | 'user_id' => $user, |
| | | 'profile_id'=> $object_id, |
| | | 'term_id' => $termID, |
| | | 'locked' => 0 |
| | | ]); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Helper function to get term_id from term_taxonomy_id |
| | | * @param int $tt_id |
| | | * |
| | | * @return int |
| | | */ |
| | | private function getTermIDFromTTID(int $tt_id):int |
| | | { |
| | | global $wpdb; |
| | | return $wpdb->get_var($wpdb->prepare( |
| | | "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d", |
| | | $tt_id |
| | | )); |
| | | } |
| | | } |
| | |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\managers\CustomTable; |
| | | class MakeVerification { |
| | | protected CustomTable $table; |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | } |
| | | |
| | | protected function defineTables():void |
| | | { |
| | | $types = implode(',', array_map(function($item) { return "`{$item}`"; },Registrar::getFeatured('verify_entry'))); |
| | | |
| | | $table = CustomTable::for('verify_entry'); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'content' |
| | | ]) |
| | | } |
| | | } |
| | |
| | | /****************************************************** |
| | | * Table Definitions |
| | | *****************************************************/ |
| | | |
| | | // MOVED TO Queue.php |
| | | // protected function queueTables():array |
| | | // { |
| | | // |
| | |
| | | // ]; |
| | | // } |
| | | |
| | | protected function errorLogTables():array |
| | | { |
| | | return [ |
| | | 'error_log'=> "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `error_type` varchar(50) NOT NULL, |
| | | `component` varchar(100) NOT NULL, |
| | | `method` varchar(100) DEFAULT NULL, |
| | | `page_url` varchar(255) DEFAULT NULL, |
| | | `message` text NOT NULL, |
| | | `context` JSON, |
| | | `severity` varchar(20) NOT NULL, |
| | | `user_id` {$this->userIDType} DEFAULT NULL, |
| | | `user_was_logged_in` tinyint(1) NOT NULL, |
| | | `source` enum('frontend','backend') NOT NULL, |
| | | `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `created_at` (`created_at`), |
| | | KEY `component_severity_date` (`component`, `severity`, `created_at`), |
| | | KEY `error_type_date` (`error_type`, `created_at`), |
| | | KEY `severity_date` (`severity`, `created_at`) |
| | | )" |
| | | ]; |
| | | } |
| | | //MOVED TO ErrorHandler.php |
| | | // protected function errorLogTables():array |
| | | // { |
| | | // return [ |
| | | // 'error_log'=> "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `error_type` varchar(50) NOT NULL, |
| | | // `component` varchar(100) NOT NULL, |
| | | // `method` varchar(100) DEFAULT NULL, |
| | | // `page_url` varchar(255) DEFAULT NULL, |
| | | // `message` text NOT NULL, |
| | | // `context` JSON, |
| | | // `severity` varchar(20) NOT NULL, |
| | | // `user_id` {$this->userIDType} DEFAULT NULL, |
| | | // `user_was_logged_in` tinyint(1) NOT NULL, |
| | | // `source` enum('frontend','backend') NOT NULL, |
| | | // `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `created_at` (`created_at`), |
| | | // KEY `component_severity_date` (`component`, `severity`, `created_at`), |
| | | // KEY `error_type_date` (`error_type`, `created_at`), |
| | | // KEY `severity_date` (`severity`, `created_at`) |
| | | // )" |
| | | // ]; |
| | | // } |
| | | |
| | | protected function userIntegrationsTable():array |
| | | { |
| | |
| | | )" |
| | | ]; |
| | | } |
| | | //MOVED TO NotificationManger.php |
| | | // protected function notificationTables():array |
| | | // { |
| | | // |
| | | // return [ |
| | | // // Main notifications table |
| | | // 'notifications' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `owner_id` {$this->userIDType} NOT NULL, |
| | | // `action_user_id` {$this->userIDType} NOT NULL, |
| | | // `target_id` bigint(20) DEFAULT NULL, |
| | | // `target_type` varchar(30) DEFAULT NULL, |
| | | // `type` enum('new_favourite','new_artist','artist_approved','artist_invitation', |
| | | // 'new_term','term_approved','term_rejected','list_shared', |
| | | // 'system_message','shop_invitation') NOT NULL, |
| | | // `status` enum('unread','read','actioned','dismissed') NOT NULL DEFAULT 'unread', |
| | | // `priority` enum('low','normal','high') NOT NULL DEFAULT 'normal', |
| | | // `message` varchar(255) DEFAULT NULL, |
| | | // `context` JSON DEFAULT NULL, |
| | | // `requires_action` tinyint(1) NOT NULL DEFAULT 0, |
| | | // `action_taken` tinyint(1) NOT NULL DEFAULT 0, |
| | | // `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // `read_at` datetime DEFAULT NULL, |
| | | // `actioned_at` datetime DEFAULT NULL, |
| | | // `emailed_at` datetime DEFAULT NULL, |
| | | // `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `user_status` (`owner_id`, `status`), |
| | | // KEY `target_lookup` (`target_id`, `target_type`), |
| | | // KEY `unread_notifications` (`owner_id`, `status`, `created_at`), |
| | | // KEY `requires_action` (`owner_id`, `requires_action`, `action_taken`), |
| | | // KEY `acting_user_lookup` (`owner_id`, `action_user_id`, `type`, `status`, `created_at`), |
| | | // CONSTRAINT `{$this->base}notify_owner` FOREIGN KEY (`owner_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}action_id` FOREIGN KEY (`action_user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )", |
| | | // |
| | | // |
| | | // 'notifications_content' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `date` date NOT NULL, |
| | | // `frequency` enum('daily','weekly','monthly') NOT NULL, |
| | | // `tattoo_count` int unsigned NOT NULL DEFAULT 0, |
| | | // `artwork_count` int unsigned NOT NULL DEFAULT 0, |
| | | // `piercing_count` int unsigned NOT NULL DEFAULT 0, |
| | | // `event_count` int unsigned NOT NULL DEFAULT 0, |
| | | // `news_count` int unsigned NOT NULL DEFAULT 0, |
| | | // `offer_count` int unsigned NOT NULL DEFAULT 0, |
| | | // `total_items` int unsigned NOT NULL DEFAULT 0, |
| | | // `has_profile_update` tinyint(1) NOT NULL DEFAULT 0, |
| | | // `new_items` JSON DEFAULT NULL, |
| | | // `updated_items` JSON DEFAULT NULL, |
| | | // `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `artist_date_frequency` (`user_id`, `date`, `frequency`), |
| | | // KEY `recent_content` (`date`, `frequency`), |
| | | // KEY `artist_frequency` (`user_id`, `frequency`), |
| | | // CONSTRAINT `{$this->base}content_artist` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )", |
| | | // |
| | | // 'notifications_user_seen' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `content_notification_id` bigint(20) unsigned NOT NULL, |
| | | // `status` enum('unread','read','dismissed') NOT NULL DEFAULT 'unread', |
| | | // `read_at` datetime DEFAULT NULL, |
| | | // `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `user_content_notif` (`user_id`, `content_notification_id`), |
| | | // KEY `user_status` (`user_id`, `status`), |
| | | // CONSTRAINT `{$this->base}user_content_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}user_content_notification` FOREIGN KEY (`content_notification_id`) |
| | | // REFERENCES `{$this->prefixed}notifications_content` (`id`) ON DELETE CASCADE |
| | | // )", |
| | | // |
| | | // // User notification preferences |
| | | // 'notification_preferences' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `item_id` bigint(20) NOT NULL, |
| | | // `notification_type` varchar(50) NOT NULL, |
| | | // `frequency` enum('never','daily','weekly','monthly') DEFAULT 'never', |
| | | // `last_sent` datetime DEFAULT NULL, |
| | | // `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `user_type` (`user_id`, `item_id`), |
| | | // KEY `user_frequency` (`user_id`, `frequency`), |
| | | // KEY `frequency_lookup` (`frequency`, `last_sent`), |
| | | // CONSTRAINT `{$this->base}notification_pref_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )", |
| | | // |
| | | // // Notification digest scheduling and tracking |
| | | // 'notification_digests' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `frequency` enum('daily','weekly','monthly') NOT NULL, |
| | | // `scheduled_at` datetime NOT NULL, |
| | | // `sent_at` datetime DEFAULT NULL, |
| | | // `status` enum('pending','processing','sent','failed') DEFAULT 'pending', |
| | | // `notification_count` int unsigned DEFAULT 0, |
| | | // `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `scheduled_digests` (`frequency`, `scheduled_at`, `status`), |
| | | // KEY `user_digests` (`user_id`, `frequency`), |
| | | // CONSTRAINT `{$this->base}digest_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )", |
| | | // |
| | | // // Analytics on notification interactions |
| | | // 'stats__notifications' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `notification_id` bigint(20) unsigned NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `action` varchar(30) NOT NULL, |
| | | // `action_source` enum('web','email','app') DEFAULT 'web', |
| | | // `action_details` JSON DEFAULT NULL, |
| | | // `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `notification_lookup` (`notification_id`), |
| | | // KEY `user_actions` (`user_id`, `action`), |
| | | // KEY `action_analysis` (`action`, `action_source`), |
| | | // CONSTRAINT `{$this->base}metrics_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}metrics_notification` FOREIGN KEY (`notification_id`) |
| | | // REFERENCES `{$this->prefixed}notifications` (`id`) ON DELETE CASCADE |
| | | // )" |
| | | // ]; |
| | | // } |
| | | |
| | | protected function notificationTables():array |
| | | { |
| | | |
| | | return [ |
| | | // Main notifications table |
| | | 'notifications' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `owner_id` {$this->userIDType} NOT NULL, |
| | | `action_user_id` {$this->userIDType} NOT NULL, |
| | | `target_id` bigint(20) DEFAULT NULL, |
| | | `target_type` varchar(30) DEFAULT NULL, |
| | | `type` enum('new_favourite','new_artist','artist_approved','artist_invitation', |
| | | 'new_term','term_approved','term_rejected','list_shared', |
| | | 'system_message','shop_invitation') NOT NULL, |
| | | `status` enum('unread','read','actioned','dismissed') NOT NULL DEFAULT 'unread', |
| | | `priority` enum('low','normal','high') NOT NULL DEFAULT 'normal', |
| | | `message` varchar(255) DEFAULT NULL, |
| | | `context` JSON DEFAULT NULL, |
| | | `requires_action` tinyint(1) NOT NULL DEFAULT 0, |
| | | `action_taken` tinyint(1) NOT NULL DEFAULT 0, |
| | | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `read_at` datetime DEFAULT NULL, |
| | | `actioned_at` datetime DEFAULT NULL, |
| | | `emailed_at` datetime DEFAULT NULL, |
| | | `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `user_status` (`owner_id`, `status`), |
| | | KEY `target_lookup` (`target_id`, `target_type`), |
| | | KEY `unread_notifications` (`owner_id`, `status`, `created_at`), |
| | | KEY `requires_action` (`owner_id`, `requires_action`, `action_taken`), |
| | | KEY `acting_user_lookup` (`owner_id`, `action_user_id`, `type`, `status`, `created_at`), |
| | | CONSTRAINT `{$this->base}notify_owner` FOREIGN KEY (`owner_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}action_id` FOREIGN KEY (`action_user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )", |
| | | //MOVED TO ApprovalManager.php |
| | | // protected function approvalTables($types):array |
| | | // { |
| | | // $tables = []; |
| | | // $save = []; |
| | | // |
| | | // foreach ($types as $type => $config) { |
| | | // $save[$type] = ($type === 'term') ? $config : 'user'; |
| | | // $tables['approval_'.$type.'_requests'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `status` enum('pending','approved','rejected','appealed','expired') DEFAULT 'pending', |
| | | // `required_approvals` int unsigned DEFAULT 3, |
| | | // `current_approvals` int unsigned DEFAULT 0, |
| | | // `current_rejections` int unsigned DEFAULT 0, |
| | | // `expires_at` datetime DEFAULT NULL, |
| | | // `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // `approved_by` json DEFAULT NULL, |
| | | // `rejected_by` json DEFAULT NULL, |
| | | // `created_item` json DEFAULT NULL, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `status` (`status`), |
| | | // KEY `expiring_requests` (`status`, `expires_at`), |
| | | // CONSTRAINT `{$this->base}{$type}_approval_requester` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )"; |
| | | // $tables['approval_'.$type.'_votes'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `request_id` bigint(20) unsigned NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `vote` enum('approve','reject','dismissed') NOT NULL, |
| | | // `notes` text DEFAULT NULL, |
| | | // `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `unique_vote` (`request_id`, `user_id`), |
| | | // KEY `user_votes` (`user_id`), |
| | | // CONSTRAINT `{$this->base}{$type}_user_approval_request` FOREIGN KEY (`request_id`) |
| | | // REFERENCES `{$this->prefixed}approval_{$type}_requests` (`id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )"; |
| | | // } |
| | | // if (!empty($save)) { |
| | | // update_option(BASE.'approvals_types', $save); |
| | | // } |
| | | // return $tables; |
| | | // } |
| | | |
| | | |
| | | 'notifications_content' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `date` date NOT NULL, |
| | | `frequency` enum('daily','weekly','monthly') NOT NULL, |
| | | `tattoo_count` int unsigned NOT NULL DEFAULT 0, |
| | | `artwork_count` int unsigned NOT NULL DEFAULT 0, |
| | | `piercing_count` int unsigned NOT NULL DEFAULT 0, |
| | | `event_count` int unsigned NOT NULL DEFAULT 0, |
| | | `news_count` int unsigned NOT NULL DEFAULT 0, |
| | | `offer_count` int unsigned NOT NULL DEFAULT 0, |
| | | `total_items` int unsigned NOT NULL DEFAULT 0, |
| | | `has_profile_update` tinyint(1) NOT NULL DEFAULT 0, |
| | | `new_items` JSON DEFAULT NULL, |
| | | `updated_items` JSON DEFAULT NULL, |
| | | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `artist_date_frequency` (`user_id`, `date`, `frequency`), |
| | | KEY `recent_content` (`date`, `frequency`), |
| | | KEY `artist_frequency` (`user_id`, `frequency`), |
| | | CONSTRAINT `{$this->base}content_artist` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )", |
| | | // protected function taxonomyRelationshipsTables():array |
| | | // { |
| | | // $tables = [ |
| | | // 'taxonomy_relationships' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `term_id` {$this->termIDType} NOT NULL, |
| | | // `related_term_id` {$this->termIDType} NOT NULL, |
| | | // `taxonomy` varchar(32) NOT NULL, |
| | | // `related_taxonomy` varchar(32) NOT NULL, |
| | | // `post_count` int(11) NOT NULL DEFAULT 0, |
| | | // `is_direct` tinyint(1) NOT NULL DEFAULT 1, |
| | | // `is_hierarchical` tinyint(1) NOT NULL DEFAULT 0, |
| | | // `last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `term_id` (`term_id`), |
| | | // KEY `related_term_id` (`related_term_id`), |
| | | // KEY `taxonomy` (`taxonomy`), |
| | | // KEY `related_taxonomy` (`related_taxonomy`), |
| | | // UNIQUE KEY `term_relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`), |
| | | // CONSTRAINT `{$this->base}tax_rel_term_id` FOREIGN KEY (`term_id`) |
| | | // REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}tax_rel_related_id` FOREIGN KEY (`related_term_id`) |
| | | // REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | // )" |
| | | // ]; |
| | | // |
| | | //// if ((array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard'] === true) || array_key_exists('use_feed_block', $this->JVB_SITE) && $this->JVB_SITE['use_feed_block']) { |
| | | // $tables['user_term_index'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `term_id` {$this->termIDType} NOT NULL, |
| | | // `taxonomy` varchar(32) NOT NULL, |
| | | // `post_count` int(11) NOT NULL DEFAULT 1, |
| | | // `is_parent` tinyint(1) NOT NULL DEFAULT 0, |
| | | // `last_used` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `user_term` (`user_id`, `term_id`, `taxonomy`), |
| | | // KEY `user_taxonomy` (`user_id`, `taxonomy`), |
| | | // KEY `taxonomy` (`taxonomy`), |
| | | // CONSTRAINT `{$this->base}user_term_user_fk` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}user_term_term_fk` FOREIGN KEY (`term_id`) |
| | | // REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | // )"; |
| | | //// } |
| | | // |
| | | // return $tables; |
| | | // } |
| | | //MOVED TO FavouritesManager.php |
| | | // protected function favouriteTables():array |
| | | // { |
| | | // |
| | | // return [ |
| | | // 'favourites' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `type` varchar(50) NOT NULL, |
| | | // `target_id` bigint(20) NOT NULL, |
| | | // `notes` text DEFAULT NULL, |
| | | // `date_added` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `unique_favourite` (`user_id`, `type`, `target_id`), |
| | | // KEY `user_type` (`user_id`, `type`), |
| | | // KEY `target_type` (`target_id`, `type`), |
| | | // CONSTRAINT `{$this->base}favourites_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )", |
| | | // 'favourites_lists' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `name` varchar(255) NOT NULL, |
| | | // `description` text, |
| | | // `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `user_lists` (`user_id`), |
| | | // CONSTRAINT `{$this->base}list_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )", |
| | | // 'favourites_list_items' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `list_id` bigint(20) unsigned NOT NULL, |
| | | // `item_type` varchar(50) NOT NULL, |
| | | // `item_id` bigint(20) NOT NULL, |
| | | // `favourite_id` bigint(20) unsigned DEFAULT NULL, |
| | | // `added_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `unique_list_item` (`list_id`, `item_type`, `item_id`), |
| | | // KEY `list_items` (`list_id`), |
| | | // KEY `favourite_id` (`favourite_id`), |
| | | // CONSTRAINT `{$this->base}list_items` FOREIGN KEY (`list_id`) |
| | | // REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}list_favourite` FOREIGN KEY (`favourite_id`) |
| | | // REFERENCES `{$this->prefixed}favourites` (`id`) ON DELETE SET NULL |
| | | // )", |
| | | // 'favourites_list_shares' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `list_id` bigint(20) unsigned NOT NULL, |
| | | // `user_id` {$this->userIDType} NULL, |
| | | // `email` varchar(255) NOT NULL, |
| | | // `permission_type` enum('view', 'edit') NOT NULL DEFAULT 'view', |
| | | // `status` enum('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending', |
| | | // `invitation_token` varchar(64) NULL, |
| | | // `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `unique_share_user` (`list_id`, `user_id`, `status`), |
| | | // UNIQUE KEY `unique_share_email` (`list_id`, `email`, `status`), |
| | | // KEY `list_shares` (`list_id`), |
| | | // KEY `status_index` (`status`), |
| | | // CONSTRAINT `{$this->base}share_list` FOREIGN KEY (`list_id`) |
| | | // REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}share_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )", |
| | | // 'favourites_list_stats' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `item_type` varchar(50) NOT NULL, |
| | | // `item_id` bigint(20) NOT NULL, |
| | | // `list_count` int NOT NULL DEFAULT 0, |
| | | // `last_added` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `unique_item_stat` (`item_type`, `item_id`), |
| | | // KEY `item_stats` (`item_type`, `item_id`) |
| | | // )", |
| | | // ]; |
| | | // } |
| | | |
| | | 'notifications_user_seen' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `content_notification_id` bigint(20) unsigned NOT NULL, |
| | | `status` enum('unread','read','dismissed') NOT NULL DEFAULT 'unread', |
| | | `read_at` datetime DEFAULT NULL, |
| | | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `user_content_notif` (`user_id`, `content_notification_id`), |
| | | KEY `user_status` (`user_id`, `status`), |
| | | CONSTRAINT `{$this->base}user_content_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}user_content_notification` FOREIGN KEY (`content_notification_id`) |
| | | REFERENCES `{$this->prefixed}notifications_content` (`id`) ON DELETE CASCADE |
| | | )", |
| | | //MOVED TO ForumManager.php |
| | | // protected function newsRelationshipsTable():array |
| | | // { |
| | | // return [ |
| | | // 'news_relationships' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `shop_id` {$this->termIDType} NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `artist_id` {$this->postIDType} NOT NULL, |
| | | // `news_count` int(10) unsigned NOT NULL DEFAULT 0, |
| | | // `last_post_date` datetime DEFAULT NULL, |
| | | // `last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `shop_user` (`shop_id`, `user_id`), |
| | | // KEY `shop_id` (`shop_id`), |
| | | // KEY `user_id` (`user_id`), |
| | | // KEY `artist_id` (`artist_id`), |
| | | // CONSTRAINT `{$this->base}nr_shop_news_shop` FOREIGN KEY (`shop_id`) |
| | | // REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}nr_shop_news_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}nr_shop_news_artist` FOREIGN KEY (`artist_id`) |
| | | // REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL |
| | | // )" |
| | | // ]; |
| | | // } |
| | | |
| | | // User notification preferences |
| | | 'notification_preferences' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `item_id` bigint(20) NOT NULL, |
| | | `notification_type` varchar(50) NOT NULL, |
| | | `frequency` enum('never','daily','weekly','monthly') DEFAULT 'never', |
| | | `last_sent` datetime DEFAULT NULL, |
| | | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `user_type` (`user_id`, `item_id`), |
| | | KEY `user_frequency` (`user_id`, `frequency`), |
| | | KEY `frequency_lookup` (`frequency`, `last_sent`), |
| | | CONSTRAINT `{$this->base}notification_pref_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )", |
| | | //MOVED TO ResponseManager.php |
| | | // protected function responseTable():array |
| | | // { |
| | | // return [ |
| | | // 'responses' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `item_id` {$this->postIDType} NOT NULL, |
| | | // `content` text NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `parent_id` bigint(20) unsigned DEFAULT NULL, |
| | | // `response` text NOT NULL, |
| | | // `status` enum('published','hidden','flagged','deleted') DEFAULT 'published', |
| | | // `is_user_deleted` tinyint(1) DEFAULT 0, |
| | | // `upvotes` int NOT NULL DEFAULT 0, |
| | | // `downvotes` int NOT NULL DEFAULT 0, |
| | | // `karma` int NOT NULL DEFAULT 0, |
| | | // `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `item_lookup` (`item_id`, `status`), |
| | | // KEY `user_comments` (`user_id`), |
| | | // KEY `parent_child` (`parent_id`), |
| | | // KEY `karma_order` (`karma`), |
| | | // CONSTRAINT `{$this->base}re_responses_news` FOREIGN KEY (`item_id`) |
| | | // REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}re_responses_parent` FOREIGN KEY (`parent_id`) |
| | | // REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE SET NULL |
| | | // )", |
| | | // 'karma_response' => "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `item_id` bigint(20) NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `vote` enum('up','down') NOT NULL, |
| | | // `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `user_post` (`user_id`,`item_id`), |
| | | // KEY `item_id` (`item_id`), |
| | | // KEY `user_id` (`user_id`), |
| | | // CONSTRAINT `{$this->base}_response_item_id` FOREIGN KEY (`item_id`) |
| | | // REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}_response_user_id` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )" |
| | | // ]; |
| | | // } |
| | | |
| | | // Notification digest scheduling and tracking |
| | | 'notification_digests' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `frequency` enum('daily','weekly','monthly') NOT NULL, |
| | | `scheduled_at` datetime NOT NULL, |
| | | `sent_at` datetime DEFAULT NULL, |
| | | `status` enum('pending','processing','sent','failed') DEFAULT 'pending', |
| | | `notification_count` int unsigned DEFAULT 0, |
| | | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `scheduled_digests` (`frequency`, `scheduled_at`, `status`), |
| | | KEY `user_digests` (`user_id`, `frequency`), |
| | | CONSTRAINT `{$this->base}digest_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )", |
| | | |
| | | // Analytics on notification interactions |
| | | 'stats__notifications' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `notification_id` bigint(20) unsigned NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `action` varchar(30) NOT NULL, |
| | | `action_source` enum('web','email','app') DEFAULT 'web', |
| | | `action_details` JSON DEFAULT NULL, |
| | | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `notification_lookup` (`notification_id`), |
| | | KEY `user_actions` (`user_id`, `action`), |
| | | KEY `action_analysis` (`action`, `action_source`), |
| | | CONSTRAINT `{$this->base}metrics_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}metrics_notification` FOREIGN KEY (`notification_id`) |
| | | REFERENCES `{$this->prefixed}notifications` (`id`) ON DELETE CASCADE |
| | | )" |
| | | ]; |
| | | } |
| | | |
| | | protected function approvalTables($types):array |
| | | { |
| | | $tables = []; |
| | | $save = []; |
| | | |
| | | foreach ($types as $type => $config) { |
| | | $save[$type] = ($type === 'term') ? $config : 'user'; |
| | | $tables['approval_'.$type.'_requests'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `status` enum('pending','approved','rejected','appealed','expired') DEFAULT 'pending', |
| | | `required_approvals` int unsigned DEFAULT 3, |
| | | `current_approvals` int unsigned DEFAULT 0, |
| | | `current_rejections` int unsigned DEFAULT 0, |
| | | `expires_at` datetime DEFAULT NULL, |
| | | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | `approved_by` json DEFAULT NULL, |
| | | `rejected_by` json DEFAULT NULL, |
| | | `created_item` json DEFAULT NULL, |
| | | PRIMARY KEY (`id`), |
| | | KEY `status` (`status`), |
| | | KEY `expiring_requests` (`status`, `expires_at`), |
| | | CONSTRAINT `{$this->base}{$type}_approval_requester` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )"; |
| | | $tables['approval_'.$type.'_votes'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `request_id` bigint(20) unsigned NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `vote` enum('approve','reject','dismissed') NOT NULL, |
| | | `notes` text DEFAULT NULL, |
| | | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `unique_vote` (`request_id`, `user_id`), |
| | | KEY `user_votes` (`user_id`), |
| | | CONSTRAINT `{$this->base}{$type}_user_approval_request` FOREIGN KEY (`request_id`) |
| | | REFERENCES `{$this->prefixed}approval_{$type}_requests` (`id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )"; |
| | | } |
| | | if (!empty($save)) { |
| | | update_option(BASE.'approvals_types', $save); |
| | | } |
| | | return $tables; |
| | | } |
| | | |
| | | |
| | | protected function taxonomyRelationshipsTables():array |
| | | { |
| | | $tables = [ |
| | | 'taxonomy_relationships' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `term_id` {$this->termIDType} NOT NULL, |
| | | `related_term_id` {$this->termIDType} NOT NULL, |
| | | `taxonomy` varchar(32) NOT NULL, |
| | | `related_taxonomy` varchar(32) NOT NULL, |
| | | `post_count` int(11) NOT NULL DEFAULT 0, |
| | | `is_direct` tinyint(1) NOT NULL DEFAULT 1, |
| | | `is_hierarchical` tinyint(1) NOT NULL DEFAULT 0, |
| | | `last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `term_id` (`term_id`), |
| | | KEY `related_term_id` (`related_term_id`), |
| | | KEY `taxonomy` (`taxonomy`), |
| | | KEY `related_taxonomy` (`related_taxonomy`), |
| | | UNIQUE KEY `term_relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`), |
| | | CONSTRAINT `{$this->base}tax_rel_term_id` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}tax_rel_related_id` FOREIGN KEY (`related_term_id`) |
| | | REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | )" |
| | | ]; |
| | | |
| | | if ((array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard'] === true) || array_key_exists('use_feed_block', $this->JVB_SITE) && $this->JVB_SITE['use_feed_block']) { |
| | | $tables['user_term_index'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `term_id` {$this->termIDType} NOT NULL, |
| | | `taxonomy` varchar(32) NOT NULL, |
| | | `post_count` int(11) NOT NULL DEFAULT 1, |
| | | `is_parent` tinyint(1) NOT NULL DEFAULT 0, |
| | | `last_used` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `user_term` (`user_id`, `term_id`, `taxonomy`), |
| | | KEY `user_taxonomy` (`user_id`, `taxonomy`), |
| | | KEY `taxonomy` (`taxonomy`), |
| | | CONSTRAINT `{$this->base}user_term_user_fk` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}user_term_term_fk` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | )"; |
| | | } |
| | | |
| | | return $tables; |
| | | } |
| | | |
| | | protected function favouriteTables():array |
| | | { |
| | | |
| | | return [ |
| | | 'favourites' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `type` varchar(50) NOT NULL, |
| | | `target_id` bigint(20) NOT NULL, |
| | | `notes` text DEFAULT NULL, |
| | | `date_added` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `unique_favourite` (`user_id`, `type`, `target_id`), |
| | | KEY `user_type` (`user_id`, `type`), |
| | | KEY `target_type` (`target_id`, `type`), |
| | | CONSTRAINT `{$this->base}favourites_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )", |
| | | 'favourites_lists' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `name` varchar(255) NOT NULL, |
| | | `description` text, |
| | | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `user_lists` (`user_id`), |
| | | CONSTRAINT `{$this->base}list_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )", |
| | | 'favourites_list_items' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `list_id` bigint(20) unsigned NOT NULL, |
| | | `item_type` varchar(50) NOT NULL, |
| | | `item_id` bigint(20) NOT NULL, |
| | | `favourite_id` bigint(20) unsigned DEFAULT NULL, |
| | | `added_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `unique_list_item` (`list_id`, `item_type`, `item_id`), |
| | | KEY `list_items` (`list_id`), |
| | | KEY `favourite_id` (`favourite_id`), |
| | | CONSTRAINT `{$this->base}list_items` FOREIGN KEY (`list_id`) |
| | | REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}list_favourite` FOREIGN KEY (`favourite_id`) |
| | | REFERENCES `{$this->prefixed}favourites` (`id`) ON DELETE SET NULL |
| | | )", |
| | | 'favourites_list_shares' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `list_id` bigint(20) unsigned NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `email` varchar(255) NOT NULL, |
| | | `permission_type` enum('view', 'edit') NOT NULL DEFAULT 'view', |
| | | `status` enum('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending', |
| | | `invitation_token` varchar(64) NULL, |
| | | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `unique_share_user` (`list_id`, `user_id`, `status`), |
| | | UNIQUE KEY `unique_share_email` (`list_id`, `email`, `status`), |
| | | KEY `list_shares` (`list_id`), |
| | | KEY `status_index` (`status`), |
| | | CONSTRAINT `{$this->base}share_list` FOREIGN KEY (`list_id`) |
| | | REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}share_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )", |
| | | 'favourites_list_stats' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `item_type` varchar(50) NOT NULL, |
| | | `item_id` bigint(20) NOT NULL, |
| | | `list_count` int NOT NULL DEFAULT 0, |
| | | `last_added` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `unique_item_stat` (`item_type`, `item_id`), |
| | | KEY `item_stats` (`item_type`, `item_id`) |
| | | )", |
| | | ]; |
| | | } |
| | | |
| | | protected function newsRelationshipsTable():array |
| | | { |
| | | return [ |
| | | 'news_relationships' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `shop_id` {$this->termIDType} NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `artist_id` {$this->postIDType} NOT NULL, |
| | | `news_count` int(10) unsigned NOT NULL DEFAULT 0, |
| | | `last_post_date` datetime DEFAULT NULL, |
| | | `last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `shop_user` (`shop_id`, `user_id`), |
| | | KEY `shop_id` (`shop_id`), |
| | | KEY `user_id` (`user_id`), |
| | | KEY `artist_id` (`artist_id`), |
| | | CONSTRAINT `{$this->base}nr_shop_news_shop` FOREIGN KEY (`shop_id`) |
| | | REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}nr_shop_news_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}nr_shop_news_artist` FOREIGN KEY (`artist_id`) |
| | | REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL |
| | | )" |
| | | ]; |
| | | } |
| | | |
| | | protected function responseTable():array |
| | | { |
| | | return [ |
| | | 'responses' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `item_id` {$this->postIDType} NOT NULL, |
| | | `content` text NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `parent_id` bigint(20) unsigned DEFAULT NULL, |
| | | `response` text NOT NULL, |
| | | `status` enum('published','hidden','flagged','deleted') DEFAULT 'published', |
| | | `is_user_deleted` tinyint(1) DEFAULT 0, |
| | | `upvotes` int NOT NULL DEFAULT 0, |
| | | `downvotes` int NOT NULL DEFAULT 0, |
| | | `karma` int NOT NULL DEFAULT 0, |
| | | `created_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `item_lookup` (`item_id`, `status`), |
| | | KEY `user_comments` (`user_id`), |
| | | KEY `parent_child` (`parent_id`), |
| | | KEY `karma_order` (`karma`), |
| | | CONSTRAINT `{$this->base}re_responses_news` FOREIGN KEY (`item_id`) |
| | | REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}re_responses_parent` FOREIGN KEY (`parent_id`) |
| | | REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE SET NULL |
| | | )", |
| | | 'karma_response' => "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `item_id` bigint(20) NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `vote` enum('up','down') NOT NULL, |
| | | `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `user_post` (`user_id`,`item_id`), |
| | | KEY `item_id` (`item_id`), |
| | | KEY `user_id` (`user_id`), |
| | | CONSTRAINT `{$this->base}_response_item_id` FOREIGN KEY (`item_id`) |
| | | REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}_response_user_id` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )" |
| | | ]; |
| | | } |
| | | |
| | | protected function karmaTables(array $types):array |
| | | { |
| | | $tables = []; |
| | | foreach ($types as $type => $config) { |
| | | $t = false; |
| | | if (array_key_exists($type, $this->JVB_CONTENT)) { |
| | | $t = 'posts'; |
| | | } elseif (array_key_exists($type, $this->JVB_TAXONOMY)) { |
| | | $t = 'terms'; |
| | | } elseif (array_key_exists($type, $this->JVB_USER)) { |
| | | $t = 'users'; |
| | | } |
| | | |
| | | if (!$t) { |
| | | continue; |
| | | } |
| | | |
| | | switch ($t) { |
| | | case 'posts': |
| | | $referenceType = $this->postIDType; |
| | | $reference_table = $this->wpdb->posts; |
| | | $reference_column = 'ID'; |
| | | break; |
| | | case 'terms': |
| | | $referenceType = $this->termIDType; |
| | | $reference_table = $this->wpdb->terms; |
| | | $reference_column = 'term_id'; |
| | | break; |
| | | case 'users': |
| | | $referenceType = $this->userIDType; |
| | | $reference_table = $this->userTable; |
| | | $reference_column = 'ID'; |
| | | break; |
| | | } |
| | | |
| | | $tables['karma_'.$type] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `item_id` {$referenceType} NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `vote` enum('up','down') NOT NULL, |
| | | `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `user_post` (`user_id`,`item_id`), |
| | | KEY `item_id` (`item_id`), |
| | | KEY `user_id` (`user_id`), |
| | | CONSTRAINT `{$this->base}kt_{$type}_item_id` FOREIGN KEY (`item_id`) |
| | | REFERENCES `{$reference_table}` (`{$reference_column}`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}kt_{$type}_user_id` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )"; |
| | | |
| | | } |
| | | |
| | | return $tables; |
| | | } |
| | | //MOVED TO KarmaManager.php |
| | | // protected function karmaTables(array $types):array |
| | | // { |
| | | // $tables = []; |
| | | // foreach ($types as $type => $config) { |
| | | // $t = false; |
| | | // if (array_key_exists($type, $this->JVB_CONTENT)) { |
| | | // $t = 'posts'; |
| | | // } elseif (array_key_exists($type, $this->JVB_TAXONOMY)) { |
| | | // $t = 'terms'; |
| | | // } elseif (array_key_exists($type, $this->JVB_USER)) { |
| | | // $t = 'users'; |
| | | // } |
| | | // |
| | | // if (!$t) { |
| | | // continue; |
| | | // } |
| | | // |
| | | // switch ($t) { |
| | | // case 'posts': |
| | | // $referenceType = $this->postIDType; |
| | | // $reference_table = $this->wpdb->posts; |
| | | // $reference_column = 'ID'; |
| | | // break; |
| | | // case 'terms': |
| | | // $referenceType = $this->termIDType; |
| | | // $reference_table = $this->wpdb->terms; |
| | | // $reference_column = 'term_id'; |
| | | // break; |
| | | // case 'users': |
| | | // $referenceType = $this->userIDType; |
| | | // $reference_table = $this->userTable; |
| | | // $reference_column = 'ID'; |
| | | // break; |
| | | // } |
| | | // |
| | | // $tables['karma_'.$type] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `item_id` {$referenceType} NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `vote` enum('up','down') NOT NULL, |
| | | // `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `user_post` (`user_id`,`item_id`), |
| | | // KEY `item_id` (`item_id`), |
| | | // KEY `user_id` (`user_id`), |
| | | // CONSTRAINT `{$this->base}kt_{$type}_item_id` FOREIGN KEY (`item_id`) |
| | | // REFERENCES `{$reference_table}` (`{$reference_column}`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}kt_{$type}_user_id` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | // )"; |
| | | // |
| | | // } |
| | | // |
| | | // return $tables; |
| | | // } |
| | | |
| | | protected function calendarTables(array $types):array |
| | | { |
| | |
| | | ]; |
| | | } |
| | | |
| | | protected function invitationTables(array $config): array |
| | | /*protected function invitationTables(array $config): array |
| | | { |
| | | if (empty($config['roles']) && empty($config['terms'])) { |
| | | return []; |
| | |
| | | $definitions .= ")"; |
| | | |
| | | return ['invitations' => $definitions]; |
| | | } |
| | | }*/ |
| | | |
| | | protected function trackChangesTables($types) |
| | | { |
| | | $tables = []; |
| | | foreach ($types as $type => $config) { |
| | | $contents = $config['for_content']; |
| | | foreach ($contents as $content) { |
| | | $tables['history_'.$content.'_'.$type] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `content_id` bigint(20) NOT NULL, |
| | | `term_id` {$this->termIDType} NOT NULL, |
| | | `role` varchar(50) DEFAULT 'artist', |
| | | `is_primary` tinyint(1) DEFAULT 0, |
| | | `start_date` date DEFAULT NULL, |
| | | `end_date` date DEFAULT NULL, |
| | | `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `content_term` (`content_id`, `term_id`), |
| | | KEY content_role (`term_id`, `role`), |
| | | CONSTRAINT `{$this->base}{$content}_{$type}_history_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}{$content}_{$type}_history_term` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | )"; |
| | | } |
| | | } |
| | | //MOVED TO MakeTrackChanges.php |
| | | // protected function trackChangesTables($types) |
| | | // { |
| | | // $tables = []; |
| | | // foreach ($types as $type => $config) { |
| | | // $contents = $config['for_content']; |
| | | // foreach ($contents as $content) { |
| | | // $tables['history_'.$content.'_'.$type] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `content_id` bigint(20) NOT NULL, |
| | | // `term_id` {$this->termIDType} NOT NULL, |
| | | // `role` varchar(50) DEFAULT 'artist', |
| | | // `is_primary` tinyint(1) DEFAULT 0, |
| | | // `start_date` date DEFAULT NULL, |
| | | // `end_date` date DEFAULT NULL, |
| | | // `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `content_term` (`content_id`, `term_id`), |
| | | // KEY content_role (`term_id`, `role`), |
| | | // CONSTRAINT `{$this->base}{$content}_{$type}_history_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}{$content}_{$type}_history_term` FOREIGN KEY (`term_id`) |
| | | // REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | // )"; |
| | | // } |
| | | // } |
| | | // |
| | | // return $tables; |
| | | // } |
| | | |
| | | return $tables; |
| | | } |
| | | protected function requestEntryTables($types) |
| | | { |
| | | $tables = []; |
| | | foreach ($types as $type => $config) { |
| | | $contents = $config['for_content']; |
| | | foreach ($contents as $content) { |
| | | $tables[$content.'_'.$type.'_requests'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `content_id` bigint(20) NOT NULL, |
| | | `term_id` {$this->termIDType} NOT NULL, |
| | | `managers` json DEFAULT NULL, |
| | | `status` ENUM('requested', 'rejected', 'accepted') DEFAULT 'requested', |
| | | `dismissed` smallint(1) unsigned DEFAULT NULL, |
| | | `created_date` timestamp DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_date` timestamp DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, |
| | | `notes` text DEFAULT NULL, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `{$this->base}content_term` (`content_id`, `term_id`), |
| | | CONSTRAINT `{$this->base}{$content}_{$type}_request_user` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}{$content}_{$type}_request_term` FOREIGN KEY (`term_id`) |
| | | REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | )"; |
| | | } |
| | | } |
| | | //MOVED TO VerifyEntryManager.php |
| | | // protected function requestEntryTables($types) |
| | | // { |
| | | // $tables = []; |
| | | // foreach ($types as $type => $config) { |
| | | // $contents = $config['for_content']; |
| | | // foreach ($contents as $content) { |
| | | // $tables[$content.'_'.$type.'_requests'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `content_id` bigint(20) NOT NULL, |
| | | // `term_id` {$this->termIDType} NOT NULL, |
| | | // `managers` json DEFAULT NULL, |
| | | // `status` ENUM('requested', 'rejected', 'accepted') DEFAULT 'requested', |
| | | // `dismissed` smallint(1) unsigned DEFAULT NULL, |
| | | // `created_date` timestamp DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_date` timestamp DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, |
| | | // `notes` text DEFAULT NULL, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `{$this->base}content_term` (`content_id`, `term_id`), |
| | | // CONSTRAINT `{$this->base}{$content}_{$type}_request_user` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}{$content}_{$type}_request_term` FOREIGN KEY (`term_id`) |
| | | // REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE |
| | | // )"; |
| | | // } |
| | | // } |
| | | // |
| | | // return $tables; |
| | | // } |
| | | |
| | | return $tables; |
| | | } |
| | | |
| | | |
| | | // MOVED TO ReferralManager.php |
| | | /** |
| | | * Create referral tracking tables |
| | | * |
| | | * Call this from the main table creation method in CheckCustomTables.php: |
| | | * $tables = array_merge($tables, $this->referralTables()); |
| | | */ |
| | | protected function referralTables(): array |
| | | { |
| | | // Create tables in dependency order |
| | | // First: referrals (depends only on wp_users) |
| | | $mainTable['referrals'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `referrer_id` {$this->userIDType} NOT NULL, |
| | | `referee_id` {$this->userIDType} NOT NULL, |
| | | `referee_name` varchar(255) NOT NULL, |
| | | `referee_email` varchar(255) NOT NULL, |
| | | `referee_phone` varchar(50) DEFAULT NULL, |
| | | `referral_code` varchar(50) NOT NULL, |
| | | `status` enum('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending', |
| | | `referred_at` datetime NOT NULL, |
| | | `consulted_at` datetime DEFAULT NULL, |
| | | `treated_at` datetime DEFAULT NULL, |
| | | `treatment_count` int DEFAULT 0, |
| | | `notes` text DEFAULT NULL, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `referee_unique` (`referee_id`), |
| | | KEY `referrer_idx` (`referrer_id`), |
| | | KEY `status_idx` (`status`), |
| | | KEY `code_idx` (`referral_code`), |
| | | KEY `date_idx` (`referred_at`), |
| | | KEY `consult_idx` (`consulted_at`), |
| | | CONSTRAINT `{$this->base}referral_referrer_fk` FOREIGN KEY (`referrer_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}referral_referee_fk` FOREIGN KEY (`referee_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )"; |
| | | |
| | | // Create the main referrals table first |
| | | $this->createTables($mainTable); |
| | | |
| | | // Now create dependent tables |
| | | $dependentTables = []; |
| | | |
| | | // Second: jane_clients (depends only on wp_users) |
| | | $dependentTables['jane_clients'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `patient_guid` varchar(50) NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `first_name` varchar(100) NOT NULL, |
| | | `last_name` varchar(100) NOT NULL, |
| | | `email` varchar(255) NOT NULL, |
| | | `imported_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `patient_guid_unique` (`patient_guid`), |
| | | KEY `user_idx` (`user_id`), |
| | | KEY `email_idx` (`email`), |
| | | CONSTRAINT `{$this->base}jane_client_user_fk` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )"; |
| | | |
| | | // Third: referral_treatments (depends on referrals AND wp_users) |
| | | $dependentTables['referral_treatments'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `referral_id` bigint(20) unsigned NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `treatment_type` varchar(100) NOT NULL COMMENT 'Tier 1-6, Brows, etc', |
| | | `treatment_date` datetime NOT NULL, |
| | | `invoice_number` varchar(50) DEFAULT NULL, |
| | | `amount` decimal(10,2) DEFAULT NULL, |
| | | `status` enum('completed', 'no_show', 'cancelled') DEFAULT 'completed', |
| | | `imported_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `referral_idx` (`referral_id`), |
| | | KEY `user_idx` (`user_id`), |
| | | KEY `date_idx` (`treatment_date`), |
| | | KEY `type_idx` (`treatment_type`), |
| | | CONSTRAINT `{$this->base}treatment_referral_fk` FOREIGN KEY (`referral_id`) |
| | | REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}treatment_user_fk` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )"; |
| | | |
| | | // Fourth: referral_rewards (depends on referrals AND wp_users) |
| | | $dependentTables['referral_rewards'] = "( |
| | | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | `referral_id` bigint(20) unsigned NOT NULL, |
| | | `user_id` {$this->userIDType} NOT NULL, |
| | | `reward_type` enum('referrer', 'referee') NOT NULL, |
| | | `amount` decimal(10,2) NOT NULL, |
| | | `reward_calculation` varchar(20) DEFAULT NULL COMMENT 'percentage or fixed', |
| | | `status` enum('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available', |
| | | `created_at` datetime NOT NULL, |
| | | `redeemed_at` datetime DEFAULT NULL, |
| | | `expires_at` datetime DEFAULT NULL, |
| | | `notes` text DEFAULT NULL, |
| | | PRIMARY KEY (`id`), |
| | | KEY `referral_idx` (`referral_id`), |
| | | KEY `user_idx` (`user_id`), |
| | | KEY `status_idx` (`status`), |
| | | KEY `type_idx` (`reward_type`), |
| | | CONSTRAINT `{$this->base}reward_referral_fk` FOREIGN KEY (`referral_id`) |
| | | REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE, |
| | | CONSTRAINT `{$this->base}reward_user_fk` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | )"; |
| | | |
| | | return $dependentTables; |
| | | } |
| | | // protected function referralTables(): array |
| | | // { |
| | | // // Create tables in dependency order |
| | | // // First: referrals (depends only on wp_users) |
| | | // $mainTable['referrals'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `referrer_id` {$this->userIDType} NOT NULL, |
| | | // `referee_id` {$this->userIDType} NOT NULL, |
| | | // `referee_name` varchar(255) NOT NULL, |
| | | // `referee_email` varchar(255) NOT NULL, |
| | | // `referee_phone` varchar(50) DEFAULT NULL, |
| | | // `referral_code` varchar(50) NOT NULL, |
| | | // `status` enum('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending', |
| | | // `referred_at` datetime NOT NULL, |
| | | // `consulted_at` datetime DEFAULT NULL, |
| | | // `treated_at` datetime DEFAULT NULL, |
| | | // `treatment_count` int DEFAULT 0, |
| | | // `notes` text DEFAULT NULL, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `referee_unique` (`referee_id`, `referral_code`), |
| | | // KEY `referrer_idx` (`referrer_id`), |
| | | // KEY `status_idx` (`status`), |
| | | // KEY `code_idx` (`referral_code`), |
| | | // KEY `date_idx` (`referred_at`), |
| | | // KEY `consult_idx` (`consulted_at`), |
| | | // CONSTRAINT `{$this->base}referral_referrer_fk` FOREIGN KEY (`referrer_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}referral_referee_fk` FOREIGN KEY (`referee_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | //)"; |
| | | // |
| | | // // Create the main referrals table first |
| | | // $this->createTables($mainTable); |
| | | // |
| | | // // Now create dependent tables |
| | | // $dependentTables = []; |
| | | // |
| | | // // Second: jane_clients (depends only on wp_users) |
| | | // $dependentTables['jane_clients'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `patient_guid` varchar(50) NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `first_name` varchar(100) NOT NULL, |
| | | // `last_name` varchar(100) NOT NULL, |
| | | // `email` varchar(255) NOT NULL, |
| | | // `imported_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // UNIQUE KEY `patient_guid_unique` (`patient_guid`), |
| | | // KEY `user_idx` (`user_id`), |
| | | // KEY `email_idx` (`email`), |
| | | // CONSTRAINT `{$this->base}jane_client_user_fk` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | //)"; |
| | | // |
| | | // // Third: referral_treatments (depends on referrals AND wp_users) |
| | | // $dependentTables['referral_treatments'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `referral_id` bigint(20) unsigned NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `treatment_type` varchar(100) NOT NULL COMMENT 'Tier 1-6, Brows, etc', |
| | | // `treatment_date` datetime NOT NULL, |
| | | // `invoice_number` varchar(50) DEFAULT NULL, |
| | | // `amount` decimal(10,2) DEFAULT NULL, |
| | | // `status` enum('completed', 'no_show', 'cancelled') DEFAULT 'completed', |
| | | // `imported_at` datetime DEFAULT CURRENT_TIMESTAMP, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `referral_idx` (`referral_id`), |
| | | // KEY `user_idx` (`user_id`), |
| | | // KEY `date_idx` (`treatment_date`), |
| | | // KEY `type_idx` (`treatment_type`), |
| | | // CONSTRAINT `{$this->base}treatment_referral_fk` FOREIGN KEY (`referral_id`) |
| | | // REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}treatment_user_fk` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | //)"; |
| | | // |
| | | // // Fourth: referral_rewards (depends on referrals AND wp_users) |
| | | // $dependentTables['referral_rewards'] = "( |
| | | // `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
| | | // `referral_id` bigint(20) unsigned NOT NULL, |
| | | // `user_id` {$this->userIDType} NOT NULL, |
| | | // `reward_type` enum('referrer', 'referee') NOT NULL, |
| | | // `amount` decimal(10,2) NOT NULL, |
| | | // `reward_calculation` varchar(20) DEFAULT NULL COMMENT 'percentage or fixed', |
| | | // `status` enum('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available', |
| | | // `created_at` datetime NOT NULL, |
| | | // `redeemed_at` datetime DEFAULT NULL, |
| | | // `expires_at` datetime DEFAULT NULL, |
| | | // `notes` text DEFAULT NULL, |
| | | // PRIMARY KEY (`id`), |
| | | // KEY `referral_idx` (`referral_id`), |
| | | // KEY `user_idx` (`user_id`), |
| | | // KEY `status_idx` (`status`), |
| | | // KEY `type_idx` (`reward_type`), |
| | | // CONSTRAINT `{$this->base}reward_referral_fk` FOREIGN KEY (`referral_id`) |
| | | // REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE, |
| | | // CONSTRAINT `{$this->base}reward_user_fk` FOREIGN KEY (`user_id`) |
| | | // REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE |
| | | //)"; |
| | | // |
| | | // return $dependentTables; |
| | | // } |
| | | |
| | | /******************************************************************************************* |
| | | * These methods help create a content-type taxonomy, like the tattoo shops in edmonton.ink |
| | |
| | | 'target_id' => $target_id |
| | | ]); |
| | | |
| | | if ($result['created']) { |
| | | if ((bool)$result) { |
| | | $this->updateFavouriteCount($type, $target_id); |
| | | $this->maybeNotifyOwner($type, $target_id, $user_id); |
| | | } |
| | |
| | | 'type' => $type, |
| | | 'target_id' => $target_id |
| | | ]); |
| | | if ($result['created']) $results['added']++; |
| | | if ((bool) $result) $results['added']++; |
| | | } else { |
| | | $deleted = $table->where([ |
| | | $deleted = $table->delete([ |
| | | 'user_id' => $user_id, |
| | | 'type' => $type, |
| | | 'target_id' => $target_id |
| | | ])->deleteResults(); |
| | | ]); |
| | | if ($deleted) $results['removed']++; |
| | | } |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\managers\TaxonomyRelationships; |
| | | use JVBase\managers\UserTermsManager; |
| | | use JVBase\rest\Route; |
| | | use JVBase\utility\Features; |
| | |
| | | |
| | | // Add relationship data if requested |
| | | if ($request->get_param('include_relationships')) { |
| | | $relationship_manager = new TaxonomyRelationships(); |
| | | $relationship_manager = JVB()->termRelationships(); |
| | | $related_taxonomies = $request->get_param('related_taxonomies') ?: ['jvb_style', 'jvb_theme', 'jvb_city']; |
| | | |
| | | $data['relationships'] = []; |
| | |
| | | if (array_key_exists('content', $data)) { |
| | | // If content_type is provided, use the specialized endpoint |
| | | $content_type = $request->get_param('content'); |
| | | global $feed_types; |
| | | if (taxIsJVBContentTax($content_type)) { |
| | | $registrar = Registrar::getInstance($content_type); |
| | | if ($registrar->hasFeature('is_content')) { |
| | | $response = $this->getTermsForContentType($request); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | |
| | | $main_context = json_decode($request->get_param('main_context'), true); |
| | | $userID = get_post_meta($main_context['id'], BASE.'link', true); |
| | | $manager = new UserTermsManager(); |
| | | $related = $manager->getUserTermIDs($userID, $taxonomy); |
| | | $related = $manager->fetchUserTerms($userID, $taxonomy); |
| | | |
| | | if (empty($related)) { |
| | | $response = $this->emptyResult(); |
| | |
| | | $main_context = json_decode($request->get_param('main_context'), true); |
| | | $thisTaxonomy = str_replace('taxonomy:', '', $main_context['context']); |
| | | $ID = (int)$main_context['id']; |
| | | $manager = new TaxonomyRelationships(); |
| | | $manager = JVB()->termRelationships(); |
| | | $related = $manager->getRelatedTerms($ID, BASE.$request->get_param('taxonomy')); |
| | | |
| | | if (empty($related)) { |
| | |
| | | $match = $request->get_param('match') ?? 'any'; |
| | | $context = json_decode($request['context'], true); |
| | | |
| | | $relationshipManager = new TaxonomyRelationships(); |
| | | $relationshipManager = JVB()->termRelationships(); |
| | | // Prepare array to collect term IDs that match the context |
| | | $related_term_ids = []; |
| | | |
| | |
| | | */ |
| | | public function getTermsForContentType(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $manager = new TaxonomyRelationships(); |
| | | $manager = JVB()->termRelationships(); |
| | | $content_type = BASE . $request->get_param('content'); |
| | | $taxonomy = BASE . $request->get_param('taxonomy'); |
| | | $search = $request->get_param('search'); |
| | |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\managers\KarmaManager; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Response; |
| | | use JVBase\rest\Rest; |
| | |
| | | |
| | | $vote = sanitize_text_field($request->get_param('vote')??''); |
| | | $itemID = absint($request->get_param('item_id')??0); |
| | | if ($itemID === 0 || !in_array($vote, ['up', 'down'])) { |
| | | if ($itemID === 0 || !in_array($vote, [true, false, null])) { |
| | | return Response::validationError(['message' => __('Invalid item or vote attempt', 'jvb')]); |
| | | } |
| | | |
| | |
| | | return Response::validationError(['message' => __('User doesn\'t match. Bot?', 'jvb')]); |
| | | } |
| | | |
| | | $operation = sanitize_text_field($request->get_param('id')); |
| | | |
| | | $type = Registrar::getInstance($content)->getType()??false; |
| | | $type = $registrar->getType()??false; |
| | | if (!$type) { |
| | | return Response::validationError(['message' => __('Invalid content type', 'jvb')]); |
| | | } |
| | | |
| | | $data = [ |
| | | 'user' => $user, |
| | | 'item_id' => $itemID, |
| | | 'content' => $content, |
| | | 'type' => $type, |
| | | 'vote' => $vote, |
| | | ]; |
| | | |
| | | error_log('Final Vote Data: '.print_r($data, true)); |
| | | error_log('Operation: '.print_r($operation, true)); |
| | | $operationID = JVB()->queue()->add( |
| | | 'karmic', |
| | | $user, |
| | | $data, |
| | | [ |
| | | 'priority' => 'high', |
| | | 'operation_id' => $operation, |
| | | ] |
| | | ); |
| | | return $this->queued($operationID['operation_id']); |
| | | } |
| | | |
| | | /** |
| | | * @param WP_Error|array $result |
| | | * @param object $operation |
| | | * @param array $data |
| | | * |
| | | * @return WP_Error|array |
| | | * @throws Exception |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | | { |
| | | if ($operation->type !== 'karmic') { |
| | | return $result; |
| | | } |
| | | |
| | | //Check if item exists |
| | | $item = match ($data['type']) { |
| | | 'post' => get_post($data['item_id']), |
| | | 'term' => get_term($data['item_id'], jvbCheckBase($data['content'])), |
| | | 'user' => get_userdata($data['item_id']), |
| | | default => false |
| | | }; |
| | | if (!$item || is_wp_error($item)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => __('Invalid item', 'jvb') |
| | | ]; |
| | | } |
| | | |
| | | $table = CustomTable::for('karma_' . $data['content']); |
| | | |
| | | return $table->transaction(function($table) use ($data) { |
| | | // Check existing vote |
| | | $existing = $table->where([ |
| | | 'item_id' => $data['item_id'], |
| | | 'user_id' => $data['user'] |
| | | ])->first(); |
| | | |
| | | $existing_vote = $existing->vote ?? null; |
| | | $new_vote = $data['vote']; |
| | | |
| | | // No previous vote - insert new |
| | | if ($existing_vote === null) { |
| | | $inserted = $table->create([ |
| | | 'item_id' => $data['item_id'], |
| | | 'user_id' => $data['user'], |
| | | 'vote' => $new_vote, |
| | | ]); |
| | | |
| | | if (!$inserted) { |
| | | throw new Exception('Failed to record vote'); |
| | | } |
| | | |
| | | $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1); |
| | | $this->cache->invalidate($data['user']); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => __('Vote recorded', 'jvb'), |
| | | ]; |
| | | } |
| | | |
| | | // Changing vote |
| | | if ($existing_vote !== $new_vote) { |
| | | $updated = $table->where([ |
| | | 'item_id' => $data['item_id'], |
| | | 'user_id' => $data['user'] |
| | | ])->updateResults(['vote' => $new_vote]); |
| | | |
| | | if (!$updated) { |
| | | throw new Exception('Failed to update vote'); |
| | | } |
| | | |
| | | // Decrement old, increment new |
| | | $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1); |
| | | $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1); |
| | | $this->cache->invalidate($data['user']); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => __('Vote updated', 'jvb'), |
| | | ]; |
| | | } |
| | | |
| | | // Toggle off - remove vote |
| | | $deleted = $table->where([ |
| | | 'item_id' => $data['item_id'], |
| | | 'user_id' => $data['user'] |
| | | ])->deleteResults(); |
| | | |
| | | if (!$deleted) { |
| | | throw new Exception('Failed to remove vote'); |
| | | } |
| | | |
| | | $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1); |
| | | $this->cache->delete($data['user']); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => __('Vote removed', 'jvb'), |
| | | ]; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * @param string $content |
| | | * @param int $ID |
| | | * @param string $vote |
| | | * @param int $value |
| | | * |
| | | * @return void |
| | | */ |
| | | protected function updateVoteCount(string $content, string $type, int $ID, string $vote, int $value):void |
| | | { |
| | | $key = ($vote === 'down') ? BASE.'downvotes' : BASE.'upvotes'; |
| | | |
| | | switch ($type) { |
| | | case 'post': |
| | | $old = (int) get_post_meta($ID, $key, true); |
| | | $new = max(0, $old + $value); |
| | | update_post_meta($ID, $key, $new); |
| | | |
| | | $up = (int) get_post_meta($ID, BASE.'upvotes', true); |
| | | $down = (int) get_post_meta($ID, BASE.'downvotes', true); |
| | | update_post_meta($ID, BASE.'karma', $up - $down); |
| | | break; |
| | | |
| | | case 'term': |
| | | $old = (int) get_term_meta($ID, $key, true); |
| | | $new = max(0, $old + $value); |
| | | update_term_meta($ID, $key, $new); |
| | | |
| | | $up = (int) get_term_meta($ID, BASE.'upvotes', true); |
| | | $down = (int) get_term_meta($ID, BASE.'downvotes', true); |
| | | update_term_meta($ID, BASE.'karma', $up - $down); |
| | | break; |
| | | |
| | | case 'user': |
| | | $old = (int) get_user_meta($ID, $key, true); |
| | | $new = max(0, $old + $value); |
| | | update_user_meta($ID, $key, $new); |
| | | |
| | | $up = (int) get_user_meta($ID, BASE.'upvotes', true); |
| | | $down = (int) get_user_meta($ID, BASE.'downvotes', true); |
| | | update_user_meta($ID, BASE.'karma', $up - $down); |
| | | break; |
| | | |
| | | case 'response': |
| | | $field = str_replace(BASE, '', $key); |
| | | CustomTable::for('responses')->query( |
| | | "UPDATE {table} |
| | | SET $field = GREATEST(0, $field + %d), |
| | | karma = (upvotes - downvotes) |
| | | WHERE id = %d", |
| | | [$value, $ID] |
| | | ); |
| | | break; |
| | | $man = KarmaManager::getInstance($type); |
| | | if (!$man) { |
| | | return Response::validationError(['message' => __('Karma not set up', 'jvb')]); |
| | | } |
| | | |
| | | [$success, $message] = $man->vote($user, $itemID, $content, $vote); |
| | | |
| | | return match ($success) { |
| | | true, 'partial' => Response::success(['message' => $message]), |
| | | default => Response::error($message), |
| | | }; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * @param WP_REST_Request $request |
| | | * |
| | |
| | | return Response::success($cache); |
| | | } |
| | | |
| | | $votes = []; |
| | | |
| | | foreach (Registrar::getFeatured('has_karma') as $type) { |
| | | $table = CustomTable::for('karma_' . $type); |
| | | |
| | | // Skip if table doesn't exist |
| | | global $wpdb; |
| | | if ($wpdb->get_var("SHOW TABLES LIKE '{$table->getFullTableName()}'") != $table->getFullTableName()) { |
| | | continue; |
| | | } |
| | | |
| | | $results = $table->where(['user_id' => $user])->getResults(); |
| | | |
| | | if (!empty($results)) { |
| | | foreach ($results as $vote) { |
| | | $votes[$type][$vote->item_id] = $vote->vote; |
| | | } |
| | | } |
| | | } |
| | | $votes = KarmaManager::getUserVotes($user); |
| | | |
| | | $this->cache->set($user, $votes); |
| | | |
| | |
| | | if ($limit) { |
| | | if ($limit === 'user') { |
| | | $manager = new UserTermsManager(); |
| | | return $manager->getUserTerms($this->user_id, $taxonomy); |
| | | return $manager->fetchUserTerms($this->user_id, $taxonomy); |
| | | } else { |
| | | $limit = (int)$limit; |
| | | } |
| | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | require(JVB_DIR . '/inc/blocks/_setup.php'); |
| | | |
| | | |