PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 3.0.4
WP STAGING – WordPress Backups, Restore, Migration & Clone v3.0.4
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 3 years ago ObjectCacheNotice.php 3 years ago OutdatedWpStagingNotice.php 3 years ago WarningsNotice.php 3 years ago
Notices.php
437 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\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\Backup\Ajax\Restore\PrepareRestore;
23
24 /**
25 * Show Admin Notices | Warnings | Messages
26 *
27 * Class Notices
28 * @package WPStaging\Framework\Notices
29 * @todo maybe split this class into multiple classes like staging notices, permission notices etc
30 * to avoid dependency hell without using service locator?
31 */
32 class Notices
33 {
34 use NoticesTrait;
35
36 /** @var string */
37 const PRO_NOTICES_ACTION = 'wpstg.notices.show_pro_notices';
38
39 /** @var string */
40 const BASIC_NOTICES_ACTION = 'wpstg.notices.show_basic_notices';
41
42 /** @var Assets */
43 private $assets;
44
45 /** @var Directory */
46 private $dirUtil;
47
48 /** @var Cache */
49 private $cache;
50
51 /** @var Logger */
52 private $logger;
53
54 /** @var CloneOptions */
55 private $cloneOptions;
56
57 /** @var ExcludedPlugins */
58 private $excludedPlugins;
59
60 /** @var FreemiusScript */
61 private $freemiusScript;
62
63 /** @var WordFence */
64 private $wordfence;
65
66 /** @var DisabledItemsNotice */
67 private $disabledItemsNotice;
68
69 /** @var WarningsNotice */
70 private $warningsNotice;
71
72 /** @var OutdatedWpStagingNotice */
73 private $outdatedWpStagingNotice;
74
75 /** @var ObjectCacheNotice */
76 private $objectCacheNotice;
77
78 /** @var wpdb */
79 private $db;
80
81 /** For testing all notices */
82 const SHOW_ALL_NOTICES = false;
83
84 /**
85 * @var string The key that holds directory listing errors in the container.
86 */
87 public static $directoryListingErrors = 'directoryListingErrors';
88
89 /** @var SiteInfo */
90 private $siteInfo;
91
92 /** @var string */
93 private $viewsNoticesPath;
94
95 public function __construct(Assets $assets)
96 {
97 $this->assets = $assets;
98 $this->viewsNoticesPath = trailingslashit($this->getPluginPath()) . "Backend/views/notices/";
99
100 // To avoid dependency hell and smooth transition we will be using service locator for below dependencies
101 $this->dirUtil = WPStaging::make(Directory::class);
102 $this->wordfence = WPStaging::make(WordFence::class);
103 $this->cloneOptions = WPStaging::make(CloneOptions::class);
104 $this->freemiusScript = WPStaging::make(FreemiusScript::class);
105 $this->excludedPlugins = WPStaging::make(ExcludedPlugins::class);
106 $this->logger = WPStaging::make("logger");
107 $this->cache = WPStaging::make("cache");
108 $this->db = WPStaging::make('wpdb');
109
110 // Notices
111 $this->disabledItemsNotice = WPStaging::make(DisabledItemsNotice::class);
112 $this->warningsNotice = WPStaging::make(WarningsNotice::class);
113 $this->outdatedWpStagingNotice = WPStaging::make(OutdatedWpStagingNotice::class);
114 $this->objectCacheNotice = WPStaging::make(ObjectCacheNotice::class);
115 $this->siteInfo = WPStaging::make(SiteInfo::class);
116 }
117
118 /**
119 * Check whether the plugin is pro version
120 *
121 * @return bool
122 */
123 protected function isPro(): bool
124 {
125 return WPStaging::isPro();
126 }
127
128 /**
129 * Load admin notices
130 * @todo refactor this method into smaller methods
131 * @throws Exception
132 */
133 public function renderNotices()
134 {
135 if (!current_user_can(WPStaging::make(Capabilities::class)->manageWPSTG())) {
136 return;
137 }
138
139 // Added this as workaround to not break multiple notices after the recent refactor
140 // @todo refactor this class to avoid this workaround
141 $viewsNoticesPath = $this->viewsNoticesPath;
142
143 if (!$this->isPro()) {
144 do_action(self::BASIC_NOTICES_ACTION);
145 }
146
147 // Never show disable mail message if it's free version
148 $outgoingMailsDisabled = false;
149
150 // Show all notices of the pro version
151 if ($this->isPro()) {
152 // Check mails disabled against both the old and new way of emails disabled option
153 $outgoingMailsDisabled = $this->cloneOptions->get(FirstRun::MAILS_DISABLED_KEY) || (get_option(FirstRun::MAILS_DISABLED_KEY, false));
154 // This hook is for internal use only. Used in PRO version to display PRO version related notices.
155 do_action(self::PRO_NOTICES_ACTION);
156 }
157
158 // Show notice about what disabled in the staging site. (Show only on staging site)
159 if (self::SHOW_ALL_NOTICES || $this->disabledItemsNotice->isEnabled()) {
160 $excludedPlugins = (array)$this->excludedPlugins->getExcludedPlugins();
161 // Show freemius notice if freemius options were deleted during cloning.
162 $freemiusOptionsCleared = $this->freemiusScript->isNoticeEnabled();
163 // Show jetpack staging mode notice if the constant is set on staging site
164 $isJetpackStagingModeActive = defined(Jetpack::STAGING_MODE_CONST) && constant(Jetpack::STAGING_MODE_CONST) === true;
165 $excludedFiles = get_option(Sites::STAGING_EXCLUDED_FILES_OPTION, []);
166 // use require here instead of require_once otherwise unit tests will always fail,
167 // as this notice is tested multiple times.
168 require $this->viewsNoticesPath . "disabled-items-notice.php";
169 }
170
171 $optionTable = $this->db->prefix . 'options';
172 $isPrimaryKeyMissing = $this->isOptionTablePrimaryKeyMissing($this->db, $optionTable);
173 $isPrimaryKeyIsOptionName = $this->isOptionTablePrimaryKeyIsOptionName($this->db, $optionTable);
174 if (self::SHOW_ALL_NOTICES || (current_user_can("manage_options") && ( $isPrimaryKeyMissing || $isPrimaryKeyIsOptionName ) && $this->isWPStagingAdminPage())) {
175 require $this->viewsNoticesPath . "wp-options-missing-pk.php";
176 }
177
178 // Show notice if WordFence Firewall is disabled
179 $this->wordfence->showNotice($this->viewsNoticesPath);
180
181 $settings = get_option('wpstg_settings', []);
182 if (self::SHOW_ALL_NOTICES || (!is_array($settings) && !is_object($settings))) {
183 require $this->viewsNoticesPath . "settings_option_corrupt.php";
184 }
185
186 $this->showWarningIfStagingUploadsFolderIsSymlinked();
187
188 $this->showTableTmpPrefixConflictNotice();
189
190 /**
191 * Display all notices below this line in WP STAGING admin pages only and only to administrators!
192 */
193 if (!current_user_can("update_plugins") || !$this->isWPStagingAdminPage()) {
194 return;
195 }
196
197 // Show notice if uploads dir is outside ABSPATH
198 if (self::SHOW_ALL_NOTICES || !$this->dirUtil->isPathInWpRoot($this->dirUtil->getUploadsDirectory())) {
199 require $this->viewsNoticesPath . "uploads-outside-wp-root.php";
200 }
201
202 /**
203 * Display outdated WP Staging version notice (Free Only)
204 */
205 $this->outdatedWpStagingNotice->showNotice($this->viewsNoticesPath);
206
207 // Show notice Outdated version of WP Staging Hooks
208 if (self::SHOW_ALL_NOTICES || ($this->objectCacheNotice->isEnabled())) {
209 require_once $this->viewsNoticesPath . "object-cache-skipped.php";
210 }
211
212 // Show notice Cache directory is not writable
213 $cacheDir = $this->cache->getCacheDir();
214 if (self::SHOW_ALL_NOTICES || (!is_dir($cacheDir) || !is_writable($cacheDir))) {
215 require_once $this->viewsNoticesPath . "cache-directory-permission-problem.php";
216 }
217
218 // Show notice Logger directory is not writable
219 $logsDir = $this->logger->getLogDir();
220 if (self::SHOW_ALL_NOTICES || (!is_dir($logsDir) || !is_writable($logsDir))) {
221 require_once $this->viewsNoticesPath . "logs-directory-permission-problem.php";
222 }
223
224 // Show notice Staging directory is not writable
225 if (self::SHOW_ALL_NOTICES || (!is_writable(ABSPATH))) {
226 require_once $this->viewsNoticesPath . "staging-directory-permission-problem.php";
227 }
228
229 // Show notice Different scheme in home and siteurl
230 if (self::SHOW_ALL_NOTICES || ($this->isDifferentScheme())) {
231 require_once $this->viewsNoticesPath . "wrong-scheme.php";
232 }
233
234 // Show notice Outdated version of WP Staging Hooks
235 if (self::SHOW_ALL_NOTICES || ($this->isUsingOutdatedWpstgHooksPlugin())) {
236 require_once $this->viewsNoticesPath . "outdated-wp-staging-hooks.php";
237 }
238
239 // Show notice mu-plugin directory is not executable
240 $varsDirectory = defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : trailingslashit(WP_CONTENT_DIR) . 'mu-plugins';
241 $wpstgSettings = (object) $settings;
242 if (
243 self::SHOW_ALL_NOTICES || (!is_writable($varsDirectory) || !is_readable($varsDirectory))
244 && isset($wpstgSettings->optimizer) && $wpstgSettings->optimizer
245 ) {
246 require $this->viewsNoticesPath . "mu-plugin-directory-permission-problem.php";
247 }
248
249 if (self::SHOW_ALL_NOTICES || empty($wpstgSettings->optimizer)) {
250 require_once $this->viewsNoticesPath . "disabled-optimizer-notice.php";
251 }
252
253 // Show notice Failed to prevent directory listing
254 $this->showDirectoryListingWarningNotice($this->viewsNoticesPath);
255
256 // Show notice if db prefix does not exist
257 if (self::SHOW_ALL_NOTICES || empty($this->db->prefix)) {
258 require_once $this->viewsNoticesPath . "no-db-prefix-notice.php";
259 }
260 }
261
262 /**
263 * @return void
264 */
265 private function showWarningIfStagingUploadsFolderIsSymlinked()
266 {
267 $uploadsPath = wp_upload_dir()['basedir'];
268 if (self::SHOW_ALL_NOTICES || (is_link($uploadsPath) && $this->siteInfo->isStagingSite())) {
269 require_once $this->viewsNoticesPath . "staging-symlink-enabled-notice.php";
270 }
271 }
272
273 /**
274 * Show warning notice if current site prefix is equal to one of the WPSTG temporary prefixes wpstgtmp_ or wpstgbak_
275 */
276 private function showTableTmpPrefixConflictNotice()
277 {
278 $disallowedPrefixes = [PrepareRestore::TMP_DATABASE_PREFIX_TO_DROP, PrepareRestore::TMP_DATABASE_PREFIX];
279 if (self::SHOW_ALL_NOTICES || in_array($this->db->prefix, $disallowedPrefixes, true)) {
280 require $this->viewsNoticesPath . "table-tmp-prefix-conflict-notice.php";
281 }
282 }
283
284 /**
285 * Check whether the wp_options table is missing primary key | auto increment
286 * @param wpdb $db
287 * @param string $optionTable
288 *
289 * @return bool
290 */
291 private function isOptionTablePrimaryKeyMissing(wpdb $db, string $optionTable): bool
292 {
293 $result = $db->dbh->query("SELECT option_id FROM {$optionTable} LIMIT 1");
294 if (!is_object($result)) {
295 return false;
296 }
297
298 $fInfo = $result->fetch_field();
299 $result->free_result();
300
301 // Check whether the flag have primary key and auto increment flag
302 if (isset($fInfo->flags) && ($fInfo->flags & MYSQLI_PRI_KEY_FLAG) && $fInfo->flags & MYSQLI_AUTO_INCREMENT_FLAG) {
303 return false;
304 }
305
306 if ($this->isOptionTablePrimaryKeyIsOptionName($db, $optionTable)) {
307 return false;
308 }
309
310 return true;
311 }
312
313 /** @return bool */
314 private function isOptionTablePrimaryKeyIsOptionName($db, $optionTable): bool
315 {
316 $result = $db->dbh->query("SELECT option_name FROM {$optionTable} LIMIT 1");
317 if (!is_object($result)) {
318 return false;
319 }
320
321 $fInfo = $result->fetch_field();
322 $result->free_result();
323
324 // Abort if flag has no primary key
325 if (!($fInfo->flags & MYSQLI_PRI_KEY_FLAG)) {
326 return false;
327 }
328
329 // Check if the field has a composite key
330 $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);
331 if (empty($results) || !is_array($results)) {
332 return true;
333 }
334
335 $found = 0;
336 while ($row = array_shift($results)) {
337 if ($row['CONSTRAINT_NAME'] === 'PRIMARY' && in_array($row['COLUMN_NAME'], ['option_name', 'option_id'])) {
338 $found++;
339 }
340
341 if ($found > 1) {
342 return false;
343 }
344 }
345
346 return true;
347 }
348
349 /**
350 * Displays the notice that we could not prevent
351 * directory listing on a sensitive folder for some reason.
352 *
353 * @param string $viewsNoticesPath The path to the views folder.
354 * @see \WPStaging\Framework\Filesystem\Filesystem::mkdir The place where all errors are enqueued
355 * to be displayed as a single notice here.
356 *
357 * Note: When refactoring this, keep in mind this code should be
358 * called only once, otherwise the message would be enqueued multiple times.
359 *
360 */
361 private function showDirectoryListingWarningNotice(string $viewsNoticesPath)
362 {
363 $directoryListingErrors = WPStaging::getInstance()->getContainer()->getFromArray(static::$directoryListingErrors);
364
365 // Early bail: No errors to show
366 if (!self::SHOW_ALL_NOTICES && empty($directoryListingErrors)) {
367 return;
368 }
369
370 // Early bail: These warnings were disabled by the user.
371 if (apply_filters('wpstg.notices.hideDirectoryListingWarnings', false)) {
372 return;
373 }
374
375 require_once "{$viewsNoticesPath}directory-listing-could-not-be-prevented.php";
376 }
377
378 /**
379 * Check if the url scheme of siteurl and home is identical
380 * @return bool
381 */
382 private function isDifferentScheme(): bool
383 {
384 $siteurlScheme = parse_url(get_option('siteurl'), PHP_URL_SCHEME);
385 $homeScheme = parse_url(get_option('home'), PHP_URL_SCHEME);
386
387 return !($siteurlScheme === $homeScheme);
388 }
389
390 /**
391 * Check if the user is using an outdated version of WP Staging Hooks plugin
392 * @return bool
393 */
394 private function isUsingOutdatedWpstgHooksPlugin(): bool
395 {
396 // Minimum version to check
397 $versionToCheck = '0.0.4';
398
399 // Path to WP Staging Hooks plugins in a directory
400 $wpstgHooksPath = 'wp-staging-hooks/wp-staging-hooks.php';
401
402 // Only show notice if plugin exists for above path
403 if (file_exists(WP_PLUGIN_DIR . '/' . $wpstgHooksPath)) {
404 $wpstgHooksData = get_plugin_data(WP_PLUGIN_DIR . '/' . $wpstgHooksPath);
405 // Only show notice if current version is below required min version.
406 return version_compare($wpstgHooksData['Version'], $versionToCheck, '>=') ? false : true;
407 }
408
409 // Path to WP Staging Hooks plugins directly in plugins dir
410 $wpstgHooksPath = 'wp-staging-hooks.php';
411
412 // Only show notice if plugin exists for above path
413 if (file_exists(WP_PLUGIN_DIR . '/' . $wpstgHooksPath)) {
414 $wpstgHooksData = get_plugin_data(WP_PLUGIN_DIR . '/' . $wpstgHooksPath);
415 // Only show notice if current version is below required min version.
416 return version_compare($wpstgHooksData['Version'], $versionToCheck, '>=') ? false : true;
417 }
418
419 return false;
420 }
421
422 /**
423 * Render the notice dismiss action
424 *
425 * @param string $viewsNoticesPath
426 * @param string $wpstgNotice
427 * @param string $cssClassSelectorDismiss
428 * @param string $cssClassSelectorNotice
429 *
430 * @todo Convert to Facade for testability?
431 */
432 public static function renderNoticeDismissAction(string $viewsNoticesPath, $wpstgNotice, $cssClassSelectorDismiss, $cssClassSelectorNotice)
433 {
434 require "{$viewsNoticesPath}_partial/notice_dismiss_action.php";
435 }
436 }
437