3500 || ! preg_match( '/^([A-Za-z0-9_%~-]+)\.([a-f0-9]{64})$/', $raw, $parts ) ) { return array(); } if ( ! hash_equals( hash_hmac( 'sha256', 'refm_link_clicks|' . $parts[1], REFM_Data::cookie_key() ), $parts[2] ) ) { return array(); } $items = json_decode( rawurldecode( $parts[1] ), true, 3 ); if ( ! is_array( $items ) || count( $items ) > self::MAX_LINKS ) { return array(); } $valid = array(); $now = time(); foreach ( $items as $key => $expires ) { if ( is_string( $key ) && preg_match( '/^[1-9][0-9]{0,18}:[a-f0-9]{64}$/', $key ) && is_int( $expires ) && $expires > $now ) { $valid[ $key ] = $expires; } } return $valid; } /** * Keep canonical same-origin public URLs, without tracking parameters or fragments. * * @param string $url Canonical URL generated by WordPress. * @return string Safe URL or an empty string. */ public static function normalize_url( $url ) { $parts = is_string( $url ) ? wp_parse_url( $url ) : false; $home = wp_parse_url( home_url( '/' ) ); if ( ! $parts || ! isset( $parts['host'], $parts['scheme'] ) || ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) || strtolower( $parts['host'] ) !== strtolower( $home['host'] ) || ( $parts['port'] ?? null ) !== ( $home['port'] ?? null ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) { return ''; } $url = preg_replace( '/#.*$/', '', $url ); $url = remove_query_arg( array_merge( REFM_Settings::referral_keys(), array( 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid' ) ), $url ); $url = set_url_scheme( esc_url_raw( $url ), $home['scheme'] ); return strlen( $url ) <= 2048 ? $url : ''; } /** Resolve the public destination from WordPress, never from an arbitrary submitted URL. */ public static function landing_url() { if ( is_admin() || wp_doing_ajax() || is_feed() || is_preview() || is_404() || is_search() || is_cart() || is_checkout() || is_account_page() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { return ''; } if ( is_singular() ) { $post = get_queried_object(); return $post instanceof WP_Post && is_post_publicly_viewable( $post ) && '' === $post->post_password ? self::normalize_url( get_permalink( $post ) ) : ''; } if ( is_front_page() || is_home() ) { $page = is_home() ? (int) get_option( 'page_for_posts' ) : 0; return self::normalize_url( $page ? get_permalink( $page ) : home_url( '/' ) ); } if ( is_tax() || is_category() || is_tag() ) { $term = get_queried_object(); $tax = $term instanceof WP_Term ? get_taxonomy( $term->taxonomy ) : false; $url = $tax && is_taxonomy_viewable( $tax ) ? get_term_link( $term ) : ''; return is_wp_error( $url ) ? '' : self::normalize_url( $url ); } if ( is_post_type_archive() ) { $type = get_queried_object(); return $type instanceof WP_Post_Type && is_post_type_viewable( $type ) ? self::normalize_url( get_post_type_archive_link( $type->name ) ) : ''; } if ( is_author() ) { return self::normalize_url( get_author_posts_url( get_queried_object_id() ) ); } if ( is_date() ) { $year = (int) get_query_var( 'year' ); $month = (int) get_query_var( 'monthnum' ); $day = (int) get_query_var( 'day' ); $url = is_day() ? get_day_link( $year, $month, $day ) : ( is_month() ? get_month_link( $year, $month ) : get_year_link( $year ) ); return self::normalize_url( $url ); } return ''; } /** * Start unified URL tracking once, without importing older product or URL counters. * * @return bool Whether the shared link store is ready. */ public static function migrate() { if ( 'yes' === get_option( 'refm_unified_link_tracking' ) ) { return self::cleanup_legacy(); } global $wpdb; $result = REFM_Store::locked( 0, static function () use ( $wpdb ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Recheck the durable initialization marker while holding the shared initialization lock. if ( 'yes' === $wpdb->get_var( $wpdb->prepare( 'SELECT option_value FROM %i WHERE option_name=%s', $wpdb->options, 'refm_unified_link_tracking' ) ) ) { return true; } // Explicitly start afresh: discard previously imported counters without touching financial/customer records. foreach ( array( 'link_daily', 'link_totals', 'links' ) as $name ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Authorized one-time reset of only the three referral analytics tables; DELETE stays transactional with the completion marker. REFM_Store::must( $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', REFM_Store::table( $name ) ) ) ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- The marker commits with initialization so a retry cannot delete newly recorded clicks. REFM_Store::must( $wpdb->query( $wpdb->prepare( 'INSERT INTO %i (option_name,option_value,autoload) VALUES (%s,%s,%s) ON DUPLICATE KEY UPDATE option_value=VALUES(option_value)', $wpdb->options, 'refm_unified_link_tracking', 'yes', 'off' ) ) ); return true; } ); wp_cache_delete( 'refm_unified_link_tracking', 'options' ); wp_cache_delete( 'notoptions', 'options' ); if ( true !== $result ) { return false; } delete_option( 'refm_link_summary_migrated' ); return self::cleanup_legacy(); } /** Remove obsolete click storage after successful initialization. */ private static function cleanup_legacy() { if ( 'yes' === get_option( 'refm_legacy_clicks_removed' ) ) { return true; } global $wpdb; foreach ( array( 'clicks', 'link_clicks', 'url_clicks' ) as $name ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Explicitly authorized retirement of three obsolete click-only tables, after initialization commits; financial/customer tables are excluded. if ( false === $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', REFM_Store::table( $name ) ) ) ) { return false; } } delete_option( 'refm_url_clicks_migrated' ); update_option( 'refm_legacy_clicks_removed', 'yes', false ); return true; } /** * Increment daily history and its ranking total together, sharing one canonical URL row. * * @param int $affiliate_id Referring affiliate. * @param string $url Canonical destination URL. * @return bool Whether both counters committed. */ private static function increment( $affiliate_id, $url ) { $result = REFM_Store::locked( $affiliate_id, static function () use ( $affiliate_id, $url ) { global $wpdb; // Refresh eligibility under the same affiliate lock as counter erasure. wp_cache_delete( $affiliate_id, 'user_meta' ); if ( ! REFM_Tracking::active( $affiliate_id ) ) { return new WP_Error( 'affiliate', 'Referral tracking is unavailable.' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Unique hash plus LAST_INSERT_ID resolves the canonical link safely under concurrent requests. REFM_Store::must( $wpdb->query( $wpdb->prepare( 'INSERT INTO %i (url_hash,link_url) VALUES (%s,%s) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)', REFM_Store::table( 'links' ), hash( 'sha256', $url ), $url ) ) ); $link_id = (int) $wpdb->insert_id; if ( ! $link_id ) { return new WP_Error( 'link', 'Could not identify the referral URL.' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Atomic daily increment in the same transaction as the ranking summary. REFM_Store::must( $wpdb->query( $wpdb->prepare( 'INSERT INTO %i (affiliate_id,link_id,click_date,clicks) VALUES (%d,%d,%s,1) ON DUPLICATE KEY UPDATE clicks=clicks+1', REFM_Store::table( 'link_daily' ), $affiliate_id, $link_id, current_time( 'Y-m-d' ) ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Keep the indexed lifetime ranking current without scanning daily history. REFM_Store::must( $wpdb->query( $wpdb->prepare( 'INSERT INTO %i (affiliate_id,link_id,clicks) VALUES (%d,%d,1) ON DUPLICATE KEY UPDATE clicks=clicks+1', REFM_Store::table( 'link_totals' ), $affiliate_id, $link_id ) ) ); return true; } ); return true === $result; } /** * Remove one affiliate's normalized counters together, leaving shared public URL metadata. * * @param int $affiliate_id Affiliate whose records must be erased. * @return int|WP_Error Number of rows removed or a storage error. */ public static function erase_counters( $affiliate_id ) { $affiliate_id = absint( $affiliate_id ); return REFM_Store::locked( $affiliate_id, static function () use ( $affiliate_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- The privacy/delete-user path removes only this affiliate's daily history under its counter lock. $daily = REFM_Store::must( $wpdb->delete( REFM_Store::table( 'link_daily' ), array( 'affiliate_id' => $affiliate_id ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Delete the corresponding summary in the same transaction so lists and charts stay consistent. $total = REFM_Store::must( $wpdb->delete( REFM_Store::table( 'link_totals' ), array( 'affiliate_id' => $affiliate_id ) ) ); return $daily + $total; } ); } /** * Count a public URL landing after capture has validated the referral token. * * @param int $affiliate_id Referring affiliate. * @return bool Whether a new URL click was counted. */ public static function record( $affiliate_id ) { $affiliate_id = absint( $affiliate_id ); if ( headers_sent() || ! REFM_Settings::program_enabled() || ! REFM_Tracking::active( $affiliate_id ) || get_current_user_id() === $affiliate_id || ! apply_filters( 'refm_tracking_allowed', true, $affiliate_id ) ) { return false; } $url = self::landing_url(); if ( ! $url || ! self::migrate() ) { return false; } $recent = self::recent_links(); $key = $affiliate_id . ':' . hash( 'sha256', $url ); if ( isset( $recent[ $key ] ) ) { return false; } if ( ! self::increment( $affiliate_id, $url ) ) { return false; } $expires = time() + 30 * MINUTE_IN_SECONDS; $recent[ $key ] = $expires; $recent = array_slice( $recent, -self::MAX_LINKS, null, true ); $packet = rawurlencode( wp_json_encode( $recent ) ); $value = $packet . '.' . hash_hmac( 'sha256', 'refm_link_clicks|' . $packet, REFM_Data::cookie_key() ); setcookie( self::COOKIE, $value, array( 'expires' => $expires, 'path' => '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ) ); $_COOKIE[ self::COOKIE ] = $value; REFM_Cache::no_cache( 'Refmora URL referral click' ); return true; } /** * Authorize managers globally, and enrolled affiliates only for their own rows. * * @param int|null $affiliate_id Null for manager-wide rankings. * @return bool Whether the caller may read this scope. */ private static function can_view( $affiliate_id ) { if ( current_user_can( 'manage_woocommerce' ) ) { return true; } return null !== $affiliate_id && absint( $affiliate_id ) > 0 && get_current_user_id() === absint( $affiliate_id ) && 'none' !== REFM_Affiliates::status( absint( $affiliate_id ) ); } /** * Read only allowed, non-mutating list filters. * * @return array Minimum click filter and search text. */ private static function requested_filters() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation; report capabilities and ownership are checked by render() and listing(). $filter = sanitize_key( REFM_Security::value( wp_unslash( $_GET ), 'refm_link_filter', 'all' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only search is sanitized and escaped with esc_like() before prepared SQL. $search = mb_substr( sanitize_text_field( REFM_Security::value( wp_unslash( $_GET ), 'refm_link_search' ) ), 0, 180 ); return array( 'filter' => in_array( $filter, array( 'all', '10', '100' ), true ) ? $filter : 'all', 'search' => $search, ); } /** * Read one bounded page of clicked referral URLs, or a five-row overview preview. * * @param int|null $affiliate_id Null for authorized managers, otherwise the permitted affiliate. * @param int $page Requested page. * @param array $options Per-page size, preview flag, click-count filter and search text. * @return array Rows, independent pagination metadata and applied filters. */ public static function listing( $affiliate_id = null, $page = 1, $options = array() ) { $empty = array( 'rows' => array(), 'count' => 0, 'page' => 1, 'pages' => 1, 'per_page' => 10, 'filter' => 'all', 'search' => '', ); if ( ! self::can_view( $affiliate_id ) ) { return $empty; } global $wpdb; $options = is_array( $options ) ? $options : array(); $preview = ! empty( $options['preview'] ); $per_page = $preview || 5 === ( $options['per_page'] ?? 10 ) ? 5 : 10; $filter = isset( $options['filter'] ) && in_array( $options['filter'], array( 'all', '10', '100' ), true ) ? $options['filter'] : 'all'; $search = isset( $options['search'] ) && is_string( $options['search'] ) ? mb_substr( sanitize_text_field( $options['search'] ), 0, 180 ) : ''; $like = '%' . $wpdb->esc_like( $search ) . '%'; $minimum = 'all' === $filter ? 1 : (int) $filter; $where = ' WHERE l.clicks>=%d'; $values = array( $minimum ); if ( null !== $affiliate_id ) { $where .= ' AND l.affiliate_id=%d'; $values[] = absint( $affiliate_id ); } $from = ' FROM %i AS l'; $args = array( REFM_Store::table( 'link_totals' ) ); if ( '' !== $search ) { $from .= ' INNER JOIN %i AS k ON k.id=l.link_id LEFT JOIN %i AS u ON u.ID=l.affiliate_id'; $args[] = REFM_Store::table( 'links' ); $args[] = $wpdb->users; $where .= ' AND (k.link_url LIKE %s OR u.display_name LIKE %s OR u.user_login LIKE %s)'; $values = array_merge( $values, array( $like, $like, $like ) ); } $count = 0; $pages = 1; $page = $preview ? 1 : REFM_Security::page_number( $page ); if ( ! $preview ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders -- Only fixed SQL fragments are combined; identifiers, ownership and filters are prepared together. $count_sql = $wpdb->prepare( 'SELECT COUNT(*)' . $from . $where, array_merge( $args, $values ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.NotPrepared -- Count summary rows only, with no daily table, grouping or sums; prepared above. $count = (int) $wpdb->get_var( $count_sql ); $pages = max( 1, (int) ceil( $count / $per_page ) ); $page = min( $page, $pages ); } if ( '' === $search ) { $from .= ' INNER JOIN %i AS k ON k.id=l.link_id'; $args[] = REFM_Store::table( 'links' ); } $row_args = array_merge( $args, $values, array( $per_page, ( $page - 1 ) * $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders -- Fixed fragments plus prepared identifiers/filters and bounded pagination; sort order matches the ranking indexes in reverse. $rows_sql = $wpdb->prepare( 'SELECT l.affiliate_id,l.link_id,k.link_url,l.clicks' . $from . $where . ' ORDER BY l.clicks DESC,l.link_id DESC,l.affiliate_id DESC LIMIT %d OFFSET %d', $row_args ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.NotPrepared -- Prepared above; indexed summary rankings fetch only five or ten rows without aggregating daily history. $rows = $wpdb->get_results( $rows_sql ); return array( 'rows' => $rows, 'count' => $preview ? count( $rows ) : $count, 'page' => $page, 'pages' => $pages, 'per_page' => $per_page, 'filter' => $filter, 'search' => $search, ); } /** * Render the dedicated manager tab's read-only search and minimum-click filter. * * @param array $listing Applied filters. */ private static function filters( $listing ) { echo '
'; } /** * Render a full-width Free table in an authorized dashboard context. * * @param int|null $affiliate_id Current affiliate ID for the frontend; null for admin. * @param string $context Overview, Refer Links tab, or affiliate dashboard. */ public static function render( $affiliate_id = null, $context = 'overview' ) { if ( ! in_array( $context, array( 'overview', 'admin', 'affiliate' ), true ) || ! self::can_view( $affiliate_id ) ) { return; } if ( 'affiliate' === $context ) { if ( ! $affiliate_id || get_current_user_id() !== absint( $affiliate_id ) ) { return; } } elseif ( null !== $affiliate_id || ! current_user_can( 'manage_woocommerce' ) ) { return; } $options = 'admin' === $context ? self::requested_filters() : array(); $options['per_page'] = 'admin' === $context ? 10 : 5; $options['preview'] = 'overview' === $context; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page selection; capabilities and affiliate ownership are enforced before querying. $listing = self::listing( $affiliate_id, 'overview' === $context ? 1 : REFM_Security::page( $_GET, 'refm_lp' ), $options ); $rows = $listing['rows']; $frontend = 'affiliate' === $context; if ( $rows ) { $users = array_unique( array_map( 'absint', array_column( $rows, 'affiliate_id' ) ) ); cache_users( $users ); update_meta_cache( 'user', $users ); } echo '' . esc_html__( 'Ranked by referral URL clicks, all time.', 'refmora' ) . '
| # | ' . esc_html__( 'Referral URL', 'refmora' ) . ' | ' . esc_html__( 'Clicks', 'refmora' ) . ' | ' . esc_html__( 'Affiliate name', 'refmora' ) . ' | ' . esc_html__( 'View Link', 'refmora' ) . ' |
|---|---|---|---|---|
| ' . esc_html( number_format_i18n( ++$rank ) ) . ' | '; if ( $link ) { echo ''; } else { echo '' . esc_html__( 'Unavailable', 'refmora' ) . ''; } echo ' | ' . esc_html( number_format_i18n( $row->clicks ) ) . ' | ' . esc_html( $user ? $user->display_name : __( 'Deleted affiliate', 'refmora' ) ) . ' | '; $view_url = $frontend ? $link : $url; echo ''; if ( $view_url ) { echo '' . wp_kses( REFM_UI::icon( 'view' ), REFM_UI::allowed_html() ) . '' . esc_html__( 'View Link', 'refmora' ) . ''; } else { echo '' . esc_html__( 'Unavailable', 'refmora' ) . ''; } echo ' |
| ' . esc_html__( 'No clicked referral links match this list.', 'refmora' ) . ' | ||||