PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
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 1 year ago BooleanNotice.php 2 years ago CliIntegrationNotice.php 1 day ago DisabledItemsNotice.php 2 years ago DismissNotice.php 1 day ago NextGenEngineNotice.php 1 day ago Notices.php 1 day ago NoticesHandler.php 11 months ago ObjectCacheNotice.php 11 months ago OutdatedWpStagingNotice.php 1 year ago WarningsNotice.php 2 years ago WpVersionCompatNotice.php 3 months ago
CliIntegrationNotice.php
442 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 $licenseData = $this->getLicenseData();
246
247 if (!$licenseData || empty($licenseData->price_id)) {
248 return __('Unregistered', 'wp-staging');
249 }
250
251 $priceId = (string)$licenseData->price_id;
252 $planNames = $licensing->getAvailableLicensePlansByPriceId();
253
254 if (isset($planNames[$priceId]['name'])) {
255 return $planNames[$priceId]['name'];
256 }
257
258 return __('Unregistered', 'wp-staging');
259 }
260
261 /**
262 * Render the dock CTA if conditions are met (called from staging listing view)
263 *
264 * @return void
265 */
266 public function maybeRenderDockCta()
267 {
268 if (!$this->shouldShowDockCta()) {
269 return;
270 }
271
272 $dockCtaView = WPSTG_VIEWS_DIR . 'cli/cli-dock-cta.php';
273 if (!file_exists($dockCtaView)) {
274 return;
275 }
276
277 include $dockCtaView;
278 }
279
280 /**
281 * Render the CLI modal content if the dock CTA should be shown
282 *
283 * This ensures the modal is available when the dock CTA is rendered
284 * server-side (when the banner was previously dismissed).
285 *
286 * @return void
287 */
288 public function maybeRenderCliModalForDockCta()
289 {
290 if (!$this->shouldShowDockCta()) {
291 return;
292 }
293
294 $this->renderCliModalContent();
295 }
296
297 /**
298 * Render the CLI modal content with all required variables
299 *
300 * @return void
301 */
302 private function renderCliModalContent()
303 {
304 if (!empty($GLOBALS['wpstg_cli_modal_rendered'])) {
305 return;
306 }
307
308 $isDeveloperOrHigher = $this->isDeveloperOrHigherLicense();
309 $backups = $this->fetchSortedBackups($isDeveloperOrHigher);
310 $urlAssets = trailingslashit(WPSTG_PLUGIN_URL) . 'assets/';
311 $licenseType = $this->getLicenseTypeSlug();
312 $licenseId = $this->getLicenseId();
313
314 $modalView = WPSTG_VIEWS_DIR . 'cli/cli-integration-modal.php';
315 if (file_exists($modalView)) {
316 include $modalView;
317 $GLOBALS['wpstg_cli_modal_rendered'] = true;
318 }
319 }
320
321 /**
322 * Get the license type slug (e.g. 'free', 'personal', 'business', 'developer', 'agency')
323 *
324 * @return string
325 */
326 private function getLicenseTypeSlug(): string
327 {
328 if (!WPStaging::isPro() || !class_exists('\WPStaging\Pro\License\Licensing')) {
329 return 'free';
330 }
331
332 $licensing = WPStaging::make(\WPStaging\Pro\License\Licensing::class);
333 $type = $licensing->getLicenseType();
334
335 return $type === 'basic' ? 'free' : $type;
336 }
337
338 /**
339 * Get the license ID from stored license status
340 * Returns empty string when unavailable.
341 *
342 * @return string
343 */
344 private function getLicenseId(): string
345 {
346 $licenseData = $this->getLicenseData();
347 if (!$licenseData) {
348 return '';
349 }
350
351 return !empty($licenseData->license_id) ? (string)$licenseData->license_id : '';
352 }
353
354 /**
355 * @return object|null
356 */
357 private function getLicenseData()
358 {
359 if (!WPStaging::isPro()) {
360 return null;
361 }
362
363 $license = get_option('wpstg_license_status', false);
364 return $license ? (object)$license : null;
365 }
366
367 /**
368 * Check a condition on the Licensing class, returning false for Basic version
369 *
370 * @param string $method The Licensing method name to call
371 * @return bool
372 */
373 private function checkLicensingCondition(string $method): bool
374 {
375 if (WPStaging::isBasic()) {
376 return false;
377 }
378
379 if (!class_exists('\WPStaging\Pro\License\Licensing')) {
380 return false;
381 }
382
383 $licensing = WPStaging::make(\WPStaging\Pro\License\Licensing::class);
384 return $licensing->$method();
385 }
386
387 /**
388 * Fetch sorted listable backups, returning an empty array on failure
389 *
390 * @param bool $isDeveloperOrHigher Whether the user has a Developer+ license
391 * @return array
392 */
393 private function fetchSortedBackups(bool $isDeveloperOrHigher = true): array
394 {
395 if (!$isDeveloperOrHigher || !class_exists('\WPStaging\Backup\Ajax\FileList\ListableBackupsCollection')) {
396 return [];
397 }
398
399 try {
400 /** @var \WPStaging\Backup\Ajax\FileList\ListableBackupsCollection $listableBackupsCollection */
401 $listableBackupsCollection = WPStaging::make(\WPStaging\Backup\Ajax\FileList\ListableBackupsCollection::class);
402 return $listableBackupsCollection->getSortedListableBackups();
403 } catch (\Exception $e) {
404 return [];
405 }
406 }
407
408 /**
409 * AJAX handler to get updated CLI modal backup list HTML
410 *
411 * @return void
412 */
413 public function ajaxGetCliBackupList()
414 {
415 if (!$this->auth->isAuthenticatedRequest('', 'manage_options')) {
416 wp_send_json_error();
417 }
418
419 $isDeveloperOrHigher = $this->isDeveloperOrHigherLicense();
420 $backups = $this->fetchSortedBackups($isDeveloperOrHigher);
421 $urlAssets = trailingslashit(WPSTG_PLUGIN_URL) . 'assets/';
422
423 // Check if there are valid (non-corrupt, non-legacy) backups
424 $hasBackups = false;
425 foreach ($backups as $backup) {
426 if (!$backup->isCorrupt && !$backup->isLegacy) {
427 $hasBackups = true;
428 break;
429 }
430 }
431
432 ob_start();
433 include WPSTG_VIEWS_DIR . 'cli/cli-backup-list.php';
434 $html = ob_get_clean();
435
436 wp_send_json_success([
437 'html' => $html,
438 'hasBackups' => $hasBackups,
439 ]);
440 }
441 }
442