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