ContentLink.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. <?php
  2. namespace App\Support;
  3. use Illuminate\Support\Facades\Validator;
  4. use Illuminate\Validation\Rule;
  5. use Illuminate\Validation\ValidationException;
  6. /** Shared announcement/activity link contract. */
  7. final class ContentLink
  8. {
  9. public const FIELDS = ['click_type', 'link', 'link_pc', 'link_h5'];
  10. public static function normalize(array $row): array
  11. {
  12. $type = $row['click_type'] ?? 'none';
  13. $type = in_array($type, ['none', 'external', 'internal'], true) ? $type : 'none';
  14. return [
  15. 'click_type' => $type,
  16. 'link' => $type !== 'none' ? ($row['link'] ?? null) : null,
  17. 'link_pc' => $type === 'internal' ? ($row['link_pc'] ?? $row['link'] ?? null) : null,
  18. 'link_h5' => $type === 'internal' ? ($row['link_h5'] ?? $row['link'] ?? null) : null,
  19. ];
  20. }
  21. public static function forSave(array $input, array $previous = []): array
  22. {
  23. $before = self::normalize($previous);
  24. $type = $input['click_type'] ?? $before['click_type'];
  25. // An explicitly empty click type is invalid, rather than an omitted field.
  26. if (array_key_exists('click_type', $input)) $type = $input['click_type'];
  27. Validator::make(['click_type' => $type], ['click_type' => ['required', 'string', Rule::in(array_keys(AnnouncementCatalog::CLICKS))]],
  28. ['click_type.in' => '点击方式仅支持无链接、外站链接、站内路径'])->validate();
  29. $result = ['click_type' => $type, 'link' => null, 'link_pc' => null, 'link_h5' => null];
  30. if ($type === 'none') return $result;
  31. if ($type === 'external') {
  32. $result['link'] = array_key_exists('link', $input) ? $input['link'] : ($before['click_type'] === $type ? $before['link'] : null);
  33. self::validateLink('link', $result['link'], $type);
  34. return $result;
  35. }
  36. $legacy = array_key_exists('link', $input) && !array_key_exists('link_pc', $input) && !array_key_exists('link_h5', $input);
  37. foreach (['link_pc' => 'PC', 'link_h5' => 'H5'] as $field => $label) {
  38. $result[$field] = $legacy ? $input['link'] : (array_key_exists($field, $input) ? $input[$field] : ($before['click_type'] === $type ? $before[$field] : null));
  39. self::validateLink($field, $result[$field], $type, $label);
  40. }
  41. // Keep old callers functional; new dual-path submissions have no shared internal target.
  42. if ($legacy) $result['link'] = $input['link'];
  43. elseif (!array_key_exists('link_pc', $input) && !array_key_exists('link_h5', $input) && $before['click_type'] === $type) $result['link'] = $before['link'];
  44. return $result;
  45. }
  46. public static function forPlatform(array $row, ?string $platform): array
  47. {
  48. $links = self::normalize($row);
  49. if ($links['click_type'] === 'internal') {
  50. $links['link'] = match ($platform) {
  51. 'PC' => $links['link_pc'], 'H5', 'APP' => $links['link_h5'],
  52. default => $links['link_pc'] === $links['link_h5'] ? $links['link_pc'] : null,
  53. };
  54. }
  55. return array_intersect_key($links, ['click_type' => true, 'link' => true]);
  56. }
  57. public static function requestPlatform(): string
  58. {
  59. $platform = request()->header('Platform');
  60. return is_string($platform) ? (ActivityEligibility::normalizePlatform($platform) ?? 'PC') : 'PC';
  61. }
  62. public static function valid(string $value, string $type): bool
  63. {
  64. if ($value === '' || preg_match('/[\x00-\x20\x7f\\\\]/', $value)) return false;
  65. if ($type === 'internal') return str_starts_with($value, '/') && !str_starts_with($value, '//');
  66. $parts = parse_url($value);
  67. return filter_var($value, FILTER_VALIDATE_URL) && $parts && in_array(strtolower($parts['scheme'] ?? ''), ['http', 'https'], true)
  68. && !isset($parts['user']) && !isset($parts['pass']);
  69. }
  70. private static function validateLink(string $field, $value, string $type, string $label = ''): void
  71. {
  72. Validator::make([$field => $value], [$field => ['required', 'string', 'max:2048']],
  73. [$field . '.required' => $type === 'internal' ? '请填写' . $label . '站内相对路径' : '请填写跳转链接'])->validate();
  74. if (!self::valid($value, $type)) throw ValidationException::withMessages([
  75. $field => $type === 'internal' ? '请填写有效的' . $label . '站内相对路径' : '请填写有效的HTTP/HTTPS外站链接',
  76. ]);
  77. }
  78. }