get_product_by_post_id($post_id); if (!$post_product) { return false; } return in_array($post_product->qixit_post_type, array( QIXIT_PREMIUM_PAY_ONCE_VIEW_ANYTIME, QIXIT_PREMIUM_PAY_PER_VIEW )); } /** * Check if current user has access to premium content * * @param int $post_id Post ID * @return bool True if has access */ function qixit_user_has_access($post_id) { // Authors always have access to their own content $post = get_post($post_id); if ($post && $post->post_author == get_current_user_id()) { return true; } // Admins always have access if (current_user_can('manage_options')) { return true; } // Check if post is premium if (!qixit_is_premium_post($post_id)) { return true; // Free content } // Check if user has paid $product = Qixit_Product::get_instance(); $post_product = $product->get_product_by_post_id($post_id); if ($post_product) { $payment = Qixit_Payment::get_instance(); return $payment->has_user_paid($post_product->product_id); } return false; } /** * Get purchase URL for a product * * @param string $product_guid Product GUID * @param string $affiliate_id Optional affiliate ID * @param string $forwarder_id Optional forwarder ID * @return string Purchase URL */ function qixit_get_purchase_url($product_guid, $affiliate_id = '', $forwarder_id = '') { $api = Qixit_API::get_instance(); return $api->get_purchase_url($product_guid, $affiliate_id, $forwarder_id); } /** * Whether site-level forwarder commissions are enabled. * * @return bool */ function qixit_is_forwarder_commission_enabled() { return (bool) get_option('qixit_enable_forwarder_commissions', false); } /** * Site-level forwarder commission percent (0 when disabled). * * @return int */ function qixit_get_forwarder_percent() { if (!qixit_is_forwarder_commission_enabled()) { return 0; } $percent = intval(get_option('qixit_forwarder_percent', 0)); if ($percent < 0) { return 0; } if ($percent > 100) { return 100; } return $percent; } /** * Merge forwarder percentage into a Products Portal API payload. * * @param array $api_data Product create/update payload * @return array */ function qixit_apply_forwarder_to_api_data($api_data) { if (!is_array($api_data)) { $api_data = array(); } $api_data['forwardPercentage'] = qixit_get_forwarder_percent(); $api_data['forwardUserId'] = ''; return $api_data; } /** * Fixed Price Optimization Category for non-post products (Vendor portal sections). * Not the same as Commercial Code (postage). * * @param string $kind One of: paid_comments, author_rights * @return array{sectionKey:string,sectionName:string} */ function qixit_price_opt_section_fixed($kind) { $kind = sanitize_key((string) $kind); if ($kind === 'paid_comments') { return array( 'sectionKey' => 'paid-comments', 'sectionName' => 'Paid Comments', ); } if ($kind === 'author_rights') { return array( 'sectionKey' => 'author-rights', 'sectionName' => 'Author Rights', ); } return array( 'sectionKey' => 'uncategorized', 'sectionName' => 'Uncategorized', ); } /** * Resolve Price Optimization Category for a paywalled post/page from WP categories. * First assigned category wins; if none, WP Post / WP Page by post_type. * * @param int $post_id Post ID * @return array{sectionKey:string,sectionName:string} */ function qixit_price_opt_section_for_post($post_id) { $post_id = intval($post_id); $post_type = $post_id > 0 ? get_post_type($post_id) : ''; if ($post_type === 'page') { $fallback = array('sectionKey' => 'wp-page', 'sectionName' => 'WP Page'); } else { $fallback = array('sectionKey' => 'wp-post', 'sectionName' => 'WP Post'); } if ($post_id <= 0) { return $fallback; } $term_ids = wp_get_post_categories($post_id, array('fields' => 'ids')); if (empty($term_ids) || !is_array($term_ids)) { return $fallback; } $term = get_term(intval($term_ids[0]), 'category'); if (!$term || is_wp_error($term) || empty($term->name)) { return $fallback; } $slug = !empty($term->slug) ? sanitize_title($term->slug) : sanitize_title($term->name); if ($slug === '') { $slug = 'wp-category-' . intval($term->term_id); } return array( 'sectionKey' => $slug, 'sectionName' => $term->name, ); } /** * Merge Price Optimization Category fields into a Products Portal API payload. * * @param array $api_data Existing payload * @param array $section Keys sectionKey, sectionName * @return array */ function qixit_apply_price_opt_section_to_api_data($api_data, $section) { if (!is_array($api_data)) { $api_data = array(); } if (!is_array($section)) { return $api_data; } if (!empty($section['sectionKey'])) { $api_data['sectionKey'] = (string) $section['sectionKey']; } if (!empty($section['sectionName'])) { $api_data['sectionName'] = (string) $section['sectionName']; } return $api_data; } /** * Human-readable rights label based on moderation policy. * * @return string "Contributor" or "Author" */ function qixit_get_onboarding_rights_label() { $requires_moderation = (bool) get_option('qixit_author_require_moderation', true); return $requires_moderation ? __('Contributor', '2waymicropay') : __('Author', '2waymicropay'); } /** * Purchase CTA label for become-an-author shortcode. * * @return string */ function qixit_get_onboarding_purchase_cta_label() { $requires_moderation = (bool) get_option('qixit_author_require_moderation', true); return $requires_moderation ? __('Purchase Contributor Rights', '2waymicropay') : __('Purchase Author Rights', '2waymicropay'); } /** * Format currency amount * * @param float $amount Amount * @param string $currency Currency code (default: USD) * @return string Formatted amount */ function qixit_format_currency($amount, $currency = 'USD') { return '$' . number_format($amount, 2); } /** * Production API and Products Web URLs (HTTPS with Let's Encrypt SSL) * SSL certificates installed on Azure VM IIS on 2026-01-26 * Certificates auto-renew via win-acme scheduled task */ if (!defined('QIXIT_PRODUCTION_API_URL')) { // Production must resolve through products.qixit.com so /api routes match the live // environment and avoid accidental stage/prod data crossover for payout checks. define('QIXIT_PRODUCTION_API_URL', 'https://products.qixit.com'); } if (!defined('QIXIT_PRODUCTION_PRODUCTS_WEB_URL')) { define('QIXIT_PRODUCTION_PRODUCTS_WEB_URL', 'https://products.qixit.com'); } /** * Development URLs (localhost - only used when development mode is enabled) */ if (!defined('QIXIT_DEV_API_URL')) { define('QIXIT_DEV_API_URL', 'http://localhost:7100'); } if (!defined('QIXIT_DEV_PRODUCTS_WEB_URL')) { define('QIXIT_DEV_PRODUCTS_WEB_URL', 'http://localhost:5176'); } if (function_exists('qixit_debug_log')) { qixit_debug_log('URL constants defined'); } /** * Get the Qixit API base URL * Returns production HTTPS URL by default, or localhost if development mode is enabled * * @return string API base URL */ function qixit_get_api_url() { // Check if development mode is enabled if (get_option('qixit_development_mode', false)) { return QIXIT_DEV_API_URL; } // Production hard guard: never allow non-production API host when not in dev mode. $expected = 'https://products.qixit.com'; $configured = rtrim(trim((string) QIXIT_PRODUCTION_API_URL), '/'); if (!empty($configured) && strcasecmp($configured, $expected) !== 0) { if (function_exists('qixit_debug_log')) { qixit_debug_log('Production API guard corrected host: ' . $configured . ' -> ' . $expected); } return $expected; } return !empty($configured) ? $configured : $expected; } /** * Get the Products Web URL (for PurchaseFast page) * * @return string Products Web URL */ function qixit_get_products_web_url() { // Check if development mode is enabled if (get_option('qixit_development_mode', false)) { return QIXIT_DEV_PRODUCTS_WEB_URL; } // Production hard guard: never allow non-production web host when not in dev mode. $expected = 'https://products.qixit.com'; $configured = rtrim(trim((string) QIXIT_PRODUCTION_PRODUCTS_WEB_URL), '/'); if (!empty($configured) && strcasecmp($configured, $expected) !== 0) { if (function_exists('qixit_debug_log')) { qixit_debug_log('Production web URL guard corrected host: ' . $configured . ' -> ' . $expected); } return $expected; } return !empty($configured) ? $configured : $expected; } /** * Get the Products API URL (same as Products Web URL) * * @return string Products API URL */ function qixit_get_products_api_url() { return qixit_get_api_url(); } /** * Append SourcePage query parameter to checkout URLs. * * This ensures purchase/payment attribution keeps the full publisher path * even when browser Referer is reduced to origin-only. * * @param string $checkout_url purchasefast/paymentfast URL * @param string $source_page_url Optional explicit publisher URL * @return string */ function qixit_append_source_page_param($checkout_url, $source_page_url = '') { $checkout_url = is_string($checkout_url) ? trim($checkout_url) : ''; if ($checkout_url === '') { return $checkout_url; } if (stripos($checkout_url, 'SourcePage=') !== false) { return qixit_append_referral_param($checkout_url); } $source_page_url = is_string($source_page_url) ? trim($source_page_url) : ''; if ($source_page_url === '') { $post_id = function_exists('get_queried_object_id') ? intval(get_queried_object_id()) : 0; if ($post_id > 0) { $source_page_url = get_permalink($post_id); } } if ($source_page_url === '') { return qixit_append_referral_param($checkout_url); } $parts = wp_parse_url($source_page_url); if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { return qixit_append_referral_param($checkout_url); } $normalized_source = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '') . (isset($parts['path']) ? $parts['path'] : '/'); $separator = (strpos($checkout_url, '?') !== false) ? '&' : '?'; return qixit_append_referral_param($checkout_url . $separator . 'SourcePage=' . rawurlencode($normalized_source)); } /** * Resolve the referral code for server-rendered checkout links. * * Referral precedence (whole platform): * 1. URL ?ref= / ?RefID= / ?refid= on the publisher page * 2. qixit_ref first-touch cookie on the publisher domain * 3. Owning vendor account username / stable referral code (server-side, platform) * 4. Platform default DAVE10_MP1_1936 (server-side, platform) * * WordPress can only see 1 and 2; the Qixit platform resolves 3 and 4 when the * checkout URL arrives without a code. * * @return string Sanitized referral code, or empty string. */ function qixit_resolve_referral_code() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only attribution, no state change. foreach (array('ref', 'RefID', 'refid') as $key) { if (isset($_GET[$key])) { $candidate = sanitize_text_field(wp_unslash($_GET[$key])); if ($candidate !== '') { return $candidate; } } } // phpcs:enable WordPress.Security.NonceVerification.Recommended if (isset($_COOKIE['qixit_ref'])) { $cookie = sanitize_text_field(wp_unslash($_COOKIE['qixit_ref'])); if ($cookie !== '') { return $cookie; } } return ''; } /** * Append the referral code to a server-rendered purchasefast/paymentfast URL. * * @param string $checkout_url Checkout URL. * @return string */ function qixit_append_referral_param($checkout_url) { $checkout_url = is_string($checkout_url) ? trim($checkout_url) : ''; if ($checkout_url === '') { return $checkout_url; } if (preg_match('/[?&](ref|RefID|refid)=/i', $checkout_url)) { return $checkout_url; } $code = qixit_resolve_referral_code(); if ($code === '') { return $checkout_url; } $separator = (strpos($checkout_url, '?') !== false) ? '&' : '?'; return $checkout_url . $separator . 'ref=' . rawurlencode($code); } /** * Create a one-time callback token for external payment redirects. * * @param string $action Callback action. * @param array $context Optional callback context. * @param int $ttl Token lifetime in seconds. * @return string */ function qixit_issue_callback_token($action, $context = array(), $ttl = 1200) { $action = sanitize_key($action); if ($action === '') { return ''; } $token = wp_generate_password(40, false, false); if (!is_array($context)) { $context = array(); } set_transient( 'qixit_callback_' . $token, array( 'action' => $action, 'context' => $context, 'created_at' => time(), ), max(60, intval($ttl)) ); return $token; } /** * Validate and escape a SQL table identifier for direct queries. * * @param string $table_name Candidate table name. * @return string Escaped table name, or empty string when invalid. */ function qixit_esc_table_name($table_name) { $table_name = (string) $table_name; if ($table_name === '' || !preg_match('/^[A-Za-z0-9_]+$/', $table_name)) { return ''; } return esc_sql($table_name); } /** * Validate payment-details table name from SHOW TABLES results. * * @param string $table_name Candidate table name. * @return string Escaped table name, or empty string when invalid. */ function qixit_esc_payment_details_table_name($table_name) { $table_name = (string) $table_name; if ($table_name === '' || strpos($table_name, 'qixit_payment_details') === false) { return ''; } return qixit_esc_table_name($table_name); } /** * Validate one-time callback token for payment redirects. * * @param string $token Callback token. * @param string $action Expected action. * @param array $required_context Optional required context key/value map. * @param bool $consume Whether to consume token on success. * @return bool */ function qixit_validate_callback_token($token, $action, $required_context = array(), $consume = false) { $token = sanitize_text_field((string) $token); $action = sanitize_key($action); if ($token === '' || $action === '') { return false; } $payload = get_transient('qixit_callback_' . $token); if (!is_array($payload)) { return false; } if (($payload['action'] ?? '') !== $action) { return false; } $context = isset($payload['context']) && is_array($payload['context']) ? $payload['context'] : array(); if (is_array($required_context)) { foreach ($required_context as $key => $value) { if (!array_key_exists($key, $context)) { return false; } if ((string) $context[$key] !== (string) $value) { return false; } } } if ($consume) { delete_transient('qixit_callback_' . $token); } return true; } /** * Require a valid one-time callback token (wp.org strict mode — no legacy bypass). * * @param string $token Callback token from query string. * @param string $action Expected callback action. * @param array $required_context Optional context map. * @param bool $consume Whether to consume token on success. * @return bool */ function qixit_require_callback_token($token, $action, $required_context = array(), $consume = true) { if (!qixit_validate_callback_token($token, $action, $required_context, $consume)) { if (function_exists('qixit_debug_log')) { qixit_debug_log('QIXIT SECURITY: rejected callback — missing or invalid token for action ' . sanitize_key($action)); } return false; } return true; } /** * Build canonical payload for ReportUrl HMAC verification (sorted query string). * * @param array $fields Associative callback fields. * @return string */ function qixit_build_report_signature_payload($fields) { if (!is_array($fields)) { return ''; } $normalized = array(); foreach ($fields as $key => $value) { $key = sanitize_key((string) $key); if ($key === '') { continue; } $normalized[$key] = is_scalar($value) ? (string) $value : ''; } ksort($normalized, SORT_STRING); $parts = array(); foreach ($normalized as $key => $value) { $parts[] = rawurlencode($key) . '=' . rawurlencode($value); } return implode('&', $parts); } /** * Mint a short-lived HMAC publisher token ("pubtok") for a checkout click. * * The platform uses this token as evidence that the referral code on a * purchasefast/paymentfast URL really came from a site holding the matching * site API key, rather than from a hand-edited query string. Signing reuses the * canonical payload format of qixit_build_report_signature_payload() so both * directions of the HMAC contract stay identical. * * Token format: base64url(json(claims)) . "." . hex(hmac-sha256(payload, api_key)) * * @param array $claims Extra claims (ref, ctx, rid). * @return string Token, or empty string when the site has no API key. */ function qixit_mint_publisher_token($claims = array()) { $api_key = get_option('qixit_site_api_key', ''); if ($api_key === '') { $api_key = get_option('qixit_developer_api_key', ''); } if ($api_key === '') { return ''; } $host = wp_parse_url(home_url('/'), PHP_URL_HOST); $fields = array( 'site' => is_string($host) ? $host : '', 'kid' => substr((string) $api_key, 0, 8), 'ts' => (string) time(), 'ref' => isset($claims['ref']) ? (string) $claims['ref'] : '', 'ctx' => isset($claims['ctx']) ? (string) $claims['ctx'] : '', 'rid' => isset($claims['rid']) ? (string) $claims['rid'] : '', ); $payload = qixit_build_report_signature_payload($fields); $signature = hash_hmac('sha256', $payload, $api_key); $encoded = rtrim(strtr(base64_encode(wp_json_encode($fields)), '+/', '-_'), '='); return $encoded . '.' . $signature; } /** * AJAX: mint a publisher token at click time. * * Minting per click (rather than baking the token into cached page HTML) keeps * the timestamp fresh so the platform can enforce a short replay window. */ function qixit_ajax_mint_publisher_token() { if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'qixit_frontend_nonce')) { wp_send_json_error(array('message' => __('Security check failed.', '2waymicropay')), 403); } $context = sanitize_key(wp_unslash($_POST['context'] ?? '')); if ($context !== 'paymentfast' && $context !== 'purchasefast') { $context = ''; } $resource_id = sanitize_text_field(wp_unslash($_POST['resourceId'] ?? '')); if ($resource_id !== '' && !preg_match('/^[a-f0-9\-]{8,64}$/i', $resource_id)) { $resource_id = ''; } $referral = sanitize_text_field(wp_unslash($_POST['ref'] ?? '')); if ($referral === '') { $referral = qixit_resolve_referral_code(); } $token = qixit_mint_publisher_token(array( 'ref' => $referral, 'ctx' => $context, 'rid' => $resource_id, )); if ($token === '') { wp_send_json_error(array('message' => __('This site has no Qixit API key configured.', '2waymicropay')), 400); } wp_send_json_success(array( 'pubtok' => $token, 'ref' => $referral, )); } add_action('wp_ajax_qixit_mint_publisher_token', 'qixit_ajax_mint_publisher_token'); add_action('wp_ajax_nopriv_qixit_mint_publisher_token', 'qixit_ajax_mint_publisher_token'); /** * Verify HMAC signature on a server-to-server ReportUrl callback. * * @param array $fields Parsed POST/REQUEST fields used for signing. * @param string $signature Header value from X-Qixit-Signature. * @return bool */ function qixit_verify_report_signature($fields, $signature) { $signature = sanitize_text_field((string) $signature); if ($signature === '') { return false; } $api_key = get_option('qixit_site_api_key', ''); if ($api_key === '') { $api_key = get_option('qixit_developer_api_key', ''); } if ($api_key === '') { return false; } $payload = qixit_build_report_signature_payload($fields); $expected = hash_hmac('sha256', $payload, $api_key); return hash_equals(strtolower($expected), strtolower($signature)); } /** * Validate ReportUrl timestamp to reduce replay window (default 10 minutes). * * @param string $timestamp ISO8601 timestamp from callback. * @param int $max_age_seconds Maximum age in seconds. * @return bool */ function qixit_validate_report_timestamp($timestamp, $max_age_seconds = 600) { $timestamp = sanitize_text_field((string) $timestamp); if ($timestamp === '') { return false; } $parsed = strtotime($timestamp); if ($parsed === false) { return false; } $age = time() - $parsed; return $age >= 0 && $age <= max(60, intval($max_age_seconds)); } /** * Verify a purchase transaction against Products Portal before granting access. * * @param string $txn_id Transaction GUID. * @param string $product_guid Optional product GUID constraint. * @param string $qixit_user Optional expected buyer username. * @param float $amount Optional expected amount. * @return array { success: bool, data?: array, message?: string } */ function qixit_verify_purchase_with_portal($txn_id, $product_guid = '', $qixit_user = '', $amount = null) { $txn_id = sanitize_text_field((string) $txn_id); if ($txn_id === '') { return array('success' => false, 'message' => 'Missing transaction ID'); } $api = Qixit_API::get_instance(); $result = $api->verify_purchase($txn_id, $product_guid); if (empty($result['success']) || empty($result['data'])) { return array( 'success' => false, 'message' => isset($result['message']) ? $result['message'] : 'Transaction verification failed', ); } $data = $result['data']; if (empty($data['valid'])) { return array('success' => false, 'message' => 'Transaction not valid'); } if (!empty($data['refunded'])) { return array('success' => false, 'message' => 'Transaction was refunded'); } if ($product_guid !== '' && !empty($data['productGuid']) && strcasecmp($product_guid, $data['productGuid']) !== 0) { return array('success' => false, 'message' => 'Product mismatch'); } if ($qixit_user !== '' && !empty($data['qixitUsername']) && strcasecmp($qixit_user, $data['qixitUsername']) !== 0) { return array('success' => false, 'message' => 'Buyer mismatch'); } if ($amount !== null && isset($data['amount'])) { if (abs(floatval($data['amount']) - floatval($amount)) > 0.0001) { return array('success' => false, 'message' => 'Amount mismatch'); } } return array('success' => true, 'data' => $data); } /** * Get branding configuration from server * Fetches CSS, JS, and config URLs from the centralized branding endpoint * Cached for 1 hour to reduce API calls * * @return array|null Branding config or null on error */ function qixit_get_branding_config() { // Check cache first $cached = get_transient('qixit_branding_config'); if ($cached !== false) { return $cached; } $api_url = qixit_get_products_api_url(); // Make request to branding config endpoint. // This runs on frontend page loads — keep timeout tight so a down VM // doesn't delay visitors. The result is cached for 1 hour anyway. $response = wp_remote_get( $api_url . '/api/branding/config', array( 'timeout' => 1.5, 'headers' => array( 'Accept' => 'application/json', ), ) ); if (is_wp_error($response)) { if (function_exists('qixit_debug_log')) { qixit_debug_log('Branding config fetch error: ' . $response->get_error_message()); } return null; } $status_code = wp_remote_retrieve_response_code($response); if ($status_code !== 200) { if (function_exists('qixit_debug_log')) { qixit_debug_log('Branding config fetch failed with status: ' . $status_code); } return null; } $body = wp_remote_retrieve_body($response); $config = json_decode($body, true); if (empty($config) || !is_array($config)) { if (function_exists('qixit_debug_log')) { qixit_debug_log('Branding config invalid JSON response'); } return null; } // Manifest contract supplies cacheTtlSeconds; enforce a sane range (5-15 minutes). $ttl = 10 * MINUTE_IN_SECONDS; if (isset($config['cacheTtlSeconds'])) { $configured_ttl = intval($config['cacheTtlSeconds']); if ($configured_ttl > 0) { $ttl = min(15 * MINUTE_IN_SECONDS, max(5 * MINUTE_IN_SECONDS, $configured_ttl)); } } set_transient('qixit_branding_config', $config, $ttl); if (function_exists('qixit_debug_log')) { qixit_debug_log('Branding config fetched and cached, version: ' . ($config['version'] ?? 'unknown')); } return $config; } /** * Clear branding config cache * Call this when admin updates settings or on version updates */ function qixit_clear_branding_cache() { delete_transient('qixit_branding_config'); } /** * Resolve branding asset URL from manifest with safe fallback. * * @param string $asset_key css|js|logoSvg|logoPng * @return string */ function qixit_get_branding_asset_url($asset_key) { $config = qixit_get_branding_config(); if (!empty($config['urls']) && !empty($config['urls'][$asset_key])) { return esc_url_raw($config['urls'][$asset_key]); } return ''; } /** * Resolve branding logo URL from manifest with API fallback. * * @return string */ function qixit_get_branding_logo_url() { // Prefer PNG first to preserve the classic branded logo mark. $png = qixit_get_branding_asset_url('logoPng'); if (!empty($png)) { return $png; } return qixit_get_branding_asset_url('logoSvg'); } /** * Check if plugin is configured * * @return bool True if configured */ function qixit_is_configured() { $site_key = get_option('qixit_site_api_key', ''); if (empty($site_key)) { $site_key = get_option('qixit_developer_api_key', ''); } return !empty($site_key); } /** * Check if authenticated with API * * @return bool True if authenticated */ function qixit_is_authenticated() { $auth = Qixit_Auth::get_instance(); return $auth->is_authenticated(); } /** * Get vendor ID * * @return int Vendor ID */ function qixit_get_vendor_id() { return intval(get_option('qixit_vendor_id', 0)); } /** * Log a message * * @param string $message Message to log * @param string $level Log level (info, error, warning) */ function qixit_log($message, $level = 'info') { $logger = Qixit_Logger::get_instance(); $logger->log($message, $level); } /** * Sanitize and validate a GUID * * @param string $guid GUID string * @return string|false Sanitized GUID or false */ function qixit_sanitize_guid($guid) { $guid = sanitize_text_field($guid); // Check if valid GUID format if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', $guid)) { return strtolower($guid); } return false; } /** * Get premium content excerpt * * @param int $post_id Post ID * @param int $word_count Number of words to show * @return string Excerpt with paywall message */ function qixit_get_premium_excerpt($post_id, $word_count = 50) { $post = get_post($post_id); if (!$post) { return ''; } // Get first N words $content = wp_strip_all_tags($post->post_content); $words = explode(' ', $content); $excerpt = implode(' ', array_slice($words, 0, $word_count)); if (count($words) > $word_count) { $excerpt .= '...'; } return $excerpt; } /** * Get paywall message HTML * * @param int $post_id Post ID * @return string Paywall HTML */ function qixit_get_paywall_html($post_id) { $product = Qixit_Product::get_instance(); $post_product = $product->get_product_by_post_id($post_id); if (!$post_product || empty($post_product->post_qixit_PID_guid)) { return '
' . esc_html__('This content is not available.', '2waymicropay') . '