From c4aa5cdb5e90ad4b420e22772797d16980232a2b Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 15 Apr 2026 18:38:55 +0000
Subject: [PATCH] =Updating custom tables to utilize CustomTable.php
---
inc/managers/CustomTable.php | 226 +
inc/managers/SEO/render/SchemaOutput.php | 169
inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php | 13
jvb.php | 2
inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php | 3
inc/managers/SEO/render/Thing/Action/InteractAction.php | 24
inc/rest/routes/TermRoutes.php | 17
inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php | 4
inc/registrar/config/seo/Resolver.php | 29
inc/managers/Notifications/_setup.php | 0
inc/registrar/_setup.php | 3
inc/managers/_setup.php | 5
inc/managers/FavouritesManager.php | 576 +++
inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php | 7
JVBase.php | 7
inc/managers/SEO/render/DataType/Time.php | 13
inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php | 18
inc/managers/SEO/render/Traits/_Properties/closesTrait.php | 5
inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php | 21
inc/managers/EmailManager.php | 35
inc/managers/SEO/render/DataType/Date.php | 6
inc/ui/CRUDSkeleton.php | 2
inc/admin/SEOAdmin.php | 3
inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php | 15
inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php | 13
inc/managers/SEO/render/Traits/_Properties/heightTrait.php | 8
inc/managers/SEO/render/Traits/_Properties/widthTrait.php | 8
inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php | 16
inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php | 2
inc/managers/SEO/render/Traits/_Properties/geoTrait.php | 10
inc/managers/SEO/render/Traits/_Properties/targetTrait.php | 23
activate.php | 9
inc/blocks/_setup.php | 5
inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php | 13
inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php | 17
inc/registrar/Posts.php | 117
inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php | 1
inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php | 21
inc/managers/SEO/render/Traits/_Properties/logoTrait.php | 3
inc/managers/SEO/render/Thing/CreativeWork/_setup.php | 1
inc/registrar/helpers/MakeVerification.php | 19
inc/managers/SEO/render/Thing/Action/Action.php | 30
inc/managers/SEO/render/Thing/Intangible/_setup.php | 1
inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php | 32
inc/managers/SEO/render/Traits/_Properties/imageTrait.php | 11
inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php | 7
inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php | 24
inc/managers/ErrorHandler.php | 38
inc/registrar/fields/Field.php | 16
inc/managers/ApprovalManager.php | 432 ++
inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php | 2
inc/registrar/helpers/HideSingle.php | 103
inc/managers/SEO/render/Thing/Action/CommunicateAction.php | 14
inc/registrar/config/SEO.php | 2
inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php | 24
inc/registrar/config/Breadcrumbs.php | 2
inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php | 15
inc/managers/VerifyEntryManager.php | 163
inc/managers/InvitationsManager.php | 59
inc/rest/routes/VoteRoutes.php | 216 -
inc/helpers/terms.php | 16
inc/managers/ForumManager.php | 99
inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php | 18
inc/managers/Notifications/Preferences.php | 124
inc/managers/Notifications/Notifications.php | 470 ++
inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php | 7
inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php | 14
inc/registrar/helpers/MakeTimelineType.php | 47
inc/managers/RoleManager.php | 41
inc/managers/SEO/render/Thing/_setup.php | 3
inc/registrar/config/seo/Schema.php | 266 +
inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php | 13
inc/blocks/TimelineBlock.php | 2
inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php | 14
inc/managers/TaxonomyRelationships.php | 360 -
inc/managers/NotificationManager.php | 1799 ----------
inc/managers/Notifications/EmailDigests.php | 391 ++
inc/managers/SEO/render/Traits/_Properties/_setup.php | 9
inc/registrar/fields/UploadField.php | 12
inc/helpers/all.php | 9
inc/managers/SEO/render/Traits/_Properties/founderTrait.php | 1
inc/registry/CheckCustomTables.php | 1250 +++---
inc/managers/SEO/BreadcrumbManager.php | 52
inc/managers/SEO/render/Thing/Action/_setup.php | 9
inc/managers/KarmaManager.php | 342 ++
inc/registrar/Registrar.php | 157
inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php | 4
inc/managers/UserTermsManager.php | 643 +--
inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php | 19
inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php | 2
inc/rest/routes/FavouritesRoutes.php | 8
inc/managers/SEO/render/Traits/_Properties/opensTrait.php | 5
inc/managers/ReferralManager.php | 325 +
inc/managers/SEO/render/Thing/Action/ScheduleAction.php | 10
inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php | 14
inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php | 26
inc/managers/ResponseManager.php | 64
inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php | 21
base/membership.php | 4
inc/managers/queue/Queue.php | 4
inc/helpers/members.php | 30
inc/registrar/Fields.php | 3
base/seo.php | 27
inc/managers/SEO/render/Traits/_Properties/addressTrait.php | 8
inc/managers/Notifications/Content.php | 108
inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php | 28
inc/registrar/helpers/MakeTrackChanges.php | 121
inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php | 24
inc/managers/SEO/render/Thing/Action/PlanAction.php | 13
inc/managers/SEO/render/Traits/ThingSchema.php | 45
inc/managers/SEO/render/Thing/Action/OrganizeAction.php | 10
inc/integrations/GoogleMaps.php | 3
/dev/null | 21
inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php | 27
inc/managers/SEO/render/Traits/_Helpers/_setup.php | 1
inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php | 30
116 files changed, 6,190 insertions(+), 3,663 deletions(-)
diff --git a/JVBase.php b/JVBase.php
index 255ff5f..a4efe54 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -20,6 +20,7 @@
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;
@@ -106,6 +107,7 @@
// 'uploads' => new UploadManager(),
'userTerms' => new UserTermsManager(),
'email' => new EmailManager(),
+ 'terms' => new TaxonomyRelationships(),
];
$this->routes = [
@@ -387,4 +389,9 @@
{
return $this->managers['schemaHelper'];
}
+
+ public function termRelationships():TaxonomyRelationships
+ {
+ return $this->managers['terms'];
+ }
}
diff --git a/activate.php b/activate.php
index a87076f..12625f6 100644
--- a/activate.php
+++ b/activate.php
@@ -4,6 +4,8 @@
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;
@@ -17,7 +19,6 @@
function jvbActivatePlugin():void
{
-
// $validator = new JVBase\utility\Validator();
// $validation = $validator->validateAll();
// error_log('Validation result: '.print_r($validation, true));
@@ -160,16 +161,16 @@
'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',
diff --git a/base/membership.php b/base/membership.php
index 11752bc..2ed3fcb 100644
--- a/base/membership.php
+++ b/base/membership.php
@@ -27,5 +27,7 @@
* ]
*/
-$membership = apply_filters('jvb_membership', []);
+$membership = apply_filters('jvb_membership', [
+ 'can_invite' => [],
+]);
define('JVB_MEMBERSHIP', $membership);
diff --git a/base/seo.php b/base/seo.php
index 41b285d..492e1a3 100644
--- a/base/seo.php
+++ b/base/seo.php
@@ -112,19 +112,37 @@
'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
@@ -257,8 +275,9 @@
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);
diff --git a/inc/admin/SEOAdmin.php b/inc/admin/SEOAdmin.php
index ec664d3..45e215f 100644
--- a/inc/admin/SEOAdmin.php
+++ b/inc/admin/SEOAdmin.php
@@ -2,7 +2,6 @@
namespace JVBase\admin;
use JVBase\meta\Form;
-use JVBase\meta\Sanitizer;
use JVBase\registrar\Registrar;
use JVBase\ui\Tabs;
@@ -29,7 +28,7 @@
'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',
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
index e2831d4..849ce91 100644
--- a/inc/blocks/TimelineBlock.php
+++ b/inc/blocks/TimelineBlock.php
@@ -48,7 +48,7 @@
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 '';
}
diff --git a/inc/blocks/_setup.php b/inc/blocks/_setup.php
index a2ad8e4..25fc594 100644
--- a/inc/blocks/_setup.php
+++ b/inc/blocks/_setup.php
@@ -21,15 +21,16 @@
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();
diff --git a/inc/helpers/all.php b/inc/helpers/all.php
index 5b7f02b..bd2d0a7 100644
--- a/inc/helpers/all.php
+++ b/inc/helpers/all.php
@@ -57,3 +57,12 @@
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
+ ];
+}
diff --git a/inc/helpers/members.php b/inc/helpers/members.php
index d62b383..d2fdb9f 100644
--- a/inc/helpers/members.php
+++ b/inc/helpers/members.php
@@ -2,6 +2,7 @@
use JVBase\managers\Cache;
use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
if (!defined('ABSPATH')) {
exit;
@@ -217,3 +218,32 @@
$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;
+ }
+ );
+}
diff --git a/inc/helpers/terms.php b/inc/helpers/terms.php
index e30cc9b..919d3af 100644
--- a/inc/helpers/terms.php
+++ b/inc/helpers/terms.php
@@ -6,6 +6,19 @@
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
@@ -128,7 +141,8 @@
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,
diff --git a/inc/integrations/GoogleMaps.php b/inc/integrations/GoogleMaps.php
index 2990c7e..68d58fa 100644
--- a/inc/integrations/GoogleMaps.php
+++ b/inc/integrations/GoogleMaps.php
@@ -5,6 +5,7 @@
*/
namespace JVBase\integrations;
+use JVBase\registrar\Registrar;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -541,7 +542,7 @@
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;
}
diff --git a/inc/managers/ApprovalManager.php b/inc/managers/ApprovalManager.php
new file mode 100644
index 0000000..f381d58
--- /dev/null
+++ b/inc/managers/ApprovalManager.php
@@ -0,0 +1,432 @@
+<?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'
+ ]
+ ];
+ }
+}
diff --git a/inc/managers/CustomTable.php b/inc/managers/CustomTable.php
index 4299b71..e344576 100644
--- a/inc/managers/CustomTable.php
+++ b/inc/managers/CustomTable.php
@@ -34,8 +34,12 @@
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
@@ -54,6 +58,19 @@
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();
@@ -94,12 +111,25 @@
}
}
+ 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;
+ }
+ }
/**
*
@@ -165,11 +195,12 @@
}
// 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;
}
@@ -190,6 +221,11 @@
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();
@@ -232,6 +268,8 @@
$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}";
@@ -409,6 +447,8 @@
return false;
}
+ $this->cache->flush();
+
return $this->wpdb->insert_id;
}
@@ -439,26 +479,19 @@
* );
* // 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);
}
/**
@@ -536,10 +569,17 @@
*/
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);
+ }
+ );
}
/**
@@ -559,34 +599,76 @@
*/
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);
+ }
+ );
+
}
/**
@@ -597,19 +679,26 @@
*/
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));
+ }
+ );
+
}
/**
@@ -660,6 +749,7 @@
if ($result === false) {
$this->logError('update', ['data' => $data, 'where' => $where]);
}
+ $this->cache->flush();
return $result;
}
@@ -689,6 +779,7 @@
if ($result === false) {
$this->logError('delete', ['where' => $where]);
}
+ $this->cache->flush();
return $result;
}
@@ -811,7 +902,7 @@
} catch (Exception $e) {
$this->rollback();
$this->logError('transaction', ['error' => $e->getMessage()]);
- throw $e;
+ return false;
}
}
@@ -866,11 +957,21 @@
$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
@@ -909,18 +1010,35 @@
/**
* 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];
}
/**
diff --git a/inc/managers/EmailManager.php b/inc/managers/EmailManager.php
index f1c7e33..68bbdef 100644
--- a/inc/managers/EmailManager.php
+++ b/inc/managers/EmailManager.php
@@ -756,11 +756,35 @@
);
}
- 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;">';
@@ -771,11 +795,16 @@
$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(
diff --git a/inc/managers/ErrorHandler.php b/inc/managers/ErrorHandler.php
index 9e6cc9a..da34d19 100644
--- a/inc/managers/ErrorHandler.php
+++ b/inc/managers/ErrorHandler.php
@@ -24,11 +24,14 @@
'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);
@@ -39,6 +42,35 @@
// 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();
diff --git a/inc/managers/FavouritesManager.php b/inc/managers/FavouritesManager.php
new file mode 100644
index 0000000..a2cfb88
--- /dev/null
+++ b/inc/managers/FavouritesManager.php
@@ -0,0 +1,576 @@
+<?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
+ }
+}
diff --git a/inc/managers/ForumManager.php b/inc/managers/ForumManager.php
new file mode 100644
index 0000000..a0dcbb3
--- /dev/null
+++ b/inc/managers/ForumManager.php
@@ -0,0 +1,99 @@
+<?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;
+ }
+}
diff --git a/inc/managers/InvitationsManager.php b/inc/managers/InvitationsManager.php
index 088556f..fab93bd 100644
--- a/inc/managers/InvitationsManager.php
+++ b/inc/managers/InvitationsManager.php
@@ -20,9 +20,12 @@
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']);
@@ -31,6 +34,60 @@
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', [
diff --git a/inc/managers/KarmaManager.php b/inc/managers/KarmaManager.php
new file mode 100644
index 0000000..6bba6da
--- /dev/null
+++ b/inc/managers/KarmaManager.php
@@ -0,0 +1,342 @@
+<?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;
+ }
+ );
+ }
+}
diff --git a/inc/managers/NotificationManager.php b/inc/managers/NotificationManager.php
index 2aaa4cf..eeec9a6 100644
--- a/inc/managers/NotificationManager.php
+++ b/inc/managers/NotificationManager.php
@@ -1,17 +1,14 @@
<?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
*
@@ -21,1747 +18,77 @@
*/
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
+ ************************************************/
}
diff --git a/inc/managers/Notifications/Content.php b/inc/managers/Notifications/Content.php
new file mode 100644
index 0000000..776d585
--- /dev/null
+++ b/inc/managers/Notifications/Content.php
@@ -0,0 +1,108 @@
+<?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;
+ }
+}
diff --git a/inc/managers/Notifications/EmailDigests.php b/inc/managers/Notifications/EmailDigests.php
new file mode 100644
index 0000000..978ff7f
--- /dev/null
+++ b/inc/managers/Notifications/EmailDigests.php
@@ -0,0 +1,391 @@
+<?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);
+ }
+}
diff --git a/inc/managers/Notifications/Notifications.php b/inc/managers/Notifications/Notifications.php
new file mode 100644
index 0000000..af3fd15
--- /dev/null
+++ b/inc/managers/Notifications/Notifications.php
@@ -0,0 +1,470 @@
+<?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;
+ }
+ }
+
+
+}
diff --git a/inc/managers/Notifications/Preferences.php b/inc/managers/Notifications/Preferences.php
new file mode 100644
index 0000000..42c1f54
--- /dev/null
+++ b/inc/managers/Notifications/Preferences.php
@@ -0,0 +1,124 @@
+<?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
+ ]);
+ }
+}
diff --git a/inc/managers/Notifications/_setup.php b/inc/managers/Notifications/_setup.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/inc/managers/Notifications/_setup.php
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 48245fa..af9f0b2 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -31,6 +31,12 @@
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'
@@ -48,6 +54,7 @@
public function __construct()
{
+ $this->defineTables();
global $wpdb;
$this->wpdb = $wpdb;
$this->cache = Cache::for('referrals', WEEK_IN_SECONDS);
@@ -63,7 +70,7 @@
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']);
@@ -97,6 +104,182 @@
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;
@@ -186,6 +369,26 @@
}
}
+ 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
*
@@ -193,31 +396,34 @@
* @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;
}
@@ -251,24 +457,11 @@
* 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
@@ -295,60 +488,58 @@
* 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');
@@ -363,10 +554,10 @@
$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;
}
diff --git a/inc/managers/ResponseManager.php b/inc/managers/ResponseManager.php
new file mode 100644
index 0000000..fe12ede
--- /dev/null
+++ b/inc/managers/ResponseManager.php
@@ -0,0 +1,64 @@
+<?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');
+ }
+
+}
diff --git a/inc/managers/RoleManager.php b/inc/managers/RoleManager.php
index 0f235d6..4de3271 100644
--- a/inc/managers/RoleManager.php
+++ b/inc/managers/RoleManager.php
@@ -16,9 +16,7 @@
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);
@@ -475,6 +473,15 @@
$user->add_cap(BASE . 'can_own_' . $termID);
$user->add_cap(BASE . 'can_manage_' . $termID);
+ $owners = get_term_meta($termID, BASE.'owners', true);
+ if (empty($owners)) {
+ $owners = [];
+ }
+ $owners[] = $userID;
+ $owners = array_unique($owners);
+ update_term_meta($termID, BASE.'owners', $owners);
+
+
do_action(BASE . 'granted_ownership', $userID, $termID, $taxonomy);
return true;
@@ -500,6 +507,16 @@
return false;
}
+ $owners = get_term_meta($termID, BASE.'owners', true);
+ if (empty($owners)) {
+ $owners = [];
+ }
+ if (in_array($userID, $owners)) {
+ unset($owners[array_search($userID, $owners)]);
+ }
+ $owners = array_unique($owners);
+ update_term_meta($termID, BASE.'owners', $owners);
+
$user->remove_cap(BASE . 'can_own_' . $termID);
do_action(BASE . 'revoked_ownership', $userID, $termID, $taxonomy);
@@ -537,6 +554,14 @@
$user->add_cap(BASE . 'can_manage_' . $termID);
+ $managers = get_term_meta($termID, BASE.'managers', true);
+ if (empty($managers)) {
+ $managers = [];
+ }
+ $managers[] = $userID;
+ $managers = array_unique($managers);
+ update_term_meta($termID, BASE.'managers', $managers);
+
do_action(BASE . 'granted_management', $userID, $termID, $taxonomy);
return true;
@@ -564,6 +589,16 @@
$user->remove_cap(BASE . 'can_manage_' . $termID);
+ $managers = get_term_meta($termID, BASE.'managers', true);
+ if (empty($managers)) {
+ $managers = [];
+ }
+ if (in_array($userID, $managers)) {
+ unset($managers[array_search($userID, $managers)]);
+ }
+ $managers = array_unique($managers);
+ update_term_meta($termID, BASE.'managers', $managers);
+
do_action(BASE . 'revoked_management', $userID, $termID, $taxonomy);
return true;
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
index 8d3288e..72929a2 100644
--- a/inc/managers/SEO/BreadcrumbManager.php
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -121,10 +121,10 @@
'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,
+// ];
}
}
@@ -150,9 +150,11 @@
// 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)) {
@@ -209,7 +211,7 @@
} 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)
];
}
@@ -366,29 +368,33 @@
}
}
- 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;
}
}
diff --git a/inc/managers/SEO/render/DataType/Date.php b/inc/managers/SEO/render/DataType/Date.php
index 7521f08..b15cfbc 100644
--- a/inc/managers/SEO/render/DataType/Date.php
+++ b/inc/managers/SEO/render/DataType/Date.php
@@ -26,13 +26,9 @@
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;
}
diff --git a/inc/managers/SEO/render/DataType/Time.php b/inc/managers/SEO/render/DataType/Time.php
index 0069ea4..1efc50e 100644
--- a/inc/managers/SEO/render/DataType/Time.php
+++ b/inc/managers/SEO/render/DataType/Time.php
@@ -1,7 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\DataType;
-use DateMalformedStringException;
-use DateTime;
+use JVBase\meta\Sanitizer;
+
if (!defined('ABSPATH')) {
exit;
}
@@ -20,14 +20,7 @@
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']);
}
}
diff --git a/inc/managers/SEO/render/SchemaOutput.php b/inc/managers/SEO/render/SchemaOutput.php
index 909e6bb..4619d94 100644
--- a/inc/managers/SEO/render/SchemaOutput.php
+++ b/inc/managers/SEO/render/SchemaOutput.php
@@ -1,13 +1,11 @@
<?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;
@@ -25,8 +23,9 @@
{
$schema = [];
- $schema[] = $this->buildWebsiteSchema(false);
+
if (is_front_page()) {
+ $schema[] = $this->buildWebsiteSchema(true);
$test = $this->buildOrganizationSchema();
if (!empty($test)) {
$schema[] = $test;
@@ -35,17 +34,17 @@
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();
}
@@ -53,7 +52,7 @@
$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();
@@ -66,10 +65,13 @@
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();
+ }
}
+
}
@@ -80,19 +82,22 @@
}
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
@@ -113,8 +118,8 @@
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();
}
@@ -125,9 +130,17 @@
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";
}
@@ -185,7 +198,6 @@
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'));
@@ -217,71 +229,78 @@
}
- 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
@@ -294,8 +313,8 @@
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.');
@@ -308,7 +327,7 @@
}
$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);
@@ -316,6 +335,7 @@
if (!$value) {continue;}
if (str_contains($value, '{{')) {
+ $meta = null;
$value = Resolver::resolve($property, $value);
}
$class->$method($value);
@@ -326,6 +346,7 @@
public function buildOrganizationSchema():array
{
$config = JVB()->schemaHelper()::schema('organization');
+
$class = JVB()->schemaHelper()::classFromConfig($config);
return ($class)? $class->outputSchema() : [];
}
diff --git a/inc/managers/SEO/render/Thing/Action.php b/inc/managers/SEO/render/Thing/Action.php
deleted file mode 100644
index a82035b..0000000
--- a/inc/managers/SEO/render/Thing/Action.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-namespace JVBase\managers\SEO\render\Thing;
-
-use JVBase\managers\SEO\render\DataType\DateTime;
-use JVBase\managers\SEO\render\DataType\Time;
-use JVBase\managers\SEO\render\Thing\CreativeWork\HowTo;
-use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
-use JVBase\managers\SEO\render\Thing\Intangible\EntryPoint;
-use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\ActionStatusType;
-use JVBase\managers\SEO\render\Thing\Intangible\VirtualLocation;
-use JVBase\managers\SEO\render\Thing\Organization\Organization;
-use JVBase\managers\SEO\render\Thing\Person\Person;
-use JVBase\managers\SEO\render\Thing\Place\Place;
-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 Action {
- use actionProcessTrait, actionStatusTrait, agentTrait,
- endTimeTrait, errorTrait, instrumentTrait, locationTrait,
- objectTrait, participantTrait, providerTrait, resultTrait,
- startTimeTrait, targetTrait;
-}
diff --git a/inc/managers/SEO/render/Thing/Action/Action.php b/inc/managers/SEO/render/Thing/Action/Action.php
new file mode 100644
index 0000000..318f89f
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/Action.php
@@ -0,0 +1,30 @@
+<?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;
+}
diff --git a/inc/managers/SEO/render/Thing/Action/CommunicateAction.php b/inc/managers/SEO/render/Thing/Action/CommunicateAction.php
new file mode 100644
index 0000000..1f7c93b
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/CommunicateAction.php
@@ -0,0 +1,14 @@
+<?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"
+}
diff --git a/inc/managers/SEO/render/Thing/Action/InteractAction.php b/inc/managers/SEO/render/Thing/Action/InteractAction.php
new file mode 100644
index 0000000..ac4caa4
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/InteractAction.php
@@ -0,0 +1,24 @@
+<?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 {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Action/OrganizeAction.php b/inc/managers/SEO/render/Thing/Action/OrganizeAction.php
new file mode 100644
index 0000000..9b03fbb
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/OrganizeAction.php
@@ -0,0 +1,10 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+class OrganizeAction extends Action {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Action/PlanAction.php b/inc/managers/SEO/render/Thing/Action/PlanAction.php
new file mode 100644
index 0000000..3bee51b
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/PlanAction.php
@@ -0,0 +1,13 @@
+<?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;
+}
diff --git a/inc/managers/SEO/render/Thing/Action/ScheduleAction.php b/inc/managers/SEO/render/Thing/Action/ScheduleAction.php
new file mode 100644
index 0000000..8b0aa9e
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/ScheduleAction.php
@@ -0,0 +1,10 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+class ScheduleAction extends PlanAction {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Action/_setup.php b/inc/managers/SEO/render/Thing/Action/_setup.php
new file mode 100644
index 0000000..6a9379d
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/_setup.php
@@ -0,0 +1,9 @@
+<?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');
+
+
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php
new file mode 100644
index 0000000..a87e4ff
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php
@@ -0,0 +1,14 @@
+<?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;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php
new file mode 100644
index 0000000..c56bd77
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php
@@ -0,0 +1,17 @@
+<?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;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php
new file mode 100644
index 0000000..c0ddd02
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php
@@ -0,0 +1,16 @@
+<?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;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php
new file mode 100644
index 0000000..ff52393
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php
@@ -0,0 +1,4 @@
+<?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');
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php b/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php
index 11ee774..62ac343 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php
@@ -55,4 +55,11 @@
* @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;
+ }
}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php
deleted file mode 100644
index 43be137..0000000
--- a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
-
-use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
-use JVBase\managers\SEO\render\Traits\_Properties\breadcrumbTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\lastReviewedTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\primaryImageOfPageTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\relatedLinkTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\reviewedByTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\significantLinkTrait;
-
-if (!defined('ABSPATH')) {
- exit;
-}
-
-
-class CollectionPage extends WebPage
-{
-
-}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php
new file mode 100644
index 0000000..39ba979
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php
@@ -0,0 +1,15 @@
+<?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
+{
+
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php
new file mode 100644
index 0000000..77a871d
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage;
+
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+
+class ImageGallery extends MediaGallery
+{
+
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php
new file mode 100644
index 0000000..057ebdb
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php
@@ -0,0 +1,15 @@
+<?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;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php
index 05e2f53..20f1c5b 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php
@@ -2,7 +2,9 @@
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');
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
index b7922b9..13850f9 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
@@ -18,3 +18,4 @@
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');
diff --git a/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php b/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php
index a14d568..4455b86 100644
--- a/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php
+++ b/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php
@@ -11,7 +11,7 @@
}
class DayOfWeek extends Enumeration {
- protected string $day;
+ protected string|array $day;
protected array $allowedDay = [
'monday' => 'https://schema.org/Monday',
'tuesday' => 'https://schema.org/Tuesday',
@@ -22,4 +22,21 @@
'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;
+ }
+
}
diff --git a/inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php b/inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php
similarity index 69%
rename from inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php
rename to inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php
index 926c607..f55514c 100644
--- a/inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php
+++ b/inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php
@@ -1,5 +1,5 @@
<?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;
diff --git a/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php b/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php
index 22a789d..bea6a3f 100644
--- a/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php
+++ b/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php
@@ -1,4 +1,4 @@
<?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');
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php
index 6f02323..160f9c3 100644
--- a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php
@@ -2,13 +2,6 @@
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;
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php
index f58293a..33c2750 100644
--- a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php
@@ -8,6 +8,7 @@
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;
@@ -18,5 +19,5 @@
class PriceSpecification extends StructuredValue {
use eligibleQuantityTrait, eligibleTransactionVolumeTrait, maxPriceTrait,
membershipPointsEarnedTrait, minPriceTrait, priceTrait, priceCurrencyTrait,
- validFromTrait, validThroughTrait;
+ validFromTrait, validThroughTrait, unitTextTrait;
}
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php
new file mode 100644
index 0000000..a467229
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php
@@ -0,0 +1,14 @@
+<?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.
+}
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php
index 0c50fcd..6e3d35e 100644
--- a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php
@@ -12,6 +12,7 @@
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');
diff --git a/inc/managers/SEO/render/Thing/Intangible/_setup.php b/inc/managers/SEO/render/Thing/Intangible/_setup.php
index 3089337..0869361 100644
--- a/inc/managers/SEO/render/Thing/Intangible/_setup.php
+++ b/inc/managers/SEO/render/Thing/Intangible/_setup.php
@@ -25,7 +25,6 @@
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');
diff --git a/inc/managers/SEO/render/Thing/_setup.php b/inc/managers/SEO/render/Thing/_setup.php
index 60065e3..e1f0ca8 100644
--- a/inc/managers/SEO/render/Thing/_setup.php
+++ b/inc/managers/SEO/render/Thing/_setup.php
@@ -1,6 +1,7 @@
<?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');
diff --git a/inc/managers/SEO/render/Traits/ThingSchema.php b/inc/managers/SEO/render/Traits/ThingSchema.php
index c58861f..7124007 100644
--- a/inc/managers/SEO/render/Traits/ThingSchema.php
+++ b/inc/managers/SEO/render/Traits/ThingSchema.php
@@ -5,9 +5,12 @@
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;
@@ -38,7 +41,7 @@
{
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) {
@@ -61,8 +64,9 @@
} 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;
@@ -114,8 +118,8 @@
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;
}
@@ -126,4 +130,35 @@
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;
+ }
+ );
+
+ }
}
diff --git a/inc/managers/SEO/render/Traits/_Helpers/_setup.php b/inc/managers/SEO/render/Traits/_Helpers/_setup.php
index 4d0fd8c..53046a4 100644
--- a/inc/managers/SEO/render/Traits/_Helpers/_setup.php
+++ b/inc/managers/SEO/render/Traits/_Helpers/_setup.php
@@ -1,2 +1,3 @@
<?php
require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php');
diff --git a/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php b/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php
index 0c5a941..e02cd4a 100644
--- a/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php
+++ b/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php
@@ -1,13 +1,20 @@
<?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);
@@ -17,7 +24,14 @@
}
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));
diff --git a/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php b/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php
new file mode 100644
index 0000000..3648e0f
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php
@@ -0,0 +1,18 @@
+<?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);
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/_setup.php b/inc/managers/SEO/render/Traits/_Properties/_setup.php
index 5359535..9a463f9 100644
--- a/inc/managers/SEO/render/Traits/_Properties/_setup.php
+++ b/inc/managers/SEO/render/Traits/_Properties/_setup.php
@@ -1,6 +1,7 @@
<?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');
@@ -26,6 +27,8 @@
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');
@@ -125,6 +128,7 @@
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');
@@ -297,6 +301,7 @@
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');
@@ -365,6 +370,7 @@
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');
@@ -381,6 +387,7 @@
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');
@@ -405,6 +412,7 @@
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');
@@ -429,6 +437,7 @@
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');
diff --git a/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php b/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php
new file mode 100644
index 0000000..76ed64e
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php
@@ -0,0 +1,24 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php b/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php
index 8102e25..ab417a8 100644
--- a/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php
@@ -23,7 +23,20 @@
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;
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/addressTrait.php b/inc/managers/SEO/render/Traits/_Properties/addressTrait.php
index 0ea5736..61d5433 100644
--- a/inc/managers/SEO/render/Traits/_Properties/addressTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/addressTrait.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
+use JVBase\base\SchemaHelper;
use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
if (!defined('ABSPATH')) {
@@ -19,7 +20,10 @@
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;
}
@@ -47,7 +51,7 @@
'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',
diff --git a/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php b/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php
index a32732f..314b685 100644
--- a/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
+use JVBase\base\SchemaHelper;
use JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating;
if (!defined('ABSPATH')) {
@@ -16,10 +17,14 @@
{
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;
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php b/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php
new file mode 100644
index 0000000..f07d737
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php
@@ -0,0 +1,21 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php b/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php
new file mode 100644
index 0000000..1528f69
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php
@@ -0,0 +1,24 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/closesTrait.php b/inc/managers/SEO/render/Traits/_Properties/closesTrait.php
index 9029118..d314184 100644
--- a/inc/managers/SEO/render/Traits/_Properties/closesTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/closesTrait.php
@@ -16,8 +16,11 @@
{
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;
}
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php b/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php
index 73b6997..96c1dc0 100644
--- a/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php
@@ -2,22 +2,27 @@
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;
}
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php b/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php
new file mode 100644
index 0000000..d8bb1c9
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php
@@ -0,0 +1,21 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/founderTrait.php b/inc/managers/SEO/render/Traits/_Properties/founderTrait.php
index d42fc6e..49f08c1 100644
--- a/inc/managers/SEO/render/Traits/_Properties/founderTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/founderTrait.php
@@ -1,6 +1,5 @@
<?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;
diff --git a/inc/managers/SEO/render/Traits/_Properties/geoTrait.php b/inc/managers/SEO/render/Traits/_Properties/geoTrait.php
index c82429f..e4bdfd1 100644
--- a/inc/managers/SEO/render/Traits/_Properties/geoTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/geoTrait.php
@@ -1,6 +1,7 @@
<?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;
@@ -17,8 +18,15 @@
{
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
diff --git a/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php b/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php
index 90f336a..0230b99 100644
--- a/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php
@@ -1,7 +1,8 @@
<?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')) {
@@ -17,8 +18,26 @@
{
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;
}
@@ -31,7 +50,8 @@
'fields' => [
'generate' => [
'type' => 'true_false',
- 'label' => 'Generate from Content type?'
+ 'label' => 'Generate from Content type?',
+ 'default' => false,
],
'content_type' => [
'type' => 'checkbox',
@@ -67,6 +87,10 @@
'description' => [
'type' => 'textarea',
'label' => 'Description'
+ ],
+ 'price' => [
+ 'type' => 'number',
+ 'label' => 'Price'
]
]
]
diff --git a/inc/managers/SEO/render/Traits/_Properties/heightTrait.php b/inc/managers/SEO/render/Traits/_Properties/heightTrait.php
index 30c0c01..12da8cb 100644
--- a/inc/managers/SEO/render/Traits/_Properties/heightTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/heightTrait.php
@@ -9,15 +9,15 @@
}
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;
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/imageTrait.php b/inc/managers/SEO/render/Traits/_Properties/imageTrait.php
index 44e8d6f..c13600a 100644
--- a/inc/managers/SEO/render/Traits/_Properties/imageTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/imageTrait.php
@@ -2,22 +2,27 @@
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;
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php b/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php
index 717e73e..4167948 100644
--- a/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php
@@ -1,7 +1,7 @@
<?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;
diff --git a/inc/managers/SEO/render/Traits/_Properties/logoTrait.php b/inc/managers/SEO/render/Traits/_Properties/logoTrait.php
index b38a0b8..e8ab818 100644
--- a/inc/managers/SEO/render/Traits/_Properties/logoTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/logoTrait.php
@@ -18,6 +18,9 @@
}
public function setLogo(string|ImageObject $logo):void
{
+ if (is_string($logo) && is_numeric($logo)) {
+ $logo = self::createImageFromID($logo);
+ }
$this->logo = $logo;
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php b/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php
index 9246787..678ff35 100644
--- a/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php
@@ -2,23 +2,28 @@
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;
}
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php b/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
index 94f601c..f9aba4a 100644
--- a/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
@@ -1,23 +1,43 @@
<?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;
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/opensTrait.php b/inc/managers/SEO/render/Traits/_Properties/opensTrait.php
index c27b70a..493b3f3 100644
--- a/inc/managers/SEO/render/Traits/_Properties/opensTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/opensTrait.php
@@ -16,8 +16,11 @@
{
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;
}
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php b/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php
new file mode 100644
index 0000000..481846a
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php
@@ -0,0 +1,32 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php b/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php
index aa895db..5c82ad3 100644
--- a/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php
@@ -1,7 +1,7 @@
<?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')) {
@@ -21,7 +21,17 @@
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;
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php b/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php
new file mode 100644
index 0000000..0a4cde5
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php
@@ -0,0 +1,26 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php b/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php
new file mode 100644
index 0000000..37b103a
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php
@@ -0,0 +1,27 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php b/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php
new file mode 100644
index 0000000..f3b4c48
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php
@@ -0,0 +1,24 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/targetTrait.php b/inc/managers/SEO/render/Traits/_Properties/targetTrait.php
index f9967fd..440a308 100644
--- a/inc/managers/SEO/render/Traits/_Properties/targetTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/targetTrait.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
+use JVBase\base\SchemaHelper;
use JVBase\managers\SEO\render\Thing\Intangible\EntryPoint;
if (!defined('ABSPATH')) {
@@ -8,16 +9,26 @@
}
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;
}
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php b/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php
new file mode 100644
index 0000000..818dcd2
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php
@@ -0,0 +1,21 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/widthTrait.php b/inc/managers/SEO/render/Traits/_Properties/widthTrait.php
index d23977b..c09c978 100644
--- a/inc/managers/SEO/render/Traits/_Properties/widthTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/widthTrait.php
@@ -9,15 +9,15 @@
}
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;
}
diff --git a/inc/managers/TaxonomyRelationships.php b/inc/managers/TaxonomyRelationships.php
index 2becd42..1f919bd 100644
--- a/inc/managers/TaxonomyRelationships.php
+++ b/inc/managers/TaxonomyRelationships.php
@@ -1,9 +1,10 @@
<?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
@@ -11,15 +12,15 @@
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();
@@ -28,13 +29,41 @@
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
@@ -59,6 +88,10 @@
*/
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;
@@ -119,8 +152,6 @@
*/
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);
@@ -130,61 +161,27 @@
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;
}
/**
@@ -197,23 +194,35 @@
*/
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));
}
@@ -230,48 +239,14 @@
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)
@@ -284,50 +259,9 @@
*/
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)
@@ -341,31 +275,34 @@
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
]
);
@@ -387,47 +324,20 @@
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
@@ -439,13 +349,7 @@
*/
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]);
}
}
diff --git a/inc/managers/UserTermsManager.php b/inc/managers/UserTermsManager.php
index 4673568..cf8d595 100644
--- a/inc/managers/UserTermsManager.php
+++ b/inc/managers/UserTermsManager.php
@@ -1,36 +1,65 @@
<?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
@@ -43,79 +72,8 @@
$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
@@ -135,174 +93,28 @@
}
$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);
@@ -310,93 +122,53 @@
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
@@ -404,16 +176,19 @@
// 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',
@@ -422,7 +197,6 @@
'users' => $users
],
[
- 'count' => count($users),
'chunk_key' => 'users',
'chunk_size' => 5,
'operation_id' => 'rebuild_user_terms_' . date('Y_m_d')
@@ -435,15 +209,22 @@
];
}
- // 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,
@@ -452,7 +233,7 @@
],
[
'count' => 1,
- 'operation_id' => 'rebuild_user_terms_' . date('Y_m_d')
+ 'operation_id' => 'rebuild_user_'.$user_id.'_terms_' . date('Y_m_d')
]
);
@@ -478,63 +259,64 @@
}
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(
@@ -558,122 +340,29 @@
*
* @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
+ );
}
}
diff --git a/inc/managers/VerifyEntryManager.php b/inc/managers/VerifyEntryManager.php
new file mode 100644
index 0000000..afbee5d
--- /dev/null
+++ b/inc/managers/VerifyEntryManager.php
@@ -0,0 +1,163 @@
+<?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);
+ }
+}
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index d29be0c..45b8f73 100644
--- a/inc/managers/_setup.php
+++ b/inc/managers/_setup.php
@@ -62,7 +62,12 @@
//}
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'))) {
diff --git a/inc/managers/queue/Queue.php b/inc/managers/queue/Queue.php
index d93078b..95b7afb 100644
--- a/inc/managers/queue/Queue.php
+++ b/inc/managers/queue/Queue.php
@@ -78,7 +78,7 @@
]);
$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`)',
@@ -111,7 +111,7 @@
]);
$stats->setKeys([
- ['key' => 'PRIMARY', 'value' => 'id'],
+ ['key' => 'PRIMARY', 'value' => '(`id`)'],
['key' => 'UNIQUE', 'value' => '(`date`, `type`)'],
'`date_idx` (`date`)',
'`type_idx` (`type`)'
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 6887534..e1e4928 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -166,8 +166,9 @@
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
diff --git a/inc/registrar/Posts.php b/inc/registrar/Posts.php
index 416f24b..ac1d5e1 100644
--- a/inc/registrar/Posts.php
+++ b/inc/registrar/Posts.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\registrar;
+use JVBase\forms\TaxonomySelector;
+
if (!defined('ABSPATH')) {
exit;
}
@@ -208,9 +210,12 @@
* @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);
}
@@ -312,6 +317,29 @@
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());
@@ -337,4 +365,93 @@
'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;
+ }
}
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index a69a13f..727e3b3 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -1,9 +1,12 @@
<?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;
@@ -14,9 +17,11 @@
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')) {
@@ -41,11 +46,13 @@
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
@@ -62,6 +69,7 @@
* @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
*/
@@ -90,6 +98,7 @@
* @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
@@ -98,11 +107,21 @@
/**
* @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
*/
@@ -113,7 +132,7 @@
/**
* @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
*/
@@ -122,10 +141,12 @@
* @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
@@ -158,7 +179,7 @@
/**
* @var bool Whether to generate a profile for this user role
*/
- protected bool $profile_link;
+ public bool $profile_link;
/**
* @var array|string
*/
@@ -274,8 +295,16 @@
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);
@@ -390,6 +419,30 @@
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;
@@ -422,6 +475,9 @@
case 'is_content':
add_action('init', [$this, 'setupContent'], 20);
break;
+ case 'is_glossary':
+ $this->hide_single = true;
+ break;
}
}
return $this;
@@ -447,13 +503,18 @@
}
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;
}));
}
@@ -478,12 +539,15 @@
}
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;
@@ -493,26 +557,25 @@
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;
}
@@ -520,7 +583,7 @@
public function getSEO():SEO
{
if (!isset($this->seo)){
- $this->seo = new SEO($this->slug, $this);
+ $this->seo = new SEO($this->slug);
}
return $this->seo;
}
@@ -585,13 +648,48 @@
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];
@@ -601,6 +699,7 @@
public static function getFieldsFor(string $slug):array
{
+ self::ensureInstanced();
if (!array_key_exists($slug, static::$instances)) {
return [];
}
@@ -610,6 +709,7 @@
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;
@@ -618,6 +718,7 @@
public static function getLabels():array
{
+ self::ensureInstanced();
return array_map(function ($instance) {
return ['singular' => $instance->getSingular(),
'plural' => $instance->getPlural()];
@@ -797,4 +898,20 @@
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
+ *****************************************************************/
+
+
+
+
}
diff --git a/inc/registrar/_setup.php b/inc/registrar/_setup.php
index 897eb11..f9d9672 100644
--- a/inc/registrar/_setup.php
+++ b/inc/registrar/_setup.php
@@ -15,7 +15,10 @@
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');
diff --git a/inc/registrar/config/Breadcrumbs.php b/inc/registrar/config/Breadcrumbs.php
index 9d7b8e5..69c0fe3 100644
--- a/inc/registrar/config/Breadcrumbs.php
+++ b/inc/registrar/config/Breadcrumbs.php
@@ -40,7 +40,7 @@
$config = [
'title' => $this->title,
];
- if (isset($this->addCrumb)) {
+ if (!empty($this->addCrumb)) {
$config['addCrumb'] = $this->addCrumb;
}
return $config;
diff --git a/inc/registrar/config/SEO.php b/inc/registrar/config/SEO.php
index 4797cf9..5248bb7 100644
--- a/inc/registrar/config/SEO.php
+++ b/inc/registrar/config/SEO.php
@@ -38,7 +38,7 @@
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';
diff --git a/inc/registrar/config/seo/Resolver.php b/inc/registrar/config/seo/Resolver.php
index 3f18043..ab46dd4 100644
--- a/inc/registrar/config/seo/Resolver.php
+++ b/inc/registrar/config/seo/Resolver.php
@@ -16,6 +16,13 @@
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(
'/\{\{([^}]+)}}/',
@@ -45,6 +52,9 @@
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),
@@ -69,6 +79,8 @@
}
}
+ error_log('resolveRelation: '.print_r($path, true));
+
$ID = $meta->get($relation);
if (!$ID || $ID === '') {
return '';
@@ -78,14 +90,16 @@
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 '';
@@ -95,6 +109,9 @@
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);
@@ -133,7 +150,11 @@
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);
}
diff --git a/inc/registrar/config/seo/Schema.php b/inc/registrar/config/seo/Schema.php
index 22e033c..15c256e 100644
--- a/inc/registrar/config/seo/Schema.php
+++ b/inc/registrar/config/seo/Schema.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\registrar\config\seo;
+use JVBase\base\SchemaHelper;
use JVBase\managers\Cache;
use JVBase\managers\SEO\render;
use JVBase\meta\Meta;
@@ -35,7 +36,7 @@
];
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}}'
];
@@ -58,7 +59,7 @@
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';
@@ -79,7 +80,7 @@
$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()
];
@@ -148,6 +149,7 @@
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)))) {
@@ -163,10 +165,19 @@
$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);
@@ -191,7 +202,7 @@
$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']);
@@ -238,12 +249,22 @@
$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;
@@ -262,6 +283,7 @@
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;
@@ -301,7 +323,7 @@
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');
@@ -331,6 +353,28 @@
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'];
@@ -392,7 +436,7 @@
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'];
@@ -405,4 +449,210 @@
}
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();
+ }
}
diff --git a/inc/registrar/fields/Field.php b/inc/registrar/fields/Field.php
index add97be..7b902cb 100644
--- a/inc/registrar/fields/Field.php
+++ b/inc/registrar/fields/Field.php
@@ -19,6 +19,8 @@
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
*/
@@ -50,7 +52,7 @@
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));
@@ -149,4 +151,16 @@
}
}, $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;
+ }
}
diff --git a/inc/registrar/fields/UploadField.php b/inc/registrar/fields/UploadField.php
index dfc2f1b..1042fdc 100644
--- a/inc/registrar/fields/UploadField.php
+++ b/inc/registrar/fields/UploadField.php
@@ -12,26 +12,26 @@
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)) {
@@ -40,7 +40,7 @@
}
$this->subtype = $subtype;
}
- protected function getSubtype():string
+ public function getSubtype():string
{
return $this->subtype;
}
diff --git a/inc/registrar/helpers/HideSingle.php b/inc/registrar/helpers/HideSingle.php
new file mode 100644
index 0000000..20ef320
--- /dev/null
+++ b/inc/registrar/helpers/HideSingle.php
@@ -0,0 +1,103 @@
+<?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;
+ }
+ }
+}
diff --git a/inc/registrar/helpers/MakeTimelineType.php b/inc/registrar/helpers/MakeTimelineType.php
new file mode 100644
index 0000000..84ea959
--- /dev/null
+++ b/inc/registrar/helpers/MakeTimelineType.php
@@ -0,0 +1,47 @@
+<?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;
+ }
+ }
+}
diff --git a/inc/registrar/helpers/MakeTrackChanges.php b/inc/registrar/helpers/MakeTrackChanges.php
index 4019f1f..3237e52 100644
--- a/inc/registrar/helpers/MakeTrackChanges.php
+++ b/inc/registrar/helpers/MakeTrackChanges.php
@@ -1,10 +1,131 @@
<?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
+ ));
+ }
}
diff --git a/inc/registrar/helpers/MakeVerification.php b/inc/registrar/helpers/MakeVerification.php
index edf6d0e..6574bdb 100644
--- a/inc/registrar/helpers/MakeVerification.php
+++ b/inc/registrar/helpers/MakeVerification.php
@@ -5,6 +5,25 @@
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'
+ ])
+ }
}
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index eaf4dbc..b04af0b 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -440,7 +440,7 @@
/******************************************************
* Table Definitions
*****************************************************/
-
+// MOVED TO Queue.php
// protected function queueTables():array
// {
//
@@ -514,30 +514,31 @@
// ];
// }
- 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
{
@@ -559,445 +560,449 @@
)"
];
}
+//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
{
@@ -1159,7 +1164,7 @@
];
}
- protected function invitationTables(array $config): array
+ /*protected function invitationTables(array $config): array
{
if (empty($config['roles']) && empty($config['terms'])) {
return [];
@@ -1212,177 +1217,180 @@
$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
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index ae49baf..3494f5b 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -505,7 +505,7 @@
'target_id' => $target_id
]);
- if ($result['created']) {
+ if ((bool)$result) {
$this->updateFavouriteCount($type, $target_id);
$this->maybeNotifyOwner($type, $target_id, $user_id);
}
@@ -582,13 +582,13 @@
'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']++;
}
diff --git a/inc/rest/routes/TermRoutes.php b/inc/rest/routes/TermRoutes.php
index 431c7b5..f536bba 100644
--- a/inc/rest/routes/TermRoutes.php
+++ b/inc/rest/routes/TermRoutes.php
@@ -1,9 +1,8 @@
<?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;
@@ -89,7 +88,7 @@
// 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'] = [];
@@ -194,8 +193,8 @@
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);
}
@@ -239,7 +238,7 @@
$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();
@@ -252,7 +251,7 @@
$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)) {
@@ -270,7 +269,7 @@
$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 = [];
@@ -584,7 +583,7 @@
*/
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');
diff --git a/inc/rest/routes/VoteRoutes.php b/inc/rest/routes/VoteRoutes.php
index 7f55f1d..c0298ec 100644
--- a/inc/rest/routes/VoteRoutes.php
+++ b/inc/rest/routes/VoteRoutes.php
@@ -2,6 +2,7 @@
namespace JVBase\rest\routes;
use JVBase\managers\CustomTable;
+use JVBase\managers\KarmaManager;
use JVBase\registrar\Registrar;
use JVBase\rest\Response;
use JVBase\rest\Rest;
@@ -65,7 +66,7 @@
$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')]);
}
@@ -74,194 +75,27 @@
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
*
@@ -276,25 +110,7 @@
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);
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index a5d0dc8..4e6d8ba 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -901,7 +901,7 @@
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;
}
diff --git a/jvb.php b/jvb.php
index b326bb1..3f2b76e 100644
--- a/jvb.php
+++ b/jvb.php
@@ -245,6 +245,8 @@
+
+
require(JVB_DIR . '/inc/blocks/_setup.php');
--
Gitblit v1.10.0