Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 83 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
| WP_REST_WPCOM_Block_Editor_Four_For_Four_Controller | |
0.00% |
0 / 83 |
|
0.00% |
0 / 7 |
870 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| register_rest_route | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
2 | |||
| permission_callback | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_four_for_four | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| set_four_for_four_status | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
| get_user_status | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| is_site_eligible | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
306 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * WP_REST_WPCOM_Block_Editor_Four_For_Four_Controller file. |
| 4 | * |
| 5 | * @package automattic/jetpack-mu-wpcom |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\NUX; |
| 9 | |
| 10 | use Automattic\Jetpack\Status; |
| 11 | use Automattic\Jetpack\Status\Host; |
| 12 | |
| 13 | /** |
| 14 | * Class WP_REST_WPCOM_Block_Editor_Four_For_Four_Controller. |
| 15 | * |
| 16 | * Eligibility and opt-in state for the "4 for 4" prompt shown after a site's |
| 17 | * first post is published. The decision lives in user meta (global across a |
| 18 | * Simple user's sites) so the same writer is asked once, on one site. |
| 19 | */ |
| 20 | class WP_REST_WPCOM_Block_Editor_Four_For_Four_Controller extends \WP_REST_Controller { |
| 21 | /** |
| 22 | * User attribute holding the writer's decision. User attributes are the |
| 23 | * global per-user store on wpcom (user meta is per blog), and the wpcom |
| 24 | * Reader endpoints read this same attribute, adding `followed_blog_ids` |
| 25 | * and moving the status to `completed`, so writes here merge rather than |
| 26 | * replace. |
| 27 | */ |
| 28 | const USER_ATTRIBUTE = 'wpcom_four_for_four'; |
| 29 | |
| 30 | /** |
| 31 | * Statuses the editor prompt may write. |
| 32 | * |
| 33 | * @var string[] |
| 34 | */ |
| 35 | const EDITOR_STATUSES = array( 'opted_in', 'opted_out' ); |
| 36 | |
| 37 | /** |
| 38 | * WP_REST_WPCOM_Block_Editor_Four_For_Four_Controller constructor. |
| 39 | */ |
| 40 | public function __construct() { |
| 41 | $this->namespace = 'wpcom/v2'; |
| 42 | $this->rest_base = 'block-editor/four-for-four'; |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * Register available routes. |
| 47 | */ |
| 48 | public function register_rest_route() { |
| 49 | register_rest_route( |
| 50 | $this->namespace, |
| 51 | $this->rest_base, |
| 52 | array( |
| 53 | array( |
| 54 | 'methods' => \WP_REST_Server::READABLE, |
| 55 | 'callback' => array( $this, 'get_four_for_four' ), |
| 56 | 'permission_callback' => array( $this, 'permission_callback' ), |
| 57 | ), |
| 58 | array( |
| 59 | 'methods' => \WP_REST_Server::EDITABLE, |
| 60 | 'callback' => array( $this, 'set_four_for_four_status' ), |
| 61 | 'permission_callback' => array( $this, 'permission_callback' ), |
| 62 | 'args' => array( |
| 63 | 'status' => array( |
| 64 | 'required' => true, |
| 65 | 'type' => 'string', |
| 66 | 'enum' => self::EDITOR_STATUSES, |
| 67 | 'validate_callback' => 'rest_validate_request_arg', |
| 68 | ), |
| 69 | ), |
| 70 | ), |
| 71 | ) |
| 72 | ); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * Callback to determine whether the request can proceed. |
| 77 | * |
| 78 | * @return boolean |
| 79 | */ |
| 80 | public function permission_callback() { |
| 81 | return current_user_can( 'edit_posts' ); |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Whether the current user should be offered the program on this site. |
| 86 | * |
| 87 | * @return \WP_REST_Response |
| 88 | */ |
| 89 | public function get_four_for_four() { |
| 90 | $eligible = $this->is_site_eligible() && ! isset( $this->get_user_status()['status'] ); |
| 91 | |
| 92 | return rest_ensure_response( array( 'eligible' => $eligible ) ); |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Record the writer's decision from the editor prompt. |
| 97 | * |
| 98 | * @param \WP_REST_Request $request Request object. |
| 99 | * @return \WP_REST_Response|\WP_Error |
| 100 | */ |
| 101 | public function set_four_for_four_status( $request ) { |
| 102 | if ( ! function_exists( 'update_user_attribute' ) ) { |
| 103 | return new \WP_Error( 'four_for_four_unavailable', 'The program is not available on this site.', array( 'status' => 501 ) ); |
| 104 | } |
| 105 | |
| 106 | $status = $request->get_param( 'status' ); |
| 107 | $current = $this->get_user_status(); |
| 108 | |
| 109 | if ( isset( $current['status'] ) && 'completed' === $current['status'] ) { |
| 110 | return new \WP_Error( 'already_completed', 'The program has already been completed.', array( 'status' => 409 ) ); |
| 111 | } |
| 112 | |
| 113 | update_user_attribute( |
| 114 | get_current_user_id(), |
| 115 | self::USER_ATTRIBUTE, |
| 116 | array_merge( |
| 117 | $current, |
| 118 | array( |
| 119 | 'status' => $status, |
| 120 | 'blog_id' => (int) get_current_blog_id(), |
| 121 | 'updated' => time(), |
| 122 | ) |
| 123 | ) |
| 124 | ); |
| 125 | |
| 126 | return rest_ensure_response( array( 'status' => $status ) ); |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * The stored decision for the current user, or an empty array. |
| 131 | * |
| 132 | * @return array |
| 133 | */ |
| 134 | private function get_user_status() { |
| 135 | if ( ! function_exists( 'get_user_attribute' ) ) { |
| 136 | return array(); |
| 137 | } |
| 138 | $state = get_user_attribute( get_current_user_id(), self::USER_ATTRIBUTE ); |
| 139 | return is_array( $state ) ? $state : array(); |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Whether this site qualifies for the prompt. Like the sibling first-post |
| 144 | * controller, this is read when the editor loads, while |
| 145 | * `has_never_published_post` is still set for the post being written. |
| 146 | * |
| 147 | * @return boolean |
| 148 | */ |
| 149 | private function is_site_eligible() { |
| 150 | /** |
| 151 | * Enables the 4 for 4 prompt. Off by default so this package can ship |
| 152 | * ahead of the wpcom endpoints the Reader page depends on; wpcom turns |
| 153 | * it on once those are live. |
| 154 | * |
| 155 | * @param bool $enabled Whether the prompt may be shown. Default false. |
| 156 | */ |
| 157 | if ( ! apply_filters( 'wpcom_four_for_four_enabled', false ) ) { |
| 158 | return false; |
| 159 | } |
| 160 | |
| 161 | $host = new Host(); |
| 162 | if ( ! $host->is_wpcom_simple() ) { |
| 163 | return false; |
| 164 | } |
| 165 | |
| 166 | if ( ! get_option( 'has_never_published_post', false ) ) { |
| 167 | return false; |
| 168 | } |
| 169 | |
| 170 | // Sites created before the launch flow have no launch status and count as launched. |
| 171 | $launch_status = get_option( 'launch-status' ); |
| 172 | if ( $launch_status && 'launched' !== $launch_status ) { |
| 173 | return false; |
| 174 | } |
| 175 | |
| 176 | // Both Coming Soon generations: the v1 option paired with a private blog, and the public v2 flag. |
| 177 | // wpcom_is_coming_soon() lives in wpcom and has no Phan stub. |
| 178 | // @phan-suppress-next-line PhanUndeclaredFunction |
| 179 | if ( function_exists( 'wpcom_is_coming_soon' ) && wpcom_is_coming_soon() ) { |
| 180 | return false; |
| 181 | } |
| 182 | if ( function_exists( 'is_wpcom_public_coming_soon_enabled' ) && is_wpcom_public_coming_soon_enabled( get_current_blog_id() ) ) { |
| 183 | return false; |
| 184 | } |
| 185 | |
| 186 | if ( ( new Status() )->is_private_site() ) { |
| 187 | return false; |
| 188 | } |
| 189 | |
| 190 | if ( ! str_starts_with( get_locale(), 'en' ) ) { |
| 191 | return false; |
| 192 | } |
| 193 | |
| 194 | if ( $host->is_p2_site() ) { |
| 195 | return false; |
| 196 | } |
| 197 | |
| 198 | $blog_id = get_current_blog_id(); |
| 199 | |
| 200 | // Spam, deleted, archived, mature, suspended and hidden sites are all excluded here. |
| 201 | // @phan-suppress-next-line PhanUndeclaredFunction |
| 202 | if ( function_exists( 'is_public_to_people' ) && ! is_public_to_people( $blog_id ) ) { |
| 203 | return false; |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Blog stickers that exclude a site from the 4 for 4 prompt. |
| 208 | * |
| 209 | * @param string[] $stickers Sticker names. |
| 210 | */ |
| 211 | $blocked_stickers = apply_filters( |
| 212 | 'wpcom_four_for_four_blocked_stickers', |
| 213 | array( 'broken-in-reader', 'is_disconnected', 'dont-recommend', 'a8c-test-blog', 'a8c-e2e-test-blog' ) |
| 214 | ); |
| 215 | foreach ( $blocked_stickers as $sticker ) { |
| 216 | if ( wpcom_has_blog_sticker( $sticker, $blog_id ) ) { |
| 217 | return false; |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | return true; |
| 222 | } |
| 223 | } |