Files
cannaiq/wordpress-plugin/cannaiq-menus.php
Kelly a96d50c481 docs(wordpress): Add deprecation comments for legacy shortcode/migration code
Clarifies that crawlsy_* and dutchie_* shortcodes are deprecated aliases
for backward compatibility only. New implementations should use cannaiq_*.

Also documents the token migration logic that preserves old API tokens.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 16:24:56 -07:00

560 lines
21 KiB
PHP

<?php
/**
* Plugin Name: CannaIQ Menus
* Plugin URI: https://cannaiq.co
* Description: Display cannabis product menus from CannaIQ with Elementor integration. Real-time menu data updated daily.
* Version: 1.6.0
* Author: CannaIQ
* Author URI: https://cannaiq.co
* License: GPL v2 or later
* Text Domain: cannaiq-menus
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
define('CANNAIQ_MENUS_VERSION', '1.6.0');
define('CANNAIQ_MENUS_API_URL', 'https://cannaiq.co/api/v1');
define('CANNAIQ_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('CANNAIQ_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__));
/**
* Main Plugin Class
*/
class CannaIQ_Menus_Plugin {
private static $instance = null;
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
public function __construct() {
add_action('plugins_loaded', [$this, 'init']);
add_action('elementor/widgets/register', [$this, 'register_elementor_widgets']);
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_init', [$this, 'register_settings']);
add_action('wp_enqueue_scripts', [$this, 'enqueue_scripts']);
}
public function init() {
// Initialize plugin
load_plugin_textdomain('cannaiq-menus', false, dirname(plugin_basename(__FILE__)) . '/languages');
// Register shortcodes - primary CannaIQ shortcodes
add_shortcode('cannaiq_products', [$this, 'products_shortcode']);
add_shortcode('cannaiq_product', [$this, 'single_product_shortcode']);
// DEPRECATED: Legacy shortcode aliases for backward compatibility only
// These allow sites that used the old plugin names to continue working
// New implementations should use [cannaiq_products] and [cannaiq_product]
add_shortcode('crawlsy_products', [$this, 'products_shortcode']); // deprecated
add_shortcode('crawlsy_product', [$this, 'single_product_shortcode']); // deprecated
add_shortcode('dutchie_products', [$this, 'products_shortcode']); // deprecated
add_shortcode('dutchie_product', [$this, 'single_product_shortcode']); // deprecated
}
/**
* Register Elementor Widgets
*/
public function register_elementor_widgets($widgets_manager) {
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-grid.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/single-product.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/brand-grid.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/category-list.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/specials-grid.php';
$widgets_manager->register(new \CannaIQ_Menus_Product_Grid_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Single_Product_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Brand_Grid_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Category_List_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Specials_Grid_Widget());
}
/**
* Enqueue Scripts and Styles
*/
public function enqueue_scripts() {
wp_enqueue_style(
'cannaiq-menus-styles',
CANNAIQ_MENUS_PLUGIN_URL . 'assets/css/cannaiq-menus.css',
[],
CANNAIQ_MENUS_VERSION
);
wp_enqueue_script(
'cannaiq-menus-script',
CANNAIQ_MENUS_PLUGIN_URL . 'assets/js/cannaiq-menus.js',
['jquery'],
CANNAIQ_MENUS_VERSION,
true
);
}
/**
* Add Admin Menu
*/
public function add_admin_menu() {
add_menu_page(
'CannaIQ Menus',
'CannaIQ Menus',
'manage_options',
'cannaiq-menus',
[$this, 'admin_page'],
'dashicons-products',
30
);
}
/**
* Register Plugin Settings
*/
public function register_settings() {
register_setting('cannaiq_menus_settings', 'cannaiq_api_token');
// MIGRATION: Auto-migrate API tokens from old plugin versions
// This runs once - if user had crawlsy or dutchie plugin, their token is preserved
// Can be removed in a future major version once all users have migrated
$old_crawlsy_token = get_option('crawlsy_api_token');
$old_dutchie_token = get_option('dutchie_api_token');
if (!get_option('cannaiq_api_token')) {
if ($old_crawlsy_token) {
update_option('cannaiq_api_token', $old_crawlsy_token);
} elseif ($old_dutchie_token) {
update_option('cannaiq_api_token', $old_dutchie_token);
}
}
}
/**
* Admin Page
*/
public function admin_page() {
?>
<div class="wrap">
<h1>CannaIQ Menus Settings</h1>
<p>Version <?php echo CANNAIQ_MENUS_VERSION; ?> by <a href="https://cannaiq.co" target="_blank">CannaIQ</a></p>
<p class="description">Display real-time cannabis menus with data updated daily from CannaIQ.</p>
<form method="post" action="options.php">
<?php settings_fields('cannaiq_menus_settings'); ?>
<?php do_settings_sections('cannaiq_menus_settings'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="cannaiq_api_token">API Token</label></th>
<td>
<input type="password" id="cannaiq_api_token" name="cannaiq_api_token"
value="<?php echo esc_attr(get_option('cannaiq_api_token')); ?>"
class="regular-text" />
<p class="description">Your authentication token from the CannaIQ admin dashboard. The token includes your store configuration.</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<hr />
<h2>Test Connection</h2>
<button type="button" id="test-api-connection" class="button button-primary">Test API Connection</button>
<div id="api-test-result" style="margin-top: 10px;"></div>
<script>
jQuery(document).ready(function($) {
$('#test-api-connection').on('click', function() {
var apiUrl = '<?php echo CANNAIQ_MENUS_API_URL; ?>';
var apiToken = $('#cannaiq_api_token').val();
if (!apiToken || apiToken.trim() === '') {
$('#api-test-result').html(
'<div class="notice notice-warning"><p><strong>Warning:</strong> Please enter an API token first.</p></div>'
);
return;
}
$('#api-test-result').html('<p><span class="spinner is-active" style="float:none;margin:0 5px 0 0;"></span>Testing connection...</p>');
$.ajax({
url: apiUrl + '/menu',
method: 'GET',
headers: {
'X-API-Key': apiToken
},
timeout: 30000,
success: function(response) {
var html = '<div class="notice notice-success"><p><strong>Success!</strong> Connected to: ' +
response.dispensary + '</p>';
if (response.menu) {
html += '<ul style="margin-left: 20px;">';
html += '<li>Total Products: ' + (response.menu.total_products || 0) + '</li>';
html += '<li>In Stock: ' + (response.menu.in_stock_count || 0) + '</li>';
html += '<li>Brands: ' + (response.menu.brand_count || 0) + '</li>';
html += '<li>Categories: ' + (response.menu.category_count || 0) + '</li>';
if (response.menu.last_updated) {
html += '<li>Last Updated: ' + new Date(response.menu.last_updated).toLocaleString() + '</li>';
}
html += '</ul>';
}
html += '</div>';
$('#api-test-result').html(html);
},
error: function(xhr, textStatus, errorThrown) {
var errorHtml = '<div class="notice notice-error">';
errorHtml += '<p><strong>Connection Failed</strong></p>';
// Show HTTP status
if (xhr.status) {
errorHtml += '<p><strong>HTTP Status:</strong> ' + xhr.status + ' ' + (xhr.statusText || '') + '</p>';
}
// Show error message from API
if (xhr.responseJSON) {
if (xhr.responseJSON.error) {
errorHtml += '<p><strong>Error:</strong> ' + xhr.responseJSON.error + '</p>';
}
if (xhr.responseJSON.message) {
errorHtml += '<p><strong>Details:</strong> ' + xhr.responseJSON.message + '</p>';
}
if (xhr.responseJSON.client_ip) {
errorHtml += '<p><strong>Your IP:</strong> ' + xhr.responseJSON.client_ip + '</p>';
}
if (xhr.responseJSON.origin) {
errorHtml += '<p><strong>Origin:</strong> ' + xhr.responseJSON.origin + '</p>';
}
} else if (textStatus === 'timeout') {
errorHtml += '<p><strong>Error:</strong> Request timed out. The API server may be unavailable.</p>';
} else if (textStatus === 'error' && !xhr.status) {
errorHtml += '<p><strong>Error:</strong> Could not connect to the API. This may be a CORS issue or network problem.</p>';
errorHtml += '<p><em>Note: If testing from wp-admin, try saving the token and testing on the frontend shortcode instead.</em></p>';
} else {
errorHtml += '<p><strong>Error:</strong> ' + (errorThrown || textStatus || 'Unknown error') + '</p>';
}
// Debug info
errorHtml += '<details style="margin-top: 10px;"><summary style="cursor:pointer;"><strong>Debug Info</strong></summary>';
errorHtml += '<pre style="background:#f5f5f5;padding:10px;margin-top:5px;overflow-x:auto;font-size:11px;">';
errorHtml += 'API URL: ' + apiUrl + '/menu\n';
errorHtml += 'Token Length: ' + apiToken.length + ' chars\n';
errorHtml += 'Token Preview: ' + apiToken.substring(0, 8) + '...' + apiToken.substring(apiToken.length - 4) + '\n';
errorHtml += 'Status: ' + xhr.status + '\n';
errorHtml += 'Response: ' + (xhr.responseText ? xhr.responseText.substring(0, 500) : 'none') + '\n';
errorHtml += '</pre></details>';
// Help text based on status
if (xhr.status === 401) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Your API token is invalid or inactive. Please check that you copied the full token from the CannaIQ admin dashboard.</p>';
} else if (xhr.status === 403) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Access denied. Your IP address or domain may not be in the allowed list. Contact support to update your permissions.</p>';
} else if (xhr.status === 500) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Server error. This may be a temporary issue. Try again in a few minutes, or contact support if the problem persists.</p>';
} else if (xhr.status === 503) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Menu data is not yet available for your dispensary. The data may still be processing.</p>';
}
errorHtml += '</div>';
$('#api-test-result').html(errorHtml);
}
});
});
});
</script>
<hr />
<h2>Usage</h2>
<h3>Shortcodes</h3>
<table class="widefat" style="max-width: 800px;">
<thead>
<tr>
<th>Shortcode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[cannaiq_products]</code></td>
<td>Display a grid of products. Options: <code>category_id</code>, <code>limit</code>, <code>columns</code>, <code>in_stock</code></td>
</tr>
<tr>
<td><code>[cannaiq_product id="123"]</code></td>
<td>Display a single product by ID</td>
</tr>
</tbody>
</table>
<h3>Elementor Widgets</h3>
<p>If you have Elementor installed, you can use the CannaIQ widgets:</p>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>CannaIQ Product Grid</strong> - Display a grid of products with filtering options</li>
<li><strong>CannaIQ Single Product</strong> - Display a single product card</li>
</ul>
</div>
<?php
}
/**
* Products Shortcode
*/
public function products_shortcode($atts) {
$atts = shortcode_atts([
'category_id' => '',
'limit' => 12,
'columns' => 3,
'in_stock' => 'true'
], $atts);
$products = $this->fetch_products($atts);
if (!$products) {
return '<p>No products found.</p>';
}
ob_start();
include CANNAIQ_MENUS_PLUGIN_DIR . 'templates/product-grid.php';
return ob_get_clean();
}
/**
* Single Product Shortcode
*/
public function single_product_shortcode($atts) {
$atts = shortcode_atts([
'id' => 0
], $atts);
if (!$atts['id']) {
return '<p>Product ID required.</p>';
}
$product = $this->fetch_product($atts['id']);
if (!$product) {
return '<p>Product not found.</p>';
}
ob_start();
include CANNAIQ_MENUS_PLUGIN_DIR . 'templates/single-product.php';
return ob_get_clean();
}
/**
* Fetch Products from API
*/
public function fetch_products($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/products?' . $query_args;
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['products'] ?? false;
}
/**
* Fetch Single Product from API
*/
public function fetch_product($id) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$url = CANNAIQ_MENUS_API_URL . '/products/' . intval($id);
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['product'] ?? false;
}
/**
* Fetch Categories from API
*/
public function fetch_categories($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/categories' . ($query_args ? '?' . $query_args : '');
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['categories'] ?? false;
}
/**
* Fetch Brands from API
*/
public function fetch_brands($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/brands' . ($query_args ? '?' . $query_args : '');
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['brands'] ?? false;
}
/**
* Fetch Specials/Deals from API
*/
public function fetch_specials($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/specials' . ($query_args ? '?' . $query_args : '');
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['products'] ?? false;
}
/**
* Get categories as options for Elementor select control
* Returns cached results for performance
*/
public function get_category_options() {
$cache_key = 'cannaiq_category_options';
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$categories = $this->fetch_categories();
$options = ['' => __('All Categories', 'cannaiq-menus')];
if ($categories) {
foreach ($categories as $cat) {
$name = $cat['type'] ?? $cat['name'] ?? '';
if ($name) {
$options[$name] = ucwords(str_replace('_', ' ', $name));
}
}
}
set_transient($cache_key, $options, 5 * MINUTE_IN_SECONDS);
return $options;
}
/**
* Get brands as options for Elementor select control
* Returns cached results for performance
*/
public function get_brand_options() {
$cache_key = 'cannaiq_brand_options';
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$brands = $this->fetch_brands(['limit' => 200]);
$options = ['' => __('All Brands', 'cannaiq-menus')];
if ($brands) {
foreach ($brands as $brand) {
$name = $brand['brand'] ?? $brand['brand_name'] ?? '';
if ($name) {
$options[$name] = $name;
}
}
}
set_transient($cache_key, $options, 5 * MINUTE_IN_SECONDS);
return $options;
}
}
// Initialize Plugin
function cannaiq_menus() {
return CannaIQ_Menus_Plugin::instance();
}
cannaiq_menus();