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