✏️ 正在编辑: wc-account.php
路径:
/home/h359620/public_html/wp-content/themes/datis/inc/includes/woocommerce/wc-account.php
提示:
您可以编辑任何文件(包括二进制文件),但请注意不当修改可能导致文件损坏。
<?php /** * Datis WooCommerce My Account Manager * * Features: * - Registers custom My Account endpoints (comments, notifications, consultations, wishlist) * - Adds menu items + icon classes * - Frontend "Fallback Avatar" uploader for users WITHOUT Gravatar * - Upload is LIMITED to this flow only (no wp.media, no wp-admin async-upload) * - Accepts only PNG / JPG / JPEG * - Auto-saves on file selection and reloads the page * - Stores selected fallback avatar as a user meta attachment ID: * meta_key: datis_fallback_avatar_id * * Security notes: * - Uses a dedicated AJAX action + nonce * - Validates mime types and file extension * - Uses wp_handle_upload + wp_insert_attachment to create a Media Library attachment * * Requirements: * - WooCommerce My Account page exists * - jQuery available (WP core) * * Usage in templates: * - Add button/link with data attributes from get_avatar_picker_button_attrs() * Example: * $attrs = Datis_WC_MyAccount_Manager::get_instance()->get_avatar_picker_button_attrs(); * <a href="#" <?php foreach($attrs as $k=>$v){echo esc_attr($k).'="'.esc_attr($v).'" ';} ?>>Edit profile image</a> */ if (! defined('ABSPATH')) { exit; } if (! class_exists('Datis_WC_MyAccount_Manager')) { class Datis_WC_MyAccount_Manager { /** * @var Datis_WC_MyAccount_Manager|null */ private static $instance = null; /** * @var string */ private $text_domain = 'datis'; /** * Template base path for Woo templates inside theme: * your-theme/woocommerce/myaccount/{endpoint}.php * * @var string */ private $template_base = 'myaccount/'; /** * Custom endpoints configuration. * * @var array */ private $custom_endpoints = array( 'comments' => array('inject_after' => 'downloads'), 'notifications' => array('inject_after' => 'edit-account'), 'consultations' => array('inject_after' => 'edit-account'), 'booking' => array('inject_after' => 'edit-account'), 'wishlist' => array('inject_after' => 'edit-account'), 'consultation-request' => array(), ); /** * Private constructor – use get_instance(). */ private function __construct() { $this->init_hooks(); } /** * Register all hooks. */ private function init_hooks() { // Register endpoints. add_action('init', array($this, 'register_endpoints')); // Add items to My Account menu. add_filter('woocommerce_account_menu_items', array($this, 'filter_menu_items'), 99); // Render endpoint content for each endpoint. foreach (array_keys($this->custom_endpoints) as $endpoint) { add_action("woocommerce_account_{$endpoint}_endpoint", array($this, 'render_endpoint')); } // Flush rewrite rules safely once when theme is switched. add_action('after_switch_theme', array($this, 'maybe_flush_rewrite_rules')); // Add icon classes. add_filter('woocommerce_account_menu_item_classes', array($this, 'add_menu_icon_classes'), 10, 2); /** * Fallback Avatar (frontend uploader) */ add_action('wp_enqueue_scripts', array($this, 'enqueue_avatar_uploader_assets')); add_action('wp_ajax_datis_upload_fallback_avatar', array($this, 'ajax_upload_fallback_avatar')); add_action('wp_ajax_datis_save_fallback_avatar', array($this, 'ajax_save_fallback_avatar')); // optional: keep for "select existing attachment" flows /** * Avatar output (sitewide): if no gravatar, use fallback attachment. */ add_filter('get_avatar_url', array($this, 'filter_avatar_url'), 10, 3); // ✅ make sure "paged" is a public query var (does not hurt) add_filter('query_vars', array($this, 'register_paged_query_var')); add_filter('datis_myaccount_consult_request_details_url', array($this, 'filter_consult_request_details_url'), 10, 3); /** * ✅ Pagination normalization for My Account endpoints * If endpoint value contains "page/2", inject standard "paged" query var. * * IMPORTANT: * - Do NOT rely on is_account_page() here (request filter runs early). * - Detect My Account via rewritten vars: pagename == my-account-slug. */ add_filter('request', array($this, 'normalize_account_endpoint_paged'), 9); /** * ✅ Ensure Woo knows our custom endpoints query vars. * Safe: adds missing keys only. */ add_filter('woocommerce_get_query_vars', array($this, 'register_custom_query_vars'), 20); } public function register_paged_query_var($vars) { if (!in_array('paged', $vars, true)) { $vars[] = 'paged'; } return $vars; } public function filter_consult_request_details_url($url, $request_id, $user_id) { $request_id = absint($request_id); $user_id = absint($user_id); if ($request_id <= 0 || $user_id <= 0 || !function_exists('wc_get_account_endpoint_url')) { return ''; } $post = get_post($request_id); if (!$post || $post->post_type !== 'consultation_request') { return ''; } // Ownership check: meta user_id OR post_author $meta_user_id = (int) get_post_meta($request_id, '_datis_cr_user_id', true); $author_id = (int) $post->post_author; if ($meta_user_id !== $user_id && $author_id !== $user_id) { return ''; } $base = wc_get_account_endpoint_url('consultation-request'); // /my-account/consultation-request/{ID}/ return trailingslashit($base) . $request_id . '/'; } /** * User meta key for fallback avatar attachment id. */ private function get_fallback_avatar_meta_key() { return 'datis_fallback_avatar_id'; } /** * Get gravatar existence for an email (cached). * * @param string $email * @return bool */ public function user_has_gravatar($email) { $email = trim(strtolower((string) $email)); if ($email === '' || ! is_email($email)) { return false; } $key = 'datis_has_gravatar_' . md5($email); $cached = get_transient($key); if ($cached !== false) { return (bool) $cached; } $hash = md5($email); $url = "https://www.gravatar.com/avatar/{$hash}?d=404&s=80"; $response = wp_remote_head($url, array('timeout' => 3)); $has = false; if (! is_wp_error($response)) { $code = (int) wp_remote_retrieve_response_code($response); $has = ($code === 200); } set_transient($key, $has ? 1 : 0, 12 * HOUR_IN_SECONDS); return $has; } /** * Enqueue JS uploader on My Account pages only. */ public function enqueue_avatar_uploader_assets() { if (! function_exists('is_account_page') || ! is_account_page()) { return; } $handle = 'datis-avatar-uploader'; $src = get_template_directory_uri() . '/assets/js/avatar-picker.js'; wp_enqueue_script( $handle, $src, array('jquery'), '1.0.0', true ); $ajax_url = admin_url('admin-ajax.php'); $ajax_url = set_url_scheme($ajax_url, is_ssl() ? 'https' : 'http'); wp_localize_script($handle, 'DatisAvatar', array( 'ajax_url' => $ajax_url, 'nonce' => wp_create_nonce('datis_avatar_upload_nonce'), 'i18n_select_file' => esc_html_x('Select an image', 'Avatar upload', $this->text_domain), 'i18n_uploading' => esc_html_x('Uploading...', 'Avatar upload', $this->text_domain), 'i18n_failed' => esc_html_x('Upload failed. Please try again.', 'Avatar upload', $this->text_domain), )); } public function ajax_upload_fallback_avatar() { if (! is_user_logged_in()) { wp_send_json_error(array('message' => 'not_logged_in'), 401); } check_ajax_referer('datis_avatar_upload_nonce', 'nonce'); $user = wp_get_current_user(); if ($user && ! empty($user->user_email) && $this->user_has_gravatar($user->user_email)) { wp_send_json_success(array('skipped' => 1)); } if (empty($_FILES['file'])) { wp_send_json_error(array('message' => 'missing_file'), 400); } $file = $_FILES['file']; $allowed_mimes = array( 'image/jpeg' => 'jpg', 'image/png' => 'png', ); $type = isset($file['type']) ? (string) $file['type'] : ''; if (!isset($allowed_mimes[$type])) { wp_send_json_error(array('message' => 'invalid_type'), 415); } $name = isset($file['name']) ? (string) $file['name'] : ''; $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if (!in_array($ext, array('jpg', 'jpeg', 'png'), true)) { wp_send_json_error(array('message' => 'invalid_extension'), 415); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $overrides = array( 'test_form' => false, 'mimes' => array( 'jpg|jpeg' => 'image/jpeg', 'png' => 'image/png', ), ); $uploaded = wp_handle_upload($file, $overrides); if (isset($uploaded['error'])) { wp_send_json_error(array('message' => $uploaded['error']), 400); } $path = $uploaded['file']; $url = $uploaded['url']; $attachment = array( 'post_mime_type' => $type, 'post_title' => sanitize_file_name(basename($path)), 'post_content' => '', 'post_status' => 'inherit', ); $attach_id = wp_insert_attachment($attachment, $path); if (is_wp_error($attach_id) || !$attach_id) { wp_send_json_error(array('message' => 'attach_failed'), 500); } $meta = wp_generate_attachment_metadata($attach_id, $path); wp_update_attachment_metadata($attach_id, $meta); update_user_meta(get_current_user_id(), $this->get_fallback_avatar_meta_key(), (int) $attach_id); wp_send_json_success(array( 'attachment_id' => (int) $attach_id, 'thumb' => wp_get_attachment_image_url($attach_id, 'thumbnail'), 'full' => wp_get_attachment_image_url($attach_id, 'full'), 'url' => $url, )); } public function ajax_save_fallback_avatar() { if (! is_user_logged_in()) { wp_send_json_error(array('message' => 'not_logged_in'), 401); } check_ajax_referer('datis_avatar_upload_nonce', 'nonce'); $user = wp_get_current_user(); if ($user && ! empty($user->user_email) && $this->user_has_gravatar($user->user_email)) { wp_send_json_success(array('skipped' => 1)); } $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0; if ($attachment_id <= 0) { wp_send_json_error(array('message' => 'invalid_attachment'), 400); } $mime = get_post_mime_type($attachment_id); if (! $mime || (strpos($mime, 'image/jpeg') !== 0 && strpos($mime, 'image/png') !== 0 && strpos($mime, 'image/') !== 0)) { wp_send_json_error(array('message' => 'not_image'), 400); } update_user_meta(get_current_user_id(), $this->get_fallback_avatar_meta_key(), $attachment_id); wp_send_json_success(array( 'attachment_id' => $attachment_id, 'thumb' => wp_get_attachment_image_url($attachment_id, 'thumbnail'), 'full' => wp_get_attachment_image_url($attachment_id, 'full'), )); } public function filter_avatar_url($url, $id_or_email, $args) { $user = $this->resolve_user_from_avatar_input($id_or_email); if (! $user || empty($user->user_email)) { return $url; } if ($this->user_has_gravatar($user->user_email)) { return $url; } $fallback_id = (int) get_user_meta($user->ID, $this->get_fallback_avatar_meta_key(), true); if ($fallback_id <= 0) { return $url; } $size = ! empty($args['size']) ? absint($args['size']) : 96; $img = wp_get_attachment_image_url($fallback_id, array($size, $size)); return $img ? $img : $url; } private function resolve_user_from_avatar_input($id_or_email) { if (is_numeric($id_or_email)) { return get_user_by('id', absint($id_or_email)); } if (is_object($id_or_email)) { if (! empty($id_or_email->user_id)) { return get_user_by('id', absint($id_or_email->user_id)); } if ($id_or_email instanceof WP_User) { return $id_or_email; } } if (is_string($id_or_email) && is_email($id_or_email)) { return get_user_by('email', $id_or_email); } return false; } public function get_avatar_picker_button_attrs($user_id = 0) { $user_id = $user_id ? absint($user_id) : get_current_user_id(); $user = get_user_by('id', $user_id); $has_gravatar = 0; if ($user && ! empty($user->user_email)) { $has_gravatar = $this->user_has_gravatar($user->user_email) ? 1 : 0; } return array( 'data-datis-avatar-picker' => '1', 'data-picker-disabled' => (string) ($has_gravatar === 1 ? 1 : 0), ); } public function register_endpoints() { foreach (array_keys($this->custom_endpoints) as $endpoint) { add_rewrite_endpoint($endpoint, EP_ROOT | EP_PAGES); } } private function get_custom_labels() { return array( 'comments' => _x('Comments', 'My Account menu label', $this->text_domain), 'notifications' => _x('Notifications', 'My Account menu label', $this->text_domain), 'consultations' => _x('Consultations', 'My Account menu label', $this->text_domain), 'booking' => _x('Consultations Booking', 'My Account menu label', $this->text_domain), 'wishlist' => _x('Wishlist', 'My Account menu label', $this->text_domain), ); } public function filter_menu_items($items) { if (! function_exists('WC')) { return $items; } $labels = $this->get_custom_labels(); $new_items = array(); foreach ($items as $key => $label) { $new_items[$key] = $label; foreach ($this->custom_endpoints as $endpoint_key => $data) { if (isset($data['inject_after']) && $data['inject_after'] === $key) { if (isset($labels[$endpoint_key])) { $new_items[$endpoint_key] = $labels[$endpoint_key]; } } } } return $new_items; } public function render_endpoint() { $current_action = current_filter(); $endpoint = str_replace(array('woocommerce_account_', '_endpoint'), '', $current_action); if (empty($endpoint) || ! isset($this->custom_endpoints[$endpoint])) { echo '<p>' . esc_html__('Invalid endpoint.', $this->text_domain) . '</p>'; return; } if (function_exists('wc_get_template')) { wc_get_template($this->template_base . $endpoint . '.php'); return; } echo '<p>' . esc_html__('WooCommerce is not available.', $this->text_domain) . '</p>'; } public function add_menu_icon_classes($classes, $endpoint) { $map = array( 'dashboard' => 'account-ico account-ico-dashboard', 'orders' => 'account-ico account-ico-orders', 'downloads' => 'account-ico account-ico-downloads', 'edit-address' => 'account-ico account-ico-address', 'edit-account' => 'account-ico account-ico-account', 'comments' => 'account-ico account-ico-comments', 'notifications' => 'account-ico account-ico-notifications', 'consultations' => 'account-ico account-ico-consultations', 'booking' => 'account-ico account-ico-booking', 'wishlist' => 'account-ico account-ico-wishlist', 'customer-logout' => 'account-ico account-ico-logout', ); if (isset($map[$endpoint])) { $classes = array_merge($classes, explode(' ', $map[$endpoint])); } return $classes; } /** * ✅ Pagination fix: * Convert endpoint pagination stored as "{endpoint} = page/2" into standard "paged = 2". * * Important: * - "request" filter runs early, so do NOT use is_account_page(). * - Detect My Account request via rewritten vars: pagename == myaccount slug. */ public function normalize_account_endpoint_paged($vars) { if (is_admin()) { return $vars; } if (!function_exists('wc_get_page_id')) { return $vars; } $page_id = wc_get_page_id('myaccount'); if ($page_id <= 0) { return $vars; } $myaccount_slug = (string) get_post_field('post_name', $page_id); if ($myaccount_slug === '') { return $vars; } // Only affect My Account requests if (empty($vars['pagename']) || (string) $vars['pagename'] !== $myaccount_slug) { return $vars; } // If paged already set and valid, do nothing if (!empty($vars['paged']) && absint($vars['paged']) > 0) { return $vars; } // Endpoints that can carry pagination like "page/2" $endpoints = array_merge( array('orders', 'downloads'), array_keys($this->custom_endpoints) ); foreach ($endpoints as $endpoint) { if (empty($vars[$endpoint]) || !is_string($vars[$endpoint])) { continue; } $val = (string) $vars[$endpoint]; // Common: "page/2" if (preg_match('~^page/(\d+)(?:/|$)~', $val, $m)) { $vars['paged'] = max(1, (int) $m[1]); break; } // Fallback: ".../page/2/..." if (preg_match('~/page/(\d+)(?:/|$)~', $val, $m)) { $vars['paged'] = max(1, (int) $m[1]); break; } // Rare: endpoint value is numeric if (ctype_digit($val)) { $vars['paged'] = max(1, (int) $val); break; } } return $vars; } /** * Ensure Woo knows our custom endpoints query vars. * (Safe: only adds vars, does not remove anything.) */ public function register_custom_query_vars($vars) { foreach (array_keys($this->custom_endpoints) as $endpoint) { if (!isset($vars[$endpoint])) { $vars[$endpoint] = $endpoint; } } return $vars; } public function maybe_flush_rewrite_rules() { $flag_key = 'datis_wc_myaccount_endpoints_flushed'; if (get_option($flag_key)) { return; } $this->register_endpoints(); flush_rewrite_rules(); update_option($flag_key, 1); } public static function get_instance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } } Datis_WC_MyAccount_Manager::get_instance(); }
💾 保存文件
← 返回文件管理器