PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.3.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.3.1
4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Notices / Notices.php
wp-staging / Framework / Notices Last commit date
BackupPluginsNotice.php 2 years ago BooleanNotice.php 2 years ago DisabledItemsNotice.php 2 years ago DismissNotice.php 2 years ago FreeBackupUpdateNotice.php 2 years ago Notices.php 2 years ago ObjectCacheNotice.php 2 years ago OutdatedWpStagingNotice.php 3 years ago WarningsNotice.php 2 years ago
Notices.php
626 lines
1 <?php
2
3 namespace WPStaging\Framework\Notices;
4
5 use Exception;
6 use wpdb;
7 use WPStaging\Core\Utils\Logger;
8 use WPStaging\Core\WPStaging;
9 use WPStaging\Framework\Adapter\Directory;
10 use WPStaging\Framework\Analytics\AnalyticsConsent;
11 use WPStaging\Framework\Assets\Assets;
12 use WPStaging\Framework\CloningProcess\ExcludedPlugins;
13 use WPStaging\Framework\Security\Capabilities;
14 use WPStaging\Framework\Staging\CloneOptions;
15 use WPStaging\Framework\Staging\FirstRun;
16 use WPStaging\Framework\Support\ThirdParty\FreemiusScript;
17 use WPStaging\Framework\Support\ThirdParty\Jetpack;
18 use WPStaging\Framework\Support\ThirdParty\WordFence;
19 use WPStaging\Framework\Traits\NoticesTrait;
20 use WPStaging\Framework\Staging\Sites;
21 use WPStaging\Framework\SiteInfo;
22 use WPStaging\Framework\Utils\ServerVars;
23 use WPStaging\Backup\Ajax\Restore\PrepareRestore;
24 use WPStaging\Framework\Utils\Cache\Cache;
25
26 /**
27 * Show Admin Notices | Warnings | Messages
28 *
29 * Class Notices
30 * @package WPStaging\Framework\Notices
31 * @todo maybe split this class into multiple classes like staging notices, permission notices etc
32 * to avoid dependency hell without using service locator?
33 */
34 class Notices
35 {
36 use NoticesTrait;
37
38 /** @var string */
39 const PRO_NOTICES_ACTION = 'wpstg.notices.show_pro_notices';
40
41 /** @var string */
42 const BASIC_NOTICES_ACTION = 'wpstg.notices.show_basic_notices';
43
44 /** @var Assets */
45 private $assets;
46
47 /** @var Directory */
48 private $dirUtil;
49
50 /** @var Cache */
51 private $cache;
52
53 /** @var Logger */
54 private $logger;
55
56 /** @var CloneOptions */
57 private $cloneOptions;
58
59 /** @var ExcludedPlugins */
60 private $excludedPlugins;
61
62 /** @var FreemiusScript */
63 private $freemiusScript;
64
65 /** @var WordFence */
66 private $wordfence;
67
68 /** @var DisabledItemsNotice */
69 private $disabledItemsNotice;
70
71 /** @var WarningsNotice */
72 private $warningsNotice;
73
74 /** @var OutdatedWpStagingNotice */
75 private $outdatedWpStagingNotice;
76
77 /** @var ObjectCacheNotice */
78 private $objectCacheNotice;
79
80 /** @var wpdb */
81 private $db;
82
83 /** For testing all notices */
84 const SHOW_ALL_NOTICES = false;
85
86 /**
87 * @var string The key that holds directory listing errors in the container.
88 */
89 public static $directoryListingErrors = 'directoryListingErrors';
90
91 /** @var SiteInfo */
92 private $siteInfo;
93
94 /** @var string */
95 private $viewsNoticesPath;
96
97 /** @var false|mixed|null */
98 private $settings;
99
100 /** @var ServerVars */
101 private $serverVars;
102
103 /** @var bool */
104 private $isWpComSite;
105
106 public function __construct(Assets $assets)
107 {
108 $this->assets = $assets;
109 $this->viewsNoticesPath = trailingslashit($this->getPluginPath()) . "Backend/views/notices/";
110
111 // To avoid dependency hell and smooth transition we will be using service locator for below dependencies
112 $this->dirUtil = WPStaging::make(Directory::class);
113 $this->wordfence = WPStaging::make(WordFence::class);
114 $this->cloneOptions = WPStaging::make(CloneOptions::class);
115 $this->freemiusScript = WPStaging::make(FreemiusScript::class);
116 $this->excludedPlugins = WPStaging::make(ExcludedPlugins::class);
117 $this->logger = WPStaging::make("logger");
118 $this->cache = WPStaging::make("cache");
119 $this->db = WPStaging::make('wpdb');
120
121 // Notices
122 $this->disabledItemsNotice = WPStaging::make(DisabledItemsNotice::class);
123 $this->warningsNotice = WPStaging::make(WarningsNotice::class);
124 $this->outdatedWpStagingNotice = WPStaging::make(OutdatedWpStagingNotice::class);
125 $this->objectCacheNotice = WPStaging::make(ObjectCacheNotice::class);
126 $this->siteInfo = WPStaging::make(SiteInfo::class);
127 $this->serverVars = WPStaging::make(ServerVars::class);
128
129 $this->isWpComSite = $this->siteInfo->isHostedOnWordPressCom();
130 }
131
132 /**
133 * Check whether the plugin is pro version
134 *
135 * @return bool
136 */
137 protected function isPro(): bool
138 {
139 return WPStaging::isPro();
140 }
141
142 /**
143 * Load admin notices
144 * @throws Exception
145 */
146 public function renderNotices()
147 {
148 if (!current_user_can(WPStaging::make(Capabilities::class)->manageWPSTG())) {
149 return;
150 }
151
152 $this->settings = get_option('wpstg_settings', []);
153
154 $this->renderNoticesBasicVersion();
155 $this->renderNoticesProVersion();
156 $this->renderNoticesOnAllWpAdminPages();
157 $this->renderNoticesOnWpStagingAdminPages();
158 }
159
160 /**
161 * @return void
162 */
163 private function renderNoticesOnAllWpAdminPages()
164 {
165 $this->noticeListItemsDisabledOnStagingSite();
166 $this->noticeDbHasMissingOrUnexpectedPrimaryKeys();
167 $this->noticeWordFenceHasBeenDisabled();
168 $this->noticeSettingsAreCorrupted();
169 $this->noticeStagingUploadsFolderIsSymlinked();
170 $this->noticeTableTmpPrefixConflictNotice();
171 $this->showAnalyticsModal();
172 }
173
174 /**
175 * @return void
176 */
177 private function renderNoticesBasicVersion()
178 {
179 if (!$this->isPro()) {
180 do_action(self::BASIC_NOTICES_ACTION);
181 }
182 }
183
184 /**
185 * @return void
186 */
187 private function renderNoticesProVersion()
188 {
189 if ($this->isPro()) {
190 // This hook is for internal use only. Used in PRO version to display PRO version related notices.
191 do_action(self::PRO_NOTICES_ACTION);
192 }
193 }
194
195 /**
196 * @return void
197 */
198 private function renderNoticesOnWpStagingAdminPages()
199 {
200 if (!current_user_can("update_plugins") || !$this->isWPStagingAdminPage()) {
201 return;
202 }
203
204 $this->noticeUploadsDirIsOutsideAbspath();
205 $this->noticeWpStagingVersionIsOutdated();
206 $this->noticeObjectCachePluginNotRestored();
207 $this->noticeCacheDirectoryNotWriteable();
208 $this->noticeLoggerDirectoryNotWriteable();
209 $this->noticeAbspathDirectoryNotWriteable();
210 $this->noticeHomeAndSiteurlHaveDifferentScheme();
211 $this->noticeWpStagingHooksPluginIsOutdated();
212 $this->noticeMuPluginDirNotWriteable();
213 $this->noticeOptimizerIsDisabled();
214 $this->noticeShowDirectoryListingWarning($this->viewsNoticesPath);
215 $this->noticeDbPrefixDoesNotExist();
216 $this->noticeWPEnginePermalinkWarning();
217 }
218
219 /**
220 * @return void
221 */
222 private function noticeStagingUploadsFolderIsSymlinked()
223 {
224 $uploadsPath = wp_upload_dir()['basedir'];
225 if (self::SHOW_ALL_NOTICES || (is_link($uploadsPath) && $this->siteInfo->isStagingSite())) {
226 require_once $this->viewsNoticesPath . "staging-symlink-enabled-notice.php";
227 }
228 }
229
230 /**
231 * Show warning notice if current site prefix is equal to one of the WPSTG temporary prefixes wpstgtmp_ or wpstgbak_
232 */
233 private function noticeTableTmpPrefixConflictNotice()
234 {
235 $disallowedPrefixes = [PrepareRestore::TMP_DATABASE_PREFIX_TO_DROP, PrepareRestore::TMP_DATABASE_PREFIX];
236 if (self::SHOW_ALL_NOTICES || in_array($this->db->prefix, $disallowedPrefixes, true)) {
237 require $this->viewsNoticesPath . "table-tmp-prefix-conflict-notice.php";
238 }
239 }
240
241 /**
242 * Check whether the wp_options table is missing primary key | auto increment
243 * @param wpdb $db
244 * @param string $optionTable
245 *
246 * @return bool
247 */
248 private function isOptionTablePrimaryKeyMissing(wpdb $db, string $optionTable): bool
249 {
250 $result = $db->dbh->query("SELECT option_id FROM {$optionTable} LIMIT 1");
251 if (!is_object($result)) {
252 return false;
253 }
254
255 $fInfo = $result->fetch_field();
256 $result->free_result();
257
258 // Check whether the flag have primary key and auto increment flag
259 if (isset($fInfo->flags) && ($fInfo->flags & MYSQLI_PRI_KEY_FLAG) && $fInfo->flags & MYSQLI_AUTO_INCREMENT_FLAG) {
260 return false;
261 }
262
263 if ($this->isOptionTablePrimaryKeyIsOptionName($db, $optionTable)) {
264 return false;
265 }
266
267 return true;
268 }
269
270 /** @return bool */
271 private function isOptionTablePrimaryKeyIsOptionName($db, $optionTable): bool
272 {
273 $result = $db->dbh->query("SELECT option_name FROM {$optionTable} LIMIT 1");
274 if (!is_object($result)) {
275 return false;
276 }
277
278 $fInfo = $result->fetch_field();
279 $result->free_result();
280
281 // Abort if flag has no primary key
282 if (!($fInfo->flags & MYSQLI_PRI_KEY_FLAG)) {
283 return false;
284 }
285
286 // Check if the field has a composite key
287 $results = $db->get_results("SELECT `CONSTRAINT_NAME`,`COLUMN_NAME` FROM `information_schema`.`KEY_COLUMN_USAGE` WHERE `table_name`='{$optionTable}' AND `table_schema`=DATABASE()", ARRAY_A);
288 if (empty($results) || !is_array($results)) {
289 return true;
290 }
291
292 $found = 0;
293 while ($row = array_shift($results)) {
294 if ($row['CONSTRAINT_NAME'] === 'PRIMARY' && in_array($row['COLUMN_NAME'], ['option_name', 'option_id'])) {
295 $found++;
296 }
297
298 if ($found > 1) {
299 return false;
300 }
301 }
302
303 return true;
304 }
305
306 /**
307 * Displays the notice that we could not prevent
308 * directory listing on a sensitive folder for some reason.
309 *
310 * @param string $viewsNoticesPath The path to the views folder.
311 * @see \WPStaging\Framework\Filesystem\Filesystem::mkdir The place where all errors are enqueued
312 * to be displayed as a single notice here.
313 *
314 * Note: When refactoring this, keep in mind this code should be
315 * called only once, otherwise the message would be enqueued multiple times.
316 *
317 */
318 private function noticeShowDirectoryListingWarning(string $viewsNoticesPath)
319 {
320 $directoryListingErrors = WPStaging::getInstance()->getContainer()->getFromArray(static::$directoryListingErrors);
321
322 // Early bail: No errors to show
323 if (!self::SHOW_ALL_NOTICES && empty($directoryListingErrors)) {
324 return;
325 }
326
327 // Early bail: These warnings were disabled by the user.
328 if (apply_filters('wpstg.notices.hideDirectoryListingWarnings', false)) {
329 return;
330 }
331
332 require_once "{$viewsNoticesPath}directory-listing-could-not-be-prevented.php";
333 }
334
335 /**
336 * Check if the url scheme of siteurl and home is identical
337 * @return bool
338 */
339 private function isDifferentScheme(): bool
340 {
341 $siteurlScheme = parse_url(get_option('siteurl'), PHP_URL_SCHEME);
342 $homeScheme = parse_url(get_option('home'), PHP_URL_SCHEME);
343
344 return !($siteurlScheme === $homeScheme);
345 }
346
347 /**
348 * Check if the user is using an outdated version of WP Staging Hooks plugin
349 * @return bool
350 */
351 private function isUsingOutdatedWpstgHooksPlugin(): bool
352 {
353 // Minimum version to check
354 $versionToCheck = '0.0.4';
355
356 // Path to WP Staging Hooks plugins in a directory
357 $wpstgHooksPath = 'wp-staging-hooks/wp-staging-hooks.php';
358
359 // Only show notice if plugin exists for above path
360 if (file_exists(WP_PLUGIN_DIR . '/' . $wpstgHooksPath)) {
361 $wpstgHooksData = get_plugin_data(WP_PLUGIN_DIR . '/' . $wpstgHooksPath);
362 // Only show notice if current version is below required min version.
363 return version_compare($wpstgHooksData['Version'], $versionToCheck, '>=') ? false : true;
364 }
365
366 // Path to WP Staging Hooks plugins directly in plugins dir
367 $wpstgHooksPath = 'wp-staging-hooks.php';
368
369 // Only show notice if plugin exists for above path
370 if (file_exists(WP_PLUGIN_DIR . '/' . $wpstgHooksPath)) {
371 $wpstgHooksData = get_plugin_data(WP_PLUGIN_DIR . '/' . $wpstgHooksPath);
372 // Only show notice if current version is below required min version.
373 return version_compare($wpstgHooksData['Version'], $versionToCheck, '>=') ? false : true;
374 }
375
376 return false;
377 }
378
379 /**
380 * Render the notice dismiss action
381 *
382 * @param string $viewsNoticesPath
383 * @param string $wpstgNotice
384 * @param string $cssClassSelectorDismiss
385 * @param string $cssClassSelectorNotice
386 *
387 * @todo Convert to Facade for testability?
388 */
389 public static function renderNoticeDismissAction(string $viewsNoticesPath, $wpstgNotice, $cssClassSelectorDismiss, $cssClassSelectorNotice)
390 {
391 require "{$viewsNoticesPath}_partial/notice_dismiss_action.php";
392 }
393
394 /**
395 * @param $settings
396 * @return bool
397 */
398 private function isSettingsCorrupt(): bool
399 {
400 if (!is_array($this->settings) && !is_object($this->settings)) {
401 return true;
402 }
403
404 return false;
405 }
406
407 /**
408 * @return void
409 */
410 private function noticeDbHasMissingOrUnexpectedPrimaryKeys()
411 {
412 $optionTable = $this->db->prefix . 'options';
413 $isPrimaryKeyMissing = $this->isOptionTablePrimaryKeyMissing($this->db, $optionTable);
414 $isPrimaryKeyIsOptionName = $this->isOptionTablePrimaryKeyIsOptionName($this->db, $optionTable);
415 if (self::SHOW_ALL_NOTICES || (current_user_can("manage_options") && ($isPrimaryKeyMissing || $isPrimaryKeyIsOptionName) && $this->isWPStagingAdminPage())) {
416 require $this->viewsNoticesPath . "wp-options-missing-pk.php";
417 }
418 }
419
420 /**
421 * @return void
422 */
423 private function noticeDbPrefixDoesNotExist()
424 {
425 if (self::SHOW_ALL_NOTICES || empty($this->db->prefix)) {
426 require_once $this->viewsNoticesPath . "no-db-prefix-notice.php";
427 }
428 }
429
430 /**
431 * @return void
432 */
433 private function noticeWPEnginePermalinkWarning()
434 {
435 if (self::SHOW_ALL_NOTICES || class_exists('WPE_API')) {
436 require_once $this->viewsNoticesPath . "wpe-permalink-issue-notice.php";
437 }
438 }
439
440 /**
441 * @param $wpstgSettings
442 * @return void
443 */
444 private function noticeOptimizerIsDisabled()
445 {
446 $wpstgSettings = (object)$this->settings;
447 if (self::SHOW_ALL_NOTICES || empty($wpstgSettings->optimizer)) {
448 require_once $this->viewsNoticesPath . "disabled-optimizer-notice.php";
449 }
450 }
451
452 /**
453 * @return void
454 */
455 private function noticeMuPluginDirNotWriteable()
456 {
457 $varsDirectory = defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : trailingslashit(WP_CONTENT_DIR) . 'mu-plugins';
458 $wpstgSettings = (object)$this->settings;
459 if (
460 self::SHOW_ALL_NOTICES || (!is_writable($varsDirectory) || !is_readable($varsDirectory))
461 && isset($wpstgSettings->optimizer) && $wpstgSettings->optimizer
462 ) {
463 require $this->viewsNoticesPath . "mu-plugin-directory-permission-problem.php";
464 }
465 }
466
467 /**
468 * @return void
469 */
470 private function noticeWpStagingHooksPluginIsOutdated()
471 {
472 if (self::SHOW_ALL_NOTICES || ($this->isUsingOutdatedWpstgHooksPlugin())) {
473 require_once $this->viewsNoticesPath . "outdated-wp-staging-hooks.php";
474 }
475 }
476
477 /**
478 * @return void
479 */
480 private function noticeHomeAndSiteurlHaveDifferentScheme()
481 {
482 if (self::SHOW_ALL_NOTICES || ($this->isDifferentScheme())) {
483 require_once $this->viewsNoticesPath . "wrong-scheme.php";
484 }
485 }
486
487 /**
488 * @return void
489 */
490 private function noticeAbspathDirectoryNotWriteable()
491 {
492 // Don't show this notice on WP Com Sites
493 if (self::SHOW_ALL_NOTICES || ((!is_writable(ABSPATH)) && !$this->isWpComSite)) {
494 require_once $this->viewsNoticesPath . "staging-directory-permission-problem.php";
495 }
496 }
497
498 /**
499 * @return void
500 */
501 private function noticeLoggerDirectoryNotWriteable()
502 {
503 $logsDir = $this->logger->getLogDir();
504 if (self::SHOW_ALL_NOTICES || (!is_dir($logsDir) || !is_writable($logsDir))) {
505 require_once $this->viewsNoticesPath . "logs-directory-permission-problem.php";
506 }
507 }
508
509 /**
510 * @return void
511 */
512 private function noticeCacheDirectoryNotWriteable()
513 {
514 $cacheDir = $this->cache->getPath();
515 if (self::SHOW_ALL_NOTICES || (!is_dir($cacheDir) || !is_writable($cacheDir))) {
516 require_once $this->viewsNoticesPath . "cache-directory-permission-problem.php";
517 }
518 }
519
520 /**
521 * @return void
522 */
523 private function noticeObjectCachePluginNotRestored()
524 {
525 if (self::SHOW_ALL_NOTICES || ($this->objectCacheNotice->isEnabled())) {
526 require_once $this->viewsNoticesPath . "object-cache-skipped.php";
527 }
528 }
529
530 /**
531 * @return void
532 */
533 private function noticeWpStagingVersionIsOutdated()
534 {
535 /**
536 * Display outdated WP Staging version notice (Free Only)
537 */
538 $this->outdatedWpStagingNotice->showNotice($this->viewsNoticesPath);
539 }
540
541 /**
542 * @return void
543 */
544 private function noticeUploadsDirIsOutsideAbspath()
545 {
546 if (self::SHOW_ALL_NOTICES || (!$this->dirUtil->isPathInWpRoot($this->dirUtil->getUploadsDirectory()) && !$this->siteInfo->isFlywheel() && !$this->isWpComSite)) {
547 require $this->viewsNoticesPath . "uploads-outside-wp-root.php";
548 }
549 }
550
551 /**
552 * @return void
553 */
554 private function noticeSettingsAreCorrupted()
555 {
556 if (self::SHOW_ALL_NOTICES || ($this->isSettingsCorrupt())) {
557 require $this->viewsNoticesPath . "settings_option_corrupt.php";
558 }
559 }
560
561 /**
562 * @return void
563 */
564 private function noticeWordFenceHasBeenDisabled()
565 {
566 $this->wordfence->showNotice($this->viewsNoticesPath);
567 }
568
569 /**
570 * @return void
571 */
572 private function noticeListItemsDisabledOnStagingSite()
573 {
574 // free version has no option to disable outgoing mails
575 $outgoingMailsDisabled = false;
576
577 if ($this->isPro()) {
578 // Check mails disabled against both the old and new way of emails disabled option
579 $outgoingMailsDisabled = $this->cloneOptions->get(FirstRun::MAILS_DISABLED_KEY) || (get_option(FirstRun::MAILS_DISABLED_KEY, false));
580 }
581
582 // Show notice about what disabled in the staging site. (Show only on staging site)
583 if (self::SHOW_ALL_NOTICES || $this->disabledItemsNotice->isEnabled()) {
584 $excludedPlugins = (array)$this->excludedPlugins->getExcludedPlugins();
585 // Show freemius notice if freemius options were deleted during cloning.
586 $freemiusOptionsCleared = $this->freemiusScript->isNoticeEnabled();
587 // Show jetpack staging mode notice if the constant is set on staging site
588 $isJetpackStagingModeActive = defined(Jetpack::STAGING_MODE_CONST) && constant(Jetpack::STAGING_MODE_CONST) === true;
589 $excludedFiles = get_option(Sites::STAGING_EXCLUDED_FILES_OPTION, []);
590 $excludedGoDaddyFiles = get_option(Sites::STAGING_EXCLUDED_GD_FILES_OPTION, []);
591 // use require here instead of require_once otherwise unit tests will always fail,
592 // as this notice is tested multiple times.
593 require $this->viewsNoticesPath . "disabled-items-notice.php";
594 }
595 }
596
597 /**
598 * @return void
599 */
600 public function showAnalyticsModal()
601 {
602 if (get_option(AnalyticsConsent::OPTION_NAME_ANALYTICS_MODAL_DISMISSED)) {
603 return;
604 }
605
606 if (!$this->isWPStagingAdminPage()) {
607 return;
608 }
609
610 if (WPStaging::make(AnalyticsConsent::class)->hasUserConsent()) {
611 return;
612 }
613
614 wp_enqueue_script(
615 "wpstg-show-analytics-modal",
616 WPSTG_PLUGIN_URL . './assets/js/dist/analytics-consent-modal.js'
617 );
618 wp_enqueue_style(
619 'wpstg-plugin-activation',
620 WPSTG_PLUGIN_URL . './assets/css/dist/analytics-consent-modal.css'
621 );
622
623 require_once "{$this->getPluginPath()}/Backend/views/notices/analytics-modal.php";
624 }
625 }
626