Ajax.php
3 months ago
Assets.php
1 month ago
Loader.php
1 year ago
Rest.php
1 year ago
SurveyLoader.php
1 year ago
SurveyManager.php
1 month ago
SurveyManager.php
460 lines
| 1 | <?php |
| 2 | |
| 3 | namespace Hostinger\Surveys; |
| 4 | |
| 5 | use Hostinger\WpHelper\Config; |
| 6 | use Hostinger\WpHelper\Utils as Helper; |
| 7 | |
| 8 | defined('ABSPATH') || exit; |
| 9 | |
| 10 | class SurveyManager |
| 11 | { |
| 12 | public const CACHE_THREE_HOURS = 3600 * 3; |
| 13 | public const TIME_24_HOURS = 86400; |
| 14 | public const CLIENT_SURVEY_IDENTIFIER = 'customer_satisfaction_score'; |
| 15 | public const CLIENT_WOO_COMPLETED_ACTIONS = 'woocommerce_task_list_tracked_completed_tasks'; |
| 16 | public const SUBMITTED_SURVEY_TRANSIENT = 'submitted_survey_transient'; |
| 17 | public const IS_CLIENT_ELIGIBLE_TRANSIENT_RESPONSE = 'client_eligibility_transient_response'; |
| 18 | public const IS_CLIENT_ELIGIBLE_TRANSIENT_REQUEST = 'survey_questions_transient_request'; |
| 19 | public const LOCATION_SLUG = 'location'; |
| 20 | public const HIDE_SURVEY_TRANSIENT = 'hts_hide_survey'; |
| 21 | |
| 22 | private Config $configHandler; |
| 23 | private Helper $helper; |
| 24 | private Rest $surveysRest; |
| 25 | |
| 26 | public function __construct( |
| 27 | Helper $helper, |
| 28 | Config $configHandler, |
| 29 | Rest $surveysRest |
| 30 | ) { |
| 31 | $this->helper = $helper; |
| 32 | $this->configHandler = $configHandler; |
| 33 | $this->surveysRest = $surveysRest; |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Get WooCommerce admin pages paths |
| 38 | * |
| 39 | * @return array Array of WooCommerce admin page paths |
| 40 | */ |
| 41 | public function getWooCommercePages(): array |
| 42 | { |
| 43 | $admin_path = parse_url(admin_url(), PHP_URL_PATH); |
| 44 | |
| 45 | $pages = [ |
| 46 | 'admin.php?page=wc-admin', |
| 47 | 'edit.php?post_type=shop_order', |
| 48 | 'admin.php?page=wc-admin&path=/customers', |
| 49 | 'edit.php?post_type=shop_coupon&legacy_coupon_menu=1', |
| 50 | 'admin.php?page=wc-admin&path=/marketing', |
| 51 | 'admin.php?page=wc-reports', |
| 52 | 'admin.php?page=wc-orders', |
| 53 | 'admin.php?page=wc-settings', |
| 54 | 'admin.php?page=wc-status', |
| 55 | 'admin.php?page=wc-admin&path=/extensions', |
| 56 | 'edit.php?post_type=product', |
| 57 | 'post-new.php?post_type=product', |
| 58 | 'edit.php?post_type=product&page=product-reviews', |
| 59 | 'edit.php?post_type=product&page=product_attributes', |
| 60 | 'edit-tags.php?taxonomy=product_cat&post_type=product', |
| 61 | 'edit-tags.php?taxonomy=product_tag&post_type=product', |
| 62 | 'admin.php?page=wc-admin&path=/analytics/overview', |
| 63 | 'admin.php?page=wc-admin', |
| 64 | ]; |
| 65 | |
| 66 | $woocommerce_pages = []; |
| 67 | foreach ($pages as $page) { |
| 68 | $woocommerce_pages[] = $admin_path . $page; |
| 69 | } |
| 70 | |
| 71 | return $woocommerce_pages; |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Check if current page is a WooCommerce admin page |
| 76 | * |
| 77 | * @return bool True if current page is a WooCommerce admin page |
| 78 | */ |
| 79 | public function isWoocommerceAdminPage(): bool |
| 80 | { |
| 81 | if (! isset($_SERVER['REQUEST_URI'])) { |
| 82 | return false; |
| 83 | } |
| 84 | |
| 85 | $currentUri = sanitize_text_field($_SERVER['REQUEST_URI']); |
| 86 | |
| 87 | if (defined('DOING_AJAX') && \DOING_AJAX) { |
| 88 | return false; |
| 89 | } |
| 90 | |
| 91 | if (isset($currentUri) && strpos($currentUri, '/wp-json/') !== false) { |
| 92 | return false; |
| 93 | } |
| 94 | |
| 95 | $woocommercePages = $this->getWooCommercePages(); |
| 96 | |
| 97 | foreach ($woocommercePages as $page) { |
| 98 | if (strpos($currentUri, $page) !== false) { |
| 99 | return true; |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | return false; |
| 104 | } |
| 105 | |
| 106 | public function isClientEligible(): bool |
| 107 | { |
| 108 | $transientRequestKey = self::IS_CLIENT_ELIGIBLE_TRANSIENT_REQUEST; |
| 109 | $transientResponseKey = self::IS_CLIENT_ELIGIBLE_TRANSIENT_RESPONSE; |
| 110 | $cachedEligibility = get_transient($transientResponseKey); |
| 111 | |
| 112 | // Check if a request is already in progress |
| 113 | if (get_transient('hts_eligible_request')) { |
| 114 | return false; |
| 115 | } |
| 116 | |
| 117 | // Check if transient response exists |
| 118 | if ($cachedEligibility && ( $cachedEligibility === 'eligible' || $cachedEligibility === 'not_eligible' )) { |
| 119 | return $cachedEligibility === 'eligible'; |
| 120 | } |
| 121 | |
| 122 | // Attempt to set transient request |
| 123 | if (! $this->helper->checkTransientEligibility($transientRequestKey, self::CACHE_THREE_HOURS)) { |
| 124 | return false; |
| 125 | } |
| 126 | |
| 127 | try { |
| 128 | // Set transient flag to indicate request in progress |
| 129 | set_transient('hts_eligible_request', 'in_progress', 60); |
| 130 | |
| 131 | $isEligible = $this->surveysRest->isClientEligible(); |
| 132 | |
| 133 | // Clear the request transient flag |
| 134 | delete_transient('hts_eligible_request'); |
| 135 | |
| 136 | if ($isEligible) { |
| 137 | set_transient($transientResponseKey, 'eligible', self::CACHE_THREE_HOURS); |
| 138 | |
| 139 | return $isEligible; |
| 140 | } |
| 141 | |
| 142 | set_transient($transientResponseKey, 'not_eligible', self::CACHE_THREE_HOURS); |
| 143 | |
| 144 | return false; |
| 145 | } catch (\Exception $exception) { |
| 146 | $this->helper->errorLog('Error checking eligibility: ' . $exception->getMessage()); |
| 147 | |
| 148 | return false; |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | public function submitSurveyAnswers(array $answers, string $surveyId, string $surveyLocation, int $is_mobile = 0): void |
| 153 | { |
| 154 | $data = [ |
| 155 | 'identifier' => self::CLIENT_SURVEY_IDENTIFIER, |
| 156 | 'is_mobile' => $is_mobile, |
| 157 | 'answers' => [ |
| 158 | [ |
| 159 | 'question_slug' => self::LOCATION_SLUG, |
| 160 | 'answer' => $surveyLocation, |
| 161 | ] |
| 162 | ], |
| 163 | ]; |
| 164 | |
| 165 | $answers = $this->addUserAnswers($data, $answers); |
| 166 | |
| 167 | $success = $this->surveysRest->submitSurveyData($answers); |
| 168 | |
| 169 | set_transient(self::SUBMITTED_SURVEY_TRANSIENT, true, self::TIME_24_HOURS); |
| 170 | |
| 171 | if ($success) { |
| 172 | delete_transient(self::IS_CLIENT_ELIGIBLE_TRANSIENT_RESPONSE); |
| 173 | $settingKey = $surveyId . '_survey_completed'; |
| 174 | update_option('hostinger_' . $settingKey, true); |
| 175 | } |
| 176 | |
| 177 | wp_send_json('Survey completed'); |
| 178 | |
| 179 | if (has_action('litespeed_purge_all')) { |
| 180 | do_action('litespeed_purge_all'); |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | public function getSpecifiedSurvey(array $activeSurvey): array |
| 185 | { |
| 186 | $specifiedSurveyQuestions = [ |
| 187 | 'pages' => [], |
| 188 | 'showQuestionNumbers' => 'off', |
| 189 | 'showTOC' => false, |
| 190 | 'pageNextText' => __('Next', 'hostinger-wp-surveys'), |
| 191 | 'pagePrevText' => __('Previous', 'hostinger-wp-surveys'), |
| 192 | 'completeText' => __('Submit', 'hostinger-wp-surveys'), |
| 193 | 'completedHtml' => __('Thank you for completing the survey !', 'hostinger-wp-surveys'), |
| 194 | 'requiredText' => '*', |
| 195 | ]; |
| 196 | |
| 197 | foreach ($activeSurvey['questions'] as $question) { |
| 198 | $element = [ |
| 199 | 'type' => $question['type'], |
| 200 | 'name' => $question['slug'], |
| 201 | 'title' => $question['question'], |
| 202 | 'requiredErrorText' => __('Response required.', 'hostinger-wp-surveys'), |
| 203 | ]; |
| 204 | |
| 205 | if ($question['slug'] == 'comment') { |
| 206 | $element['maxLength'] = 250; |
| 207 | } |
| 208 | |
| 209 | if ($question['slug'] == 'score') { |
| 210 | $element['rateMin'] = '1'; |
| 211 | $element['rateMax'] = '10'; |
| 212 | $element['minRateDescription'] = __('Poor', 'hostinger-wp-surveys'); |
| 213 | $element['maxRateDescription'] = __('Excellent', 'hostinger-wp-surveys'); |
| 214 | } |
| 215 | |
| 216 | if ($question['isRequired']) { |
| 217 | $element['isRequired'] = true; |
| 218 | } |
| 219 | |
| 220 | $questionData = [ |
| 221 | 'name' => $question['slug'], |
| 222 | 'elements' => [ $element ], |
| 223 | ]; |
| 224 | |
| 225 | $specifiedSurveyQuestions['pages'][] = $questionData; |
| 226 | } |
| 227 | |
| 228 | return $specifiedSurveyQuestions; |
| 229 | } |
| 230 | |
| 231 | public static function getHostingerSurveys(): array |
| 232 | { |
| 233 | return apply_filters('hostinger_add_surveys', []); |
| 234 | } |
| 235 | |
| 236 | public function generateSurveyHtml(): string |
| 237 | { |
| 238 | $allSurveys = SurveyManager::getHostingerSurveys(); |
| 239 | |
| 240 | if (empty($allSurveys)) { |
| 241 | return ''; |
| 242 | } |
| 243 | |
| 244 | $activeSurvey = $this->getHighestPrioritySurvey($allSurveys); |
| 245 | ob_start(); |
| 246 | ?> |
| 247 | <div class="hts-survey-wrapper hts-survey" |
| 248 | data-survey-id="<?php echo esc_attr($activeSurvey['id']) ?>" |
| 249 | data-location="<?php echo esc_attr($activeSurvey['location']) ?>" |
| 250 | data-surveys="<?php echo esc_attr(json_encode($this->getSpecifiedSurvey($activeSurvey))) ?>"> |
| 251 | <div class="close-btn"> |
| 252 | <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 24 24"> |
| 253 | <path d="M 4.9902344 3.9902344 A 1.0001 1.0001 0 0 0 4.2929688 5.7070312 L 10.585938 12 L 4.2929688 18.292969 A 1.0001 1.0001 0 1 0 5.7070312 19.707031 L 12 13.414062 L 18.292969 19.707031 A 1.0001 1.0001 0 1 0 19.707031 18.292969 L 13.414062 12 L 19.707031 5.7070312 A 1.0001 1.0001 0 0 0 18.980469 3.9902344 A 1.0001 1.0001 0 0 0 18.292969 4.2929688 L 12 10.585938 L 5.7070312 4.2929688 A 1.0001 1.0001 0 0 0 4.9902344 3.9902344 z"></path> |
| 254 | </svg> |
| 255 | </div> |
| 256 | <div id="hostinger-feedback-survey" class="hostinger-feedback-survey"></div> |
| 257 | <div id="hts-questionsLeft"> |
| 258 | <span id="hts-currentQuestion">1</span> |
| 259 | <?php |
| 260 | echo esc_html( |
| 261 | __( |
| 262 | 'Question', |
| 263 | 'hostinger-wp-surveys' |
| 264 | ) |
| 265 | ); |
| 266 | ?> |
| 267 | <?php echo esc_html(__('of ', 'hostinger-wp-surveys')); ?> |
| 268 | <span id="hts-allQuestions"></span> |
| 269 | </div> |
| 270 | </div> |
| 271 | <?php if (isset($activeSurvey['review_url'])) { ?> |
| 272 | <div id="hts-survey-review" |
| 273 | class="hts-survey-wrapper" |
| 274 | data-review-min-required-score="<?php echo esc_attr($activeSurvey['review_min_required_score']) ?>"> |
| 275 | <div class="close-btn"> |
| 276 | <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 24 24"> |
| 277 | <path d="M 4.9902344 3.9902344 A 1.0001 1.0001 0 0 0 4.2929688 5.7070312 L 10.585938 12 L 4.2929688 18.292969 A 1.0001 1.0001 0 1 0 5.7070312 19.707031 L 12 13.414062 L 18.292969 19.707031 A 1.0001 1.0001 0 1 0 19.707031 18.292969 L 13.414062 12 L 19.707031 5.7070312 A 1.0001 1.0001 0 0 0 18.980469 3.9902344 A 1.0001 1.0001 0 0 0 18.292969 4.2929688 L 12 10.585938 L 5.7070312 4.2929688 A 1.0001 1.0001 0 0 0 4.9902344 3.9902344 z"></path> |
| 278 | </svg> |
| 279 | </div> |
| 280 | <div class="hostinger-feedback-survey"> |
| 281 | <div class="sd-question__header"> |
| 282 | <h5><?php echo esc_html(__('Rate this plugin on WordPress.org', 'hostinger-wp-surveys')); ?></h5> |
| 283 | </div> |
| 284 | <div class="hts-survey-stars"> |
| 285 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 286 | <path fill-rule="evenodd" |
| 287 | clip-rule="evenodd" |
| 288 | d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z" |
| 289 | fill="#673DE6"/> |
| 290 | </svg> |
| 291 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 292 | <path fill-rule="evenodd" |
| 293 | clip-rule="evenodd" |
| 294 | d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z" |
| 295 | fill="#673DE6"/> |
| 296 | </svg> |
| 297 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 298 | <path fill-rule="evenodd" |
| 299 | clip-rule="evenodd" |
| 300 | d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z" |
| 301 | fill="#673DE6"/> |
| 302 | </svg> |
| 303 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 304 | <path fill-rule="evenodd" |
| 305 | clip-rule="evenodd" |
| 306 | d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z" |
| 307 | fill="#673DE6"/> |
| 308 | </svg> |
| 309 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 310 | <path fill-rule="evenodd" |
| 311 | clip-rule="evenodd" |
| 312 | d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.63L12 2L9.19 8.63L2 9.24L7.46 13.97L5.82 21L12 17.27Z" |
| 313 | fill="#673DE6"/> |
| 314 | </svg> |
| 315 | </div> |
| 316 | <div class="hts-survey-footer"> |
| 317 | <a href="<?php echo esc_url($activeSurvey['review_url']); ?>" target="_blank" class="hts-submit-review sd-btn"><?php echo esc_html(__('Add review', 'hostinger-wp-surveys')); ?></a> |
| 318 | </div> |
| 319 | </div> |
| 320 | </div> |
| 321 | <?php } ?> |
| 322 | <?php |
| 323 | return ob_get_clean(); |
| 324 | } |
| 325 | |
| 326 | |
| 327 | public function defaultWoocommerceSurveyCompleted(): bool |
| 328 | { |
| 329 | $completedActions = get_option(self::CLIENT_WOO_COMPLETED_ACTIONS, []); |
| 330 | $requiredCompletedActions = [ 'products', 'payments' ]; |
| 331 | |
| 332 | return empty(array_diff($requiredCompletedActions, $completedActions)); |
| 333 | } |
| 334 | |
| 335 | public function getOldestProductDate(): int |
| 336 | { |
| 337 | global $wpdb; |
| 338 | |
| 339 | $getProductDate = $wpdb->prepare( |
| 340 | " |
| 341 | SELECT MIN(post_date) |
| 342 | FROM {$wpdb->posts} |
| 343 | WHERE post_type = %s |
| 344 | AND post_status = %s |
| 345 | ", |
| 346 | 'product', |
| 347 | 'publish' |
| 348 | ); |
| 349 | |
| 350 | $oldestProductDate = $wpdb->get_var($getProductDate); |
| 351 | |
| 352 | if ($oldestProductDate !== null) { |
| 353 | $oldestProductDateTimestamp = strtotime($oldestProductDate); |
| 354 | |
| 355 | if ($oldestProductDateTimestamp !== false) { |
| 356 | return $oldestProductDateTimestamp; |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | return strtotime('-1 year'); |
| 361 | } |
| 362 | |
| 363 | public function isTimeElapsed(string $firstLoginAt, int $timeInSeconds): bool |
| 364 | { |
| 365 | $currentTime = time(); |
| 366 | $timeElapsed = $currentTime - $timeInSeconds; |
| 367 | |
| 368 | return $timeElapsed >= $firstLoginAt; |
| 369 | } |
| 370 | |
| 371 | public function activeSurveyHtml(): void |
| 372 | { |
| 373 | if (! did_action('activeSurvey')) { |
| 374 | $survey_html = $this->generateSurveyHtml(''); |
| 375 | echo $survey_html; |
| 376 | do_action('activeSurvey'); |
| 377 | } |
| 378 | } |
| 379 | |
| 380 | /** |
| 381 | * @param string $surveyId The unique ID for this survey |
| 382 | * @param string $scoreQuestion The question for the score step |
| 383 | * @param string $commentQuestion The question for the comment step |
| 384 | * @param string $location The location of the survey |
| 385 | * @param int $priority The priority of the survey. The bigger the number, the higher the priority |
| 386 | * @param string|null $review_url Optional. The url to review the plugin |
| 387 | * @param int $review_min_required_score The minimum rating in the score step to show the Review step |
| 388 | * @return array |
| 389 | */ |
| 390 | public static function addSurvey(string $surveyId, string $scoreQuestion, string $commentQuestion, string $location, int $priority, string $review_url = null, int $review_min_required_score = 7): array |
| 391 | { |
| 392 | $surveyData = [ |
| 393 | 'id' => $surveyId, |
| 394 | 'questions' => [ |
| 395 | [ |
| 396 | 'type' => 'rating', |
| 397 | 'slug' => 'score', |
| 398 | 'question' => $scoreQuestion, |
| 399 | 'isRequired' => true |
| 400 | ], |
| 401 | [ |
| 402 | 'type' => 'comment', |
| 403 | 'slug' => 'comment', |
| 404 | 'question' => $commentQuestion, |
| 405 | 'isRequired' => false |
| 406 | ], |
| 407 | ], |
| 408 | 'location' => $location, |
| 409 | 'priority' => $priority, |
| 410 | 'review_url' => $review_url, |
| 411 | 'review_min_required_score' => $review_min_required_score, |
| 412 | ]; |
| 413 | |
| 414 | $surveys[] = $surveyData; |
| 415 | |
| 416 | return $surveys; |
| 417 | } |
| 418 | |
| 419 | public function getHighestPrioritySurvey(array $allSurveys): array |
| 420 | { |
| 421 | $surveys = array_merge([], ...$allSurveys); |
| 422 | |
| 423 | return array_reduce($surveys, function ($highestPrioritySurvey, $currentSurvey) { |
| 424 | return ( ! isset($highestPrioritySurvey['priority']) || |
| 425 | ( isset($currentSurvey['priority']) && $currentSurvey['priority'] > $highestPrioritySurvey['priority'] ) ) |
| 426 | ? $currentSurvey : $highestPrioritySurvey; |
| 427 | }, []); |
| 428 | } |
| 429 | |
| 430 | /** |
| 431 | * Checks if a specific survey is not completed |
| 432 | * @param string $surveyId |
| 433 | * @return bool |
| 434 | */ |
| 435 | public function isSurveyNotCompleted(string $surveyId): bool |
| 436 | { |
| 437 | $optionKey = 'hostinger_' . $surveyId . '_survey_completed'; |
| 438 | $notCompleted = ! get_option($optionKey, ''); |
| 439 | return $notCompleted; |
| 440 | } |
| 441 | |
| 442 | private function addUserAnswers(array $data, array $answers): array |
| 443 | { |
| 444 | foreach ($answers as $answerSlug => $answer) { |
| 445 | $data['answers'][] = [ |
| 446 | 'question_slug' => $answerSlug, |
| 447 | 'answer' => $answer, |
| 448 | ]; |
| 449 | } |
| 450 | |
| 451 | return $data; |
| 452 | } |
| 453 | |
| 454 | public function isSurveyHidden(): bool |
| 455 | { |
| 456 | $surveyVisibility = get_transient(self::HIDE_SURVEY_TRANSIENT); |
| 457 | return $surveyVisibility !== false; |
| 458 | } |
| 459 | } |
| 460 |