PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.10.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.10.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 / CliIntegrationNotice.php
wp-staging / Framework / Notices Last commit date
BackupPluginsNotice.php 4 days ago BooleanNotice.php 2 years ago CliIntegrationNotice.php 4 days ago DisabledItemsNotice.php 2 years ago DismissNotice.php 1 month ago NextGenEngineNotice.php 1 month ago Notices.php 1 month ago NoticesHandler.php 4 days ago ObjectCacheNotice.php 1 year ago OutdatedWpStagingNotice.php 1 year ago WarningsNotice.php 2 years ago WpVersionCompatNotice.php 5 months ago
CliIntegrationNotice.php
431 lines
1 <?php
2
3 namespace WPStaging\Framework\Notices;
4
5 use WPStaging\Core\WPStaging;
6 use WPStaging\Framework\Security\Auth;
7 use WPStaging\Framework\Traits\NoticesTrait;
8
9 /**
10 * Displays a dismissible banner promoting the WP Staging CLI tool
11 *
12 * The banner appears on both Staging and Backup tabs for both free and Pro version users.
13 * "Later" dismissal hides the banner for 24 hours; permanent dismissal hides it for good.
14 * Both dismissal states are stored in wp_options so the banner can be suppressed server-side,
15 * avoiding a flash where the banner renders and is then hidden by JavaScript.
16 */
17 class CliIntegrationNotice
18 {
19 use NoticesTrait;
20
21 const IS_ENABLED = true;
22
23 /**
24 * @var string Option key for permanent dismissal
25 */
26 const OPTION_CLI_NOTICE_HIDDEN_FOREVER = 'wpstg_cli_notice_hidden_forever';
27
28 /**
29 * @var string Option key for showing dock CTA after banner dismissal
30 */
31 const OPTION_CLI_DOCK_CTA_SHOWN = 'wpstg_cli_dock_cta_shown';
32
33 /**
34 * @var string Option key holding the Unix timestamp until which the banner stays hidden after "Later"
35 */
36 const OPTION_CLI_NOTICE_DISMISSED_UNTIL = 'wpstg_cli_notice_dismissed_until';
37
38 /**
39 * @var Auth
40 */
41 private $auth;
42
43 /**
44 * @param Auth $auth
45 */
46 public function __construct(Auth $auth)
47 {
48 $this->auth = $auth;
49 }
50
51 /**
52 * Display the CLI integration banner if conditions are met
53 *
54 * @return void
55 */
56 public function maybeShowCliNotice()
57 {
58 if (!self::IS_ENABLED) {
59 return;
60 }
61
62 if (!$this->isWPStagingAdminPage()) {
63 return;
64 }
65
66 if (!current_user_can('manage_options')) {
67 return;
68 }
69
70 if (get_option(self::OPTION_CLI_NOTICE_HIDDEN_FOREVER)) {
71 return;
72 }
73
74 if ($this->isTemporarilyDismissed()) {
75 return;
76 }
77
78 $notice = WPSTG_VIEWS_DIR . 'notices/cli-integration-notice.php';
79
80 if (!file_exists($notice)) {
81 return;
82 }
83
84 $isDeveloperOrHigher = $this->isDeveloperOrHigherLicense();
85 $hasActiveLicense = $this->hasActiveLicense();
86 $planName = $this->getLicensePlanName();
87 $backups = $this->fetchSortedBackups($isDeveloperOrHigher);
88 $urlAssets = trailingslashit(WPSTG_PLUGIN_URL) . 'assets/';
89 $licenseType = $this->getLicenseTypeSlug();
90 $licenseId = $this->getLicenseId();
91
92 include $notice;
93 }
94
95 /**
96 * Whether the banner is within its 24-hour "Later" dismissal window.
97 * Clears the expired option so the banner reappears once the window has passed.
98 *
99 * @return bool
100 */
101 private function isTemporarilyDismissed(): bool
102 {
103 $dismissedUntil = (int)get_option(self::OPTION_CLI_NOTICE_DISMISSED_UNTIL, 0);
104 if ($dismissedUntil === 0) {
105 return false;
106 }
107
108 if (time() < $dismissedUntil) {
109 return true;
110 }
111
112 delete_option(self::OPTION_CLI_NOTICE_DISMISSED_UNTIL);
113 return false;
114 }
115
116 /**
117 * AJAX handler to dismiss the CLI notice temporarily.
118 * Hides the banner for 24 hours and persists the dock CTA flag, both server-side,
119 * so the banner is suppressed on the next page load without a flash.
120 *
121 * @return void
122 */
123 public function ajaxCliNoticeClose()
124 {
125 if (!$this->auth->isAuthenticatedRequest('', 'manage_options')) {
126 wp_send_json_error();
127 }
128
129 update_option(self::OPTION_CLI_NOTICE_DISMISSED_UNTIL, time() + DAY_IN_SECONDS, false);
130 update_option(self::OPTION_CLI_DOCK_CTA_SHOWN, true, false);
131 wp_send_json_success();
132 }
133
134 /**
135 * AJAX handler to permanently dismiss the CLI notice
136 *
137 * @return void
138 */
139 public function ajaxCliNoticeHideForever()
140 {
141 if (!$this->auth->isAuthenticatedRequest('', 'manage_options')) {
142 wp_send_json_error();
143 }
144
145 update_option(self::OPTION_CLI_NOTICE_HIDDEN_FOREVER, true);
146 update_option(self::OPTION_CLI_DOCK_CTA_SHOWN, true, false);
147 wp_send_json_success();
148 }
149
150 /**
151 * Check if the dock CTA should be shown (banner was dismissed).
152 * Shows for all users after banner dismissal. Non-developer users see a "Pro" badge
153 * and an upgrade notice inside the modal.
154 *
155 * @return bool
156 */
157 public function shouldShowDockCta(): bool
158 {
159 if (!self::IS_ENABLED) {
160 return false;
161 }
162
163 if (!$this->isWPStagingAdminPage()) {
164 return false;
165 }
166
167 if (!current_user_can('manage_options')) {
168 return false;
169 }
170
171 if (!get_option(self::OPTION_CLI_DOCK_CTA_SHOWN)) {
172 return false;
173 }
174
175 // The dock CTA is the collapsed form of the banner, so it must never show alongside it.
176 if (!$this->isBannerDismissed()) {
177 return false;
178 }
179
180 return true;
181 }
182
183 /**
184 * Whether the banner is currently dismissed, either permanently or within its 24-hour window.
185 *
186 * @return bool
187 */
188 private function isBannerDismissed(): bool
189 {
190 return (bool)get_option(self::OPTION_CLI_NOTICE_HIDDEN_FOREVER) || $this->isTemporarilyDismissed();
191 }
192
193 /**
194 * Check if the user has a Developer or higher license plan.
195 * For Basic version, always returns false.
196 *
197 * @return bool
198 */
199 public function isDeveloperOrHigherLicense(): bool
200 {
201 return $this->checkLicensingCondition('isActiveAgencyOrDeveloperPlan');
202 }
203
204 /**
205 * Check if the user has an expired Developer or Agency license plan
206 *
207 * @return bool
208 */
209 public function isExpiredDeveloperOrHigherLicense(): bool
210 {
211 return $this->checkLicensingCondition('isExpiredDeveloperOrAgencyPlan');
212 }
213
214 /**
215 * Whether the user has a valid, active pro license (not free, not expired, not unregistered).
216 * Used to decide if the upgrade button should link to the internal license page or external checkout.
217 */
218 private function hasActiveLicense(): bool
219 {
220 return $this->checkLicensingCondition('isValidOrExpiredLicenseKey');
221 }
222
223 /**
224 * Get the license plan name for the current license.
225 * For Basic version or invalid licenses, returns "Unregistered".
226 *
227 * @return string
228 */
229 public function getLicensePlanName(): string
230 {
231 if (WPStaging::isBasic()) {
232 return __('Free', 'wp-staging');
233 }
234
235 if (!class_exists('\WPStaging\Pro\License\Licensing')) {
236 return __('Unregistered', 'wp-staging');
237 }
238
239 $licensing = WPStaging::make(\WPStaging\Pro\License\Licensing::class);
240
241 if (!$licensing->isValidOrExpiredLicenseKey()) {
242 return __('Unregistered', 'wp-staging');
243 }
244
245 $planName = $licensing->getPlanDisplayName();
246
247 return $planName !== '' ? $planName : __('Unregistered', 'wp-staging');
248 }
249
250 /**
251 * Render the dock CTA if conditions are met (called from staging listing view)
252 *
253 * @return void
254 */
255 public function maybeRenderDockCta()
256 {
257 if (!$this->shouldShowDockCta()) {
258 return;
259 }
260
261 $dockCtaView = WPSTG_VIEWS_DIR . 'cli/cli-dock-cta.php';
262 if (!file_exists($dockCtaView)) {
263 return;
264 }
265
266 include $dockCtaView;
267 }
268
269 /**
270 * Render the CLI modal content if the dock CTA should be shown
271 *
272 * This ensures the modal is available when the dock CTA is rendered
273 * server-side (when the banner was previously dismissed).
274 *
275 * @return void
276 */
277 public function maybeRenderCliModalForDockCta()
278 {
279 if (!$this->shouldShowDockCta()) {
280 return;
281 }
282
283 $this->renderCliModalContent();
284 }
285
286 /**
287 * Render the CLI modal content with all required variables
288 *
289 * @return void
290 */
291 private function renderCliModalContent()
292 {
293 if (!empty($GLOBALS['wpstg_cli_modal_rendered'])) {
294 return;
295 }
296
297 $isDeveloperOrHigher = $this->isDeveloperOrHigherLicense();
298 $backups = $this->fetchSortedBackups($isDeveloperOrHigher);
299 $urlAssets = trailingslashit(WPSTG_PLUGIN_URL) . 'assets/';
300 $licenseType = $this->getLicenseTypeSlug();
301 $licenseId = $this->getLicenseId();
302
303 $modalView = WPSTG_VIEWS_DIR . 'cli/cli-integration-modal.php';
304 if (file_exists($modalView)) {
305 include $modalView;
306 $GLOBALS['wpstg_cli_modal_rendered'] = true;
307 }
308 }
309
310 /**
311 * Get the license type slug (e.g. 'free', 'personal', 'business', 'developer', 'agency')
312 *
313 * @return string
314 */
315 private function getLicenseTypeSlug(): string
316 {
317 if (!WPStaging::isPro() || !class_exists('\WPStaging\Pro\License\Licensing')) {
318 return 'free';
319 }
320
321 $licensing = WPStaging::make(\WPStaging\Pro\License\Licensing::class);
322 $type = $licensing->getLicenseType();
323
324 return $type === 'basic' ? 'free' : $type;
325 }
326
327 /**
328 * Get the license ID from stored license status
329 * Returns empty string when unavailable.
330 *
331 * @return string
332 */
333 private function getLicenseId(): string
334 {
335 $licenseData = $this->getLicenseData();
336 if (!$licenseData) {
337 return '';
338 }
339
340 return !empty($licenseData->license_id) ? (string)$licenseData->license_id : '';
341 }
342
343 /**
344 * @return object|null
345 */
346 private function getLicenseData()
347 {
348 if (!WPStaging::isPro()) {
349 return null;
350 }
351
352 $license = get_option('wpstg_license_status', false);
353 return $license ? (object)$license : null;
354 }
355
356 /**
357 * Check a condition on the Licensing class, returning false for Basic version
358 *
359 * @param string $method The Licensing method name to call
360 * @return bool
361 */
362 private function checkLicensingCondition(string $method): bool
363 {
364 if (WPStaging::isBasic()) {
365 return false;
366 }
367
368 if (!class_exists('\WPStaging\Pro\License\Licensing')) {
369 return false;
370 }
371
372 $licensing = WPStaging::make(\WPStaging\Pro\License\Licensing::class);
373 return $licensing->$method();
374 }
375
376 /**
377 * Fetch sorted listable backups, returning an empty array on failure
378 *
379 * @param bool $isDeveloperOrHigher Whether the user has a Developer+ license
380 * @return array
381 */
382 private function fetchSortedBackups(bool $isDeveloperOrHigher = true): array
383 {
384 if (!$isDeveloperOrHigher || !class_exists('\WPStaging\Backup\Ajax\FileList\ListableBackupsCollection')) {
385 return [];
386 }
387
388 try {
389 /** @var \WPStaging\Backup\Ajax\FileList\ListableBackupsCollection $listableBackupsCollection */
390 $listableBackupsCollection = WPStaging::make(\WPStaging\Backup\Ajax\FileList\ListableBackupsCollection::class);
391 return $listableBackupsCollection->getSortedListableBackups();
392 } catch (\Exception $e) {
393 return [];
394 }
395 }
396
397 /**
398 * AJAX handler to get updated CLI modal backup list HTML
399 *
400 * @return void
401 */
402 public function ajaxGetCliBackupList()
403 {
404 if (!$this->auth->isAuthenticatedRequest('', 'manage_options')) {
405 wp_send_json_error();
406 }
407
408 $isDeveloperOrHigher = $this->isDeveloperOrHigherLicense();
409 $backups = $this->fetchSortedBackups($isDeveloperOrHigher);
410 $urlAssets = trailingslashit(WPSTG_PLUGIN_URL) . 'assets/';
411
412 // Check if there are valid (non-corrupt, non-legacy) backups
413 $hasBackups = false;
414 foreach ($backups as $backup) {
415 if (!$backup->isCorrupt && !$backup->isLegacy) {
416 $hasBackups = true;
417 break;
418 }
419 }
420
421 ob_start();
422 include WPSTG_VIEWS_DIR . 'cli/cli-backup-list.php';
423 $html = ob_get_clean();
424
425 wp_send_json_success([
426 'html' => $html,
427 'hasBackups' => $hasBackups,
428 ]);
429 }
430 }
431