HEX
Server: nginx/1.27.0
System: Linux 20d173156d2c 6.8.0-88-generic #89-Ubuntu SMP PREEMPT_DYNAMIC Sat Oct 11 01:02:46 UTC 2025 x86_64
User: www-data (33)
PHP: 8.1.29
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/content-egg/application/PriceAlert.php
<?php

namespace ContentEgg\application;

defined('\ABSPATH') || exit;

use ContentEgg\application\models\PriceAlertModel;
use ContentEgg\application\helpers\TextHelper;
use ContentEgg\application\models\PriceHistoryModel;
use ContentEgg\application\components\ContentManager;
use ContentEgg\application\helpers\TemplateHelper;
use ContentEgg\application\admin\GeneralConfig;
use ContentEgg\application\components\ContentProduct;

/**
 * PriceAlert class file
 *
 * @author keywordrush.com <support@keywordrush.com>
 * @link https://www.keywordrush.com
 * @copyright Copyright &copy; 2025 keywordrush.com
 */
class PriceAlert
{

    private static $instance = null;
    private $tickbox_message;

    public static function getInstance()
    {
        if (self::$instance == null)
            self::$instance = new self;

        return self::$instance;
    }

    private function __construct()
    {
    }

    public function init()
    {
        if (!self::isPriceAlertAllowed())
            return;

        if (\is_admin())
        {
            // anonymous visitors
            \add_action('wp_ajax_nopriv_start_tracking', array($this, 'ajaxTrackProduct'));
            // logged in users
            \add_action('wp_ajax_start_tracking', array($this, 'ajaxTrackProduct'));
        }

        if (self::isRehubTheme())
            \add_action('init', array($this, 'registerJs'));

        \add_action('template_redirect', array($this, 'subscriptionManager'));
    }

    public function ajaxTrackProduct()
    {
        if (!isset($_POST['nonce']) || !\wp_verify_nonce(sanitize_key($_POST['nonce']), 'cegg-price-alert'))
            die('Invalid nonce');

        $module_id = isset($_POST['module_id']) ? TextHelper::clear(sanitize_text_field(wp_unslash($_POST['module_id']))) : null;
        $unique_id = isset($_POST['unique_id']) ? TextHelper::clearId(sanitize_text_field(wp_unslash($_POST['unique_id']))) : null;
        $price = isset($_POST['price']) ? (float) TextHelper::parsePriceAmount(sanitize_text_field(wp_unslash($_POST['price']))) : null;
        $post_id = isset($_POST['post_id']) ? intval(wp_unslash($_POST['post_id'])) : null;
        $email = isset($_POST['email']) ? strtolower(sanitize_email(wp_unslash($_POST['email']))) : null;
        $accepted = isset($_POST['accepted']) ? boolval(wp_unslash($_POST['accepted'])) : null;

        if (!$module_id || !$unique_id || !$post_id)
            die('Invalid params');

        $current = PriceHistoryModel::model()->getLastPrices($unique_id, $module_id, $limit = 1);
        if (!$current)
            die('Product not found.');
        $current = $current[0];

        // find product, check post_id
        $product = ContentManager::getProductbyUniqueId($current['unique_id'], $current['module_id'], $post_id);
        if (!$product)
            die('Product not found.');

        if (!$price || !$email)
            $this->jsonError(__('All fields are required.', 'content-egg-tpl'));

        if (TemplateHelper::getPrivacyUrl() && !$accepted)
            $this->jsonError(__('Please indicate that you have read and agree to the Privacy Policy.', 'content-egg-tpl'));

        if (!\is_email($email))
            $this->jsonError(__('Your email address is invalid.', 'content-egg-tpl'));

        if ($price >= $current['price'])
            $this->jsonError(__('The price has already been reached.', 'content-egg-tpl'));

        $where = array(
            'unique_id = %s AND module_id = %s AND email = %s AND status != %d',
            array($unique_id, $module_id, $email, PriceAlertModel::STATUS_DELETED)
        );
        if (PriceAlertModel::model()->find(array('where' => $where)))
            $this->jsonError(__('You already tracking this product.', 'content-egg-tpl'));

        $alert = array(
            'unique_id' => $current['unique_id'],
            'module_id' => $current['module_id'],
            'post_id' => $post_id,
            'email' => $email,
            'price' => $price,
            'start_price' => $current['price'],
            'status' => PriceAlertModel::STATUS_INACTIVE,
            'activkey' => TextHelper::randomPassword(16),
        );

        // save
        if (PriceAlertModel::model()->save($alert))
        {
            // email
            $this->sendActivationEmail($email, $product, $alert);
            $this->jsonResult(__('We are now tracking this product for you. Please verify your email address to be notified of price drops.', 'content-egg-tpl'), 'success');
        }
        else
            $this->jsonError(__('Internal Error. Please notify the administrator.', 'content-egg-tpl'));
        exit;
    }

    private function sendActivationEmail($email, $product, $alert)
    {
        $subject = sprintf(__('Welcome to %s', 'content-egg-tpl'), \esc_html(\get_bloginfo('name')));
        $product_title = \esc_html(TextHelper::truncate($product['title']));

        $unsubscribe_url = \add_query_arg(array(
            'ceggaction' => 'unsubscribe',
            'email' => urlencode($email),
            'key' => urlencode($alert['activkey']),
        ), \get_site_url());

        $uri = \add_query_arg(array(
            'ceggaction' => 'validate',
            'email' => urlencode($email),
            'key' => urlencode($alert['activkey']),
        ), \get_permalink($alert['post_id']));
        $uri .= '#' . urlencode($alert['unique_id']);

        if ($template = GeneralConfig::getInstance()->option('email_template_activation'))
        {
            $tags = array(
                '%POST_ID%' => $alert['post_id'],
                '%POST_URL%' => \get_permalink($alert['post_id']),
                '%POST_TITLE%' => \get_the_title($alert['post_id']),
                '%PRODUCT_TITLE%' => $product_title,
                '%VALIDATE_URL%' => \esc_url($uri),
                '%UNSUBSCRIBE_URL%' => $unsubscribe_url,
            );
            $body = self::buildTemplate($template, $tags);
        }
        else
        {
            $body = '<p>' . __('Hello,', 'content-egg-tpl') . '<br></p>';
            $body .= '<p>' . sprintf(__('You have successfully set a price drop alert for %s.', 'content-egg-tpl'), $product_title) . '</p>';
            $body .= '<p>' . __('We will not send you any price alerts until you verified your email address.', 'content-egg-tpl');
            $body .= ' ' . sprintf(__('Please open this link to validate your email address:<br> <a href="%s">%s</a>', 'content-egg-tpl'), \esc_url($uri), \esc_url($uri)) . '</p>';
            $body .= '<p>' . sprintf(__('If you don\'t want to receive any price alerts from us in the future, <a href="%s">please click here</a>.', 'content-egg-tpl'), $unsubscribe_url) . '</p>';
        }

        $body .= $this->getEmailSignature();

        self::mail($email, $subject, $body);
    }

    private function getEmailSignature()
    {
        if ($signature = GeneralConfig::getInstance()->option('email_signature'))
            return $signature;
        else
            return "<pre class=\"moz-signature\" cols=\"72\">--\r\n" . sprintf(__('Thank You,', 'content-egg-tpl') . "\r\n" . __('The %s Team', 'content-egg-tpl') . "\r\n%s", \get_bloginfo('name'), \get_home_url()) . "</pre>";
    }

    private function jsonResult($message, $status = 'success')
    {
        header("Content-Type: application/json");
        echo json_encode(array(
            'status' => $status,
            'message' => $message
        ));
        exit;
    }

    private function jsonError($message)
    {
        $this->jsonResult($message, 'error');
        exit;
    }

    public function subscriptionManager()
    {
        $action = isset($_GET['ceggaction']) ? sanitize_key(wp_unslash($_GET['ceggaction'])) : '';
        if (!$action)
            return;

        switch ($action)
        {
            case 'validate':
                $this->actionValidateEmail();
                return;
            case 'unsubscribe':
                $this->actionUnsubscribeAll();
                return;
            case 'delete':
                $this->actionDeleteSubscription();
                return;
            default:
                return;
        }
    }

    private function actionValidateEmail()
    {
        $email = isset($_GET['email']) ? strtolower(sanitize_email(wp_unslash($_GET['email']))) : '';
        $key = isset($_GET['key']) ? TextHelper::clear(sanitize_text_field(wp_unslash($_GET['key']))) : '';

        if (!$email || !$key)
            return;

        $where = array(
            'email = %s AND activkey = %s AND status = %d',
            array($email, $key, PriceAlertModel::STATUS_INACTIVE)
        );
        $alert = PriceAlertModel::model()->find(array('where' => $where));
        if (!$alert)
            return;
        $alert['status'] = PriceAlertModel::STATUS_ACTIVE;
        // save
        PriceAlertModel::model()->save($alert);
        // tickbox
        $this->openTickbox(__('Your email has been verified. We will let you know by email when the Price Drops.', 'content-egg-tpl'), __('Success!', 'content-egg-tpl'));
    }

    private function actionUnsubscribeAll()
    {
        $email = isset($_GET['email']) ? strtolower(sanitize_email(wp_unslash($_GET['email']))) : '';
        $key = isset($_GET['key']) ? TextHelper::clear(sanitize_text_field(wp_unslash($_GET['key']))) : '';

        if (!$email || !$key)
            return;

        $where = array(
            'email = %s AND activkey = %s',
            array($email, $key)
        );
        $alert = PriceAlertModel::model()->find(array('where' => $where));
        if (!$alert)
            return;

        PriceAlertModel::model()->unsubscribeAll($alert['email']);
        $this->openTickbox(__('You are now unsubscribed from our Price Alerts via email.', 'content-egg-tpl'), __('Unsubscribed!', 'content-egg-tpl'));
    }

    private function actionDeleteSubscription()
    {
        $email = isset($_GET['email']) ? strtolower(sanitize_email(wp_unslash($_GET['email']))) : '';
        $key = isset($_GET['key']) ? TextHelper::clear(sanitize_text_field(wp_unslash($_GET['key']))) : '';

        if (!$email || !$key)
            return;

        $where = array(
            'email = %s AND activkey = %s',
            array($email, $key)
        );
        $alert = PriceAlertModel::model()->find(array('where' => $where));
        if (!$alert)
            return;
        // save
        PriceAlertModel::model()->delete($alert['id']);
        // tickbox
        $this->openTickbox(__('Your subscription has been deleted from our database.', 'content-egg-tpl'), __('Success!', 'content-egg-tpl'));
    }

    public function openTickbox($message, $subject = "")
    {
        $this->tickbox_message = strip_tags($message);
        $this->tickbox_subject = strip_tags($subject);
        \add_action('wp_footer', array($this, 'tickboxInlineScript'));
    }

    public function tickboxInlineScript()
    {
        echo '<script>window.onload = function() {alert("' . esc_js($this->tickbox_message) . '");};</script>';
    }

    public static function mail($to, $subject, $message, $headers = '', $attachments = array())
    {
        \add_filter('wp_mail_content_type', array(__CLASS__, 'setMailContentType'));
        if (GeneralConfig::getInstance()->option('from_email'))
            \add_filter('wp_mail_from', array(__CLASS__, 'setMailFrom'));
        if (GeneralConfig::getInstance()->option('from_name'))
            \add_filter('wp_mail_from_name', array(__CLASS__, 'setMailFromName'));

        \wp_mail($to, $subject, $message, $headers, $attachments);

        \remove_filter('wp_mail_content_type', array(__CLASS__, 'setMailContentType'));
        \remove_filter('wp_mail_from', array(__CLASS__, 'setMailFrom'));
        \remove_filter('wp_mail_from_name', array(__CLASS__, 'setMailFromName'));
    }

    public static function setMailContentType()
    {
        return 'text/html';
    }

    public static function setMailFrom()
    {
        return GeneralConfig::getInstance()->option('from_email');
    }

    public static function setMailFromName()
    {
        return GeneralConfig::getInstance()->option('from_name');
    }

    public function sendAlerts(array $data, $module_id, $post_id)
    {
        $total = 0;
        foreach ($data as $key => $d)
        {
            $stock_status = isset($d['stock_status']) ? $d['stock_status'] : ContentProduct::STOCK_STATUS_UNKNOWN;
            if (empty($d['unique_id']) || empty($d['price']) || $stock_status == ContentProduct::STOCK_STATUS_OUT_OF_STOCK)
                continue;

            // Price drops?
            $previous_price = PriceHistoryModel::model()->getPreviousPriceValue($d['unique_id'], $module_id);
            if (!$previous_price || (float) $previous_price <= (float) $d['price']) //!!!!!
                continue;

            $price_alert_mode = GeneralConfig::getInstance()->option('price_alert_mode');
            if ($price_alert_mode == 'post')
                $where = array('post_id=%s AND status=%d AND price >= %f', array($post_id, PriceAlertModel::STATUS_ACTIVE, $d['price']));
            else
                $where = array('unique_id=%s AND module_id=%s AND status=%d AND price >= %f', array($d['unique_id'], $module_id, PriceAlertModel::STATUS_ACTIVE, $d['price']));

            if (!$subscribers = PriceAlertModel::model()->findAll(array('where' => $where)))
                continue;

            // remove dublicates by email column
            $subscribers = array_reverse(array_values(array_column(array_reverse($subscribers), null, 'email')));

            $total += count($subscribers);
            $this->sendAlertEmails($subscribers, $d, $post_id);
        }

        // clean up & optimize
        if ($total && rand(1, 5) == 5)
            PriceAlertModel::model()->cleanOld(PriceAlertModel::CLEAN_DELETED_DAYS);
    }

    private function sendAlertEmails($alerts, $product, $post_id)
    {
        foreach ($alerts as $alert)
        {
            $product_title = \esc_html(TextHelper::truncate($product['title']));
            $subject = sprintf(__('Price alert: "%s"', 'content-egg-tpl'), $product_title);
            $post_url = \get_permalink($post_id) . '#' . urlencode($product['unique_id']);

            $unsubscribe_url = \add_query_arg(array(
                'ceggaction' => 'unsubscribe',
                'email' => urlencode($alert['email']),
                'key' => urlencode($alert['activkey']),
            ), \get_site_url());

            $desired_price = TemplateHelper::formatPriceCurrency($alert['price'], $product['currencyCode']);
            $current_price = TemplateHelper::formatPriceCurrency($product['price'], $product['currencyCode']);
            $start_price = TemplateHelper::formatPriceCurrency($alert['start_price'], $product['currencyCode']);
            $saved_amount = round($alert['start_price'] - $product['price'], 2);
            $saved_amount = TemplateHelper::formatPriceCurrency($saved_amount, $product['currencyCode']);
            $saved_percentage = round(100 - (100 * $product['price']) / $alert['start_price'], 2);

            if ($template = GeneralConfig::getInstance()->option('email_template_alert'))
            {
                $tags = array(
                    '%POST_ID%' => $post_id,
                    '%POST_URL%' => $post_url,
                    '%POST_TITLE%' => \get_the_title($post_id),
                    '%PRODUCT_TITLE%' => $product_title,
                    '%SAVED_AMOUNT%' => $saved_amount,
                    '%SAVED_PERCENTAGE%' => $saved_percentage,
                    '%DESIRED_PRICE%' => $desired_price,
                    '%CURRENT_PRICE%' => $current_price,
                    '%START_PRICE%' => $start_price,
                    '%UPDATE_DATE%' => TemplateHelper::getLastUpdateFormatted($alert['module_id'], $post_id),
                    '%UNSUBSCRIBE_URL%' => $unsubscribe_url,
                );
                $body = self::buildTemplate($template, $tags);
            }
            else
            {
                $body = '<p>' . __('Good news!', 'content-egg-tpl') . '<br></p>';
                $body .= '<p>' . __('The price target you set for the item has been reached.', 'content-egg-tpl');
                $body .= '<p>' . sprintf(__('<a href="%s">Save %s (%s%%) on %s</a>', 'content-egg-tpl'), $post_url, $saved_amount, $saved_percentage, $product_title);
                $body .= '<ul>';
                $body .= '<li>' . sprintf(__('Desired Price: %s', 'content-egg-tpl'), $desired_price) . '</li>';
                $body .= '<li>' . sprintf(__('Current Price: <strong>%s</strong>', 'content-egg-tpl'), $current_price)
                    . ' (' . __('as of', 'content-egg-tpl') . ' ' . TemplateHelper::getLastUpdateFormatted($alert['module_id'], $post_id) . ')</li>';
                $body .= '<li>' . sprintf(__('Price dropped from %s to %s', 'content-egg-tpl'), $start_price, $current_price) . '</li>';
                $body .= '</ul><br>';
                $body .= sprintf(__('<a href="%s">More info...</a>', 'content-egg-tpl'), $post_url);
                $body .= '</p><br>';

                $body .= '<p>' . sprintf(__('This present alert has now expired. You may <a href="%s">create a new alert</a> for this item.', 'content-egg-tpl'), $post_url);
                $body .= '<br>' . sprintf(__('If you don\'t want to receive any price alerts from us in the future, <a href="%s">please click here</a>.', 'content-egg-tpl'), $unsubscribe_url) . '</p>';
            }
            $body .= $this->getEmailSignature();

            // send alert email
            self::mail($alert['email'], $subject, $body);

            // delete alert
            $alert['status'] = PriceAlertModel::STATUS_DELETED;
            $alert['complet_date'] = \current_time('mysql');
            PriceAlertModel::model()->save($alert);
        }
    }

    public static function isPriceAlertAllowed($unique_id = null, $module_id = null)
    {

        if (!GeneralConfig::getInstance()->option('price_history_days'))
            return false;

        if (!GeneralConfig::getInstance()->option('price_alert_enabled'))
            return false;

        if ($unique_id && $module_id)
        {
            if (!PriceHistoryModel::model()->getLastPrices($unique_id, $module_id, 1))
                return false;
        }
        return true;
    }

    public static function buildTemplate($template, array $tags)
    {
        return str_ireplace(array_keys($tags), array_values($tags), $template);
    }

    // for backward compatibility with Rehub
    public function registerJs()
    {
        \wp_enqueue_script('cegg-price-alert', \ContentEgg\PLUGIN_RES . '/js/price_alert.js', array('jquery'), Plugin::version());
        \wp_localize_script('cegg-price-alert', 'ceggPriceAlert', array(
            'ajaxurl' => \admin_url('admin-ajax.php'),
            'nonce' => \wp_create_nonce('cegg-price-alert')
        ));
    }

    public static function isRehubTheme()
    {
        return (in_array(basename(\get_template_directory()), array('rehub', 'rehub-theme', 'rehub-theme-new'))) ? true : false;
    }
}