| @@ -43,8 +43,53 @@ | ||
| 43 | 43 | * |
| 44 | 44 | * @param array $otherInfo A variant's other_info payload |
| 45 | 45 | * @return string|null Error message, or null when valid |
| 46 | 46 | */ |
| 47 | + /** | |
| 48 | + * One resolver for every `photo` a FluentCart record appends: the | |
| 49 | + * uploaded customer photo for the user when one is set, otherwise the | |
| 50 | + * WordPress avatar by user id or email. Gravatar is asked with | |
| 51 | + * default=404, so an address with no avatar of its own fails to load | |
| 52 | + * and the UI falls back to its placeholder icon instead of Gravatar's | |
| 53 | + * stock image. esc_url_raw throughout — the value travels through | |
| 54 | + * JSON, where esc_url's entity-encoded ampersands would break the | |
| 55 | + * query string. | |
| 56 | + * | |
| 57 | + * @param int|string|null $userId user id owning a possible uploaded photo | |
| 58 | + * @param string|null $email fallback identity when there is no user | |
| 59 | + * @return string the photo URL, or '' when avatars are off or the | |
| 60 | + * record carries no identity to resolve one from | |
| 61 | + */ | |
| 62 | + public static function getUserAvatarUrl($userId, $email, int $size = 100): string | |
| 63 | + { | |
| 64 | + if (!get_option('show_avatars')) { | |
| 65 | + return ''; | |
| 66 | + } | |
| 67 | + | |
| 68 | + if ($userId) { | |
| 69 | + $customPhotoUrl = esc_url_raw( | |
| 70 | + (string) get_user_meta((int) $userId, 'fc_customer_photo_url', true) | |
| 71 | + ); | |
| 72 | + | |
| 73 | + if ($customPhotoUrl) { | |
| 74 | + return $customPhotoUrl; | |
| 75 | + } | |
| 76 | + } | |
| 77 | + | |
| 78 | + $userIdOrEmail = ((int) $userId) ?: (string) $email; | |
| 79 | + | |
| 80 | + if (!$userIdOrEmail) { | |
| 81 | + return ''; | |
| 82 | + } | |
| 83 | + | |
| 84 | + $url = get_avatar_url($userIdOrEmail, [ | |
| 85 | + 'size' => $size, | |
| 86 | + 'default' => '404', | |
| 87 | + ]); | |
| 88 | + | |
| 89 | + return $url ? esc_url_raw($url) : ''; | |
| 90 | + } | |
| 91 | + | |
| 47 | 92 | public static function installmentTimesError($otherInfo) |
| 48 | 93 | { |
| 49 | 94 | if (Arr::get($otherInfo, 'installment', 'no') !== 'yes') { |
| 50 | 95 | return null; |
| @@ -609,10 +654,12 @@ | ||
| 609 | 654 | if (!$user) { |
| 610 | 655 | return null; |
| 611 | 656 | } |
| 612 | 657 | |
| 658 | + // Identity only — see CustomerResource::getCurrentCustomer() for why an | |
| 659 | + // email match must never reach a customer row. | |
| 613 | 660 | return Customer::query()->where('user_id', $user->ID) |
| 614 | - ->orWhere('email', $user->user_email) | |
| 661 | + ->orderBy('id', 'ASC') | |
| 615 | 662 | ->first(); |
| 616 | 663 | } |
| 617 | 664 | |
| 618 | 665 | /** |
| @@ -1402,14 +1449,16 @@ | ||
| 1402 | 1449 | $month = (int) Arr::get($anchor, 'month', 0); |
| 1403 | 1450 | |
| 1404 | 1451 | if ($month && $day) { |
| 1405 | 1452 | /* translators: %s is a date, e.g. "on Aug 19" */ |
| 1406 | - return sprintf(__('on %s', 'fluent-cart'), gmdate('M', gmmktime(12, 0, 0, $month, 1, 2001)) . ' ' . $day); | |
| 1453 | + // wp_date(), not gmdate(): gmdate() has no locale, so this month | |
| 1454 | + // name stayed English inside an otherwise translated sentence. | |
| 1455 | + return sprintf(__('on %s', 'fluent-cart'), wp_date('M', gmmktime(12, 0, 0, $month, 1, 2001), new \DateTimeZone('UTC')) . ' ' . $day); | |
| 1407 | 1456 | } |
| 1408 | 1457 | |
| 1409 | 1458 | if ($month) { |
| 1410 | 1459 | /* translators: %s is a month name, e.g. "in August" */ |
| 1411 | - return sprintf(__('in %s', 'fluent-cart'), gmdate('F', gmmktime(12, 0, 0, $month, 1, 2001))); | |
| 1460 | + return sprintf(__('in %s', 'fluent-cart'), wp_date('F', gmmktime(12, 0, 0, $month, 1, 2001), new \DateTimeZone('UTC'))); | |
| 1412 | 1461 | } |
| 1413 | 1462 | |
| 1414 | 1463 | /* translators: %s is an ordinal day of month, e.g. "on the 10th" */ |
| 1415 | 1464 | return sprintf(__('on the %s', 'fluent-cart'), gmdate('jS', gmmktime(12, 0, 0, 1, $day, 2001))); |
| @@ -2107,6 +2156,40 @@ | ||
| 2107 | 2156 | } |
| 2108 | 2157 | return (int) $order->tax_behavior === 2 |
| 2109 | 2158 | ? esc_html__('(Included)', 'fluent-cart') |
| 2110 | 2159 | : esc_html__('(Excluded)', 'fluent-cart'); |
| 2160 | + } | |
| 2161 | + | |
| 2162 | + /** | |
| 2163 | + * Product placeholder illustration (an open box holding a picture) shown | |
| 2164 | + * wherever a product has no image. The web copy is a 1200px WebP (sharp on 2x screens); email | |
| 2165 | + * clients (Outlook) and PDF renderers get a 300px PNG of the same art. | |
| 2166 | + */ | |
| 2167 | + const PRODUCT_PLACEHOLDER_IMAGE = 'images/product-placeholder.webp'; | |
| 2168 | + const PRODUCT_PLACEHOLDER_EMAIL_IMAGE = 'images/product-placeholder.png'; | |
| 2169 | + | |
| 2170 | + public static function getProductPlaceholderUrl(): string | |
| 2171 | + { | |
| 2172 | + return \FluentCart\App\Vite::getAssetUrl(static::PRODUCT_PLACEHOLDER_IMAGE); | |
| 2173 | + } | |
| 2174 | + | |
| 2175 | + public static function getProductPlaceholderEmailUrl(): string | |
| 2176 | + { | |
| 2177 | + return \FluentCart\App\Vite::getAssetUrl(static::PRODUCT_PLACEHOLDER_EMAIL_IMAGE); | |
| 2178 | + } | |
| 2179 | + | |
| 2180 | + /** | |
| 2181 | + * A product image URL, or the placeholder when there is none. Carts store | |
| 2182 | + * the thumbnail URL in cart_data, so carts built before the illustration | |
| 2183 | + * still carry the old generic icon (images/placeholder.svg); map that too. | |
| 2184 | + */ | |
| 2185 | + public static function productImageOrPlaceholder($url): string | |
| 2186 | + { | |
| 2187 | + $url = is_string($url) ? $url : ''; | |
| 2188 | + | |
| 2189 | + if ($url === '' || preg_match('#/images/placeholder\.svg(\?.*)?$#', $url)) { | |
| 2190 | + return static::getProductPlaceholderUrl(); | |
| 2191 | + } | |
| 2192 | + | |
| 2193 | + return $url; | |
| 2111 | 2194 | } |
| 2112 | 2195 | } |