Block
1 week ago
Listing
1 month ago
RestApi
1 month ago
Templates
2 years ago
Util
2 months ago
ApiDataSanitizer.php
2 weeks ago
AssetsController.php
2 months ago
BlockStylesRenderer.php
3 years ago
BlockWrapperRenderer.php
3 years ago
BlocksRenderer.php
1 week ago
DisplayFormInWPContent.php
2 months ago
FormHtmlSanitizer.php
1 month ago
FormMessageController.php
4 years ago
FormSaveController.php
1 year ago
FormsRepository.php
1 year ago
PreviewPage.php
2 months ago
PreviewWidget.php
1 year ago
Renderer.php
2 months ago
Widget.php
2 months ago
index.php
3 years ago
DisplayFormInWPContent.php
427 lines
| 1 | <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing |
| 2 | |
| 3 | namespace MailPoet\Form; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\API\JSON\API; |
| 9 | use MailPoet\Config\Renderer as TemplateRenderer; |
| 10 | use MailPoet\Entities\FormEntity; |
| 11 | use MailPoet\Subscribers\SubscribersRepository; |
| 12 | use MailPoet\Subscribers\SubscriberSubscribeController; |
| 13 | use MailPoet\WooCommerce\Helper as WCHelper; |
| 14 | use MailPoet\WP\Functions as WPFunctions; |
| 15 | |
| 16 | class DisplayFormInWPContent { |
| 17 | |
| 18 | const NO_FORM_TRANSIENT_KEY = 'no_forms_displayed_bellow_content'; |
| 19 | |
| 20 | const TYPES = [ |
| 21 | FormEntity::DISPLAY_TYPE_BELOW_POST, |
| 22 | FormEntity::DISPLAY_TYPE_POPUP, |
| 23 | FormEntity::DISPLAY_TYPE_FIXED_BAR, |
| 24 | FormEntity::DISPLAY_TYPE_SLIDE_IN, |
| 25 | ]; |
| 26 | |
| 27 | const WITH_COOKIE_TYPES = [ |
| 28 | FormEntity::DISPLAY_TYPE_POPUP, |
| 29 | FormEntity::DISPLAY_TYPE_FIXED_BAR, |
| 30 | FormEntity::DISPLAY_TYPE_SLIDE_IN, |
| 31 | ]; |
| 32 | |
| 33 | const SUPPORTED_POST_TYPES = [ |
| 34 | 'post', |
| 35 | 'product', |
| 36 | 'job_listing', |
| 37 | ]; |
| 38 | |
| 39 | /** @var WPFunctions */ |
| 40 | private $wp; |
| 41 | |
| 42 | /** @var FormsRepository */ |
| 43 | private $formsRepository; |
| 44 | |
| 45 | /** @var Renderer */ |
| 46 | private $formRenderer; |
| 47 | |
| 48 | /** @var AssetsController */ |
| 49 | private $assetsController; |
| 50 | |
| 51 | /** @var TemplateRenderer */ |
| 52 | private $templateRenderer; |
| 53 | |
| 54 | /** @var SubscribersRepository */ |
| 55 | private $subscribersRepository; |
| 56 | |
| 57 | /** @var SubscriberSubscribeController */ |
| 58 | private $subscriberSubscribeController; |
| 59 | |
| 60 | /** @var WCHelper */ |
| 61 | private $woocommerceHelper; |
| 62 | |
| 63 | private $wooShopPageId = null; |
| 64 | |
| 65 | private $inWooProductLoop = false; |
| 66 | |
| 67 | private $renderedDisplayTypes = []; |
| 68 | |
| 69 | public function __construct( |
| 70 | WPFunctions $wp, |
| 71 | FormsRepository $formsRepository, |
| 72 | Renderer $formRenderer, |
| 73 | AssetsController $assetsController, |
| 74 | TemplateRenderer $templateRenderer, |
| 75 | SubscriberSubscribeController $subscriberSubscribeController, |
| 76 | SubscribersRepository $subscribersRepository, |
| 77 | WCHelper $woocommerceHelper |
| 78 | ) { |
| 79 | $this->wp = $wp; |
| 80 | $this->formsRepository = $formsRepository; |
| 81 | $this->formRenderer = $formRenderer; |
| 82 | $this->assetsController = $assetsController; |
| 83 | $this->templateRenderer = $templateRenderer; |
| 84 | $this->subscriberSubscribeController = $subscriberSubscribeController; |
| 85 | $this->subscribersRepository = $subscribersRepository; |
| 86 | $this->woocommerceHelper = $woocommerceHelper; |
| 87 | } |
| 88 | |
| 89 | private function getFormMarkup(): string { |
| 90 | $formMarkup = ''; |
| 91 | $forms = $this->getForms(); |
| 92 | if (count($forms) === 0) { |
| 93 | return $formMarkup; |
| 94 | } |
| 95 | foreach ($forms as $displayType => $form) { |
| 96 | $formMarkup .= $this->getContentBellow($form, $displayType); |
| 97 | } |
| 98 | |
| 99 | return $formMarkup; |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Hooked to the_content filter |
| 104 | */ |
| 105 | public function contentDisplay($content = null) { |
| 106 | $this->inWooProductLoop = false; |
| 107 | return $this->getContentWithFormMarkup($content); |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Hooked to woocommerce_product_loop_end filter |
| 112 | */ |
| 113 | public function wooProductListDisplay($content = null) { |
| 114 | $this->inWooProductLoop = true; |
| 115 | return $this->getContentWithFormMarkup($content); |
| 116 | } |
| 117 | |
| 118 | private function getContentWithFormMarkup($content = null) { |
| 119 | if (!is_string($content) || !$this->shouldDisplay()) { |
| 120 | return $content; |
| 121 | } |
| 122 | $formsMarkup = $this->getFormMarkup(); |
| 123 | if ($formsMarkup === '') { |
| 124 | return $content; |
| 125 | } |
| 126 | $this->assetsController->setupFrontEndDependencies(); |
| 127 | return $content . $formsMarkup; |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * Hooked to wp_footer action. |
| 132 | * |
| 133 | * @return void |
| 134 | */ |
| 135 | public function maybeRenderFormsInFooter(): void { |
| 136 | if ($this->wp->isArchive() || $this->wp->isFrontPage() || $this->wp->isHome() || $this->isWooProductPageWithoutContent()) { |
| 137 | $formMarkup = $this->getFormMarkup(); |
| 138 | if (!empty($formMarkup)) { |
| 139 | $this->assetsController->setupFrontEndDependencies(); |
| 140 | // We are in control of the template and the data can be considered safe at this point |
| 141 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 142 | echo $formMarkup; |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | /** |
| 148 | * @return bool |
| 149 | */ |
| 150 | public function isWooProductPageWithoutContent(): bool { |
| 151 | if ( |
| 152 | !$this->wp->isSingular('product') |
| 153 | || !$this->wp->didAction('wp_footer') |
| 154 | ) { |
| 155 | return false; |
| 156 | } |
| 157 | |
| 158 | return $this->wp->getTheContent() === ''; |
| 159 | } |
| 160 | |
| 161 | private function shouldDisplay(): bool { |
| 162 | $result = true; |
| 163 | // This is a fix Yoast plugin and Shapely theme compatibility |
| 164 | // This is to make sure we only display once for each page |
| 165 | // Yoast plugin calls `get_the_excerpt` which also triggers hook `the_content` we don't want to include our form in that |
| 166 | // Shapely calls the hook `the_content` multiple times on the page as well and we would display popup multiple times - not ideal |
| 167 | if (!$this->wp->inTheLoop() || !$this->wp->isMainQuery()) { |
| 168 | $result = $this->wp->applyFilters('mailpoet_display_form_is_main_loop', false); |
| 169 | } |
| 170 | // this code ensures that we display the form only on a page which is related to single post |
| 171 | if (!$this->wp->isSingle() && !$this->wp->isPage()) $result = $this->wp->applyFilters('mailpoet_display_form_is_single', false); |
| 172 | |
| 173 | // Ensure form does not show up multiple times when called from the woocommerce_product_loop_end filter |
| 174 | if ($this->inWooProductLoop) $result = $this->displayFormInProductListPage(); |
| 175 | |
| 176 | $noFormsCache = $this->wp->getTransient(DisplayFormInWPContent::NO_FORM_TRANSIENT_KEY); |
| 177 | if ($noFormsCache === '1') $result = false; |
| 178 | return (bool)$result; |
| 179 | } |
| 180 | |
| 181 | private function displayFormInProductListPage(): bool { |
| 182 | $displayCheck = $this->wp->applyFilters('mailpoet_display_form_in_product_listing', true); |
| 183 | |
| 184 | $shopPageId = $this->woocommerceHelper->wcGetPageId('shop'); |
| 185 | $this->wooShopPageId = $shopPageId && $shopPageId > 0 ? $shopPageId : null; |
| 186 | |
| 187 | if ($displayCheck && !is_null($this->wooShopPageId) && $this->wp->isPage($this->wooShopPageId)) { |
| 188 | return true; |
| 189 | } |
| 190 | |
| 191 | return $displayCheck && $this->wp->isArchive() && $this->wp->isPostTypeArchive('product'); |
| 192 | } |
| 193 | |
| 194 | private function saveNoForms() { |
| 195 | $this->wp->setTransient(DisplayFormInWPContent::NO_FORM_TRANSIENT_KEY, '1'); |
| 196 | } |
| 197 | |
| 198 | /** |
| 199 | * @return array<string, FormEntity> |
| 200 | */ |
| 201 | private function getForms(): array { |
| 202 | $forms = $this->formsRepository->findBy([ |
| 203 | 'deletedAt' => null, |
| 204 | 'status' => FormEntity::STATUS_ENABLED, |
| 205 | ], ['updatedAt' => 'ASC']); |
| 206 | if (count($forms) === 0) { |
| 207 | $this->saveNoForms(); |
| 208 | } |
| 209 | $forms = $this->filterOneFormInEachDisplayType($forms); |
| 210 | return $forms; |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * @param FormEntity[] $forms |
| 215 | * @return array<string, FormEntity> |
| 216 | */ |
| 217 | private function filterOneFormInEachDisplayType($forms): array { |
| 218 | $formsFiltered = []; |
| 219 | foreach ($forms as $form) { |
| 220 | foreach (self::TYPES as $displayType) { |
| 221 | if ($this->shouldDisplayFormType($form, $displayType)) { |
| 222 | $formsFiltered[$displayType] = $form; |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | return $formsFiltered; |
| 227 | } |
| 228 | |
| 229 | private function getContentBellow(FormEntity $form, string $displayType): string { |
| 230 | if (!$this->shouldDisplayFormType($form, $displayType)) return ''; |
| 231 | |
| 232 | $formSettings = $form->getSettings(); |
| 233 | if (!is_array($formSettings)) return ''; |
| 234 | $htmlId = 'mp_form_' . $displayType . $form->getId(); |
| 235 | $templateData = [ |
| 236 | 'form_html_id' => $htmlId, |
| 237 | 'form_id' => $form->getId(), |
| 238 | 'form_success_message' => $formSettings['success_message'] ?? null, |
| 239 | 'form_type' => $displayType, |
| 240 | 'styles' => $this->formRenderer->renderStyles($form, '#' . $htmlId, $displayType), |
| 241 | 'html' => $this->formRenderer->renderHTML($form), |
| 242 | 'close_button_icon' => $formSettings['close_button'] ?? 'round_white', |
| 243 | ]; |
| 244 | |
| 245 | // (POST) non ajax success/error variables |
| 246 | $templateData['success'] = ( |
| 247 | isset($_GET['mailpoet_success']) |
| 248 | && is_numeric($_GET['mailpoet_success']) |
| 249 | && (int)$_GET['mailpoet_success'] === $form->getId() |
| 250 | ); |
| 251 | $templateData['error'] = ( |
| 252 | isset($_GET['mailpoet_error']) |
| 253 | && is_numeric($_GET['mailpoet_error']) |
| 254 | && (int)$_GET['mailpoet_error'] === $form->getId() |
| 255 | ); |
| 256 | |
| 257 | $templateData['delay'] = $formSettings['form_placement'][$displayType]['delay'] ?? 0; |
| 258 | $templateData['position'] = $formSettings['form_placement'][$displayType]['position'] ?? ''; |
| 259 | $templateData['animation'] = $formSettings['form_placement'][$displayType]['animation'] ?? ''; |
| 260 | $templateData['triggerMode'] = $formSettings['form_placement'][$displayType]['trigger_mode'] ?? 'auto'; |
| 261 | $templateData['clickTriggerSelector'] = $formSettings['form_placement'][$displayType]['click_trigger_selector'] ?? ''; |
| 262 | $templateData['fontFamily'] = $formSettings['font_family'] ?? ''; |
| 263 | $templateData['enableExitIntent'] = false; |
| 264 | // Set default value for cookie expiration for backward compatibility with forms without this value |
| 265 | $templateData['cookieFormExpirationTime'] = $formSettings['form_placement'][$displayType]['cookieExpiration'] ?? 7; |
| 266 | if ( |
| 267 | isset($formSettings['form_placement'][$displayType]['exit_intent_enabled']) |
| 268 | && ($formSettings['form_placement'][$displayType]['exit_intent_enabled'] === '1') |
| 269 | ) { |
| 270 | $templateData['enableExitIntent'] = true; |
| 271 | } |
| 272 | |
| 273 | // generate security token |
| 274 | $templateData['token'] = $this->wp->wpCreateNonce('mailpoet_token'); |
| 275 | |
| 276 | // add API version |
| 277 | $templateData['api_version'] = API::CURRENT_VERSION; |
| 278 | $this->renderedDisplayTypes[] = $displayType; |
| 279 | return $this->templateRenderer->render('form/front_end_form.html', $templateData); |
| 280 | } |
| 281 | |
| 282 | /** |
| 283 | * Checks if the form should be displayed for current WordPress user |
| 284 | * |
| 285 | * @param FormEntity $form The form to check |
| 286 | * @param string $formType Display type of the current form, from self::TYPES |
| 287 | * @return bool False if form can be dismissed and user is subscribed to any of the form's lists |
| 288 | */ |
| 289 | private function shouldDisplayFormForWPUser(FormEntity $form, string $formType): bool { |
| 290 | if (!in_array($formType, self::WITH_COOKIE_TYPES, true)) return true; |
| 291 | |
| 292 | $subscriber = $this->subscribersRepository->getCurrentWPUser(); |
| 293 | if (!$subscriber) return true; |
| 294 | |
| 295 | if ($this->subscriberSubscribeController->isSubscribedToAnyFormSegments($form, $subscriber)) { |
| 296 | return false; |
| 297 | } |
| 298 | return true; |
| 299 | } |
| 300 | |
| 301 | private function shouldDisplayFormType(FormEntity $form, string $formType): bool { |
| 302 | if ($this->wasDisplayTypeAlreadyRendered($formType)) { |
| 303 | return false; |
| 304 | } |
| 305 | $settings = $form->getSettings(); |
| 306 | // check the structure just to be sure |
| 307 | |
| 308 | if ( |
| 309 | !is_array($settings) |
| 310 | || !isset($settings['form_placement'][$formType]) |
| 311 | || !is_array($settings['form_placement'][$formType]) |
| 312 | ) return false; |
| 313 | |
| 314 | $setup = $settings['form_placement'][$formType]; |
| 315 | if ($setup['enabled'] !== '1') { |
| 316 | return false; |
| 317 | } |
| 318 | |
| 319 | if (!$this->shouldDisplayFormForWPUser($form, $formType)) return false; |
| 320 | |
| 321 | if ($this->wp->isFrontPage() && $this->shouldDisplayFormOnFrontPage($setup)) { |
| 322 | return true; |
| 323 | } |
| 324 | |
| 325 | /** |
| 326 | * This is a special case when a site is configured with a specific "Posts page" in the Settings > Reading |
| 327 | * WordPress settings. In that case, the only conditional function that returns true is is_home. |
| 328 | */ |
| 329 | if ((!$this->wp->isFrontPage() && $this->wp->isHome()) && $this->shouldDisplayFormOnHome($setup)) { |
| 330 | return true; |
| 331 | } |
| 332 | |
| 333 | $supportedPostTypes = $this->wp->applyFilters('mailpoet_display_form_supported_post_types', self::SUPPORTED_POST_TYPES); |
| 334 | if ($this->wp->isSingular(is_array($supportedPostTypes) || is_string($supportedPostTypes) ? $supportedPostTypes : self::SUPPORTED_POST_TYPES)) { |
| 335 | if ($this->shouldDisplayFormOnPost($setup, 'posts')) return true; |
| 336 | if ($this->shouldDisplayFormOnCategory($setup)) return true; |
| 337 | if ($this->shouldDisplayFormOnTag($setup)) return true; |
| 338 | return false; |
| 339 | } |
| 340 | if ($this->wp->isPage() && $this->shouldDisplayFormOnPost($setup, 'pages')) { |
| 341 | return true; |
| 342 | } |
| 343 | |
| 344 | if ($this->wp->isTag() || $this->wp->isTax('product_tag')) { |
| 345 | if ($this->shouldDisplayFormOnTagArchive($setup)) return true; |
| 346 | } |
| 347 | |
| 348 | if ($this->wp->isCategory() || $this->wp->isTax('product_cat')) { |
| 349 | if ($this->shouldDisplayFormOnCategoryArchive($setup)) return true; |
| 350 | } |
| 351 | |
| 352 | if ($this->displayFormInProductListPage()) { |
| 353 | // Allow form display on Woo Shop listing page |
| 354 | if (is_null($this->wooShopPageId)) return false; |
| 355 | if ($this->shouldDisplayFormOnPost($setup, 'pages', $this->wooShopPageId)) return true; |
| 356 | } |
| 357 | |
| 358 | return false; |
| 359 | } |
| 360 | |
| 361 | private function shouldDisplayFormOnPost(array $setup, string $postsKey, $postId = null): bool { |
| 362 | if (!isset($setup[$postsKey])) { |
| 363 | return false; |
| 364 | } |
| 365 | if (isset($setup[$postsKey]['all']) && $setup[$postsKey]['all'] === '1') { |
| 366 | return true; |
| 367 | } |
| 368 | $post = $this->wp->getPost($postId, ARRAY_A); |
| 369 | if (isset($setup[$postsKey]['selected']) && in_array($post['ID'], $setup[$postsKey]['selected'])) { |
| 370 | return true; |
| 371 | } |
| 372 | return false; |
| 373 | } |
| 374 | |
| 375 | private function shouldDisplayFormOnCategory(array $setup): bool { |
| 376 | if (!isset($setup['categories'])) return false; |
| 377 | if ($this->wp->hasCategory($setup['categories'])) return true; |
| 378 | if ($this->wp->hasTerm($setup['categories'], 'product_cat')) return true; |
| 379 | return false; |
| 380 | } |
| 381 | |
| 382 | private function shouldDisplayFormOnTag(array $setup): bool { |
| 383 | if (!isset($setup['tags'])) return false; |
| 384 | if ($this->wp->hasTag($setup['tags'])) return true; |
| 385 | if ($this->wp->hasTerm($setup['tags'], 'product_tag')) return true; |
| 386 | return false; |
| 387 | } |
| 388 | |
| 389 | private function shouldDisplayFormOnFrontPage(array $setup): bool { |
| 390 | if (($setup['homepage'] ?? false) === '1') { |
| 391 | return true; |
| 392 | } |
| 393 | return false; |
| 394 | } |
| 395 | |
| 396 | private function shouldDisplayFormOnHome($setup) { |
| 397 | if (($setup['pages']['all'] ?? false) === '1') { |
| 398 | return true; |
| 399 | } |
| 400 | $selectedPages = $setup['pages']['selected'] ?? []; |
| 401 | if (in_array((string)$this->wp->getQueriedObjectId(), $selectedPages)) { |
| 402 | return true; |
| 403 | } |
| 404 | return false; |
| 405 | } |
| 406 | |
| 407 | private function shouldDisplayFormOnCategoryArchive($setup): bool { |
| 408 | if (!isset($setup['categoryArchives'])) return false; |
| 409 | if (($setup['categoryArchives']['all'] ?? false) === '1') return true; |
| 410 | $selectedCategories = $setup['categoryArchives']['selected'] ?? []; |
| 411 | if ($selectedCategories === []) return false; |
| 412 | return $this->wp->hasCategory($selectedCategories) || $this->wp->hasTerm($selectedCategories, 'product_cat'); |
| 413 | } |
| 414 | |
| 415 | private function shouldDisplayFormOnTagArchive($setup): bool { |
| 416 | if (!isset($setup['tagArchives'])) return false; |
| 417 | if (($setup['tagArchives']['all'] ?? false) === '1') return true; |
| 418 | $selectedTags = $setup['tagArchives']['selected'] ?? []; |
| 419 | if ($selectedTags === []) return false; |
| 420 | return $this->wp->hasTag($selectedTags) || $this->wp->hasTerm($selectedTags, 'product_tag'); |
| 421 | } |
| 422 | |
| 423 | private function wasDisplayTypeAlreadyRendered(string $displayType): bool { |
| 424 | return in_array($displayType, $this->renderedDisplayTypes); |
| 425 | } |
| 426 | } |
| 427 |