Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7
wordpress-plugin/openpanel/LICENSE
Normal file
7
wordpress-plugin/openpanel/LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||
(Full text available at https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt)
|
||||
2
wordpress-plugin/openpanel/index.php
Normal file
2
wordpress-plugin/openpanel/index.php
Normal file
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Silence is golden.
|
||||
560
wordpress-plugin/openpanel/openpanel.php
Normal file
560
wordpress-plugin/openpanel/openpanel.php
Normal file
@@ -0,0 +1,560 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: OpenPanel
|
||||
* Description: Activate OpenPanel to start tracking your website. Supports both Cloud and Self-Hosted instances.
|
||||
* Version: 1.1.1
|
||||
* Author: OpenPanel / Airano
|
||||
* License: GPLv2 or later
|
||||
* Requires at least: 5.8
|
||||
* Requires PHP: 7.4
|
||||
* Tested up to: 6.8
|
||||
* Text Domain: openpanel
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) { exit; }
|
||||
|
||||
final class OP_WP_Proxy {
|
||||
const VERSION = '1.1.1';
|
||||
const OPTION_KEY = 'op_wp_proxy_settings';
|
||||
const TRANSIENT_JS = 'op_wp_op1_js';
|
||||
const REST_NS = 'openpanel';
|
||||
const REST_ROUTE = '/(?P<path>.*)'; // wildcard path passthrough
|
||||
const CACHE_TIMEOUT = WEEK_IN_SECONDS;
|
||||
|
||||
// Cloud defaults
|
||||
const CLOUD_JS_URL = 'https://openpanel.dev/op1.js';
|
||||
const CLOUD_API_BASE = 'https://api.openpanel.dev/';
|
||||
|
||||
public function __construct() {
|
||||
add_action('admin_init', [$this, 'register_settings']);
|
||||
add_action('admin_menu', [$this, 'add_settings_page']);
|
||||
add_action('wp_enqueue_scripts', [$this, 'inject_inline_sdk'], 0);
|
||||
add_action('rest_api_init', [$this, 'register_proxy_route']);
|
||||
add_action('admin_init', [$this, 'handle_cache_clear']);
|
||||
}
|
||||
|
||||
/** ---------------- Settings ---------------- */
|
||||
public function register_settings() {
|
||||
register_setting(self::OPTION_KEY, self::OPTION_KEY, [
|
||||
'type' => 'array',
|
||||
'show_in_rest' => false,
|
||||
'sanitize_callback' => function($input) {
|
||||
$out = [];
|
||||
$out['hosting_mode'] = isset($input['hosting_mode']) && $input['hosting_mode'] === 'selfhosted' ? 'selfhosted' : 'cloud';
|
||||
$out['client_id'] = isset($input['client_id']) ? sanitize_text_field($input['client_id']) : '';
|
||||
$out['api_url'] = isset($input['api_url']) ? esc_url_raw(untrailingslashit($input['api_url'])) : '';
|
||||
$out['dashboard_url'] = isset($input['dashboard_url']) ? esc_url_raw(untrailingslashit($input['dashboard_url'])) : '';
|
||||
$out['track_screen'] = !empty($input['track_screen']) ? 1 : 0;
|
||||
$out['track_outgoing'] = !empty($input['track_outgoing']) ? 1 : 0;
|
||||
$out['track_attributes'] = !empty($input['track_attributes']) ? 1 : 0;
|
||||
return $out;
|
||||
}
|
||||
]);
|
||||
|
||||
// Section: Hosting Mode
|
||||
add_settings_section('op_hosting', __('Hosting Mode', 'openpanel'), function() {
|
||||
echo '<p>' . esc_html__('Choose between OpenPanel Cloud or your own Self-Hosted instance.', 'openpanel') . '</p>';
|
||||
}, self::OPTION_KEY);
|
||||
|
||||
add_settings_field('hosting_mode', __('Mode', 'openpanel'), function() {
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
|
||||
?>
|
||||
<label>
|
||||
<input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="cloud" <?php checked($mode, 'cloud'); ?> class="op-hosting-mode">
|
||||
<?php esc_html_e('Cloud (openpanel.dev)', 'openpanel'); ?>
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="selfhosted" <?php checked($mode, 'selfhosted'); ?> class="op-hosting-mode">
|
||||
<?php esc_html_e('Self-Hosted', 'openpanel'); ?>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e('Select Self-Hosted if you run your own OpenPanel instance.', 'openpanel'); ?></p>
|
||||
<?php
|
||||
}, self::OPTION_KEY, 'op_hosting');
|
||||
|
||||
// Section: Self-Hosted URLs
|
||||
add_settings_section('op_selfhosted', __('Self-Hosted Settings', 'openpanel'), function() {
|
||||
echo '<p>' . esc_html__('Configure your Self-Hosted OpenPanel URLs. Only required if using Self-Hosted mode.', 'openpanel') . '</p>';
|
||||
}, self::OPTION_KEY);
|
||||
|
||||
add_settings_field('api_url', __('API URL', 'openpanel'), function() {
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
printf('<input type="url" name="%s[api_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://api.openpanel.yourdomain.com"/>',
|
||||
esc_attr(self::OPTION_KEY),
|
||||
isset($opts['api_url']) ? esc_attr($opts['api_url']) : ''
|
||||
);
|
||||
echo '<p class="description">' . esc_html__('Your OpenPanel API endpoint (e.g., https://api.openpanel.yourdomain.com)', 'openpanel') . '</p>';
|
||||
}, self::OPTION_KEY, 'op_selfhosted');
|
||||
|
||||
add_settings_field('dashboard_url', __('Dashboard URL', 'openpanel'), function() {
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
printf('<input type="url" name="%s[dashboard_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://openpanel.yourdomain.com"/>',
|
||||
esc_attr(self::OPTION_KEY),
|
||||
isset($opts['dashboard_url']) ? esc_attr($opts['dashboard_url']) : ''
|
||||
);
|
||||
echo '<p class="description">' . esc_html__('Your OpenPanel Dashboard URL — used to load op1.js from your server instead of the CDN (e.g., https://openpanel.yourdomain.com)', 'openpanel') . '</p>';
|
||||
}, self::OPTION_KEY, 'op_selfhosted');
|
||||
|
||||
// Section: Main Settings
|
||||
add_settings_section('op_main', __('OpenPanel Settings', 'openpanel'), function() {
|
||||
echo '<p>' . esc_html__('Set your OpenPanel Client ID. The SDK and requests are served from your domain to avoid ad blockers.', 'openpanel') . '</p>';
|
||||
}, self::OPTION_KEY);
|
||||
|
||||
add_settings_field('client_id', __('Client ID', 'openpanel'), function() {
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
printf('<input type="text" name="%s[client_id]" value="%s" class="regular-text" placeholder="op_client_..."/>',
|
||||
esc_attr(self::OPTION_KEY),
|
||||
isset($opts['client_id']) ? esc_attr($opts['client_id']) : ''
|
||||
);
|
||||
}, self::OPTION_KEY, 'op_main');
|
||||
|
||||
add_settings_field('toggles', __('Auto-tracking (optional)', 'openpanel'), function() {
|
||||
$o = get_option(self::OPTION_KEY);
|
||||
// Default track_screen to true if not set
|
||||
$track_screen = isset($o['track_screen']) ? !empty($o['track_screen']) : true;
|
||||
?>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_screen]" <?php checked($track_screen); ?>> <?php esc_html_e('Track page views automatically', 'openpanel'); ?></label><br>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_outgoing]" <?php checked(!empty($o['track_outgoing'])); ?>> <?php esc_html_e('Track clicks on outgoing links', 'openpanel'); ?></label><br>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_attributes]" <?php checked(!empty($o['track_attributes'])); ?>> <?php esc_html_e('Track additional page attributes', 'openpanel'); ?></label>
|
||||
<?php
|
||||
}, self::OPTION_KEY, 'op_main');
|
||||
}
|
||||
|
||||
public function add_settings_page() {
|
||||
add_options_page(
|
||||
'OpenPanel',
|
||||
'OpenPanel',
|
||||
'manage_options',
|
||||
'op-wp-proxy',
|
||||
[$this, 'render_settings_page']
|
||||
);
|
||||
}
|
||||
|
||||
public function handle_cache_clear() {
|
||||
if (isset($_POST['op_clear_cache']) && current_user_can('manage_options')) {
|
||||
if (isset($_POST['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'op_clear_cache_nonce')) {
|
||||
delete_transient(self::TRANSIENT_JS);
|
||||
add_action('admin_notices', function() {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' .
|
||||
esc_html__('OpenPanel cache cleared successfully. The latest op1.js will be fetched on the next page load.', 'openpanel') .
|
||||
'</p></div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function render_settings_page() {
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>OpenPanel</h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields(self::OPTION_KEY);
|
||||
do_settings_sections(self::OPTION_KEY);
|
||||
submit_button(__('Save Settings', 'openpanel'));
|
||||
?>
|
||||
</form>
|
||||
|
||||
<hr style="margin: 2rem 0;">
|
||||
|
||||
<h2><?php esc_html_e('Cache Management', 'openpanel'); ?></h2>
|
||||
<p><?php esc_html_e('Clear the cached op1.js file to force fetch the latest version from OpenPanel.', 'openpanel'); ?></p>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('op_clear_cache_nonce'); ?>
|
||||
<input type="submit" name="op_clear_cache" class="button button-secondary" value="<?php esc_attr_e('Clear Cache & Force Refresh', 'openpanel'); ?>">
|
||||
</form>
|
||||
|
||||
<?php
|
||||
// Check if transient exists and get expiration info
|
||||
$cached_js = get_transient(self::TRANSIENT_JS);
|
||||
$timeout_option = '_transient_timeout_' . self::TRANSIENT_JS;
|
||||
$cached_time = get_option($timeout_option);
|
||||
|
||||
if ($cached_js !== false && $cached_time) {
|
||||
$time_remaining = $cached_time - time();
|
||||
if ($time_remaining > 0) {
|
||||
echo '<p style="margin-top:1rem;color:#666;">' .
|
||||
/* translators: %s: human readable time difference */
|
||||
sprintf(esc_html__('Cache expires in %s', 'openpanel'), esc_html(human_time_diff(time(), $cached_time))) .
|
||||
'</p>';
|
||||
} else {
|
||||
echo '<p style="margin-top:1rem;color:#666;">' .
|
||||
esc_html__('Cache has expired and will refresh on next page load.', 'openpanel') .
|
||||
'</p>';
|
||||
}
|
||||
} else {
|
||||
echo '<p style="margin-top:1rem;color:#666;">' .
|
||||
esc_html__('No cached version found. op1.js will be fetched on next page load.', 'openpanel') .
|
||||
'</p>';
|
||||
}
|
||||
?>
|
||||
|
||||
<p style="margin-top:1rem;color:#666;">
|
||||
<?php esc_html_e('The plugin fetches and inlines op1.js (cached for 1 week). If fetching fails, it falls back to the CDN script.', 'openpanel'); ?>
|
||||
</p>
|
||||
|
||||
<hr style="margin: 2rem 0;">
|
||||
|
||||
<h2><?php esc_html_e('Current Configuration', 'openpanel'); ?></h2>
|
||||
<table class="widefat" style="max-width: 600px;">
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e('Mode', 'openpanel'); ?></strong></td>
|
||||
<td><?php echo esc_html($mode === 'selfhosted' ? 'Self-Hosted' : 'Cloud'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e('API URL', 'openpanel'); ?></strong></td>
|
||||
<td><code><?php echo esc_html($this->get_api_base()); ?></code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e('JS URL', 'openpanel'); ?></strong></td>
|
||||
<td><code><?php echo esc_html($this->get_js_url()); ?></code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e('Proxy Endpoint', 'openpanel'); ?></strong></td>
|
||||
<td><code><?php echo esc_html(rest_url(self::REST_NS . '/')); ?></code></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.op-selfhosted-section { transition: opacity 0.3s; }
|
||||
.op-selfhosted-section.hidden { opacity: 0.5; pointer-events: none; }
|
||||
</style>
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
function toggleSelfHostedFields() {
|
||||
var mode = $('input[name="<?php echo esc_js(self::OPTION_KEY); ?>[hosting_mode]"]:checked').val();
|
||||
var $selfHostedFields = $('.op-selfhosted-field').closest('tr');
|
||||
var $selfHostedSection = $selfHostedFields.closest('table');
|
||||
|
||||
if (mode === 'selfhosted') {
|
||||
$selfHostedFields.show();
|
||||
} else {
|
||||
$selfHostedFields.hide();
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelfHostedFields();
|
||||
$('input[name="<?php echo esc_js(self::OPTION_KEY); ?>[hosting_mode]"]').on('change', toggleSelfHostedFields);
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/** ---------------- Helper Methods ---------------- */
|
||||
|
||||
/**
|
||||
* Get the API base URL based on hosting mode
|
||||
*/
|
||||
private function get_api_base() {
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
|
||||
|
||||
if ($mode === 'selfhosted' && !empty($opts['api_url'])) {
|
||||
return trailingslashit($opts['api_url']);
|
||||
}
|
||||
|
||||
return self::CLOUD_API_BASE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JS URL based on hosting mode
|
||||
* Cloud: loads from official CDN (openpanel.dev)
|
||||
* Self-hosted: loads from dashboard URL if configured, otherwise falls back to CDN
|
||||
*/
|
||||
private function get_js_url() {
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
|
||||
|
||||
if ($mode === 'selfhosted' && !empty($opts['dashboard_url'])) {
|
||||
return trailingslashit($opts['dashboard_url']) . 'op1.js';
|
||||
}
|
||||
|
||||
return self::CLOUD_JS_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed hosts for proxy validation
|
||||
*/
|
||||
private function get_allowed_hosts() {
|
||||
$hosts = [
|
||||
'api.openpanel.dev',
|
||||
'openpanel.dev'
|
||||
];
|
||||
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
|
||||
|
||||
if ($mode === 'selfhosted') {
|
||||
if (!empty($opts['api_url'])) {
|
||||
$parsed = wp_parse_url($opts['api_url']);
|
||||
if (isset($parsed['host'])) {
|
||||
$hosts[] = $parsed['host'];
|
||||
}
|
||||
}
|
||||
if (!empty($opts['dashboard_url'])) {
|
||||
$parsed = wp_parse_url($opts['dashboard_url']);
|
||||
if (isset($parsed['host'])) {
|
||||
$hosts[] = $parsed['host'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($hosts);
|
||||
}
|
||||
|
||||
/** ---------------- Inline SDK ---------------- */
|
||||
public function inject_inline_sdk() {
|
||||
if (is_admin()) return;
|
||||
|
||||
$opts = get_option(self::OPTION_KEY);
|
||||
$clientId = isset($opts['client_id']) ? trim($opts['client_id']) : '';
|
||||
if ($clientId === '') return; // don't inject if not configured
|
||||
|
||||
// For self-hosted, also require API URL to be configured
|
||||
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
|
||||
if ($mode === 'selfhosted' && empty($opts['api_url'])) {
|
||||
return; // don't inject if self-hosted but not configured
|
||||
}
|
||||
|
||||
$apiUrl = untrailingslashit( rest_url(self::REST_NS . '/') );
|
||||
$jsUrl = $this->get_js_url();
|
||||
|
||||
$init = [
|
||||
'clientId' => $clientId,
|
||||
'apiUrl' => $apiUrl,
|
||||
'trackScreenViews' => isset($opts['track_screen']) ? !empty($opts['track_screen']) : true,
|
||||
'trackOutgoingLinks' => !empty($opts['track_outgoing']),
|
||||
'trackAttributes' => !empty($opts['track_attributes']),
|
||||
];
|
||||
|
||||
$bootstrap = "(function(){window.op=window.op||function(){(window.op.q=window.op.q||[]).push(arguments)};window.op('init'," . wp_json_encode($init) . ");})();";
|
||||
|
||||
$op_js = get_transient(self::TRANSIENT_JS);
|
||||
if ($op_js === false) {
|
||||
$res = wp_remote_get($jsUrl, ['timeout' => 8]);
|
||||
if (!is_wp_error($res) && 200 === wp_remote_retrieve_response_code($res)) {
|
||||
$op_js = wp_remote_retrieve_body($res);
|
||||
set_transient(self::TRANSIENT_JS, $op_js, self::CACHE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
wp_register_script('op-inline-stub', false, [], self::VERSION, true);
|
||||
wp_enqueue_script('op-inline-stub');
|
||||
|
||||
wp_add_inline_script('op-inline-stub', $bootstrap, 'before');
|
||||
|
||||
if (!empty($op_js)) {
|
||||
// Validate cached JavaScript content before output
|
||||
if ($this->is_valid_javascript_content($op_js)) {
|
||||
wp_add_inline_script('op-inline-stub', $op_js, 'after');
|
||||
} else {
|
||||
// Fall back to external script if cached content appears invalid or unsafe
|
||||
wp_enqueue_script('openpanel-op1', $jsUrl, [], self::VERSION, true);
|
||||
}
|
||||
} else {
|
||||
wp_enqueue_script('openpanel-op1', $jsUrl, [], self::VERSION, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** ---------------- Proxy Route ---------------- */
|
||||
public function register_proxy_route() {
|
||||
register_rest_route(self::REST_NS, self::REST_ROUTE, [
|
||||
'methods' => \WP_REST_Server::ALLMETHODS,
|
||||
// INTENTIONALLY PUBLIC ENDPOINT: This endpoint must be publicly accessible to receive
|
||||
// analytics data from frontend JavaScript running in users' browsers. No authentication
|
||||
// is required as this acts as a proxy for OpenPanel analytics collection.
|
||||
//
|
||||
// Security measures in place:
|
||||
// 1. Only proxies to whitelisted OpenPanel API endpoints (is_valid_proxy_target)
|
||||
// 2. All input data is sanitized and validated before forwarding
|
||||
// 3. Proper CORS headers are set for same-origin requests only
|
||||
// 4. No sensitive WordPress data is exposed through this endpoint
|
||||
'permission_callback' => '__return_true',
|
||||
'callback' => [$this, 'proxy_request'],
|
||||
'args' => [
|
||||
'path' => [
|
||||
'description' => 'Remaining path to forward',
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function proxy_request(\WP_REST_Request $request) {
|
||||
try {
|
||||
// Handle CORS preflight quickly
|
||||
if (strtoupper($request->get_method()) === 'OPTIONS') {
|
||||
$resp = new \WP_REST_Response(null, 204);
|
||||
$this->add_cors_headers($resp);
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$path = ltrim($request->get_param('path') ?? '', '/');
|
||||
$api_base = $this->get_api_base();
|
||||
$target = rtrim($api_base, '/') . '/' . $path;
|
||||
|
||||
// Security: Ensure we only proxy to OpenPanel API endpoints
|
||||
if (!$this->is_valid_proxy_target($target)) {
|
||||
$resp = new \WP_REST_Response(['error' => 'invalid_target', 'message' => 'Invalid proxy target'], 403);
|
||||
$this->add_cors_headers($resp);
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$method = strtoupper($request->get_method());
|
||||
$body = $request->get_body();
|
||||
|
||||
$incoming = $this->collect_request_headers();
|
||||
|
||||
$query = $request->get_query_params();
|
||||
if (!empty($query)) {
|
||||
$target = add_query_arg($query, $target);
|
||||
}
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'timeout' => 10,
|
||||
'headers' => $incoming,
|
||||
'body' => in_array($method, ['POST','PUT','PATCH','DELETE'], true) ? $body : null,
|
||||
];
|
||||
|
||||
$res = wp_remote_request($target, $args);
|
||||
|
||||
if (is_wp_error($res)) {
|
||||
$resp = new \WP_REST_Response(['error' => 'proxy_failed', 'message' => $res->get_error_message()], 502);
|
||||
$this->add_cors_headers($resp);
|
||||
$resp->header('Cache-Control', 'no-store');
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code($res);
|
||||
$bodyOut = wp_remote_retrieve_body($res);
|
||||
|
||||
// Handle empty response (common for tracking endpoints returning 202)
|
||||
if (empty($bodyOut)) {
|
||||
$resp = new \WP_REST_Response(['success' => true], $code ?: 202);
|
||||
$resp->header('Content-Type', 'application/json; charset=utf-8');
|
||||
$resp->header('Cache-Control', 'no-store');
|
||||
$this->add_cors_headers($resp);
|
||||
return $resp;
|
||||
}
|
||||
|
||||
// Try to decode JSON response, otherwise use raw body
|
||||
$decoded = json_decode($bodyOut, true);
|
||||
$responseData = (json_last_error() === JSON_ERROR_NONE && $decoded !== null) ? $decoded : ['raw' => $bodyOut];
|
||||
|
||||
$resp = new \WP_REST_Response($responseData, $code);
|
||||
|
||||
// Set Content-Type header (skip copying other headers to avoid compatibility issues)
|
||||
$resp->header('Content-Type', 'application/json; charset=utf-8');
|
||||
$resp->header('Cache-Control', 'no-store');
|
||||
$this->add_cors_headers($resp);
|
||||
return $resp;
|
||||
} catch (\Exception $e) {
|
||||
$resp = new \WP_REST_Response(['error' => 'exception', 'message' => $e->getMessage()], 500);
|
||||
$this->add_cors_headers($resp);
|
||||
return $resp;
|
||||
} catch (\Error $e) {
|
||||
$resp = new \WP_REST_Response(['error' => 'error', 'message' => $e->getMessage()], 500);
|
||||
$this->add_cors_headers($resp);
|
||||
return $resp;
|
||||
}
|
||||
}
|
||||
|
||||
private function add_cors_headers(\WP_REST_Response $resp) {
|
||||
$origin = get_site_url();
|
||||
$resp->header('Access-Control-Allow-Origin', $origin);
|
||||
$resp->header('Access-Control-Allow-Credentials', 'true');
|
||||
$resp->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
$resp->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
||||
$resp->header('Vary', 'Origin');
|
||||
}
|
||||
|
||||
private function collect_request_headers() {
|
||||
$headers = [];
|
||||
|
||||
// Content-Type is a special header NOT prefixed with HTTP_ in PHP
|
||||
// This is critical for OpenPanel API which requires application/json
|
||||
if (!empty($_SERVER['CONTENT_TYPE'])) {
|
||||
$headers['Content-Type'] = sanitize_text_field($_SERVER['CONTENT_TYPE']);
|
||||
}
|
||||
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
// Sanitize the header name and ensure it starts with HTTP_
|
||||
$name = sanitize_text_field($name);
|
||||
if (strpos($name, 'HTTP_') === 0) {
|
||||
// Extract and sanitize the header suffix
|
||||
$header_suffix = substr($name, 5);
|
||||
$header_suffix = sanitize_text_field($header_suffix);
|
||||
$header = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $header_suffix))));
|
||||
// remove hop-by-hop headers and Origin (we'll set it ourselves)
|
||||
$lk = strtolower($header);
|
||||
if (in_array($lk, ['host','content-length','accept-encoding','connection','keep-alive','transfer-encoding','upgrade','via','origin'], true)) {
|
||||
continue;
|
||||
}
|
||||
// Sanitize the header value
|
||||
$headers[$header] = sanitize_text_field($value);
|
||||
}
|
||||
}
|
||||
if (!isset($headers['User-Agent'])) {
|
||||
$headers['User-Agent'] = 'OpenPanel-WP-Proxy';
|
||||
}
|
||||
// Set Origin header to WordPress site URL (required for OpenPanel CORS)
|
||||
$headers['Origin'] = get_site_url();
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function is_valid_proxy_target($target) {
|
||||
$allowed_hosts = $this->get_allowed_hosts();
|
||||
|
||||
$parsed = wp_parse_url($target);
|
||||
if (!$parsed || !isset($parsed['host'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($parsed['host'], $allowed_hosts, true) &&
|
||||
(empty($parsed['scheme']) || in_array($parsed['scheme'], ['https', 'http'], true));
|
||||
}
|
||||
|
||||
private function is_valid_javascript_content($js_content) {
|
||||
// Comprehensive validation to ensure the content is safe JavaScript
|
||||
if (empty($js_content) || !is_string($js_content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: We don't modify $js_content with wp_kses here because we need to preserve
|
||||
// the original JavaScript content for validation. wp_add_inline_script() handles
|
||||
// proper escaping when outputting to the page.
|
||||
|
||||
// Ensure it doesn't contain HTML tags or script injection attempts
|
||||
if (preg_match('/<(?:script|iframe|object|embed|form|input|meta|link)/i', $js_content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for potential XSS patterns
|
||||
if (preg_match('/(?:javascript:|data:|vbscript:|on\w+\s*=)/i', $js_content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that it's a reasonable size for a JavaScript file (typical op1.js is ~5KB)
|
||||
$trimmed = trim($js_content);
|
||||
if (strlen($trimmed) < 10 || strlen($trimmed) > 20480) { // Max 20KB
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should contain typical JavaScript patterns (works for both minified and unminified)
|
||||
return (strpos($trimmed, 'function') !== false ||
|
||||
strpos($trimmed, 'var ') !== false ||
|
||||
strpos($trimmed, 'let ') !== false ||
|
||||
strpos($trimmed, 'const ') !== false ||
|
||||
strpos($trimmed, '=>') !== false ||
|
||||
// Handle minified code patterns like the actual op1.js
|
||||
preg_match('/[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*function/', $trimmed) ||
|
||||
preg_match('/\(\s*function\s*\(/', $trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
new OP_WP_Proxy();
|
||||
214
wordpress-plugin/openpanel/readme.txt
Normal file
214
wordpress-plugin/openpanel/readme.txt
Normal file
@@ -0,0 +1,214 @@
|
||||
=== OpenPanel ===
|
||||
Contributors: openpanel, airano
|
||||
Tags: analytics, web analytics, privacy-friendly, tracking, proxy, self-hosted
|
||||
Requires at least: 5.8
|
||||
Tested up to: 6.8
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.1.1
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
OpenPanel WordPress plugin - Privacy-friendly analytics with ad-blocker resistance. Supports both OpenPanel Cloud and Self-Hosted instances. Inline tracking scripts and proxy API calls through your domain.
|
||||
|
||||
== Description ==
|
||||
|
||||
**OpenPanel** is an open-source web and product analytics platform that serves as a privacy-friendly alternative to traditional analytics solutions. This WordPress plugin seamlessly integrates [OpenPanel](https://openpanel.dev) with your WordPress site while maximizing reliability and avoiding ad-blocker interference.
|
||||
|
||||
= Key Features =
|
||||
|
||||
* **🏠 Self-Hosted Support**: Works with both OpenPanel Cloud and your own Self-Hosted instance
|
||||
* **🚀 Ad-Blocker Resistant**: Serves analytics scripts and API calls from your own domain
|
||||
* **📊 Real-Time Analytics**: Get instant insights without processing delays
|
||||
* **🔒 Privacy-Friendly**: Cookie-less tracking that respects user privacy (no cookie banners needed!)
|
||||
* **⚡ Performance Optimized**: Caches scripts locally and uses efficient proxying
|
||||
* **🎯 Product Analytics**: Funnel analysis, retention tracking, and conversion metrics
|
||||
* **📈 Web Analytics**: Visitors, referrals, top pages, devices, sessions, and bounce rates
|
||||
|
||||
= How It Works =
|
||||
|
||||
This plugin integrates OpenPanel with WordPress in a blocker-resistant way:
|
||||
|
||||
* **Inlines** `op1.js` directly into your pages (cached locally for 1 week; falls back to CDN if needed)
|
||||
* **Bootstraps** the OpenPanel SDK with your Client ID automatically
|
||||
* **Proxies** all SDK requests through WordPress REST API (`/wp-json/openpanel/`)
|
||||
* **Preserves** all request methods, headers, query parameters, and body data
|
||||
* **Handles** CORS properly for cross-origin requests
|
||||
|
||||
**Why use a proxy?** Serving scripts and data from your own domain origin avoids third-party blocking and improves tracking reliability significantly.
|
||||
|
||||
= Privacy Benefits =
|
||||
|
||||
* **🍪 No Cookie Banners Required**: OpenPanel uses cookie-less tracking, so you don't need annoying cookie consent banners
|
||||
* **🛡️ GDPR Friendly**: Compliant with privacy regulations without requiring user consent for basic analytics
|
||||
* **🔐 Data Ownership**: You maintain full control over your analytics data
|
||||
* **🚫 No Personal Data Collection**: Tracks behavior patterns without collecting personally identifiable information
|
||||
|
||||
**Learn more at [OpenPanel.dev](https://openpanel.dev)**
|
||||
|
||||
== Installation ==
|
||||
|
||||
= Getting Started (Cloud) =
|
||||
|
||||
1. **Get your OpenPanel Client ID**:
|
||||
* Sign up for an account at [OpenPanel.dev](https://openpanel.dev)
|
||||
* Create a new project for your website
|
||||
* Copy your Client ID (starts with `op_client_`)
|
||||
|
||||
2. **Install the Plugin**:
|
||||
* Upload the plugin ZIP file via **Plugins → Add New → Upload Plugin**
|
||||
* Or place the `openpanel` folder in `/wp-content/plugins/`
|
||||
* Activate the plugin via **Plugins → Installed Plugins**
|
||||
|
||||
3. **Configure Settings**:
|
||||
* Go to **Settings → OpenPanel** in your WordPress admin
|
||||
* Select **Cloud (openpanel.dev)** as hosting mode
|
||||
* Paste your **Client ID** in the settings
|
||||
* Optionally enable auto-tracking features:
|
||||
- ✅ **Track page views automatically**
|
||||
- ✅ **Track clicks on outgoing links**
|
||||
- ✅ **Track additional page attributes**
|
||||
|
||||
4. **Verify Installation**:
|
||||
* Visit your website frontend
|
||||
* Check browser developer tools - you should see OpenPanel tracking requests to your own domain
|
||||
* Check your OpenPanel dashboard for incoming data
|
||||
|
||||
**That's it!** No theme edits or manual code insertion required.
|
||||
|
||||
= Self-Hosted Setup =
|
||||
|
||||
If you run your own OpenPanel instance (e.g., on Coolify, Docker, or any self-hosted environment):
|
||||
|
||||
1. **Install the Plugin** (same as above)
|
||||
|
||||
2. **Configure Self-Hosted Settings**:
|
||||
* Go to **Settings → OpenPanel** in your WordPress admin
|
||||
* Select **Self-Hosted** as hosting mode
|
||||
* Enter your **API URL** (e.g., `https://api.openpanel.yourdomain.com`)
|
||||
* Enter your **Dashboard URL** (e.g., `https://openpanel.yourdomain.com`)
|
||||
* Paste your **Client ID** from your self-hosted OpenPanel instance
|
||||
* Enable desired auto-tracking features
|
||||
|
||||
3. **Verify Configuration**:
|
||||
* Check the "Current Configuration" table at the bottom of settings page
|
||||
* Ensure API URL and JS URL point to your self-hosted instance
|
||||
* Visit your website frontend and verify tracking works
|
||||
|
||||
= Self-Hosted URL Examples =
|
||||
|
||||
| Setting | Example Value |
|
||||
|---------|---------------|
|
||||
| API URL | `https://api.openpanel.yourdomain.com` |
|
||||
| Dashboard URL | `https://openpanel.yourdomain.com` |
|
||||
| Client ID | Your project's client ID from OpenPanel |
|
||||
|
||||
**Note**: In Self-Hosted mode, op1.js is loaded from your Dashboard URL. In Cloud mode, it is loaded from the official OpenPanel CDN.
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= What is OpenPanel? =
|
||||
OpenPanel is an open-source web and product analytics platform designed as a privacy-friendly alternative to traditional analytics solutions. It provides real-time insights, funnel analysis, retention tracking, and more while respecting user privacy.
|
||||
|
||||
= Does this plugin support Self-Hosted OpenPanel? =
|
||||
Yes! Version 1.1.0 adds full support for self-hosted OpenPanel instances. Simply select "Self-Hosted" mode in settings and enter your API URL and Dashboard URL.
|
||||
|
||||
= Where is the proxy endpoint? =
|
||||
The proxy endpoint is at `/wp-json/openpanel/` (followed by the OpenPanel API path). The SDK automatically points to this endpoint, so all analytics requests go through your WordPress site instead of directly to OpenPanel servers (Cloud or Self-Hosted).
|
||||
|
||||
= Why do I need this proxy approach? =
|
||||
Serving analytics scripts and API requests from your own domain significantly reduces blocking by ad-blockers and privacy tools. This improves data collection reliability and ensures more accurate analytics.
|
||||
|
||||
= Does it respect CORS and security? =
|
||||
Yes. The proxy responds with proper CORS headers allowing your site origin and credentials. It also sanitizes all inputs and only forwards legitimate OpenPanel API requests.
|
||||
|
||||
= What if inlining `op1.js` fails? =
|
||||
The plugin automatically falls back to loading the script externally. In Self-Hosted mode, it loads from your Dashboard URL; in Cloud mode, from the OpenPanel CDN (`https://openpanel.dev/op1.js`).
|
||||
|
||||
= How is the script cached? =
|
||||
The `op1.js` script is cached locally for 1 week using WordPress transients. You can manually clear the cache from the plugin settings page if needed.
|
||||
|
||||
= Can I limit tracking to certain users or pages? =
|
||||
Yes! The plugin includes hooks and checks. For example, tracking is automatically disabled for admin pages. You can extend this by modifying the `inject_inline_sdk()` method or using WordPress filters.
|
||||
|
||||
= Will this affect my site performance? =
|
||||
No, the plugin is designed for minimal performance impact. Scripts are cached locally, loaded asynchronously, and the proxy only handles analytics requests efficiently.
|
||||
|
||||
= Do I need to modify my theme? =
|
||||
No theme modifications are required. The plugin automatically injects the necessary tracking code into all frontend pages.
|
||||
|
||||
= Is my data secure? =
|
||||
Yes. OpenPanel respects privacy by design with cookie-less tracking. Your data ownership is maintained, and you can export or delete data as needed. Since no cookies are used, you don't need cookie consent banners on your site.
|
||||
|
||||
= Do I need cookie consent banners with OpenPanel? =
|
||||
No! OpenPanel uses cookie-less tracking technology, which means no cookies are stored on your visitors' devices. This eliminates the need for cookie consent banners and makes your site GDPR compliant for basic analytics without requiring user consent.
|
||||
|
||||
= Where can I get support? =
|
||||
For plugin-specific issues, use the WordPress plugin support forum. For OpenPanel platform questions, visit [OpenPanel.dev](https://openpanel.dev) or their community channels.
|
||||
|
||||
== External Services ==
|
||||
|
||||
This plugin connects to external OpenPanel.dev services to provide web analytics functionality. Here's what you need to know:
|
||||
|
||||
**OpenPanel.dev Analytics Service**
|
||||
|
||||
* **What it is**: OpenPanel.dev is an open-source web and product analytics platform that combines the power of Mixpanel with the ease of Plausible and one of the best Google Analytics replacements.
|
||||
|
||||
* **What data is sent**:
|
||||
- **User Agent**: Browser and device information for compatibility and analytics
|
||||
- **IP Address**: Collected from X-Forwarded-For header for geolocation (no personal identification)
|
||||
- **Page Information**: Current page URL/path and referrer information
|
||||
- **Event Data**: Other user-defined properties when custom events are triggered via `window.op('track', 'event_name')`
|
||||
|
||||
* **When data is sent**:
|
||||
- **Page Views**: Only when "Track page views automatically" is enabled in settings
|
||||
- **Outgoing Link Clicks**: Only when "Track clicks on outgoing links" is enabled in settings
|
||||
- **Custom Events**: Only when website owner implements `window.op('track', 'custom_event')` method
|
||||
- **No background tracking** - data is sent only for the specific interactions you've configured
|
||||
|
||||
* **External endpoints used**:
|
||||
- `https://openpanel.dev/op1.js` - Analytics tracking script (cached locally)
|
||||
- `https://api.openpanel.dev/` - Analytics data collection API (proxied through your WordPress site)
|
||||
|
||||
* **Legal Information**:
|
||||
- Service Terms: https://openpanel.dev/terms
|
||||
- Privacy Policy: https://openpanel.dev/privacy
|
||||
|
||||
This integration is essential for the plugin's core functionality of providing website analytics. The plugin uses a proxy approach to serve requests through your own domain to improve reliability and avoid ad-blocker interference.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.1.1 =
|
||||
* **Self-Hosted JS Loading Fix** - op1.js now loads from Dashboard URL in Self-Hosted mode
|
||||
* ✅ Fixes ERR_SSL_PROTOCOL_ERROR when openpanel.dev CDN is blocked/filtered
|
||||
* ✅ Both inline cache and external fallback now use the correct self-hosted URL
|
||||
|
||||
= 1.1.0 =
|
||||
* **Self-Hosted Support** - Full support for self-hosted OpenPanel instances
|
||||
* ✅ New hosting mode selector: Cloud vs Self-Hosted
|
||||
* ✅ Configurable API URL for self-hosted instances
|
||||
* ✅ Configurable Dashboard URL for op1.js loading
|
||||
* ✅ Dynamic proxy validation for custom domains
|
||||
* ✅ Current configuration display in settings
|
||||
* ✅ Toggle visibility for self-hosted fields
|
||||
|
||||
= 1.0.0 =
|
||||
* **Initial Release** - Complete OpenPanel WordPress integration
|
||||
* ✅ Automatic script inlining with local caching (1 week cache duration)
|
||||
* ✅ REST API proxy for ad-blocker resistant tracking
|
||||
* ✅ Auto-tracking options: page views, outgoing links, page attributes
|
||||
* ✅ CORS-compliant request handling
|
||||
* ✅ Cache management with manual clear functionality
|
||||
* ✅ Fallback to CDN if local caching fails
|
||||
* ✅ Admin interface for easy configuration
|
||||
* ✅ No theme modifications required
|
||||
|
||||
== Upgrade Notice ==
|
||||
|
||||
= 1.1.1 =
|
||||
Fixes op1.js loading for self-hosted instances. Previously op1.js always loaded from openpanel.dev CDN even in Self-Hosted mode, causing SSL errors in regions where the CDN is blocked.
|
||||
|
||||
= 1.1.0 =
|
||||
Adds full support for self-hosted OpenPanel instances. You can now use the plugin with your own OpenPanel deployment on Coolify, Docker, or any self-hosted environment.
|
||||
|
||||
= 1.0.0 =
|
||||
Initial release of the OpenPanel WordPress plugin. Provides ad-blocker resistant analytics with local script caching and API proxying.
|
||||
Reference in New Issue
Block a user