dga_feedback-2.0.0/src/Controller/DgaFeedbackController.php

src/Controller/DgaFeedbackController.php
<?php

namespace Drupal\dga_feedback\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\dga_feedback\Service\DgaFeedbackService;
use Drupal\node\Entity\Node;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

/**
 * Controller for handling feedback AJAX requests.
 */
class DgaFeedbackController extends ControllerBase {

  /**
   * The feedback service.
   *
   * @var \Drupal\dga_feedback\Service\DgaFeedbackService
   */
  protected $feedbackService;

  /**
   * Constructs a DgaFeedbackController object.
   *
   * @param \Drupal\dga_feedback\Service\DgaFeedbackService $feedback_service
   *   The feedback service.
   */
  public function __construct(DgaFeedbackService $feedback_service) {
    $this->feedbackService = $feedback_service;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('dga_feedback.service'));
  }

  /**
   * Handles feedback submission.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The HTTP request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response.
   */
  public function submitFeedback(Request $request) {
    $config = $this->config('dga_feedback.settings');
    $language_manager = \Drupal::languageManager();
    $current_lang = $language_manager->getCurrentLanguage()->getId();
    $is_arabic = ($current_lang === 'ar');

    $getTranslation = function ($base_key, $default_en, $default_ar = NULL) use ($config, $is_arabic) {
      $value_en = $config->get($base_key . '_en');
      $value_ar = $config->get($base_key . '_ar');

      if ($is_arabic) {
        if (!empty($value_ar)) {
          return $value_ar;
        }
        if (!empty($value_en)) {
          return $value_en;
        }
        return $default_ar ?? $default_en;
      }

      if (!empty($value_en)) {
        return $value_en;
      }
      return $default_en;
    };

    // Handle CORS preflight (OPTIONS) requests
    if ($request->getMethod() === 'OPTIONS') {
      $response = new JsonResponse([]);
      $response->headers->set('Access-Control-Allow-Origin', '*');
      $response->headers->set('Access-Control-Allow-Methods', 'POST, OPTIONS');
      $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-Token');
      return $response;
    }

    // Reject GET requests - this endpoint is POST only (AJAX)
    if ($request->getMethod() !== 'POST') {
      return new JsonResponse([
        'success' => FALSE,
        'message' => $getTranslation('api_method_not_allowed', 'Method not allowed. Use POST.', 'الطريقة غير مسموحة. استخدم POST.'),
      ], 405);
    }

    $user = $this->currentUser();
    $is_anon = !$user->isAuthenticated();

    // Parse JSON from AJAX request
    $data = json_decode($request->getContent(), TRUE);
    if (!$data) {
      return new JsonResponse([
        'success' => FALSE,
        'message' => $getTranslation('api_invalid_json', 'Invalid JSON data', 'بيانات JSON غير صالحة'),
      ], 400);
    }

    // Validate is_useful
    if (!isset($data['is_useful']) || !in_array($data['is_useful'], ['yes', 'no'])) {
      return new JsonResponse([
        'success' => FALSE,
        'message' => $getTranslation('api_invalid_useful', 'is_useful must be "yes" or "no"', 'يجب أن يكون is_useful "yes" أو "no"'),
      ], 400);
    }

    // Normalize URL
    $url = isset($data['url']) ? trim($data['url']) : '/';
    if (empty($url) || $url === '/') {
      $url = '/';
    } else {
      if (preg_match('#^/[a-z]{2}(/.*)?$#', $url, $matches)) {
        $url = isset($matches[1]) ? $matches[1] : '/';
        if (empty($url)) $url = '/';
      }
      $url = rtrim($url, '/') ?: '/';
    }

    // Save data
    $entity_id = 0;
    if ($user->isAuthenticated() && isset($data['entity_id']) && (int) $data['entity_id'] > 0) {
      $entity_id = (int) $data['entity_id'];
    }

    // Get configurable limits from settings
    $reason_max_length = (int) ($config->get('reason_max_length') ?? 200);
    $reason_max_count = (int) ($config->get('reason_max_count') ?? 10);
    $feedback_max_length = (int) ($config->get('feedback_max_length') ?? 5000);

    // Validate and sanitize required fields
    // SECURITY: Validate reasons array
    if (empty($data['reasons']) || !is_array($data['reasons']) || count($data['reasons']) === 0) {
      return new JsonResponse([
        'success' => FALSE,
        'message' => $getTranslation('validation_reason_required', 'Please select at least one reason', 'يرجى اختيار سبب واحد على الأقل'),
      ], 400);
    }

    // SECURITY: Sanitize and limit reasons array (configurable limits)
    $sanitized_reasons = [];
    foreach ($data['reasons'] as $reason) {
      if (is_string($reason)) {
        $reason = trim(strip_tags($reason));
        if (strlen($reason) > 0 && strlen($reason) <= $reason_max_length) {
          $sanitized_reasons[] = $reason;
        }
      }
      if (count($sanitized_reasons) >= $reason_max_count) {
        break; // Limit to configured max reasons
      }
    }

    if (count($sanitized_reasons) === 0) {
      return new JsonResponse([
        'success' => FALSE,
        'message' => $getTranslation('validation_reason_invalid', 'At least one valid reason must be selected.', 'يجب اختيار سبب واحد صالح على الأقل.'),
      ], 400);
    }

    // SECURITY: Validate and sanitize feedback text (configurable max length)
    $feedback_text = isset($data['feedback']) ? trim($data['feedback']) : '';
    if (empty($feedback_text)) {
      return new JsonResponse([
        'success' => FALSE,
        'message' => $getTranslation('validation_feedback_required', 'Please provide feedback text', 'يرجى تقديم نص التعليق'),
      ], 400);
    }

    // SECURITY: Limit feedback length and strip HTML tags
    $feedback_text = strip_tags($feedback_text);
    if (strlen($feedback_text) > $feedback_max_length) {
      $feedback_text = substr($feedback_text, 0, $feedback_max_length);
    }

    // SECURITY: Validate gender
    if (empty($data['gender']) || !in_array($data['gender'], ['male', 'female'])) {
      return new JsonResponse([
        'success' => FALSE,
        'message' => $getTranslation('validation_gender_required', 'Please select your gender', 'يرجى اختيار جنسك'),
      ], 400);
    }

    // SECURITY: Rate limiting for anonymous users (configurable)
    // This prevents spam while allowing legitimate testing and usage
    if ($is_anon) {
      $rate_limit_max = (int) ($config->get('rate_limit_max_submissions') ?? 20);
      $rate_limit_window = (int) ($config->get('rate_limit_time_window') ?? 3600);

      // Only apply rate limiting if enabled (max > 0)
      if ($rate_limit_max > 0) {
        $ip_address = $request->getClientIp();
        $recent_count = \Drupal::database()
          ->select('dga_feedback', 'f')
          ->condition('f.ip_address', $ip_address)
          ->condition('f.created', time() - $rate_limit_window, '>=')
          ->countQuery()
          ->execute()
          ->fetchField();

        if ($recent_count >= $rate_limit_max) {
          return new JsonResponse([
            'success' => FALSE,
            'message' => $getTranslation('api_rate_limit', 'Too many submissions. Please try again later.', 'عدد كبير جدًا من الإرسالات. يرجى المحاولة مرة أخرى لاحقًا.'),
          ], 429);
        }
      }
    }

    $save_data = [
      'entity_type' => $data['entity_type'] ?? 'node',
      'entity_id' => $entity_id,
      'is_useful' => $data['is_useful'],
      'reasons' => $sanitized_reasons,
      'feedback' => $feedback_text,
      'gender' => $data['gender'],
      'url' => $url,
      'user_id' => $is_anon ? NULL : $user->id(),
      'ip_address' => $request->getClientIp(),
    ];

    // CSRF token check skipped for anonymous users

    // Save feedback
    $feedback_id = $this->feedbackService->saveFeedback($save_data);

    // Log submission details for debugging (with truncated feedback preview)
    if ($feedback_id && is_numeric($feedback_id) && (int) $feedback_id > 0) {
      $feedback_preview = $save_data['feedback'];
      if (strlen($feedback_preview) > 100) {
        $feedback_preview = substr($feedback_preview, 0, 100) . '...';
      }

      \Drupal::logger('dga_feedback')->info('Feedback saved - ID: @id, User: @user, Useful: @useful, Reasons: @reasons, Feedback: @feedback, Gender: @gender, URL: @url, Entity: @entity', [
        '@id' => (int) $feedback_id,
        '@user' => $is_anon ? 'anonymous' : 'user_' . $user->id(),
        '@useful' => $save_data['is_useful'] === 'yes' ? 'Yes' : 'No',
        '@reasons' => implode(', ', $save_data['reasons']),
        '@feedback' => $feedback_preview,
        '@gender' => $save_data['gender'] ?? 'N/A',
        '@url' => $save_data['url'],
        '@entity' => $save_data['entity_type'] . ':' . $save_data['entity_id'],
      ]);
    }

    // Handle the case where saveFeedback returns FALSE or invalid value
    if ($feedback_id === FALSE || !is_numeric($feedback_id) || (int) $feedback_id <= 0) {
      $feedback_preview = $save_data['feedback'];
      if (strlen($feedback_preview) > 100) {
        $feedback_preview = substr($feedback_preview, 0, 100) . '...';
      }

      \Drupal::logger('dga_feedback')->error('Save FAILED - User: @user, Useful: @useful, Reasons count: @reasons_count, Feedback: @feedback, Gender: @gender, URL: @url', [
        '@user' => $is_anon ? 'anonymous' : 'user_' . $user->id(),
        '@useful' => $save_data['is_useful'] === 'yes' ? 'Yes' : 'No',
        '@reasons_count' => count($save_data['reasons']),
        '@feedback' => $feedback_preview,
        '@gender' => $save_data['gender'] ?? 'N/A',
        '@url' => $save_data['url'],
      ]);

      // FOR ANONYMOUS: Try direct database insert as fallback (EXACTLY like dga_rating)
      if ($is_anon) {
        try {
          $db = \Drupal::database();

          // Prepare fields exactly as service would - matching DgaFeedbackService::saveFeedback()
          // The service expects reasons as array and encodes it, but for direct insert we need JSON string
          $direct_fields = [
            'entity_type' => $save_data['entity_type'],
            'entity_id' => (int) $save_data['entity_id'],
            'is_useful' => $save_data['is_useful'] === 'yes' ? 'yes' : 'no',
            'reasons' => isset($save_data['reasons']) && is_array($save_data['reasons']) ? json_encode($save_data['reasons']) : json_encode([]),
            'feedback' => isset($save_data['feedback']) ? trim($save_data['feedback']) : '',
            'gender' => isset($save_data['gender']) && in_array($save_data['gender'], ['male', 'female']) ? $save_data['gender'] : NULL,
            'url' => $save_data['url'],
            'user_id' => NULL, // Explicitly NULL for anonymous
            'ip_address' => $save_data['ip_address'],
            'created' => time(),
          ];

          // Use direct insert - same approach as dga_rating but with properly formatted fields
          $direct_id = $db->insert('dga_feedback')
            ->fields($direct_fields)
            ->execute();

          if ($direct_id && is_numeric($direct_id) && (int) $direct_id > 0) {
            $feedback_id = $direct_id;
            // Invalidate cache after successful insert
            \Drupal::service('cache_tags.invalidator')->invalidateTags(['dga_feedback:submissions']);
          }
        } catch (\Exception $e) {
          \Drupal::logger('dga_feedback')->error('Direct DB insert failed: @msg', [
            '@msg' => $e->getMessage(),
          ]);
        }
      }

      // If still failed after fallback, return error
      if ($feedback_id === FALSE || !is_numeric($feedback_id) || (int) $feedback_id <= 0) {
        return new JsonResponse([
          'success' => FALSE,
          'message' => $getTranslation('api_save_failed', 'Failed to save feedback.', 'فشل حفظ التعليق.'),
        ], 500);
      }
    }

    // Verify the record exists - but don't fail if there's a slight delay
    $db = \Drupal::database();

    // Try verification up to 3 times with small delay (in case of replication lag)
    $verify = NULL;
    for ($i = 0; $i < 3; $i++) {
      $verify = $db->select('dga_feedback', 'f')
        ->fields('f', ['id', 'is_useful', 'url', 'user_id'])
        ->condition('f.id', (int) $feedback_id)
        ->execute()
        ->fetchObject();

      if ($verify) {
        break;
      }

      // Small delay before retry
      if ($i < 2) {
        usleep(100000); // 0.1 second
      }
    }

    // Verification is optional - if record not found immediately, continue anyway
    // (may be due to replication lag or timing)

    // Get stats - wait a moment for database to be ready (especially for anonymous)
    if ($is_anon) {
      usleep(200000); // 0.2 second delay for anonymous users
    }

    $route_match = \Drupal::routeMatch();
    $node = $route_match->getParameter('node');
    $stats = ['yes_percentage' => 0.0, 'total_count' => 0];

    if ($node && $node instanceof Node) {
      $entity_id_for_query = $node->id();
      if ($entity_id_for_query > 0) {
        $stats = $this->feedbackService->getStatistics('node', $entity_id_for_query, $url);
      }
    }

    if (($stats['total_count'] ?? 0) == 0) {
      $stats = $this->feedbackService->getStatisticsByUrl($url);
    }

    $success_message = $getTranslation('api_success_message', 'Thank you for your feedback!', 'شكرًا لك على تعليقك!');

    return new JsonResponse([
      'success' => TRUE,
      'message' => $success_message,
      'feedback_id' => (int) $feedback_id,
      'statistics' => [
        'yes_percentage' => (float) ($stats['yes_percentage'] ?? 0.0),
        'total_count' => (int) ($stats['total_count'] ?? 0),
      ],
      'yes_percentage' => number_format($stats['yes_percentage'] ?? 0.0, 0, '.', ''),
      'total_count' => (int) ($stats['total_count'] ?? 0),
    ]);
  }

  /**
   * Refreshes the feedback block statistics via AJAX.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The HTTP request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with updated statistics.
   */
  public function refreshBlock(Request $request) {
    $url = $request->query->get('url');
    $entity_type = $request->query->get('entity_type');
    $entity_id = $request->query->get('entity_id');

    if (!$url) {
      $url = \Drupal::service('path.current')->getPath();
    }

    $url = trim($url);
    if (empty($url) || $url === '/') {
      $url = '/';
    }
    else {
      if (preg_match('#^/[a-z]{2}(/.*)?$#', $url, $matches)) {
        $url = isset($matches[1]) ? $matches[1] : '/';
        if (empty($url)) {
          $url = '/';
        }
      }
      $url = rtrim($url, '/') ?: '/';
    }

    $stats = ['yes_percentage' => 0.0, 'total_count' => 0];
    if ($entity_type && $entity_id && (int) $entity_id > 0) {
      $stats = $this->feedbackService->getStatistics($entity_type, (int) $entity_id, $url);
    }
    else {
      $stats = $this->feedbackService->getStatisticsByUrl($url);
    }

    return new JsonResponse([
      'success' => TRUE,
      'statistics' => [
        'yes_percentage' => (float) ($stats['yes_percentage'] ?? 0.0),
        'total_count' => (int) ($stats['total_count'] ?? 0),
      ],
      'yes_percentage' => (float) ($stats['yes_percentage'] ?? 0.0),
      'total_count' => (int) ($stats['total_count'] ?? 0),
    ]);
  }

  /**
   * Gets feedback statistics.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The HTTP request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with statistics.
   */
  public function getStats(Request $request) {
    $url = $request->query->get('url');
    $entity_type = $request->query->get('entity_type');
    $entity_id = $request->query->get('entity_id');

    $stats = ['yes_percentage' => 0.0, 'total_count' => 0];

    if ($entity_type && $entity_id) {
      if ($url) {
        $stats = $this->feedbackService->getStatistics($entity_type, (int) $entity_id, $url);
      } else {
        $stats = $this->feedbackService->getStatisticsByEntity($entity_type, (int) $entity_id);
      }
    } elseif ($url) {
      $stats = $this->feedbackService->getStatisticsByUrl($url);
    }

    return new JsonResponse(['success' => TRUE, 'statistics' => $stats]);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc