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