=Cleaning up the Integrations setup a bit, started the Helcim integration work too
53 files modified
2 files deleted
1 files added
| | |
| | | // Add submenu pages for configured integrations |
| | | foreach ($this->getAvailableServices() as $service) { |
| | | $integration = $this->getService($service); |
| | | if ($integration->isSetUp() && method_exists($integration, 'getAllowedContent') && !empty($integration->getAllowedContent())) { |
| | | |
| | | if (!empty(Registrar::withIntegration($service)) && $integration->isSetUp() && method_exists($integration, 'getAllowedContent') && !empty($integration->getAllowedContent())) { |
| | | add_submenu_page( |
| | | 'jvb-integrations', |
| | | $integration->getTitle() . ' Defaults', |
| | |
| | | wp_enqueue_style( |
| | | 'jvb-dash', |
| | | ); |
| | | wp_enqueue_style( |
| | | 'jvb-forms' |
| | | ); |
| | | |
| | | wp_enqueue_script( |
| | | 'jvb-admin-scripts', |
| | |
| | | public function renderPage(): void |
| | | { |
| | | ?> |
| | | <style> |
| | | form.integration .actions.x-btw.x-btw { |
| | | justify-content: flex-start; |
| | | } |
| | | form.integration button { |
| | | display: flex; |
| | | gap: .5rem; |
| | | padding: .25rem .5rem; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | form.integration button svg { |
| | | width: 1.5em; |
| | | height: 1.5em; |
| | | } |
| | | </style> |
| | | <div class="wrap jvb-admin-wrap"> |
| | | <h1>Integrations</h1> |
| | | |
| | |
| | | <?php |
| | | foreach ($this->getAvailableServices() as $service) { |
| | | $integration = $this->getService($service); |
| | | if ($integration->isSetUp() && method_exists($integration, 'renderConnection')) { |
| | | if (method_exists($integration, 'renderConnection')) { |
| | | $integration->renderConnection(); |
| | | } |
| | | } |
| | |
| | | |
| | | .item-grid.integrations { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); |
| | | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| | | gap: 2rem; |
| | | margin-top: 2rem; |
| | | } |
| | |
| | | height: auto; |
| | | } |
| | | |
| | | #wpfooter#wpfooter { |
| | | position: relative; |
| | | } |
| | | </style> |
| | | <?php |
| | | } |
| | |
| | | <?php |
| | | |
| | | use JVBase\blocks\CustomBlocks; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | |
| | | //require(JVB_DIR . '/inc/blocks/RegisterBlocks.php'); |
| | | require(JVB_DIR . '/inc/blocks/CustomBlocks.php'); |
| | | |
| | | CustomBlocks::getInstance(); |
| | | |
| | | add_action('init', 'jvb_conditionally_load_blocks', 1); |
| | | function jvb_conditionally_load_blocks(): void |
| | |
| | | return ''; |
| | | } |
| | | } |
| | | if (is_null($term)) { |
| | | return ''; |
| | | } |
| | | $cache = Cache::for($term->taxonomy.'_link', 'term'); |
| | | $key = $term->term_id; |
| | | if (is_null($key)) { |
| | | return ''; |
| | | } |
| | | return $cache->remember( |
| | | $key, |
| | | function() use ($term) { |
| | |
| | | |
| | | trait Admin { |
| | | use _Base; |
| | | protected bool $canTest = false; |
| | | //An array of AdminAction objects |
| | | protected array $actions = []; |
| | | protected array $instructions = []; |
| | |
| | | 'label' => 'Save Changes', |
| | | 'icon' => 'floppy-disk', |
| | | 'action' => [$this, 'saveCredentials'] |
| | | ], |
| | | [ |
| | | ] |
| | | ]; |
| | | if ($this->isSetUp()) { |
| | | $actions[] = [ |
| | | 'name' => 'Clear Credentials', |
| | | 'label' => 'Clear Credentials', |
| | | 'icon' => 'broom', |
| | | 'action' => [$this, 'deleteCredentials'] |
| | | ], |
| | | ]; |
| | | ]; |
| | | } |
| | | if ($this->canTest) { |
| | | $actions[] = [ |
| | | 'name' => 'Test Connection', |
| | | 'label' => 'Test Connection', |
| | | 'icon' => 'plugs-connected', |
| | | 'action' => [$this, 'testConnectionAction'] |
| | | ]; |
| | | } |
| | | foreach ($actions as $a) { |
| | | $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null); |
| | | } |
| | | |
| | | $this->defineActions(); |
| | | } |
| | | |
| | | abstract protected function defineActions():void; |
| | | |
| | | public function addAction(string $name, callable $callback, ?string $slug = null, ?string $icon = null, ?string $label = null):void |
| | | { |
| | | $action = new AdminActions($name, $callback); |
| | |
| | | $isConnected = !empty($credentials); |
| | | |
| | | $meta = ''; |
| | | if ($isConnected && array_key_exists('updated_at', $credentials) && $credentials['updated_at'] > 0) { |
| | | $meta = sprintf( |
| | | '<div class="meta"> |
| | | <small>Last updated: %s ago</small> |
| | | </div>', |
| | | human_time_diff($credentials['updated_at']) |
| | | ); |
| | | if ($isConnected) { |
| | | $updated = Auth::getInstance()->getLastUpdated($this->service_name, $this->userID); |
| | | $meta = []; |
| | | if ($updated) { |
| | | $meta[] = sprintf( |
| | | '<small>Last updated: %s ago</small>', |
| | | human_time_diff(strtotime($updated)) |
| | | ); |
| | | } |
| | | $tested = Auth::getInstance()->getLastTested($this->service_name, $this->userID); |
| | | if ($tested && $tested !== '0000-00-00 00:00:00') { |
| | | $meta[] = sprintf( |
| | | '<small>Last tested: %s ago</small>', |
| | | human_time_diff(strtotime($tested)) |
| | | ); |
| | | } |
| | | if (!empty($meta)) { |
| | | $meta = sprintf( |
| | | '<div class="meta">%s</div>', |
| | | implode('<br>', $meta) |
| | | ); |
| | | } |
| | | } |
| | | |
| | | $form = sprintf( |
| | |
| | | $meta, |
| | | ); |
| | | |
| | | if (!empty($this->instructions)) { |
| | | if (!$this->isSetUp() && !empty($this->instructions)) { |
| | | $form .= sprintf( |
| | | '<details%s> |
| | | <summary>Instructions</summary> |
| | |
| | | </details>', |
| | | $isConnected ? '' : ' open', |
| | | implode('',array_map(function($el) { |
| | | if (is_array($el)) { |
| | | $bits = implode('', array_map(function ($b) { |
| | | return sprintf('<li>%s</li>', $b); |
| | | }, $el)); |
| | | return sprintf('<ol>%s</ol>', $bits); |
| | | } |
| | | return sprintf('<li>%s</li>', $el); |
| | | }, $this->instructions)) |
| | | ); |
| | | } |
| | | |
| | | $setupFields = array_filter($this->fields, function($field) { |
| | | return $field->isSetupField(); |
| | | }); |
| | | |
| | | if (current_user_can('manage_options')) { |
| | | if ((method_exists($this, 'userCanConnect') && $this->userCanConnect(get_current_user_id())) || current_user_can('manage_options')) { |
| | | $form .= sprintf( |
| | | '<details class="initial-setup"%s> |
| | | <summary>Setup</summary> |
| | |
| | | %s |
| | | </details>', |
| | | $isConnected ? '' : ' open', |
| | | $this->outputFields($this->credentialFields), |
| | | $this->outputFields($setupFields), |
| | | method_exists($this, 'renderWebhookUrl') ? $this->renderWebhookUrl() : '' |
| | | ); |
| | | } |
| | |
| | | $form .= $this->renderOAuthConnectionForm(); |
| | | } |
| | | |
| | | if (!empty($this->optionalFields) || $this->hasExtraOptions()) { |
| | | $form .= '<div class="integration-content">'; |
| | | if (!empty($this->optionalFields)) { |
| | | $form .= sprintf( |
| | | $advancedFields = array_filter($this->fields, function($field) { |
| | | return $field->isAdvancedField(); |
| | | }); |
| | | |
| | | if (!empty($advancedFields) && $this->isSetUp()) { |
| | | $form .= sprintf( |
| | | '<details class="advanced"> |
| | | <summary>Advanced Settings</summary>%s</details>', |
| | | $this->outputFields($this->optionalFields) |
| | | <summary>Advanced Options</summary> |
| | | %s |
| | | </details>', |
| | | $this->outputFields($advancedFields), |
| | | ); |
| | | } |
| | | |
| | | $optionalFields = array_filter($this->fields, function($field) { |
| | | return $field->isAdditionalField(); |
| | | }); |
| | | |
| | | if ($this->isSetUp() && (!empty($optionalFields) || $this->hasExtraOptions())) { |
| | | $form .= '<div class="integration-content">'; |
| | | if (!empty($optionalFields)) { |
| | | $form .= sprintf( |
| | | '<details class="advanced"%s> |
| | | <summary>Optional Settings</summary>%s</details>', |
| | | $this->isSetUp() ? ' open' : '', |
| | | $this->outputFields($optionalFields) |
| | | ); |
| | | } |
| | | if ($this->hasExtraOptions()) { |
| | |
| | | $label .= $action->getLabel(); |
| | | return sprintf( |
| | | '<button type="button" data-action="%s"%s%s>%s</button>', |
| | | $action->getSlug, |
| | | $action->getSlug(), |
| | | $action->getName(), |
| | | empty($action->getConfirm()) ? '' : ' data-confirm="'.$action->getConfirm().'"', |
| | | $label |
| | |
| | | if (!empty($value)) { |
| | | $config['readonly'] = true; |
| | | } |
| | | if (!empty($value) && $f->isHideStored()) { |
| | | return sprintf( |
| | | '<span class="label">%s</span><code>%s</code>', |
| | | $config['label'], |
| | | preg_replace('/.(?=.{4})/', '*', $value) |
| | | ); |
| | | } |
| | | $config['base'] = $this->service_name.'_'; |
| | | return Form::render($f->name, $value, $f->getConfig()); |
| | | }, $fields)); |
| | |
| | | $this->fields[$name]->setRequired(); |
| | | return true; |
| | | } |
| | | public function updateField(string $name, Field $field):void |
| | | { |
| | | $this->fields[$name] = $field; |
| | | } |
| | | |
| | | |
| | | public function testConnectionAction(?array $data = null):array |
| | | { |
| | | $success = $this->testConnection(); |
| | | $message = $success ? 'Successfully tested connection' : 'Could not test connection'; |
| | | return $this->response($success, $message); |
| | | } |
| | | |
| | | } |
| | |
| | | return (bool)$updated; |
| | | } |
| | | |
| | | public function getLastUpdated(string $service, ?int $userID = null):?string |
| | | { |
| | | $find = $this->findBy($service, $userID); |
| | | return $this->table->pluck('updated_at', $find)[0]??null; |
| | | } |
| | | |
| | | /** |
| | | * Retrieve and decrypt credentials |
| | | */ |
| | |
| | | ], $find); |
| | | } |
| | | |
| | | public function getLastTested(string $service, ?int $userID = null):?string |
| | | { |
| | | $find = $this->findBy($service, $userID); |
| | | return $this->table->pluck('last_tested', $find)[0]??null; |
| | | } |
| | | |
| | | public function markUnhealthy(string $service, string $type = '', ?int $userID = null):void |
| | | { |
| | | $update = []; |
| | |
| | | protected bool $hidden = false; |
| | | protected int $min; |
| | | protected int $max; |
| | | protected bool $isAdditionalField = false; |
| | | protected bool $isSetupField = false; |
| | | protected bool $isAdvancedField = false; |
| | | protected bool $hideStored = false; |
| | | |
| | | public function __construct(string $name, string $label, string $type, callable|string|bool $permission) |
| | | { |
| | |
| | | { |
| | | $this->required = true; |
| | | } |
| | | public function setHideStored():void |
| | | { |
| | | $this->hideStored = true; |
| | | } |
| | | public function isHideStored():bool |
| | | { |
| | | return $this->hideStored; |
| | | } |
| | | public function setOptions(array $options):void |
| | | { |
| | | $this->options = $options; |
| | |
| | | $this->default = $default; |
| | | } |
| | | |
| | | public function makeSetupField():void |
| | | { |
| | | $this->isSetupField = true; |
| | | } |
| | | public function isSetupField():bool |
| | | { |
| | | return $this->isSetupField; |
| | | } |
| | | public function makeAdvancedField():void |
| | | { |
| | | $this->isAdvancedField = true; |
| | | } |
| | | public function isAdvancedField():bool{ |
| | | return $this->isAdvancedField; |
| | | } |
| | | public function makeAdditionalField():void |
| | | { |
| | | $this->isAdditionalField = true; |
| | | } |
| | | public function isAdditionalField():bool |
| | | { |
| | | return $this->isAdditionalField; |
| | | } |
| | | |
| | | public function setHint(string $hint):void |
| | | { |
| | | $this->hint = $hint; |
| | |
| | | if (isset($this->hint)) { |
| | | $conf['hint'] = $this->hint; |
| | | } |
| | | if (isset($this->subType)) { |
| | | $conf['subtype'] = $this->subType; |
| | | |
| | | if (isset($this->subtype)) { |
| | | $conf['subtype'] = $this->subtype; |
| | | } |
| | | if ($this->hidden) { |
| | | $conf['hidden'] = true; |
| | |
| | | public function testConnection(bool $force = false): bool |
| | | { |
| | | if (!$this->isSetUp()) { |
| | | return false; |
| | | return true; |
| | | } |
| | | $this->ensureInitialized(); |
| | | |
| | |
| | | return false; |
| | | } |
| | | |
| | | // Cache test results to avoid excessive API calls |
| | | $cacheKey = "connection_test_$this->service_name" . ($this->userID ? "_$this->userID" : ''); |
| | | $cached = $this->cache->get($cacheKey); |
| | | |
| | | if ($cached !== false && !$force) { |
| | | return (bool)$cached; |
| | | } |
| | | |
| | | try { |
| | | if (!$this->hasOAuthCredentials()){ |
| | |
| | | } |
| | | |
| | | $this->updateLastTestedTime(); |
| | | $this->cache->set($cacheKey, $result, 300); // Cache for 5 minutes |
| | | return $result; |
| | | } catch (Exception $e) { |
| | | $this->logError('Connection test failed', ['error' => $e->getMessage()]); |
| | | $this->cache->set($cacheKey, false, 60); // Cache failure for 1 minute |
| | | $this->logError('testConnection', $e->getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | |
| | | return $this->icon; |
| | | } |
| | | |
| | | public function getUserID():?int |
| | | { |
| | | return $this->userID; |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | function addOAuthActions():void |
| | | { |
| | | $actions = [ |
| | | [ |
| | | 'name' => 'Connect to Service', |
| | | 'label' => 'Connect', |
| | | 'icon' => 'plugs-connected', |
| | | 'action' => [$this, 'handleOAuthConnect'] |
| | | ], |
| | | [ |
| | | 'name' => 'Disconnect from Service', |
| | | 'icon' => 'plugs-disconnected', |
| | | 'action' => [$this, 'handleOAuthDisconnect'] |
| | | ], |
| | | if ($this->isSetUp()) { |
| | | $actions = [ |
| | | [ |
| | | 'name' => 'Connect to Service', |
| | | 'label' => 'Connect', |
| | | 'icon' => 'plugs-connected', |
| | | 'action' => [$this, 'handleOAuthConnect'] |
| | | ], |
| | | [ |
| | | 'name' => 'Disconnect from Service', |
| | | 'label' => 'Disconnect', |
| | | 'icon' => 'plugs-disconnected', |
| | | 'action' => [$this, 'handleOAuthDisconnect'] |
| | | ], |
| | | // [ |
| | | // 'name' => 'Refresh Token', |
| | | // 'icon' => 'arrows-clockwise', |
| | | // 'action' => [$this, 'handleOAuthRefreshToken'] |
| | | // ] |
| | | ]; |
| | | foreach ($actions as $a) { |
| | | $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null); |
| | | ]; |
| | | foreach ($actions as $a) { |
| | | $this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null); |
| | | } |
| | | } |
| | | } |
| | | public function hasOAuthCredentials(?int $userID = null):bool |
| | |
| | | |
| | | public function revokeOAuth():bool |
| | | { |
| | | if (empty($this->oauth['revoke'])) { |
| | | if (empty($this->oauth->getRevoke())) { |
| | | error_log('No revoke url set for '.$this->service_name); |
| | | return false; |
| | | } |
| | |
| | | if (empty($credentials['access_token'])) { |
| | | error_log('Could not send revoke request, no access_token for '.$this->service_name); |
| | | } else { |
| | | $response = $this->postOAuthRequest($this->oauth['revoke'], [ |
| | | $response = $this->postOAuthRequest($this->oauth->getRevoke(), [ |
| | | 'body' => [ |
| | | 'token' => $credentials['access_token'] |
| | | ] |
| | |
| | | $credentials = $this->loadCredentials(); |
| | | $hasCredentials = $this->hasValidOAuth(); |
| | | $returnURL = $this->getReturnURL(); |
| | | |
| | | if ($hasCredentials) { |
| | | if (!$this->isSetUp()) { |
| | | return ''; |
| | | } |
| | | if (!$hasCredentials) { |
| | | return sprintf( |
| | | '<div class="oauth-connect"> |
| | | <h3>Connect to your %s account</h3> |
| | | <a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a>', |
| | | <a href="%s" class="btn jvb-oauth-connect" data-service="%s">%s Authorize Connection</a> |
| | | </div>', |
| | | $this->title, |
| | | $this->getOAuthURL($returnURL), |
| | | esc_attr($this->service_name), |
| | | $this->outputIcon($this->icon) |
| | | ); |
| | | } |
| | | $form = sprintf( |
| | | '<details open> |
| | | } else { |
| | | $form = sprintf( |
| | | '<details open> |
| | | <summary>Connected Account |
| | | <div class="connection-status connected"> |
| | | <span class="status-indicator">●</span> |
| | |
| | | </summary> |
| | | <span class="label">OAuth Redirect URL:</span> |
| | | <code>%s</code>', |
| | | $this->getRedirectUri() |
| | | ); |
| | | if (!empty($credentials['updated_at'])) { |
| | | $form .= sprintf( |
| | | '<div class="oauth-meta"><small>Token expires: %s</small></div>', |
| | | array_key_exists('expires_at', $credentials) ? |
| | | $this->getRedirectUri() |
| | | ); |
| | | if (!empty($credentials['updated_at'])) { |
| | | $form .= sprintf( |
| | | '<div class="oauth-meta"><small>Token expires: %s</small></div>', |
| | | array_key_exists('expires_at', $credentials) ? |
| | | human_time_diff($credentials['expires_at']) : |
| | | 'Never' |
| | | ); |
| | | ); |
| | | } |
| | | // |
| | | if (method_exists($this, 'renderOAuthConnectedOptions')) { |
| | | $form .= $this->renderOAuthConnectedOptions(); |
| | | } |
| | | $form .= '</details>'; |
| | | return $form; |
| | | } |
| | | // |
| | | if (method_exists($this, 'renderOAuthConnectedOptions')) { |
| | | $form .= $this->renderOAuthConnectedOptions(); |
| | | } |
| | | $form .= '</details'; |
| | | return $form; |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | |
| | | $credentials = $this->loadCredentials(); |
| | | if (empty($this->oauth['authorize'])) { |
| | | if (empty($this->oauth->getAuthorize())) { |
| | | $this->logError('getOAuthUrl', 'OAuth authorize URL not configured'); |
| | | return ''; |
| | | } |
| | |
| | | $params = $this->addOAuthParams($params); |
| | | } |
| | | |
| | | return $this->oauth['authorize'] . '?' . http_build_query($params); |
| | | return $this->oauth->getAuthorize() . '?' . http_build_query($params); |
| | | } |
| | | |
| | | protected function getRedirectUri():string |
| | |
| | | public function getServiceUser(string $service, ?int $userID = null):Integrations|false |
| | | { |
| | | $service = $this->getService($service); |
| | | if ($service->userID !== $userID && method_exists($service, 'switchUser')) { |
| | | if ($service->getUserID() !== $userID && method_exists($service, 'switchUser')) { |
| | | $service->switchUser($userID); |
| | | } |
| | | return $service; |
| | |
| | | |
| | | public function getAvailableServices():array |
| | | { |
| | | return array_filter(array_keys($this->serviceMap), function ($service) { |
| | | return Site::hasIntegration($service); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | return array_keys(Site::getIntegrations()); |
| | | } |
| | | } |
| | |
| | | ->description($options['description']) |
| | | ->content($this->getContentTypeContent($allowed)); |
| | | } |
| | | |
| | | $tabs->render(false); |
| | | } |
| | | public function getContentTypeContent(array $content):string |
| | |
| | | } |
| | | |
| | | trait SyncHelpers { |
| | | use _Base; |
| | | use _Base, SyncDefaults; |
| | | /** |
| | | * Queue types |
| | | * These types match with IntegrationExecutor |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Errors; |
| | | use JVBase\managers\IconsManager; |
| | |
| | | } |
| | | |
| | | trait _Base { |
| | | use ErrorHelper; |
| | | /** |
| | | * @var string Unique identifier for this service (ex: square, facebook) |
| | | */ |
| | |
| | | */ |
| | | public function isSetUp(): bool |
| | | { |
| | | return !empty($this->loadCredentials()); |
| | | $tmp = null; |
| | | if ($this->userID) { |
| | | $tmp = $this->userID; |
| | | $this->switchUser(); |
| | | } |
| | | $isSetUp = !empty($this->loadCredentials()??[]); |
| | | if ($tmp) { |
| | | $this->switchUser($tmp); |
| | | } |
| | | return $isSetUp; |
| | | } |
| | | |
| | | public function loadCredentials():array |
| | |
| | | |
| | | protected function logError(string $method, string $message, array $context = []):void |
| | | { |
| | | |
| | | $context['service'] = $this->service_name; |
| | | Errors::getInstance()->log('['.self::class.']'.$method, $message, $context); |
| | | Errors::log($message, array_merge( |
| | | [ |
| | | 'service' => $this->service_name, |
| | | 'method' => $method, |
| | | 'class' => static::class, |
| | | ], |
| | | $context |
| | | )); |
| | | } |
| | | |
| | | protected function getReturnURL():string |
| | |
| | | { |
| | | if (is_admin()) { |
| | | $icons = IconsManager::for('admin'); |
| | | return $icons->getRawSvg($icon); |
| | | return $icons->getRawSvg($icon)??''; |
| | | } |
| | | return jvbDashIcon($icon); |
| | | } |
| | |
| | | $site_key = $this->addField('site_key', 'Site Key', 'text', 'manage_options'); |
| | | $site_key->setRequired(); |
| | | $site_key->setHint('Public key for Turnstile Widget'); |
| | | $site_key->makeSetUpField(); |
| | | $this->updateField('site_key', $site_key); |
| | | |
| | | $secret_key = $this->addField('secret_key', 'Secret Key', 'text', 'manage_options'); |
| | | $secret_key->setRequired(); |
| | | $secret_key->setSubType('password'); |
| | | $secret_key->setHint('Secret key for server-side verification of Turnstile Widget'); |
| | | $secret_key->makeSetUpField(); |
| | | $this->updateField('secret_key', $secret_key); |
| | | } |
| | | |
| | | protected function defineInstructions():void |
| | |
| | | return; |
| | | } |
| | | } |
| | | |
| | | protected function defineActions(): void |
| | | { |
| | | } |
| | | } |
| | |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | | $this->defineOAuth(); |
| | | $this->defineActions(); |
| | | $this->defineContentTypes(); |
| | | |
| | | $this->baseUrl = 'https://graph.facebook.com/'.$this->apiVersion; |
| | |
| | | $this->apiBase = 'https://maps.googleapis.com/maps/api/'; |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | | $this->defineActions(); |
| | | |
| | | parent::__construct(); |
| | | // Auto-enqueue maps script when needed |
| | |
| | | ); |
| | | $client_id->setRequired(); |
| | | $client_id->setHint('From your Google Cloud Console OAuth credentials.'); |
| | | $client_id->makeSetupField(); |
| | | $this->updateField('client_id', $client_id); |
| | | |
| | | $secret = $this->addField( |
| | | 'client_secret', |
| | |
| | | ); |
| | | $secret->setSubType('password'); |
| | | $secret->setRequired(); |
| | | $secret->makeSetupField(); |
| | | $this->updateField('client_secret', $client_id); |
| | | |
| | | if ($this->getAccessToken()) { |
| | | $accs = $this->addField('accounts', 'Accounts', 'select', [$this, 'testUserCanConnect']); |
| | |
| | | 'Add redirect URI: <code>'.esc_html($this->getRedirectUri()).'</code>', |
| | | 'Use the "Authorize" button below to complete OAuth flow' |
| | | ]; |
| | | |
| | | $this->instructions = [ |
| | | 'Go to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a>', |
| | | 'Create OAuth 2.0 Client ID credentials', |
| | | 'Enable the following APIs in your project:', |
| | | [ |
| | | '<a href="https://console.cloud.google.com/apis/library/mybusinessaccountmanagement.googleapis.com" target="_blank">My Business Account Management API</a>', |
| | | '<a href="https://console.cloud.google.com/apis/library/mybusinessbusinessinformation.googleapis.com" target="_blank">My Business Business Information API</a>', |
| | | '<a href="https://console.cloud.google.com/apis/library/mybusiness.googleapis.com" target="_blank">Google My Business API</a> (legacy v4, required for reviews)', |
| | | ], |
| | | 'On the OAuth consent screen, add these scopes:', |
| | | [ |
| | | '<code>https://www.googleapis.com/auth/business.manage</code>', |
| | | '<code>https://www.googleapis.com/auth/userinfo.email</code>', |
| | | '<code>https://www.googleapis.com/auth/userinfo.profile</code>', |
| | | ], |
| | | 'Add redirect URI: <code>'.esc_html($this->getRedirectUri()).'</code>', |
| | | 'Use the "Authorize" button below to complete OAuth flow' |
| | | ]; |
| | | } |
| | | protected function defineOAuth():void |
| | | { |
| | |
| | | } |
| | | protected function defineActions():void |
| | | { |
| | | $this->addAction( |
| | | 'Update Location', |
| | | [$this, 'handleUpdateLocation'], |
| | | 'update_location', |
| | | ); |
| | | if ($this->hasOAuthCredentials($this->userID)) { |
| | | $this->addAction( |
| | | 'Update Location', |
| | | [$this, 'handleUpdateLocation'], |
| | | 'update_location', |
| | | ); |
| | | } |
| | | } |
| | | public function handleUpdateLocation(array $data):array |
| | | { |
| | |
| | | */ |
| | | class Helcim extends Integrations |
| | | { |
| | | use Admin, Instance, OAuth, Requests, UserConnection, SyncToPost; |
| | | use Admin, Instance, Requests, UserConnection, SyncToPost; |
| | | |
| | | protected function __construct() |
| | | { |
| | | $this->canTest = true; |
| | | $this->service_name = 'helcim'; |
| | | $this->title = 'Helcim'; |
| | | $this->icon = 'currency-dollar'; |
| | |
| | | 'delete' => true, |
| | | ]; |
| | | |
| | | $this->baseUrl = 'https://api.helcim.com/v2'; |
| | | $this->apiVersion = 'v2'; |
| | | $this->baseUrl = sprintf('https://api.helcim.com/%s/', $this->apiVersion); |
| | | |
| | | $this->defineFields(); |
| | | $this->defineInstructions(); |
| | | $this->defineActions(); |
| | | |
| | | parent::__construct(); |
| | | } |
| | |
| | | $api_token->setSubType('password'); |
| | | $api_token->setRequired(); |
| | | $api_token->setHint('Found in Helcim Dashboard → Settings → API Access'); |
| | | $api_token->makeSetupField(); |
| | | $api_token->setHideStored(); |
| | | $this->updateField('api_token', $api_token); |
| | | |
| | | $currency = $this->addField( |
| | | 'currency', |
| | |
| | | 'USD' => 'USD' |
| | | ]); |
| | | $currency->setDefault('CAD'); |
| | | $currency->makeAdditionalField(); |
| | | |
| | | $feeSaver = $this->addField( |
| | | 'fee_saver', |
| | |
| | | [$this, 'testUserSetup'] |
| | | ); |
| | | $feeSaver->setHint('Pass processing fees to customers? (Not compatible with Google Pay)'); |
| | | $feeSaver->makeAdditionalField(); |
| | | |
| | | $allowACH = $this->addField( |
| | | 'allow_ach', |
| | |
| | | [$this, 'testUserSetup'] |
| | | ); |
| | | $allowACH->setHint('Enable bank account payments alongside credit card'); |
| | | $allowACH->makeAdditionalField(); |
| | | } |
| | | public function testUserCanConnect():bool |
| | | { |
| | |
| | | |
| | | protected function defineActions():void |
| | | { |
| | | $this->addAction( |
| | | 'Initialize Checkout', |
| | | [$this, 'initializeCheckout'], |
| | | 'initialize_checkout' |
| | | ); |
| | | if ($this->isSetUp()) { |
| | | $this->addAction( |
| | | 'Sync to Helcim', |
| | | [$this, 'syncToHelcim'], |
| | | 'sync_to_helcim', |
| | | 'upload-simple' |
| | | ); |
| | | $this->addAction( |
| | | 'Sync to Site', |
| | | [$this, 'syncToSite'], |
| | | 'sync_to_site', |
| | | 'download-simple' |
| | | ); |
| | | } |
| | | |
| | | } |
| | | |
| | | protected function initialize(): void |
| | |
| | | // TODO: Implement initialize() method. |
| | | } |
| | | |
| | | protected function refreshAccessToken(OAuthCredentials $credentials): array |
| | | { |
| | | // TODO: Implement refreshAccessToken() method. |
| | | } |
| | | |
| | | protected function getRequestHeaders(): array |
| | | { |
| | | // TODO: Implement getRequestHeaders() method. |
| | | } |
| | | |
| | | protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array |
| | | { |
| | | // TODO: Implement handleResponse() method. |
| | | return empty($this->getApiToken()) ? [] : [ |
| | | 'api-token' => $this->getApiToken(), |
| | | 'Accept' => 'application/json', |
| | | ]; |
| | | } |
| | | |
| | | protected function setContentTypes(): void |
| | | { |
| | | // TODO: Implement setContentTypes() method. |
| | | $this->contentTypes = [ |
| | | 'recurring' => $this->buildRecurringPaymentFields(), |
| | | 'product' => $this->buildPaymentFields(), |
| | | 'customer' => $this->buildCustomerFields(), |
| | | 'invoice' => $this->buildInvoiceFields(), |
| | | ]; |
| | | } |
| | | protected function buildRecurringPaymentFields(): array |
| | | { |
| | | return []; |
| | | } |
| | | protected function buildPaymentFields(): array |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | protected function buildCustomerFields():array |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | protected function buildInvoiceFields():array |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | protected function formatForService(int $itemID, string $type = 'post'): array |
| | | { |
| | | return []; |
| | | // TODO: Implement formatForService() method. |
| | | } |
| | | |
| | | public function initializeCheckout():void |
| | | { |
| | | |
| | | } |
| | | |
| | | public function syncToHelcim():void |
| | | { |
| | | |
| | | } |
| | | public function syncToSite():void |
| | | { |
| | | |
| | | } |
| | | /************************************************************************* |
| | | * REQUESTS |
| | | *************************************************************************/ |
| | | |
| | | protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array |
| | | { |
| | | if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') { |
| | | return $this->response(false, 'Rate limit exceeded'); |
| | | } |
| | | return match ($endpoint) { |
| | | 'connection-test' => $this->response(true,'Connection successful', $response), |
| | | default => $this->response(false, 'Unhandled response for '.$endpoint), |
| | | }; |
| | | } |
| | | protected function performConnectionTest(): bool |
| | | { |
| | | $response = $this->getRequest( |
| | | 'connection-test' |
| | | ); |
| | | error_log('Helcim connection test: '.print_r($response, true)); |
| | | return $response['success']??false; |
| | | } |
| | | /************************************************************************* |
| | | * UTILITY |
| | | *************************************************************************/ |
| | |
| | | $this->defineInstructions(); |
| | | |
| | | parent::__construct(); |
| | | $this->defineActions(); |
| | | } |
| | | |
| | | protected function defineFields():void |
| | | { |
| | | $token = $this->addField('service_token', 'Server API Token', 'text', 'manage_options'); |
| | | $token->setSubType('password'); |
| | | $token->makeSetupField(); |
| | | $this->updateField('service_token', $token); |
| | | |
| | | $stream = $this->addField('message_stream', 'Message Stream ID', 'text', 'manage_options'); |
| | | $stream->setDefault('outbound'); |
| | | $stream->setHint('Message stream to use (default: outbound)'); |
| | | $stream->makeAdditionalField(); |
| | | $this->updateField('message_stream', $stream); |
| | | |
| | | $from = $this->addField('from_email', 'From Email', 'email', 'manage_options'); |
| | | $from->setPlaceholder('noreply@yourdomain.com'); |
| | | $from->setDefault(get_option('admin_email')); |
| | | $from->makeAdditionalField(); |
| | | $this->updateField('from_email', $from); |
| | | |
| | | $name = $this->addField('from_name', 'From Name', 'text', 'manage_options'); |
| | | $name->setDefault(get_bloginfo('name')); |
| | | $name->setHint('Default sender name'); |
| | | $name->makeAdditionalField(); |
| | | $this->updateField('from_name', $from); |
| | | |
| | | $opens = $this->addField('track_opens', 'Track Opens', 'select', 'manage_options'); |
| | | $opens->setOptions([ |
| | |
| | | ]); |
| | | $opens->setDefault('false'); |
| | | $opens->setHint('Track when emails are opened'); |
| | | $opens->makeAdvancedField(); |
| | | $this->updateField('track_opens', $opens); |
| | | |
| | | $links = $this->addField('track_links', 'Track Links', 'select', 'manage_options'); |
| | | $links->setOptions([ |
| | |
| | | ]); |
| | | $links->setDefault('None'); |
| | | $links->setHint('Track link clicks in emails'); |
| | | $links->makeAdvancedField(); |
| | | $this->updateField('track_links', $links); |
| | | } |
| | | protected function defineInstructions():void |
| | | { |
| | |
| | | ? 'https://connect.squareup.com' |
| | | : 'https://connect.squareupsandbox.com'; |
| | | |
| | | $this->oauth['authorize'] = $baseUrl . '/oauth2/authorize'; |
| | | $this->oauth['token'] = $baseUrl . '/oauth2/token'; |
| | | $this->oauth['revoke'] = $baseUrl . '/oauth2/revoke'; |
| | | $this->oauth->getAuthorize() = $baseUrl . '/oauth2/authorize'; |
| | | $this->oauth->getToken() = $baseUrl . '/oauth2/token'; |
| | | $this->oauth->getRevoke() = $baseUrl . '/oauth2/revoke'; |
| | | |
| | | // Set up API endpoints with version |
| | | $this->apiEndpoints = [ |
| | |
| | | 'redirect_uri' => $this->getRedirectUri() |
| | | ]; |
| | | |
| | | $response = wp_remote_post($this->oauth['token'], [ |
| | | $response = wp_remote_post($this->oauth->getToken(), [ |
| | | 'body' => json_encode($body), |
| | | 'headers' => [ |
| | | 'Content-Type' => 'application/json', |
| | |
| | | return false; |
| | | } |
| | | |
| | | $response = wp_remote_post($this->oauth['token'], [ |
| | | $response = wp_remote_post($this->oauth->getToken(), [ |
| | | 'body' => [ |
| | | 'client_id' => $this->credentials['client_id'], |
| | | 'client_secret' => $this->credentials['client_secret'], |
| | |
| | | try { |
| | | // Revoke the token with Square |
| | | if (!empty($this->credentials['access_token'])) { |
| | | wp_remote_post($this->oauth['revoke'], [ |
| | | wp_remote_post($this->oauth->getRevoke(), [ |
| | | 'body' => [ |
| | | 'client_id' => $this->credentials['client_id'] ?? '', |
| | | 'access_token' => $this->credentials['access_token'] |
| | |
| | | protected function defineFields(): void |
| | | { |
| | | $id = $this->addField('website_id', 'Website ID', 'text', 'manage_options'); |
| | | $id->setHint('Found in your Umami dashboard under Settings->Websites'); |
| | | $id->setRequired(); |
| | | $id->setHint('Found in your Umami dashboard under Settings->Websites'); |
| | | $id->setRequired(); |
| | | $id->makeSetupField(); |
| | | $this->updateField('website_id', $id); |
| | | |
| | | $api_url = $this->addField('api_url', 'API URL', 'text', 'manage_options'); |
| | | $api_url->setHint('Your Umami server URL. Leave blank if using umami.is cloud service.'); |
| | | $api_url->setHint('Your Umami server URL. Leave blank if using umami.is cloud service.'); |
| | | $api_url->makeAdvancedField(); |
| | | $this->updateField('api_url', $api_url); |
| | | |
| | | $api_token = $this->addField('api_token', 'API Token (Optional)', 'text', 'manage_options'); |
| | | $api_token->setSubType('password'); |
| | | $api_token->setHint('Required only if you want to retrieve analytics data for dashboards.'); |
| | | $api_token->setSubType('password'); |
| | | $api_token->setHint('Required only if you want to retrieve analytics data for dashboards.'); |
| | | $api_token->makeAdvancedField(); |
| | | $this->updateField('api_token', $api_token); |
| | | } |
| | | protected function defineInstructions():void |
| | | { |
| | |
| | | |
| | | return implode(' ', $strings); |
| | | } |
| | | |
| | | protected function defineActions(): void |
| | | { |
| | | // TODO: Implement defineActions() method. |
| | | } |
| | | } |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use wpdb; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | */ |
| | | class CustomTable |
| | | { |
| | | |
| | | use ErrorHelper; |
| | | private static ?wpdb $wpdb = null; |
| | | private static string $prefix; |
| | | protected string $tableName; |
| | |
| | | ); |
| | | |
| | | if ($result === false) { |
| | | $this->logError('insert', $data); |
| | | $this->logError(self::$wpdb->last_error, 'insert', $data); |
| | | return false; |
| | | } |
| | | |
| | |
| | | $result = self::$wpdb->query(self::$wpdb->prepare($query, $values)); |
| | | |
| | | if ($result === false) { |
| | | $this->logError('bulkInsert', ['rows' => count($rows)]); |
| | | $this->logError(self::$wpdb->last_error, 'bulkInsert', ['rows' => count($rows)]); |
| | | return false; |
| | | } |
| | | |
| | |
| | | $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"]); |
| | | $this->logError("Column '{$column}' not in definition", 'pluck'); |
| | | return []; |
| | | } |
| | | $args = ['where' => $where]; |
| | |
| | | ); |
| | | |
| | | if ($result === false) { |
| | | $this->logError('update', ['data' => $data, 'where' => $where]); |
| | | $this->logError(self::$wpdb->last_error,'update', ['data' => $data, 'where' => $where]); |
| | | } |
| | | $this->cache->flush(); |
| | | |
| | |
| | | ); |
| | | |
| | | if ($result === false) { |
| | | $this->logError('delete', ['where' => $where]); |
| | | $this->logError(self::$wpdb->last_error, 'delete', ['where' => $where]); |
| | | } |
| | | $this->cache->flush(); |
| | | |
| | |
| | | return $result; |
| | | } catch (Exception $e) { |
| | | $this->rollback(); |
| | | $this->logError('transaction', ['error' => $e->getMessage()]); |
| | | $this->logError(self::$wpdb->last_error,'transaction', ['error' => $e->getMessage()]); |
| | | return false; |
| | | } |
| | | } |
| | |
| | | return isset($cache[$this->tableName][$column]); |
| | | } |
| | | |
| | | /** |
| | | * Log database errors |
| | | */ |
| | | private function logError(string $operation, array $context = []): void |
| | | { |
| | | if (function_exists('JVB')) { |
| | | Errors::getInstance()->log( |
| | | $this->tableName, |
| | | "CustomTable {$operation} failed: " . self::$wpdb->last_error, |
| | | $context |
| | | ); |
| | | } else { |
| | | error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . self::$wpdb->last_error); |
| | | } |
| | | } |
| | | |
| | | public function sum(string $column, array $where):float |
| | | { |
| | | $rows = $this->pluck($column, $where); |
| | |
| | | $registrar = Registrar::getInstance($c); |
| | | $page = $this->addPage($registrar->getPlural(), $c, $registrar->getIcon()); |
| | | $page->setPermission(Roles::getPermissionName('edit', $c)); |
| | | $this->sections[$c] = new Section($registrar->getPlural(), $c, $registrar->getIcon(), 'content'); |
| | | $page->setSection($c); |
| | | |
| | | |
| | | if ($registrar->hasFeature('verify_entry')) { |
| | | |
| | | } |
| | | |
| | | //TODO: add artist management and invitations, if applicable |
| | | } |
| | | } |
| | |
| | | 'public' => true, |
| | | 'menu_icon' => jvbCSSIcon('list-dashes'), |
| | | 'publicly_queryable' => true, |
| | | 'show_in_menu' => true, |
| | | 'show_in_menu' => false, |
| | | 'show_in_admin_bar' => false, |
| | | 'has_archive' => true, |
| | | 'hierarchical' => true, |
| New file |
| | |
| | | <?php
|
| | |
|
| | | namespace JVBase\inc\managers;
|
| | |
|
| | | use JVBase\managers\Errors;
|
| | |
|
| | | trait ErrorHelper
|
| | | {
|
| | | /**
|
| | | * Log errors with proper context
|
| | | */
|
| | | protected function logError(string $message, string $caller, array $context = []): void
|
| | | {
|
| | | Errors::log($message, array_merge([
|
| | | 'class' => static::class,
|
| | | 'method' => $caller,
|
| | | ], $context));
|
| | | }
|
| | |
|
| | | protected static function staticLogError(string $message, string $caller, array $context = []): void
|
| | | {
|
| | | Errors::log($message, array_merge([
|
| | | 'class' => static::class,
|
| | | 'method' => $caller,
|
| | | ], $context));
|
| | | }
|
| | | }
|
| | |
| | | class Errors |
| | | { |
| | | use Instance; |
| | | protected int $notification_threshold = 5; // Critical errors within 1 hour |
| | | |
| | | protected array $error_levels = [ |
| | | 'debug' => 0, |
| | | 'info' => 1, |
| | | 'warning' => 2, |
| | | 'error' => 3, |
| | | 'critical' => 4 |
| | | ]; |
| | | |
| | | protected CustomTable $table; |
| | | |
| | | private function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | // global $wpdb; |
| | | // $this->wpdb = $wpdb; |
| | | // $this->tableName = $wpdb->prefix . BASE . 'error_log'; |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | |
| | | // Hook our send function to the cron event |
| | | add_action('jvb_daily_error_summary', [$this, 'sendErrorSummary']); |
| | | |
| | | jvb_register_do_once('error_admin_action_registered', [$this, 'registerAdminAction']); |
| | | // add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3); |
| | | } |
| | | |
| | | public function defineTables():void |
| | | /** |
| | | * @param string $message |
| | | * @param array $context |
| | | * @return void |
| | | */ |
| | | public static function log(string $message, array $context = []):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 = AdminPages::getInstance(); |
| | | |
| | | $admin->registerSubpage( |
| | | 'Error Log', |
| | | 'Errors', |
| | | 'manage_options', |
| | | 'jvb-error-log', |
| | | [$this, 'renderAdminPage'], |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Process error log operations queued through OperationQueue |
| | | * |
| | | * @param WP_Error|array $result The result we are filtering |
| | | * @param object $operation The operation information |
| | | * @param array $data The current chunk of data to process |
| | | * |
| | | * @return WP_Error|array Either the unfiltered WP_Error, or the result of the operation |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | | { |
| | | if ($operation->type !== 'error_log') { |
| | | return $result; |
| | | } |
| | | |
| | | try { |
| | | |
| | | // Extract error data |
| | | $component = sanitize_text_field($data['component'] ?? ''); |
| | | $message = sanitize_textarea_field($data['message'] ?? ''); |
| | | $context = $data['context'] ?? []; |
| | | $severity = sanitize_text_field($data['severity'] ?? 'error'); |
| | | |
| | | // Validate severity level |
| | | if (!array_key_exists($severity, $this->error_levels)) { |
| | | $severity = 'error'; // Default to error if invalid severity provided |
| | | } |
| | | |
| | | // Determine error type from context or use component as fallback |
| | | $error_type = sanitize_text_field($context['error_type'] ?? $component); |
| | | |
| | | // Store context as JSON |
| | | $context_json = json_encode($context); |
| | | if (json_last_error() !== JSON_ERROR_NONE) { |
| | | // If JSON encoding fails, store only basic info |
| | | $context_json = json_encode([ |
| | | 'error' => 'Failed to encode original context', |
| | | 'json_error' => json_last_error_msg() |
| | | ]); |
| | | } |
| | | |
| | | // Insert into database |
| | | $result = $this->table->insert( |
| | | [ |
| | | 'error_type' => $error_type, |
| | | 'component' => $component, |
| | | 'message' => $message, |
| | | 'context' => $context_json, |
| | | 'severity' => $severity, |
| | | 'user_id' => get_current_user_id(), |
| | | 'created_at' => current_time('mysql') |
| | | ] |
| | | ); |
| | | |
| | | if (!$result) { |
| | | // If insert fails, log to PHP error log as fallback |
| | | error_log("[ErrorHandler] Database insert failed: " . $this->table->getLastError()); |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => "[ErrorHandler] Database insert failed: " . $this->table->getLastError() |
| | | ]; |
| | | } |
| | | |
| | | // Potentially track error metrics or trigger additional actions for critical errors |
| | | if ($severity === 'critical') { |
| | | $this->checkErrorThreshold($error_type, $component); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => $result |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | // Fallback to PHP error log if our error handler itself fails |
| | | error_log("[ErrorHandler Exception] " . $e->getMessage()); |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check if error threshold has been reached for a specific error type/component |
| | | * and take additional actions if needed |
| | | * |
| | | * @param string $error_type The type of error |
| | | * @param string $component The component where the error occurred |
| | | */ |
| | | protected function checkErrorThreshold(string $error_type, string $component) |
| | | { |
| | | // Get count of similar critical errors in the last hour |
| | | $count = $this->table->count([ |
| | | 'error_type' => $error_type, |
| | | 'component' => $component, |
| | | 'severity' => $this->error_levels[$error_type], |
| | | 'created_at' => ['>','DATE_SUB(NOW(), INTERVAL 1 HOUR)'] |
| | | ]); |
| | | |
| | | // If threshold reached, take additional actions (e.g., notify developers) |
| | | if ($count >= $this->notification_threshold) { |
| | | // You could send an urgent notification, Slack message, etc. |
| | | $admin_email = get_option('admin_email'); |
| | | $subject = "[URGENT] Error Threshold Exceeded for {$component}"; |
| | | $message = "The error '{$error_type}' in component '{$component}' has occurred {$count} times in the last hour."; |
| | | |
| | | wp_mail($admin_email, $subject, $message); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @param string $component Where the error is being logged from |
| | | * @param string $message The error message, usually extracted from $e->getMessage()) |
| | | * @param array $context Additional context (i.e: data that was attempted to be acted on) |
| | | * @param string $severity |
| | | * |
| | | * @return array |
| | | */ |
| | | public function log(string $component, string $message, array $context = [], string $severity = 'error'): array |
| | | { |
| | | try { |
| | | // Validate severity |
| | | if (!array_key_exists($severity, $this->error_levels)) { |
| | | $severity = 'error'; |
| | | } |
| | | |
| | | // Extract info |
| | | $error_type = sanitize_text_field($context['error_type'] ?? $component); |
| | | $method = isset($context['method']) ? sanitize_text_field($context['method']) : null; |
| | | $page_url = isset($context['url']) ? esc_url_raw($context['url']) : null; |
| | | $user_id = get_current_user_id(); |
| | | $user_was_logged_in = $user_id > 0 || (!empty($context['isLoggedIn'])); |
| | | |
| | | // Determine source from context |
| | | $source = $context['source'] ?? (isset($context['url']) ? 'frontend' : 'backend'); |
| | | |
| | | $result = $this->table->insert( |
| | | [ |
| | | 'error_type' => $error_type, |
| | | 'component' => $component, |
| | | 'method' => $method, |
| | | 'page_url' => $page_url, |
| | | 'message' => sanitize_textarea_field($message), |
| | | 'context' => json_encode($context), |
| | | 'severity' => $severity, |
| | | 'user_id' => $user_id ?: null, |
| | | 'user_was_logged_in' => $user_was_logged_in ? 1 : 0, |
| | | 'source' => $source, |
| | | 'created_at' => current_time('mysql') |
| | | ]); |
| | | |
| | | if (!$result) { |
| | | error_log("[ErrorHandler] Database insert failed: " . $this->table->getLastError()); |
| | | return ['success' => false, 'message' => $this->table->getLastError()]; |
| | | } |
| | | |
| | | if ($severity === 'critical') { |
| | | $this->checkErrorThreshold($error_type, $component); |
| | | } |
| | | |
| | | return ['success' => true, 'id' => $result]; |
| | | |
| | | } catch (Exception $e) { |
| | | error_log("[ErrorHandler Exception] " . $e->getMessage()); |
| | | return ['success' => false, 'message' => $e->getMessage()]; |
| | | $msg = '{'.BASE.'}'; |
| | | if (array_key_exists('class', $context)) { |
| | | $msg .= '['.$context['class'].']'; |
| | | unset($context['class']); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @param string $component What class or function logs the error |
| | | * @param string $message Details on the error |
| | | * @param array $context additional context of the data that was being processed |
| | | * |
| | | * @return bool Whether the notification is sent successfully |
| | | */ |
| | | protected function notifyAdmin(string $component, string $message, array $context):bool |
| | | { |
| | | $admin_email = get_option('admin_email'); |
| | | $subject = "[" . get_bloginfo('name') . " Critical Error] {$component}"; |
| | | |
| | | $body = Emails::getInstance()->alert( |
| | | 'A critical error has occurred and requires immediate attention', |
| | | 'error' |
| | | ); |
| | | |
| | | $body .= Emails::getInstance()->h2('Error Details'); |
| | | $body .= Emails::getInstance()->card( |
| | | '<p><strong>Component:</strong> ' . esc_html($component) . '</p>' . |
| | | '<p><strong>Message:</strong></p>' . |
| | | Emails::getInstance()->codeBlock($message), |
| | | 'Error Information' |
| | | ); |
| | | |
| | | if (array_key_exists('method', $context)) { |
| | | $msg .= '::'.$context['method']; |
| | | unset($context['method']); |
| | | } |
| | | if (array_key_exists('function', $context)) { |
| | | $msg .= $context['function']; |
| | | unset($context['function']); |
| | | } |
| | | error_log($msg.' '.$message); |
| | | if (!empty($context)) { |
| | | $body .= Emails::getInstance()->h3('Additional Context'); |
| | | $body .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); |
| | | error_log('Context: '.print_r($context, true)); |
| | | } |
| | | |
| | | return Emails::getInstance()->sendEmail($admin_email, $subject, $body, 'CRITICAL ERROR'); |
| | | } |
| | | |
| | | /** |
| | | * Gather summary of the most important errors |
| | | * @param ?string $start_date Defaults to today |
| | | * @param ?string $end_date Defaults to today |
| | | * @return array |
| | | */ |
| | | public function gatherErrorSummary(?string $start_date = null, ?string $end_date = null): array |
| | | { |
| | | if (!$start_date) { |
| | | $start_date = gmdate('Y-m-d 00:00:00', strtotime('-1 day')); |
| | | } |
| | | if (!$end_date) { |
| | | $end_date = gmdate('Y-m-d 23:59:59'); |
| | | } |
| | | |
| | | // Most frequent error patterns (deduplicated by component/method/message) |
| | | $frequent = $this->table->getMany([ |
| | | 'where' => [ |
| | | 'created_at' => ['BETWEEN', "{$start_date} AND {$end_date}"] |
| | | ] |
| | | ]); |
| | | |
| | | return [ |
| | | 'errors' => $frequent, |
| | | 'date_range' => ['start' => $start_date, 'end' => $end_date] |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Send daily error summary email to administrator |
| | | * @return bool Whether email is sent |
| | | */ |
| | | public function sendErrorSummary():bool |
| | | { |
| | | $summary = $this->gatherErrorSummary(); |
| | | |
| | | if (empty($summary['frequent']) && empty($summary['critical'])) { |
| | | return false; |
| | | } |
| | | |
| | | $admin_email = get_option('admin_email'); |
| | | $site_name = get_bloginfo('name'); |
| | | $yesterday = date('Y-m-d', strtotime('-1 day')); |
| | | $subject = "[{$site_name}] Daily Error Summary - " . date('Y-m-d'); |
| | | |
| | | // Header with alert |
| | | $body = Emails::getInstance()->h1('Daily Error Summary'); |
| | | $body .= sprintf('<p>Error summary for <strong>%s</strong></p>', $yesterday); |
| | | |
| | | // Summary stats in a grid |
| | | if (!empty($summary['stats'])) { |
| | | $stats = [ |
| | | Emails::getInstance()->stat($summary['stats']->total_errors, 'Total Errors'), |
| | | Emails::getInstance()->stat($summary['stats']->critical_count, 'Critical', 'Requires attention'), |
| | | Emails::getInstance()->stat($summary['stats']->error_count, 'Errors'), |
| | | Emails::getInstance()->stat($summary['stats']->warning_count, 'Warnings') |
| | | ]; |
| | | $body .= Emails::getInstance()->grid($stats, 4); |
| | | } |
| | | |
| | | // Alert if critical errors exist |
| | | if (!empty($summary['critical'])) { |
| | | $body .= Emails::getInstance()->alert( |
| | | sprintf('Found %d critical errors that need immediate attention', count($summary['critical'])), |
| | | 'error' |
| | | ); |
| | | } |
| | | |
| | | $body .= Emails::getInstance()->spacer(); |
| | | |
| | | // Frequent errors section |
| | | if (!empty($summary['frequent'])) { |
| | | $body .= Emails::getInstance()->h2('Most Frequent Errors'); |
| | | |
| | | foreach ($summary['frequent'] as $error) { |
| | | $cardContent = Emails::getInstance()->badge($error->count . 'x', 'warning') . ' '; |
| | | $cardContent .= '<strong>' . esc_html($error->error_type) . '</strong>'; |
| | | $cardContent .= '<p style="margin:10px 0 5px 0;font-size:13px;">' . esc_html(wp_trim_words($error->message, 15)) . '</p>'; |
| | | $cardContent .= '<p style="margin:0;font-size:12px;color:' . Emails::getInstance()->colours['dark-200'] . ';"> |
| | | Source: ' . esc_html($error->source) . ' | |
| | | Logged in: ' . $error->logged_in_count . ' | |
| | | Logged out: ' . $error->logged_out_count . ' |
| | | </p>'; |
| | | |
| | | $body .= Emails::getInstance()->card($cardContent, $error->component); |
| | | } |
| | | } |
| | | |
| | | // Critical errors section |
| | | if (!empty($summary['critical'])) { |
| | | $body .= Emails::getInstance()->spacer(30); |
| | | $body .= Emails::getInstance()->h2('Recent Critical Errors'); |
| | | |
| | | foreach ($summary['critical'] as $error) { |
| | | $cardContent = '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>'; |
| | | $cardContent .= '<p><strong>Message:</strong></p>'; |
| | | $cardContent .= Emails::getInstance()->codeBlock($error->message); |
| | | |
| | | // Include context if available |
| | | if (!empty($error->context)) { |
| | | $context = json_decode($error->context, true); |
| | | if (is_array($context)) { |
| | | $cardContent .= '<p><strong>Context:</strong></p>'; |
| | | $cardContent .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); |
| | | } |
| | | } |
| | | |
| | | $body .= Emails::getInstance()->card($cardContent, $error->component . ': ' . $error->error_type); |
| | | } |
| | | } |
| | | |
| | | // Dashboard link |
| | | $admin_url = admin_url('admin.php?page=jvb-error-logs'); |
| | | $body .= Emails::getInstance()->spacer(30); |
| | | $body .= Emails::getInstance()->button($admin_url, 'View Detailed Logs'); |
| | | |
| | | return Emails::getInstance()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY'); |
| | | } |
| | | |
| | | /** |
| | | * Get HTML version of the error summary for nicer emails |
| | | * |
| | | * @param array $summary The error summary data |
| | | * @return string HTML formatted error summary |
| | | */ |
| | | protected function getHtmlErrorSummary(array $summary):string |
| | | { |
| | | $html = '<div style="font-family: sans-serif; max-width: 800px; margin: 0 auto;">'; |
| | | $html .= '<h1 style="color:#FF0080;border-bottom:2px solid #1B1B1B;padding-bottom:10px;">Error Summary</h1>'; |
| | | |
| | | // Frequent errors table |
| | | if (!empty($summary['frequent'])) { |
| | | $html .= '<h2>Most Frequent Errors</h2>'; |
| | | $html .= '<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">'; |
| | | $html .= '<tr style="background-color: #1B1B1B; color: #EFEFEF;">'; |
| | | $html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Component</th>'; |
| | | $html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Error Type</th>'; |
| | | $html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Message</th>'; |
| | | $html .= '<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Count</th>'; |
| | | $html .= '</tr>'; |
| | | |
| | | foreach ($summary['frequent'] as $error) { |
| | | $html .= '<tr>'; |
| | | $html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html($error->component) . '</td>'; |
| | | $html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html($error->error_type) . '</td>'; |
| | | $html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html(wp_trim_words($error->message, 10, '...')) . '</td>'; |
| | | $html .= '<td style="padding:8px;border:1px solid #ddd;text-align:center;"><strong>' . esc_html($error->count) . '</strong></td>'; |
| | | $html .= '</tr>'; |
| | | } |
| | | |
| | | $html .= '</table>'; |
| | | } |
| | | |
| | | // Critical errors section |
| | | if (!empty($summary['critical'])) { |
| | | $html .= '<h2>Recent Critical Errors</h2>'; |
| | | |
| | | foreach ($summary['critical'] as $error) { |
| | | $html .= '<div style="margin-bottom: 20px; padding: 15px; border-left: 4px solid #FF0080; background-color: #fff9fb;">'; |
| | | $html .= '<h3 style="margin-top: 0; color: #1B1B1B;">' . esc_html($error->component) . ': ' . esc_html($error->error_type) . '</h3>'; |
| | | $html .= '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>'; |
| | | $html .= '<p><strong>Message:</strong> ' . esc_html($error->message) . '</p>'; |
| | | |
| | | // Context display |
| | | if (!empty($error->context)) { |
| | | $context = json_decode($error->context, true); |
| | | if (is_array($context)) { |
| | | $html .= '<div style="margin-top: 10px;">'; |
| | | $html .= '<strong>Context:</strong>'; |
| | | $html .= '<pre style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow: auto; max-height: 200px;">'; |
| | | $html .= esc_html(json_encode($context, JSON_PRETTY_PRINT)); |
| | | $html .= '</pre>'; |
| | | $html .= '</div>'; |
| | | } |
| | | } |
| | | |
| | | $html .= '</div>'; |
| | | } |
| | | } |
| | | |
| | | // Dashboard link |
| | | $admin_url = admin_url('admin.php?page=jvb-error-logs'); |
| | | $html .= '<p style="margin-top: 30px; padding-top: 15px; border-top: 1px solid #ddd;">'; |
| | | $html .= '<a href="' . esc_url($admin_url) . '" style="display: inline-block; background-color: #FF0080; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px;">View All Errors in Dashboard</a>'; |
| | | $html .= '</p>'; |
| | | |
| | | $html .= '</div>'; |
| | | |
| | | return $html; |
| | | } |
| | | |
| | | public function renderAdminPage() |
| | | { |
| | | ?> |
| | | <h1>Error Logs</h1> |
| | | <?php |
| | | } |
| | | |
| | | public function getErrors(WP_REST_Request $request) { |
| | | $params = $this->buildParams($request); |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | protected function buildParams(WP_REST_Request $request):array { |
| | | $allowedSeverity = [ |
| | | 'all', |
| | | 'notice', |
| | | 'warning', |
| | | 'error', |
| | | 'critical', |
| | | ]; |
| | | $allowedComponents = [ |
| | | 'all', |
| | | //Get all components |
| | | ]; |
| | | $severity = $request->get_param('severity')??'all'; |
| | | $component = $request->get_param('component')??'all'; |
| | | |
| | | return [ |
| | | 'page' => (int)$request->get_param('page'), |
| | | 'severity' => (in_array($severity, $allowedSeverity)) ? $severity : 'all', |
| | | 'component' => (in_array($component, $allowedComponents)) ? $component : 'all', |
| | | ]; |
| | | } |
| | | } |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\Rest; |
| | |
| | | } |
| | | class Favourites |
| | | { |
| | | use Instance; |
| | | use Instance, ErrorHelper; |
| | | |
| | | protected CustomTable $favourites; |
| | | protected CustomTable $lists; |
| | |
| | | 'item_id' => $post_id |
| | | ])->deleteResults(); |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log('cleanupPostFavourites', $e->getMessage(), [ |
| | | $this->logError($e->getMessage(), 'cleanupPostFavourites',[ |
| | | 'post_id' => $post_id |
| | | ]); |
| | | } |
| | |
| | | 'item_id' => $term_id |
| | | ])->deleteResults(); |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log('cleanupTermFavourites', $e->getMessage(), [ |
| | | $this->logError($e->getMessage(), 'cleanupTermFavourites',[ |
| | | 'term_id' => $term_id, |
| | | 'taxonomy'=> $taxonomy |
| | | ]); |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\queue\executors\InvitationExecutor; |
| | | use JVBase\managers\queue\Queue; |
| | |
| | | |
| | | class InvitationsManager |
| | | { |
| | | use Instance; |
| | | use Instance, ErrorHelper; |
| | | protected array $inviteConfig; |
| | | protected CustomTable $table; |
| | | protected int $expiryDays = 14; |
| | |
| | | |
| | | } catch (Exception $e) { |
| | | $this->table->rollback(); |
| | | |
| | | Errors::getInstance()->log( |
| | | 'invitation_create', |
| | | 'Error processing invitations: ' . $e->getMessage(), |
| | | ['user_id' => $userID], |
| | | ); |
| | | $this->logError($e->getMessage(), 'processInvitations', ['user_id' => $userID]); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | |
| | | |
| | | use JetBrains\PhpStorm\NoReturn; |
| | | use JVBase\base\Site; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\integrations\services\Cloudflare; |
| | | use JVBase\meta\Form; |
| | |
| | | */ |
| | | class MagicLinks |
| | | { |
| | | use Instance; |
| | | use Instance, ErrorHelper; |
| | | protected Cache $cache; |
| | | protected Cache $referral_cache; |
| | | protected Cache $rateLimitCache; |
| | |
| | | #[NoReturn] protected function processSignup(array $token_data): void |
| | | { |
| | | if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) { |
| | | Errors::getInstance()->log('MagicLinkManager', 'Could not process Signup'); |
| | | $this->logError('Could not process signup', 'processSignup', ['token_data' => $token_data]); |
| | | |
| | | $this->cleanURL(); |
| | | } |
| | | $user_id = wp_create_user( |
| | |
| | | protected function processReferralSignup(array $token_data): void |
| | | { |
| | | if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) { |
| | | Errors::getInstance()->log('[MagicLinkManager]Could not process Referral Signup'); |
| | | $this->logError('Could not process referral signup', 'processReferralSignup', ['token_data' => $token_data]); |
| | | return; |
| | | } |
| | | |
| | |
| | | exit; |
| | | } |
| | | } else { |
| | | Errors::getInstance()->log( |
| | | '[MagicLinkManager]', |
| | | $user_id->get_error_message(), |
| | | $token_data |
| | | ); |
| | | $this->logError($user_id->get_error_message(), 'processReferralSignup'); |
| | | |
| | | $this->cleanURL(); |
| | | } |
| | | } |
| | |
| | | $user = get_user_by('ID', $token_data['user_id']); |
| | | |
| | | if (!$user) { |
| | | Errors::getInstance()->log( |
| | | 'MagicLinkManager', |
| | | 'Invalid user, could not reset password', |
| | | ); |
| | | $this->logError('Invalid user - could not reset password', 'processPasswordReset', ['token_data' => $token_data]); |
| | | $this->cleanURL(); |
| | | } |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\routes\ResponseRoutes; |
| | |
| | | */ |
| | | class Responses |
| | | { |
| | | use Instance; |
| | | use Instance, ErrorHelper; |
| | | protected CustomTable $responses; |
| | | protected KarmaManager $karma; |
| | | protected ResponseRoutes $route; |
| | |
| | | ], [ |
| | | 'user_id' => $user_id, |
| | | ]); |
| | | |
| | | Errors::getInstance()->log( |
| | | 'Responses', |
| | | 'Anonymized responses for deleted user', |
| | | $this->logError('Anonymized responses for deleted user','handleUserDeletion', |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'count' => $updated, |
| | | ], |
| | | 'info' |
| | | ); |
| | | ]); |
| | | } |
| | | |
| | | protected function getNotifyableUsers(int $id):array |
| | |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use Exception; |
| | | use Imagick; |
| | |
| | | */ |
| | | class UploadManager |
| | | { |
| | | use Instance; |
| | | use Instance, ErrorHelper; |
| | | protected array $allowedTypes = [ |
| | | // Images |
| | | 'image/jpeg' => 'image', |
| | |
| | | 'stored_at' => current_time('mysql') |
| | | ]; |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log('UploadManager','Failed to store uploaded file: ' . print_r($e->getMessage(), true)); |
| | | $this->logError('Failed to store uploaded file', 'secureUploadedFile', ['error' => $e->getMessage()]); |
| | | return []; |
| | | } |
| | | } |
| | |
| | | }; |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | '[UploadManager]:processUpload', |
| | | $e->getMessage(), |
| | | ['temp_path' => $temp_path, 'context' => $context] |
| | | ); |
| | | $this->logError($e->getMessage(), 'processUpload'); |
| | | |
| | | return new WP_Error('upload_failed', $e->getMessage()); |
| | | } |
| | |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log('Uploader', 'Image conversion failed: ' . print_r($e->getMessage(), true)); |
| | | $this->logError($e->getMessage(), 'processImage'); |
| | | // Fallback to original |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | | $final_path = "{$full_dir}/{$filename}.{$ext}"; |
| | |
| | | try { |
| | | $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id); |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log('Uploader', 'Attachment creation failed: '.print_r($e->getMessage(), true)); |
| | | $this->logError($e->getMessage(), 'processVideo'); |
| | | return ['success' => false]; |
| | | } |
| | | |
| | |
| | | try { |
| | | $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id); |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log('Uploader','Conversion failed: '.print_r($e->getMessage(), true)); |
| | | $this->logError($e->getMessage(), 'processDocument'); |
| | | return ['success' => false]; |
| | | } |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | } |
| | | class UserTermsManager |
| | | { |
| | | use Instance; |
| | | use Instance, ErrorHelper; |
| | | protected CustomTable $index; |
| | | |
| | | private function __construct() |
| | |
| | | 'result' => $results |
| | | ]; |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | '[UserTermsManager]:processOperation', |
| | | "Exception during operation processing: " . $e->getMessage(), |
| | | [ |
| | | 'operation' => $operation->id, |
| | | ] |
| | | ); |
| | | $this->logError($e->getMessage(), 'processOperation'); |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\base\Site; |
| | | require(JVB_DIR . '/inc/managers/ScriptLoader.php'); |
| | | require(JVB_DIR . '/inc/managers/Errors.php'); |
| | | require(JVB_DIR . '/inc/managers/CustomTable.php'); |
| | | //require(JVB_DIR . '/inc/managers/CacheManager.php'); |
| | | require(JVB_DIR . '/inc/managers/Cache.php'); |
| | |
| | | } |
| | | |
| | | use Exception; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\AdminPages; |
| | | use JVBase\managers\Errors; |
| | |
| | | |
| | | class Queue |
| | | { |
| | | use Instance; |
| | | use Instance, ErrorHelper; |
| | | private Storage $storage; |
| | | private Processor $processor; |
| | | private TypeRegistry $registry; |
| | |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log('queue', $e->getMessage(), $data, 'high'); |
| | | $this->logError($e->getMessage(), 'add', $data); |
| | | |
| | | return new WP_Error('queue_failed', $e->getMessage()); |
| | | } |
| | | } |
| | |
| | | |
| | | use DateTime; |
| | | use JVBase\managers\queue\{Executor, Operation, Progress, Result}; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\managers\Errors; |
| | | use JVBase\managers\Roles; |
| | | use JVBase\meta\Meta; |
| | |
| | | */ |
| | | final class ContentExecutor implements Executor |
| | | { |
| | | use ErrorHelper; |
| | | private const HANDLED_TYPES = [ |
| | | 'content_update', |
| | | // 'batch_creation', |
| | |
| | | }; |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | '[ContentExecutor]:execute', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type, |
| | | 'user_id' => $operation->userId, |
| | | ] |
| | | ); |
| | | $this->logError($e->getMessage(), 'execute', [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type, |
| | | 'user_id' => $operation->userId, |
| | | ]); |
| | | |
| | | return new Result( |
| | | outcome: 'failed', |
| | |
| | | |
| | | use JVBase\managers\queue\{Executor, Operation, Progress, Result}; |
| | | use Exception; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\integrations\ServiceMap; |
| | | use JVBase\managers\Errors; |
| | | |
| | |
| | | */ |
| | | final class IntegrationExecutor implements Executor |
| | | { |
| | | use ServiceMap; |
| | | use ServiceMap, ErrorHelper; |
| | | |
| | | |
| | | public function execute(Operation $operation, Progress $progress): Result |
| | |
| | | }; |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | '[IntegrationExecutor]:execute', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type, |
| | | 'user_id' => $operation->userId, |
| | | ] |
| | | ); |
| | | $this->logError($e->getMessage(),'execute', [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type, |
| | | 'user_id' => $operation->userId, |
| | | ]); |
| | | |
| | | return new Result( |
| | | outcome: 'failed', |
| | |
| | | namespace JVBase\managers\queue\executors; |
| | | |
| | | use JVBase\managers\queue\{Executor, Operation, Progress, Queue, Result}; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Errors; |
| | | use JVBase\managers\UploadManager; |
| | |
| | | */ |
| | | final class UploadExecutor implements Executor |
| | | { |
| | | use ErrorHelper; |
| | | private const HANDLED_TYPES = [ |
| | | 'image_upload', |
| | | 'video_upload', |
| | |
| | | }; |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | '[UploadExecutor]:execute', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type, |
| | | 'user_id' => $operation->userId, |
| | | ] |
| | | ); |
| | | $this->logError($e->getMessage(), 'execute', [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type, |
| | | 'user_id' => $operation->userId, |
| | | ]); |
| | | |
| | | return new Result( |
| | | outcome: 'failed', |
| | |
| | | namespace JVBase\meta; |
| | | |
| | | use Exception; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Errors; |
| | | use wpdb; |
| | |
| | | */ |
| | | class Storage |
| | | { |
| | | use ErrorHelper; |
| | | protected wpdb $wpdb; |
| | | |
| | | public function __construct() |
| | |
| | | |
| | | } catch (Exception $e) { |
| | | $this->wpdb->query('ROLLBACK'); |
| | | Errors::getInstance()->log('meta_storage', $e->getMessage(), [ |
| | | 'item_id' => $item->id, |
| | | 'object_type' => $item->objectType, |
| | | 'fields' => array_keys($dirty) |
| | | ]); |
| | | $this->logError($e->getMessage(), 'save'); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | |
| | | $results[$meta->item()->id ?? 'options'] = false; |
| | | } |
| | | |
| | | Errors::getInstance()->log('meta_storage', 'Bulk save failed: ' . $e->getMessage()); |
| | | self::staticLogError($e->getMessage(), 'save'); |
| | | |
| | | return $results; |
| | | } |
| | |
| | | namespace JVBase\registrar; |
| | | |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\managers\Errors; |
| | | use WP_Post; |
| | | |
| | |
| | | exit; |
| | | } |
| | | final class Posts { |
| | | use ErrorHelper; |
| | | public string $postType; |
| | | public string $singular; |
| | | public string $plural; |
| | |
| | | } |
| | | |
| | | if (is_wp_error($registered)) { |
| | | Errors::getInstance()->log('JVBase\registrar\Posts', 'Could not register post type', $registered->get_error_messages()); |
| | | $this->logError('Could not register post type', 'register', $registered->get_error_messages()); |
| | | } |
| | | } |
| | | |
| | |
| | |
|
| | | $schema = is_array($rawSchema) ? $this->schema($rawSchema) : $rawSchema;
|
| | |
|
| | | $default = $config['type'] === 'repeater' ?
|
| | | $this->determineDefault($schema['schema']['properties']??[]) :
|
| | | $config['default'] ?? MetaTypeManager::getType($config['type'])['default'] ?? '';
|
| | |
|
| | | register_post_meta($this->registrar->getBased(), jvbCheckBase($name), [
|
| | | 'single' => true,
|
| | | 'type' => $metaType,
|
| | | 'default' => $config['default'] ?? MetaTypeManager::getType($config['type'])['default'] ?? '',
|
| | | 'default' => $default,
|
| | | 'sanitize_callback' => fn($value) => (Sanitizer::sanitize($value, $config)),
|
| | | 'auth_callback' => fn($allowed, $key, $postID) => current_user_can('edit_post', $postID),
|
| | | 'show_in_rest' => $schema,
|
| | |
| | | {
|
| | |
|
| | | }
|
| | |
|
| | | protected function determineDefault(array $properties):array
|
| | | {
|
| | | $out = [];
|
| | | foreach ($properties as $prop => $value) {
|
| | | $out[$prop] = match ($value) {
|
| | | 'object' => [],
|
| | | default => ''
|
| | | };
|
| | | }
|
| | | return $out;
|
| | | }
|
| | | }
|
| | |
| | | <?php |
| | | namespace JVBase\rest; |
| | | |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\integrations\services\Cloudflare; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\Errors; |
| | |
| | | */ |
| | | abstract class Rest |
| | | { |
| | | use ErrorHelper; |
| | | protected string $namespace = 'jvb/v1'; |
| | | protected ?Cache $cache = null; |
| | | protected string $cacheName = ''; |
| | |
| | | ]); |
| | | |
| | | try { |
| | | Errors::getInstance()->log('security_audit', $event, $context, 'info'); |
| | | $this->logError($event,'auditLog', $context); |
| | | } catch (Exception $e) { |
| | | error_log("Security Audit: {$event} - " . json_encode($context)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Log errors with proper context |
| | | */ |
| | | protected function logError(string $message, array $context = [], string $severity = 'error'): void |
| | | { |
| | | try { |
| | | Errors::getInstance()->log(static::class, $message, $context, $severity); |
| | | } catch (Exception $e) { |
| | | error_log(static::class . " Error: {$message} - " . json_encode($context)); |
| | | } |
| | | } |
| | | |
| | | /************************************************************ |
| | | SESSION FINGERPRINT |
| | | ************************************************************/ |
| | |
| | | |
| | | return $this->error('Failed to create request'); |
| | | } catch (Exception $e) { |
| | | $this->logError('createRequest', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(),'createRequest', [ |
| | | 'user_id' => $userID, |
| | | 'term_id' => $termID |
| | | ]); |
| | |
| | | return $this->success(['message' => 'Request ' . $status]); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('processRequest', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(), 'processRequest', [ |
| | | 'request_id' => $requestID, |
| | | 'action' => $action |
| | | ]); |
| | |
| | | return $this->error('Failed to update permissions'); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('updatePermissions', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(),'updatePermissions', [ |
| | | 'term_id' => $termID, |
| | | 'target_user' => $targetUserID, |
| | | 'role' => $role |
| | |
| | | // Determine severity based on error type |
| | | $severity = $this->getSeverityFromType($error_type); |
| | | |
| | | Errors::getInstance()->log( |
| | | $contextData['component'] ?? 'client-js', |
| | | Errors::log( |
| | | $message, |
| | | $contextData, |
| | | $severity |
| | | ); |
| | | |
| | | return Response::success(['message'=>'Error logged']); |
| | |
| | | return ['added' => $added, 'errors' => $errors]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('addItemsToList', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(),'addItemsToList', [ |
| | | 'user_id' => $user_id, |
| | | 'list_id' => $list_id, |
| | | ]); |
| | |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('getListDetails', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(), 'getListDetails', [ |
| | | 'user_id' => $user_id, |
| | | 'list_id' => $list_id |
| | | ]); |
| | |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('updateList', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(),'updateList', [ |
| | | 'user_id' => $user_id, |
| | | 'list_id' => $list_id |
| | | ]); |
| | |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('removeFromList', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(), 'removeFromList', [ |
| | | 'user_id' => $user_id, |
| | | 'list_id' => $list_id |
| | | ]); |
| | |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('unshareList', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(),'unshareList', [ |
| | | 'user_id' => $user_id, |
| | | 'list_id' => $list_id, |
| | | 'email' => $email |
| | |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('acceptListInvitation', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(),'acceptListInvitation', [ |
| | | 'token' => $token, |
| | | 'email' => $email |
| | | ]); |
| | |
| | | return $this->success($result); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('Form submission error', [ |
| | | $this->logError('Form submission error', 'submitForm', [ |
| | | 'message' => $e->getMessage(), |
| | | 'trace' => $e->getTraceAsString() |
| | | ]); |
| | |
| | | $results = $importer->importFromCSV($file['tmp_name'], $options); |
| | | |
| | | if (is_wp_error($results)) { |
| | | $this->logError('Client import failed', [ |
| | | $this->logError('Client import failed','importClients', [ |
| | | 'error' => $results->get_error_message(), |
| | | 'file' => $file['name'] |
| | | ]); |
| | |
| | | $results = $importer->importFromCSV($file['tmp_name'], $options); |
| | | |
| | | if (is_wp_error($results)) { |
| | | $this->logError('Sales import failed', [ |
| | | $this->logError('Sales import failed', 'importSales', [ |
| | | 'error' => $results->get_error_message(), |
| | | 'file' => $file['name'] |
| | | ]); |
| | |
| | | return $this->success($result); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('Helcim checkout init failed', ['error' => $e->getMessage()]); |
| | | $this->logError('Helcim checkout init failed','handleInitializeCheckout', ['error' => $e->getMessage()]); |
| | | return $this->error($e->getMessage()); |
| | | } |
| | | } |
| | |
| | | |
| | | $theUserID = current_user_can('manage_options') ? null : get_current_user_id(); |
| | | if ($theUserID === false) { |
| | | error_log('User not logged in'); |
| | | return $this->validationError(['message' => 'Not logged in']); |
| | | } |
| | | $integration = $this->getService($service); |
| | | if (!method_exists($integration, 'switchUser')) { |
| | | return $this->validationError(['message' => 'Cannot get user integration']); |
| | | } |
| | | $integration = $this->getServiceUser($service, $theUserID); |
| | | |
| | | if (!$integration || !method_exists($integration, 'processAction')) { |
| | | error_log('No processAction method'); |
| | | return $this->validationError(['message'=>'Invalid service']); |
| | | } |
| | | |
| | | $integration->switchUser($theUserID); |
| | | |
| | | $integration->getCredentials(); |
| | | |
| | | // Handle the action |
| | |
| | | return $this->success($result); |
| | | |
| | | } catch (Exception $e) { |
| | | error_log(print_r($e->getMessage(), true)); |
| | | return $this->error($e->getMessage()); |
| | | } |
| | | } |
| | |
| | | return $this->success($result); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('Payment processing failed', [ |
| | | $this->logError('Payment processing failed', 'handlePaymentProcessing',[ |
| | | 'error' => $e->getMessage(), |
| | | 'idempotency_key' => $data['idempotency_key'] |
| | | ]); |
| | |
| | | ); |
| | | |
| | | if (is_wp_error($user_id)) { |
| | | $this->logError('Registration failed', [ |
| | | $this->logError('Registration failed', 'handleRegister', [ |
| | | 'email' => $email, |
| | | 'error' => $user_id->get_error_message(), |
| | | ]); |
| | |
| | | $result = MagicLinks::getInstance()->sendMagicLink($email, 'reset'); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $this->logError('Magic link send failed', [ |
| | | $this->logError('Magic link send failed', 'handleLostPassword',[ |
| | | 'email' => $email, |
| | | 'error' => $result->get_error_message(), |
| | | ]); |
| | |
| | | // Standard WordPress password reset |
| | | $key = get_password_reset_key($user); |
| | | if (is_wp_error($key)) { |
| | | $this->logError('Reset key generation failed', [ |
| | | $this->logError('Reset key generation failed', 'handleLostPassword', [ |
| | | 'email' => $email, |
| | | 'error' => $key->get_error_message(), |
| | | ]); |
| | |
| | | $result = MagicLinks::getInstance()->sendMagicLink($email, $type, $context); |
| | | |
| | | if (is_wp_error($result)) { |
| | | $this->logError('Magic link send failed', [ |
| | | $this->logError('Magic link send failed', 'handleMagicLink', [ |
| | | 'email' => $email, |
| | | 'type' => $type, |
| | | 'error' => $result->get_error_message(), |
| | |
| | | try { |
| | | Referrals::getInstance()->processReferralCode($user_id, $referral_code); |
| | | } catch (Exception $e) { |
| | | $this->logError('Referral processing failed', [ |
| | | $this->logError('Referral processing failed', 'processReferralCode', [ |
| | | 'user_id' => $user_id, |
| | | 'code' => $referral_code, |
| | | 'error' => $e->getMessage(), |
| | | ], 'warning'); |
| | | ]); |
| | | } |
| | | } |
| | | |
| | |
| | | } catch (Exception $e) { |
| | | global $wpdb; |
| | | $wpdb->query('ROLLBACK'); |
| | | $this->logError('markContentNotificationRead exception', [ |
| | | $this->logError($e->getMessage(),'markContentNotificationRead', [ |
| | | 'seen_id' => $seen_id, |
| | | 'user_id' => $user_id, |
| | | 'error' => $e->getMessage() |
| | | ]); |
| | | return [ |
| | | 'success' => false, |
| | |
| | | ]); |
| | | } catch (Exception $e) { |
| | | // Don't fail the request if metrics tracking fails |
| | | $this->logError('trackMetrics failed', [ |
| | | 'error' => $e->getMessage(), |
| | | $this->logError($e->getMessage(),'trackMetrics', [ |
| | | 'user_id' => $user_id, |
| | | 'action' => $action |
| | | ]); |
| | |
| | | ], 201); // 201 Created |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | 'terms', |
| | | $this->logError( |
| | | 'Term creation failed: ' . $e->getMessage(), |
| | | 'createTermRequest', |
| | | ['user_id' => $user_id, 'taxonomy' => $taxonomy, 'name' => $name] |
| | | ); |
| | | |
| | |
| | | $secured_files = $this->secureFiles($files, $args); |
| | | |
| | | if (empty($secured_files)) { |
| | | $this->logError('No valid files to upload'); |
| | | $this->logError('No valid files to upload', 'handleUpload'); |
| | | return $this->error('No valid files to upload'); |
| | | } |
| | | |
| | |
| | | |
| | | } catch (Exception $e) { |
| | | // Error handling... |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleUploadRequest', |
| | | $e->getMessage(), |
| | | [ |
| | | 'request_data' => $request->get_params(), |
| | | 'files_info' => $this->getFilesInfo($_FILES), |
| | | 'trace' => $e->getTraceAsString() |
| | | ] |
| | | ); |
| | | $this->logError($e->getMessage(), 'handleUpload'); |
| | | return $this->error($e->getMessage()); |
| | | } |
| | | } |
| | |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleMetadataUpdate', |
| | | $e->getMessage(), |
| | | [ |
| | | 'request_data' => $request->get_params(), |
| | | 'trace' => $e->getTraceAsString() |
| | | ] |
| | | ); |
| | | $this->logError($e->getMessage(), 'handleMetadataUpdate'); |
| | | |
| | | return $this->sendResponse( |
| | | false, |
| | |
| | | |
| | | return $this->queued($ID['operation_id'], 'Files uploaded and posts queued for creation'); |
| | | } catch (Exception $e) { |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleGroupingRequest', |
| | | $e->getMessage(), |
| | | [ |
| | | 'request_data' => $request->get_params(), |
| | | 'trace' => $e->getTraceAsString() |
| | | ] |
| | | ); |
| | | $this->logError($e->getMessage(), 'handleGroupingRequest'); |
| | | |
| | | return $this->error('Grouping operation failed: '.$e->getMessage()); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Cache; |
| | |
| | | require(JVB_DIR.'/inc/Instance.php'); |
| | | require(JVB_DIR.'/base/_setup.php'); |
| | | |
| | | require(JVB_DIR . '/inc/managers/Errors.php'); |
| | | require(JVB_DIR . '/inc/managers/ErrorHelper.php'); |
| | | |
| | | require(JVB_DIR.'/inc/utility/setup.php'); |
| | | require(JVB_DIR.'/checks.php'); |
| | | |
| | |
| | | use JVBase\rest\routes\ErrorRoutes; |
| | | use JVBase\rest\routes\FeedRoutes; |
| | | use JVBase\rest\routes\FormRoutes; |
| | | use JVBase\rest\routes\IntegrationsRoutes; |
| | | use JVBase\rest\routes\NewsRoutes; |
| | | use JVBase\rest\routes\OptionsRoutes; |
| | | use JVBase\rest\routes\SEORoutes; |
| | | use JVBase\rest\routes\SettingsRoutes; |
| | | use JVBase\rest\routes\TermRoutes; |
| | | use JVBase\rest\routes\UploadRoutes; |
| | | use JVBase\rest\routes\UserRoutes; |
| | | |
| | | function jvbLoadRoutes():void |
| | | { |
| | |
| | | } |
| | | UserRoutes::getInstance(); |
| | | |
| | | if (Site::hasAnyIntegration()) { |
| | | IntegrationsRoutes::getInstance(); |
| | | } |
| | | |
| | | if (Site::has('feed_block')) { |
| | | FeedRoutes::getInstance(); |
| | | } |