Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.75% covered (success)
93.75%
90 / 96
100.00% covered (success)
100.00%
6 / 6
CRAP
n/a
0 / 0
wpcom_write_print_post_publish_url_cleanup
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
wpcom_write_should_show_post_publish_checklist
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
wpcom_write_post_publish_launch_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wpcom_write_get_post_publish_checklist_strings
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
wpcom_write_enqueue_post_publish_checklist_assets
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
3
wpcom_write_render_post_publish_checklist
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Write — post-publish next-steps checklist.
4 *
5 * After a post is published in the Write editor, the editor redirects the author
6 * to the published post with a `wpcom_write_published` marker (see view.js). On a
7 * Coming Soon (private-by-default) site that post is not yet public, so we overlay
8 * a short next-steps checklist — Launch your site, Share your post — onto the post.
9 * This keeps "Publish" honest (it didn't make the post public) and guides the
10 * author toward launching and sharing.
11 *
12 * The overlay only renders on the immediate post-publish navigation (the marker is
13 * present), for a viewer who can actually launch the site, while the site is still
14 * Coming Soon. Public sites, ordinary visitors, and later visits to the post are
15 * unaffected.
16 *
17 * @package automattic/jetpack-mu-wpcom
18 */
19
20use Automattic\Jetpack\Jetpack_Mu_Wpcom\Common;
21
22require_once __DIR__ . '/../../common/site-launch-publish-copy.php';
23
24if ( ! defined( 'ABSPATH' ) ) {
25    exit;
26}
27
28/**
29 * Query-arg marker the Write editor appends to the published-post URL on publish.
30 */
31const WPCOM_WRITE_PUBLISHED_MARKER = 'wpcom_write_published';
32
33/**
34 * Strip the post-publish query args from the author's address bar.
35 *
36 * The redirect tags the permalink so the post-publish surfaces know a publish
37 * just happened, but the author is left looking at their own live post with our
38 * bookkeeping attached — and would share that URL if they copied it. Cleaning up
39 * belongs here rather than in either card: both are conditional, and the writer
40 * who has already answered the survey sees no card at all, so nothing would run.
41 *
42 * Server-side state has already been read by the time this executes, so removing
43 * the args client-side changes nothing but the visible URL.
44 *
45 * @return void
46 */
47function wpcom_write_print_post_publish_url_cleanup() {
48    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only marker; writes nothing.
49    if ( ! isset( $_GET[ WPCOM_WRITE_PUBLISHED_MARKER ] ) || ! is_singular( 'post' ) ) {
50        return;
51    }
52
53    // `source` is only stripped alongside the marker, so a campaign link that
54    // carries its own `source` to a post is left alone.
55    // Hex-escaped so the encoded value can never break out of the inline script tag.
56    $args = wp_json_encode( array( WPCOM_WRITE_PUBLISHED_MARKER, 'source' ), JSON_HEX_TAG | JSON_HEX_AMP );
57
58    wp_print_inline_script_tag(
59        '( function () {
60            try {
61                var url = new URL( window.location.href );
62                ' . $args . '.forEach( function ( arg ) { url.searchParams.delete( arg ); } );
63                window.history.replaceState( null, "", url.href );
64            } catch ( e ) {}
65        } )();'
66    );
67}
68add_action( 'wp_footer', 'wpcom_write_print_post_publish_url_cleanup', 5 );
69
70/**
71 * Whether the post-publish checklist overlay should render on the current request.
72 *
73 * All of the following must hold:
74 *  - the request carries the Write editor's post-publish marker;
75 *  - we're on a single post's front-end view;
76 *  - the viewer can launch the site (`manage_options`) — which also means they
77 *    bypass the Coming Soon splash and therefore see the post we overlay onto;
78 *  - the site is still Coming Soon, so the post isn't publicly visible yet.
79 *
80 * @return bool True when the overlay should be shown.
81 */
82function wpcom_write_should_show_post_publish_checklist() {
83    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only marker; writes nothing, mirrors the coming-soon share-code read.
84    if ( ! isset( $_GET[ WPCOM_WRITE_PUBLISHED_MARKER ] ) ) {
85        return false;
86    }
87
88    if ( ! is_singular( 'post' ) ) {
89        return false;
90    }
91
92    // Only the author/manager who can launch the site — anyone who can't launch
93    // also can't act on the checklist, and would see the Coming Soon splash here.
94    if ( ! current_user_can( 'manage_options' ) ) {
95        return false;
96    }
97
98    // Only while the site is private by default; a launched site needs no nudge.
99    return 1 === (int) get_option( 'wpcom_public_coming_soon' );
100}
101
102/**
103 * URL the "Launch your site" task links to.
104 *
105 * Points at Calypso's canonical launch-site flow, which takes the user through
106 * domain/plan (skipped if already chosen) and flips a Coming Soon site public.
107 * Unlike the launchpad, it does not depend on the site having
108 * launchpad_screen='full', so it works for these sites as created today.
109 *
110 * @return string The launch URL for this site.
111 */
112function wpcom_write_post_publish_launch_url() {
113    return Common\wpcom_get_launch_site_url();
114}
115
116/**
117 * Translated strings for the post-publish checklist overlay.
118 *
119 * @return array<string, string> Map of key -> translated string.
120 */
121function wpcom_write_get_post_publish_checklist_strings() {
122    $publish = wpcom_site_launch_uses_publish_copy();
123
124    return array(
125        'heading'         => __( 'Your post is saved!', 'jetpack-mu-wpcom' ),
126        'description'     => $publish
127            ? __( 'Your site is still private, so only you can see this post. Publish your site to share it with the world.', 'jetpack-mu-wpcom' )
128            : __( 'Your site is still private, so only you can see this post. Launch your site to share it with the world.', 'jetpack-mu-wpcom' ),
129        'launchTitle'     => $publish ? __( 'Publish your site', 'jetpack-mu-wpcom' ) : __( 'Launch your site', 'jetpack-mu-wpcom' ),
130        'launchDesc'      => __( 'Make your site public.', 'jetpack-mu-wpcom' ),
131        'shareTitle'      => __( 'Share your post', 'jetpack-mu-wpcom' ),
132        'shareDesc'       => $publish ? __( 'Available after you publish your site.', 'jetpack-mu-wpcom' ) : __( 'Available after you launch.', 'jetpack-mu-wpcom' ),
133        'launchCta'       => $publish ? __( 'Publish your site', 'jetpack-mu-wpcom' ) : __( 'Launch your site', 'jetpack-mu-wpcom' ),
134        'dismiss'         => __( 'Maybe later', 'jetpack-mu-wpcom' ),
135        'close'           => __( 'Close', 'jetpack-mu-wpcom' ),
136        // Inline email-verification step, swapped in when the author tries to
137        // launch with an unverified email (see post-publish-checklist.js).
138        'verifyTitle'     => $publish ? __( 'Confirm your email to publish your site', 'jetpack-mu-wpcom' ) : __( 'Confirm your email to launch', 'jetpack-mu-wpcom' ),
139        'verifyDesc'      => __( 'Your site can go public once your email is confirmed. Check your inbox for the confirmation link.', 'jetpack-mu-wpcom' ),
140        'confirmCta'      => $publish ? __( "I've confirmed — publish", 'jetpack-mu-wpcom' ) : __( "I've confirmed — launch", 'jetpack-mu-wpcom' ),
141        'resendCta'       => __( 'Resend verification email', 'jetpack-mu-wpcom' ),
142        'resendSent'      => __( 'Verification email sent. Check your inbox.', 'jetpack-mu-wpcom' ),
143        'resendError'     => __( 'Something went wrong. Please try again.', 'jetpack-mu-wpcom' ),
144        'checking'        => __( 'Checking…', 'jetpack-mu-wpcom' ),
145        'stillUnverified' => __( "We couldn't confirm your email yet. Click the link in the email, then try again.", 'jetpack-mu-wpcom' ),
146    );
147}
148
149/**
150 * Enqueue the overlay assets when the checklist should render.
151 *
152 * @return void
153 */
154function wpcom_write_enqueue_post_publish_checklist_assets() {
155    if ( ! wpcom_write_should_show_post_publish_checklist() ) {
156        return;
157    }
158
159    wp_enqueue_style(
160        'wpcom-write-post-publish-checklist',
161        wpcom_write_asset_url( 'post-publish-checklist.css' ),
162        array(),
163        WPCOM_WRITE_VERSION
164    );
165
166    wp_enqueue_script(
167        'wpcom-write-post-publish-checklist',
168        wpcom_write_asset_url( 'post-publish-checklist.js' ),
169        array(),
170        WPCOM_WRITE_VERSION,
171        true
172    );
173
174    $strings = wpcom_write_get_post_publish_checklist_strings();
175
176    wp_localize_script(
177        'wpcom-write-post-publish-checklist',
178        'wpcomWritePostPublishChecklist',
179        array(
180            'launchUrl' => wpcom_write_post_publish_launch_url(),
181            'marker'    => WPCOM_WRITE_PUBLISHED_MARKER,
182            // Computed server-side: a Simple site serves no REST at its own host, so
183            // the overlay can't fetch this — it reads the value rendered into the page.
184            // Sent as a '1'/'0' string because wp_localize_script() stringifies scalars
185            // (bool true -> '1', false -> ''); the JS compares against '1'.
186            'blocked'   => wpcom_write_launch_blocked_for_unverified_email() ? '1' : '0',
187            // admin-ajax transport for the inline resend / re-check (a Simple site
188            // serves no REST at its own host); nonce guards both actions.
189            'ajaxUrl'   => admin_url( 'admin-ajax.php' ),
190            'nonce'     => wp_create_nonce( WPCOM_WRITE_EMAIL_VERIFICATION_NONCE ),
191            'strings'   => array(
192                'verifyTitle'     => $strings['verifyTitle'],
193                'verifyDesc'      => $strings['verifyDesc'],
194                'confirmCta'      => $strings['confirmCta'],
195                'resendCta'       => $strings['resendCta'],
196                'resendSent'      => $strings['resendSent'],
197                'resendError'     => $strings['resendError'],
198                'checking'        => $strings['checking'],
199                'stillUnverified' => $strings['stillUnverified'],
200            ),
201        )
202    );
203}
204add_action( 'wp_enqueue_scripts', 'wpcom_write_enqueue_post_publish_checklist_assets' );
205
206/**
207 * Render the post-publish checklist overlay markup in the footer.
208 */
209add_action( 'wp_footer', 'wpcom_write_render_post_publish_checklist' );
210
211/**
212 * Output the overlay markup.
213 *
214 * Plain markup wired up by post-publish-checklist.js — no Interactivity API, since
215 * this renders on an arbitrary theme front-end, not the Write editor surface.
216 *
217 * @return void
218 */
219function wpcom_write_render_post_publish_checklist() {
220    if ( ! wpcom_write_should_show_post_publish_checklist() ) {
221        return;
222    }
223
224    $strings = wpcom_write_get_post_publish_checklist_strings();
225    ?>
226    <div class="wpcom-write-ppc" role="dialog" aria-modal="true" aria-labelledby="wpcom-write-ppc-title" hidden>
227        <div class="wpcom-write-ppc__backdrop" data-wpcom-write-ppc-dismiss></div>
228        <div class="wpcom-write-ppc__card">
229            <button type="button" class="wpcom-write-ppc__close" data-wpcom-write-ppc-dismiss aria-label="<?php echo esc_attr( $strings['close'] ); ?>">&times;</button>
230            <div class="wpcom-write-ppc__badge" aria-hidden="true">&#10003;</div>
231            <h2 id="wpcom-write-ppc-title" class="wpcom-write-ppc__title"><?php echo esc_html( $strings['heading'] ); ?></h2>
232            <p class="wpcom-write-ppc__desc"><?php echo esc_html( $strings['description'] ); ?></p>
233            <ul class="wpcom-write-ppc__list">
234                <li class="wpcom-write-ppc__item">
235                    <span class="wpcom-write-ppc__bullet" aria-hidden="true"></span>
236                    <span class="wpcom-write-ppc__item-text">
237                        <span class="wpcom-write-ppc__item-title"><?php echo esc_html( $strings['launchTitle'] ); ?></span>
238                        <span class="wpcom-write-ppc__item-desc"><?php echo esc_html( $strings['launchDesc'] ); ?></span>
239                    </span>
240                </li>
241                <li class="wpcom-write-ppc__item is-disabled" aria-disabled="true">
242                    <span class="wpcom-write-ppc__bullet" aria-hidden="true"></span>
243                    <span class="wpcom-write-ppc__item-text">
244                        <span class="wpcom-write-ppc__item-title"><?php echo esc_html( $strings['shareTitle'] ); ?></span>
245                        <span class="wpcom-write-ppc__item-desc"><?php echo esc_html( $strings['shareDesc'] ); ?></span>
246                    </span>
247                </li>
248            </ul>
249            <button type="button" class="wpcom-write-ppc__launch" data-wpcom-write-ppc-launch><?php echo esc_html( $strings['launchCta'] ); ?></button>
250            <button type="button" class="wpcom-write-ppc__dismiss" data-wpcom-write-ppc-dismiss><?php echo esc_html( $strings['dismiss'] ); ?></button>
251        </div>
252    </div>
253    <?php
254}