Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
63.80% |
178 / 279 |
|
34.04% |
16 / 47 |
CRAP | |
0.00% |
0 / 1 |
| Launchpad_Task_Lists | |
63.80% |
178 / 279 |
|
34.04% |
16 / 47 |
1113.12 | |
0.00% |
0 / 1 |
| get_instance | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| register_task_list | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| register_task | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| unregister_task_list | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| unregister_task | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| get_task_list | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| is_task_list_enabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| is_task_list_dismissed | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| is_task_list_dismissible | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| set_task_list_dismissed | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
| set_task_list_dismissed_until | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
3.10 | |||
| is_temporally_dismissed | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
| get_task_list_dismissed_status | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| get_task_list_dismissed_until | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| has_task_lists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| get_all_task_lists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_task | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| get_required_task_ids | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| get_require_last_task_completion | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| is_task_list_completed | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| get_all_tasks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| build | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
| is_visible | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
| build_task | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
7 | |||
| load_value_from_callback | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| load_extra_data | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| load_title | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
| load_subtitle | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
| load_repetition_count | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| load_calypso_path | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
| is_valid_admin_url_or_absolute_path | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
5.07 | |||
| is_task_disabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_task_complete | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| get_task_key | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| is_task_id_complete | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| validate_task_list | |
83.78% |
31 / 37 |
|
0.00% |
0 / 1 |
13.72 | |||
| get_active_tasks | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| get_completed_tasks | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| has_active_tasks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| add_hooks_for_active_tasks | |
14.29% |
2 / 14 |
|
0.00% |
0 / 1 |
87.20 | |||
| mark_task_complete | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| mark_task_complete_if_active | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| maybe_disable_fullscreen_launchpad | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| validate_task | |
61.11% |
11 / 18 |
|
0.00% |
0 / 1 |
20.47 | |||
| is_fullscreen_launchpad_enabled | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| disable_fullscreen_launchpad | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_task_list_title | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Launchpad Task Lists Registry |
| 4 | * |
| 5 | * @package automattic/jetpack-mu-wpcom |
| 6 | * @since 1.5.0 |
| 7 | */ |
| 8 | |
| 9 | // Type aliases used in a bunch of places in this file. Unfortunately Phan doesn't have a way to set these more globally than copy-pasting them into each file needing them. |
| 10 | <<<'PHAN' |
| 11 | @phan-type Task_List = array{id:string, task_ids:string[], required_task_ids?:string[], visible_tasks_callback?:callable, require_last_task_completion?:bool, get_title?:callable, is_dismissible?:bool, is_enabled_callback?:callable} |
| 12 | @phan-type Task = array{id:string, title?:string, get_title?:callable, id_map?:string, add_listener_callback?:callable, badge_text_callback?:callable, extra_data_callback?:callable, get_calypso_path?:callable, is_complete_callback?:callable, is_disabled_callback?:callable, isLaunchTask?:bool, is_visible_callback?:callable, target_repetitions?:int, repetition_count_callback?:callable, subtitle?:callable, completed?:bool} |
| 13 | PHAN; |
| 14 | |
| 15 | /** |
| 16 | * Launchpad Task List |
| 17 | * |
| 18 | * This file provides a Launchpad Task List class that manages the current list |
| 19 | * of Launchpad checklists that are available to be used. |
| 20 | * |
| 21 | * @package automattic/jetpack-mu-wpcom |
| 22 | */ |
| 23 | class Launchpad_Task_Lists { |
| 24 | /** |
| 25 | * Internal storage for registered Launchpad Task Lists |
| 26 | * |
| 27 | * @var Task_List[] |
| 28 | */ |
| 29 | private $task_list_registry = array(); |
| 30 | |
| 31 | /** |
| 32 | * Internal storage for registered Launchpad Task Lists |
| 33 | * |
| 34 | * @var Task[] |
| 35 | */ |
| 36 | private $task_registry = array(); |
| 37 | |
| 38 | /** |
| 39 | * Internal reference for the current site slug. |
| 40 | * |
| 41 | * @var string|null |
| 42 | */ |
| 43 | private $site_slug = null; |
| 44 | |
| 45 | /** |
| 46 | * Singleton instance |
| 47 | * |
| 48 | * @var Launchpad_Task_Lists |
| 49 | */ |
| 50 | private static $instance = null; |
| 51 | |
| 52 | /** |
| 53 | * Get the singleton instance |
| 54 | * |
| 55 | * @return Launchpad_Task_Lists |
| 56 | */ |
| 57 | public static function get_instance() { |
| 58 | if ( ! self::$instance ) { |
| 59 | self::$instance = new self(); |
| 60 | } |
| 61 | |
| 62 | return self::$instance; |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * Register a new Launchpad Task List |
| 67 | * |
| 68 | * @param Task_List $task_list Task List definition. |
| 69 | * |
| 70 | * @return bool True if successfully registered, false if not. |
| 71 | */ |
| 72 | public function register_task_list( $task_list = array() ) { |
| 73 | if ( self::validate_task_list( $task_list ) !== null ) { |
| 74 | return false; |
| 75 | } |
| 76 | |
| 77 | // If no is_completed_callback is set, use the default. |
| 78 | if ( ! isset( $task_list['is_completed_callback'] ) ) { |
| 79 | $task_list['is_completed_callback'] = 'wpcom_default_launchpad_task_list_completed'; |
| 80 | } |
| 81 | |
| 82 | $this->task_list_registry[ $task_list['id'] ] = $task_list; |
| 83 | return true; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Register a new Launchpad Task |
| 88 | * |
| 89 | * @param Task $task Task definition. |
| 90 | * |
| 91 | * @return bool True if successful, false if not. |
| 92 | */ |
| 93 | public function register_task( $task = array() ) { |
| 94 | if ( ! static::validate_task( $task ) ) { |
| 95 | return false; |
| 96 | } |
| 97 | // TODO: Handle duplicate tasks |
| 98 | $this->task_registry[ $task['id'] ] = $task; |
| 99 | return true; |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Unregister a Launchpad Task List |
| 104 | * |
| 105 | * @param string $id Task List id. |
| 106 | * |
| 107 | * @return bool True if successfully unregistered, false if not found. |
| 108 | */ |
| 109 | public function unregister_task_list( $id ) { |
| 110 | if ( ! array_key_exists( $id, $this->task_list_registry ) ) { |
| 111 | return false; |
| 112 | } |
| 113 | |
| 114 | unset( $this->task_list_registry[ $id ] ); |
| 115 | return true; |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Unregister a Launchpad Task |
| 120 | * |
| 121 | * @param string $id Task id. |
| 122 | * |
| 123 | * @return bool True if successful, false if not. |
| 124 | */ |
| 125 | public function unregister_task( $id ) { |
| 126 | if ( ! array_key_exists( $id, $this->task_registry ) ) { |
| 127 | return false; |
| 128 | } |
| 129 | |
| 130 | unset( $this->task_registry[ $id ] ); |
| 131 | return true; |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * Get a Launchpad Task List definition |
| 136 | * |
| 137 | * @param string $id Task List id. |
| 138 | * |
| 139 | * @return Task_List Task List. |
| 140 | */ |
| 141 | protected function get_task_list( $id ) { |
| 142 | if ( ! array_key_exists( $id, $this->task_list_registry ) ) { |
| 143 | return array(); |
| 144 | } |
| 145 | |
| 146 | return $this->task_list_registry[ $id ]; |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | * Check if a task list is enabled by checking its is_enabled_callback callback. |
| 151 | * |
| 152 | * @param string $id Task List id. |
| 153 | * @return bool|null True if enabled, false if not, null if not found. |
| 154 | */ |
| 155 | public function is_task_list_enabled( $id ) { |
| 156 | $task_list = $this->get_task_list( $id ); |
| 157 | |
| 158 | return $this->load_value_from_callback( $task_list, 'is_enabled_callback', null ); |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * Check if a task list was dismissed by the user. |
| 163 | * |
| 164 | * @param string $id Task List id. |
| 165 | * @return bool|null True if dismissed, false if not. |
| 166 | */ |
| 167 | public function is_task_list_dismissed( $id ) { |
| 168 | $task_list_dismissed_status = $this->get_task_list_dismissed_status(); |
| 169 | $is_dismissed = isset( $task_list_dismissed_status[ $id ] ) && true === $task_list_dismissed_status[ $id ]; |
| 170 | |
| 171 | // Return true if the task list is on the dismissed status array and its value is true. |
| 172 | return $is_dismissed || $this->is_temporally_dismissed( $id ); |
| 173 | } |
| 174 | |
| 175 | /** |
| 176 | * Check if a task list is dismissible. |
| 177 | * |
| 178 | * @param string $id Task List id. |
| 179 | * @return bool True if dismissible, false if not. |
| 180 | */ |
| 181 | public function is_task_list_dismissible( $id ) { |
| 182 | $task_list = $this->get_task_list( $id ); |
| 183 | if ( ! isset( $task_list['is_dismissible'] ) ) { |
| 184 | return false; |
| 185 | } |
| 186 | return $task_list['is_dismissible']; |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * Set wether a task list is dismissed or not for a site. |
| 191 | * |
| 192 | * @param string $id Task List id. |
| 193 | * @param bool $is_dismissed True if dismissed, false if not. |
| 194 | */ |
| 195 | public function set_task_list_dismissed( $id, $is_dismissed ) { |
| 196 | $task_list = $this->get_task_list( $id ); |
| 197 | $launchpad_config = get_option( 'wpcom_launchpad_config', array() ); |
| 198 | |
| 199 | if ( empty( $id ) || empty( $task_list ) ) { |
| 200 | return; |
| 201 | } |
| 202 | |
| 203 | $task_list_dismissed_status = $this->get_task_list_dismissed_status(); |
| 204 | $is_dismissed = (bool) $is_dismissed; |
| 205 | |
| 206 | if ( $is_dismissed ) { |
| 207 | $task_list_dismissed_status[ $id ] = true; |
| 208 | } else { |
| 209 | unset( $task_list_dismissed_status[ $id ] ); |
| 210 | } |
| 211 | |
| 212 | $launchpad_config['task_list_dismissed_status'] = $task_list_dismissed_status; |
| 213 | update_option( 'wpcom_launchpad_config', $launchpad_config ); |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Set the date until a task list is dismissed. |
| 218 | * |
| 219 | * @param string $checklist_slug Checklist slug. |
| 220 | * @param int $dismissed_until Timestamp with the date until the task list is dismissed. |
| 221 | */ |
| 222 | public function set_task_list_dismissed_until( $checklist_slug, $dismissed_until ) { |
| 223 | |
| 224 | if ( empty( $checklist_slug ) ) { |
| 225 | return; |
| 226 | } |
| 227 | |
| 228 | $task_list_dismissed_until = $this->get_task_list_dismissed_until(); |
| 229 | |
| 230 | if ( isset( $dismissed_until ) ) { |
| 231 | $task_list_dismissed_until[ $checklist_slug ] = $dismissed_until; |
| 232 | } else { |
| 233 | unset( $task_list_dismissed_until[ $checklist_slug ] ); |
| 234 | } |
| 235 | |
| 236 | $launchpad_config = get_option( 'wpcom_launchpad_config', array() ); |
| 237 | $launchpad_config['task_list_dismissed_until'] = $task_list_dismissed_until; |
| 238 | |
| 239 | update_option( 'wpcom_launchpad_config', $launchpad_config ); |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * Returns true if the task list is temporally dismissed. |
| 244 | * |
| 245 | * @param string $checklist_slug Checklist slug. |
| 246 | * @return bool True if temporally dismissed, false if not. |
| 247 | */ |
| 248 | protected function is_temporally_dismissed( $checklist_slug ): bool { |
| 249 | $task_list_dismissed_until = $this->get_task_list_dismissed_until(); |
| 250 | |
| 251 | if ( ! isset( $task_list_dismissed_until ) || ! isset( $task_list_dismissed_until[ $checklist_slug ] ) ) { |
| 252 | return false; |
| 253 | } |
| 254 | |
| 255 | $task_list_dismissed_until = $task_list_dismissed_until[ $checklist_slug ]; |
| 256 | $current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); |
| 257 | $dismissed_until = new DateTime( '@' . $task_list_dismissed_until, new DateTimeZone( 'UTC' ) ); |
| 258 | |
| 259 | return $current_time <= $dismissed_until; |
| 260 | } |
| 261 | |
| 262 | /** |
| 263 | * Get the task list visibility status for a site. |
| 264 | * |
| 265 | * @return array |
| 266 | */ |
| 267 | protected function get_task_list_dismissed_status() { |
| 268 | $launchpad_config = get_option( 'wpcom_launchpad_config', array() ); |
| 269 | if ( ! isset( $launchpad_config['task_list_dismissed_status'] ) || ! is_array( $launchpad_config['task_list_dismissed_status'] ) ) { |
| 270 | return array(); |
| 271 | } |
| 272 | |
| 273 | return $launchpad_config['task_list_dismissed_status']; |
| 274 | } |
| 275 | |
| 276 | /** |
| 277 | * Get the task list dismissed until date when available. |
| 278 | * |
| 279 | * @return array |
| 280 | */ |
| 281 | public function get_task_list_dismissed_until() { |
| 282 | $launchpad_config = get_option( 'wpcom_launchpad_config', array() ); |
| 283 | |
| 284 | if ( ! isset( $launchpad_config['task_list_dismissed_until'] ) || ! is_array( $launchpad_config['task_list_dismissed_until'] ) ) { |
| 285 | return array(); |
| 286 | } |
| 287 | |
| 288 | return $launchpad_config['task_list_dismissed_until']; |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * See if the task list registry has any task lists. |
| 293 | * |
| 294 | * @return bool True if there are task lists, false if not. |
| 295 | */ |
| 296 | public function has_task_lists() { |
| 297 | return is_countable( $this->task_list_registry ) && count( $this->task_list_registry ) > 0; |
| 298 | } |
| 299 | |
| 300 | /** |
| 301 | * Get all registered Launchpad Task Lists. |
| 302 | * |
| 303 | * @return Task_List[] All registered Launchpad Task Lists. |
| 304 | */ |
| 305 | public function get_all_task_lists() { |
| 306 | return $this->task_list_registry; |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Get a Launchpad Task definition |
| 311 | * |
| 312 | * @param string $id Task id. |
| 313 | * |
| 314 | * @return Task Task. |
| 315 | */ |
| 316 | public function get_task( $id ) { |
| 317 | if ( ! array_key_exists( $id, $this->task_registry ) ) { |
| 318 | return array(); |
| 319 | } |
| 320 | |
| 321 | return $this->task_registry[ $id ]; |
| 322 | } |
| 323 | |
| 324 | /** |
| 325 | * Get the required task ids for a given task list. |
| 326 | * |
| 327 | * @param string $task_list_id Task list ID. |
| 328 | * @return array Required task ids. |
| 329 | */ |
| 330 | public function get_required_task_ids( $task_list_id ) { |
| 331 | $task_list = $this->get_task_list( $task_list_id ); |
| 332 | if ( ! isset( $task_list['required_task_ids'] ) ) { |
| 333 | return array(); |
| 334 | } |
| 335 | return $task_list['required_task_ids']; |
| 336 | } |
| 337 | |
| 338 | /** |
| 339 | * Check if the task list requires the last task to be completed in order to consider |
| 340 | * the task list complete. |
| 341 | * |
| 342 | * @param string $task_list_id Task list id. |
| 343 | * @return bool True if the last task must be completed, false if not. |
| 344 | */ |
| 345 | public function get_require_last_task_completion( $task_list_id ) { |
| 346 | $task_list = $this->get_task_list( $task_list_id ); |
| 347 | if ( ! isset( $task_list['require_last_task_completion'] ) ) { |
| 348 | return false; |
| 349 | } |
| 350 | return $task_list['require_last_task_completion']; |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * Check if a task list is completed. |
| 355 | * |
| 356 | * @param string $task_list_id Task list id. |
| 357 | * @return bool True if the task list is completed, false if not. |
| 358 | */ |
| 359 | public function is_task_list_completed( $task_list_id ) { |
| 360 | $task_list = $this->get_task_list( $task_list_id ); |
| 361 | return $this->load_value_from_callback( $task_list, 'is_completed_callback', false ); |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * Get all registered Launchpad Tasks. |
| 366 | * |
| 367 | * @return Task[] All registered Launchpad Tasks. |
| 368 | */ |
| 369 | public function get_all_tasks() { |
| 370 | return $this->task_registry; |
| 371 | } |
| 372 | |
| 373 | /** |
| 374 | * Builds a collection of tasks for a given task list |
| 375 | * |
| 376 | * @param string $id Task list id. |
| 377 | * @param string|null $launchpad_context Optional. Screen in which launchpad is loading. |
| 378 | * |
| 379 | * @return Task[] Collection of tasks associated with a task list. |
| 380 | */ |
| 381 | public function build( $id, $launchpad_context = null ) { |
| 382 | $task_list = $this->get_task_list( $id ); |
| 383 | $tasks_for_task_list = array(); |
| 384 | |
| 385 | if ( empty( $task_list['task_ids'] ) ) { |
| 386 | return $tasks_for_task_list; |
| 387 | } |
| 388 | |
| 389 | // Filter the task list's task ids to only include visible tasks if a callback is provided. |
| 390 | $task_ids = $this->load_value_from_callback( $task_list, 'visible_tasks_callback', $task_list['task_ids'] ); |
| 391 | |
| 392 | // Takes a registered task list, looks at its associated task ids, |
| 393 | // and returns a collection of associated tasks. |
| 394 | foreach ( $task_ids as $task_id ) { |
| 395 | $task_definition = $this->get_task( $task_id ); |
| 396 | |
| 397 | // if task can't be found don't add anything |
| 398 | if ( $this->is_visible( $task_definition, $launchpad_context ) ) { |
| 399 | $tasks_for_task_list[] = $this->build_task( $task_definition, $launchpad_context ); |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | return $tasks_for_task_list; |
| 404 | } |
| 405 | |
| 406 | /** |
| 407 | * Allows a function to be called to determine if a task should be visible. |
| 408 | * For instance: we don't even want to show the verify_email task if it's already done. |
| 409 | * |
| 410 | * @param Task $task_definition A task definition. |
| 411 | * @param string|null $launchpad_context Optional. Screen in which launchpad is loading. |
| 412 | * @return boolean True if task is visible, false if not. |
| 413 | */ |
| 414 | protected function is_visible( $task_definition, $launchpad_context = null ) { |
| 415 | if ( empty( $task_definition ) ) { |
| 416 | return false; |
| 417 | } |
| 418 | |
| 419 | $data = array( |
| 420 | 'launchpad_context' => $launchpad_context, |
| 421 | ); |
| 422 | |
| 423 | return $this->load_value_from_callback( $task_definition, 'is_visible_callback', true, $data ); |
| 424 | } |
| 425 | |
| 426 | /** |
| 427 | * Builds a single task with current state |
| 428 | * |
| 429 | * @param Task $task Task definition. |
| 430 | * @param string|null $launchpad_context Optional. Screen where Launchpad is loading. |
| 431 | * @return Task Task with current state. |
| 432 | */ |
| 433 | private function build_task( $task, $launchpad_context = null ) { |
| 434 | $built_task = array( |
| 435 | 'id' => $task['id'], |
| 436 | ); |
| 437 | |
| 438 | $built_task['title'] = $this->load_title( $task ); |
| 439 | $built_task['completed'] = $this->is_task_complete( $task ); |
| 440 | $built_task['disabled'] = $this->is_task_disabled( $task ); |
| 441 | $built_task['subtitle'] = $this->load_subtitle( $task ); |
| 442 | $built_task['badge_text'] = $this->load_value_from_callback( $task, 'badge_text_callback' ); |
| 443 | $built_task['isLaunchTask'] = isset( $task['isLaunchTask'] ) ? $task['isLaunchTask'] : false; |
| 444 | $extra_data = $this->load_extra_data( $task ); |
| 445 | |
| 446 | if ( is_array( $extra_data ) && array() !== $extra_data ) { |
| 447 | $built_task['extra_data'] = $extra_data; |
| 448 | } |
| 449 | |
| 450 | if ( isset( $task['target_repetitions'] ) ) { |
| 451 | $built_task['target_repetitions'] = $task['target_repetitions']; |
| 452 | $built_task['repetition_count'] = min( $this->load_repetition_count( $task ), $task['target_repetitions'] ); |
| 453 | } |
| 454 | |
| 455 | if ( isset( $task['get_calypso_path'] ) ) { |
| 456 | $calypso_path = $this->load_calypso_path( $task, $launchpad_context ); |
| 457 | |
| 458 | if ( ! empty( $calypso_path ) ) { |
| 459 | $built_task['calypso_path'] = $calypso_path; |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | return $built_task; |
| 464 | } |
| 465 | |
| 466 | /** |
| 467 | * Given a task or task list definition and a possible callback, call it and return the value. |
| 468 | * |
| 469 | * @param Task|Task_List $item The task or task list definition. |
| 470 | * @param string $callback The callback to attempt to call. |
| 471 | * @param mixed $default The default value, passed to the callback if it exists. |
| 472 | * @param array $data Any additional data specific to the callback. |
| 473 | * @return mixed The value returned by the callback, or the default value. |
| 474 | */ |
| 475 | private function load_value_from_callback( $item, $callback, $default = '', $data = array() ) { |
| 476 | if ( isset( $item[ $callback ] ) && is_callable( $item[ $callback ] ) ) { |
| 477 | return call_user_func_array( $item[ $callback ], array( $item, $default, $data ) ); |
| 478 | } |
| 479 | return $default; |
| 480 | } |
| 481 | |
| 482 | /** |
| 483 | * Loads any extra data for a task, calling the `extra_data_callback` callback to get the data if the callback is defined. |
| 484 | * Returns null if there is no callback or the callback returns an empty array or a non-array. |
| 485 | * |
| 486 | * @param Task $task A task definition. |
| 487 | * @return array|null The extra data for the task. |
| 488 | */ |
| 489 | private function load_extra_data( $task ) { |
| 490 | $extra_data = $this->load_value_from_callback( $task, 'extra_data_callback' ); |
| 491 | if ( is_array( $extra_data ) && array() !== $extra_data ) { |
| 492 | return $extra_data; |
| 493 | } |
| 494 | |
| 495 | return null; |
| 496 | } |
| 497 | |
| 498 | /** |
| 499 | * Loads a title for a task, calling the 'get_title' callback if it exists, |
| 500 | * or falling back on the value for the 'title' key if it is set. |
| 501 | * We prefer the callback so we can defer the translation until after the |
| 502 | * user's locale has been set up. |
| 503 | * |
| 504 | * @param Task $task A task definition. |
| 505 | * @return string The title for the task. |
| 506 | */ |
| 507 | private function load_title( $task ) { |
| 508 | $title = $this->load_value_from_callback( $task, 'get_title' ); |
| 509 | if ( ! empty( $title ) ) { |
| 510 | return $title; |
| 511 | } |
| 512 | |
| 513 | if ( isset( $task['title'] ) ) { |
| 514 | return $task['title']; |
| 515 | } |
| 516 | |
| 517 | return ''; |
| 518 | } |
| 519 | |
| 520 | /** |
| 521 | * Loads a subtitle for a task, calling the callback if it exists. |
| 522 | * |
| 523 | * @param Task $task A task definition. |
| 524 | * @return string The subtitle for the task. |
| 525 | */ |
| 526 | private function load_subtitle( $task ) { |
| 527 | $subtitle = $this->load_value_from_callback( $task, 'subtitle' ); |
| 528 | if ( ! empty( $subtitle ) ) { |
| 529 | return $subtitle; |
| 530 | } |
| 531 | // if it wasn't a callback, but still a string, return it. |
| 532 | if ( isset( $task['subtitle'] ) ) { |
| 533 | $task['subtitle']; |
| 534 | } |
| 535 | return ''; |
| 536 | } |
| 537 | |
| 538 | /** |
| 539 | * Loads the repetition count for a task, calling the callback if it exists. |
| 540 | * |
| 541 | * @param Task $task A task definition. |
| 542 | * @return int|null The repetition count for the task. |
| 543 | */ |
| 544 | private function load_repetition_count( $task ) { |
| 545 | return $this->load_value_from_callback( $task, 'repetition_count_callback', 0 ); |
| 546 | } |
| 547 | |
| 548 | /** |
| 549 | * Helper function to load the Calypso path for a task. |
| 550 | * |
| 551 | * @param Task $task A task definition. |
| 552 | * @param string|null $launchpad_context Optional. Screen where Launchpad is loading. |
| 553 | * @return string|null |
| 554 | */ |
| 555 | private function load_calypso_path( $task, $launchpad_context = null ) { |
| 556 | if ( null === $this->site_slug ) { |
| 557 | $this->site_slug = wpcom_get_site_slug(); |
| 558 | } |
| 559 | |
| 560 | $data = array( |
| 561 | 'site_slug' => $this->site_slug, |
| 562 | 'site_slug_encoded' => rawurlencode( $this->site_slug ), |
| 563 | 'launchpad_context' => $launchpad_context, |
| 564 | ); |
| 565 | |
| 566 | $calypso_path = $this->load_value_from_callback( $task, 'get_calypso_path', null, $data ); |
| 567 | |
| 568 | if ( ! is_string( $calypso_path ) ) { |
| 569 | return null; |
| 570 | } |
| 571 | |
| 572 | if ( ! $this->is_valid_admin_url_or_absolute_path( $calypso_path ) ) { |
| 573 | return null; |
| 574 | } |
| 575 | |
| 576 | return $calypso_path; |
| 577 | } |
| 578 | |
| 579 | /** |
| 580 | * Checks if a string is a Stripe connection, valid admin URL, or absolute path. |
| 581 | * |
| 582 | * @param string $input The string to check. |
| 583 | * @return boolean |
| 584 | */ |
| 585 | private function is_valid_admin_url_or_absolute_path( $input ) { |
| 586 | // Allow Stripe connection URLs for `set_up_payments` task. |
| 587 | if ( strpos( $input, 'https://connect.stripe.com' ) === 0 ) { |
| 588 | return true; |
| 589 | } |
| 590 | |
| 591 | // Checks if the string is URL starting with the admin URL. |
| 592 | if ( strpos( $input, admin_url() ) === 0 ) { |
| 593 | return true; |
| 594 | } |
| 595 | |
| 596 | // Require that the string start with a slash, but not two slashes. |
| 597 | if ( str_starts_with( $input, '/' ) && ! str_starts_with( $input, '//' ) ) { |
| 598 | return true; |
| 599 | } |
| 600 | |
| 601 | return false; |
| 602 | } |
| 603 | |
| 604 | /** |
| 605 | * Checks if a task is disabled |
| 606 | * |
| 607 | * @param Task $task Task definition. |
| 608 | * @return boolean |
| 609 | */ |
| 610 | public function is_task_disabled( $task ) { |
| 611 | return $this->load_value_from_callback( $task, 'is_disabled_callback', false ); |
| 612 | } |
| 613 | |
| 614 | /** |
| 615 | * Checks if a task is complete, relying on task-defined callbacks if available |
| 616 | * |
| 617 | * @param Task $task Task definition. |
| 618 | * @return boolean |
| 619 | */ |
| 620 | public function is_task_complete( $task ) { |
| 621 | // First we calculate the value from our statuses option. This will get passed to the callback, if it exists. |
| 622 | // Othewise there is the temptation for the callback to fall back to the option, which would cause infinite recursion |
| 623 | // as it continues to calculate the callback which falls back to the option: ∞. |
| 624 | $statuses = get_option( 'launchpad_checklist_tasks_statuses', array() ); |
| 625 | $key = $this->get_task_key( $task ); |
| 626 | $is_complete = isset( $statuses[ $key ] ) ? $statuses[ $key ] : false; |
| 627 | |
| 628 | return (bool) $this->load_value_from_callback( $task, 'is_complete_callback', $is_complete ); |
| 629 | } |
| 630 | |
| 631 | /** |
| 632 | * Gets the task key, which is used to store and retrieve the task's status. |
| 633 | * Either the task's id_map or id is used. |
| 634 | * |
| 635 | * @param Task $task Task definition. |
| 636 | * @return string The task key to use. |
| 637 | */ |
| 638 | public function get_task_key( $task ) { |
| 639 | return isset( $task['id_map'] ) ? $task['id_map'] : $task['id']; |
| 640 | } |
| 641 | |
| 642 | /** |
| 643 | * Checks if a task wight given ID is complete. |
| 644 | * |
| 645 | * @param string $task_id The task ID. |
| 646 | * @return boolean |
| 647 | */ |
| 648 | public function is_task_id_complete( $task_id ) { |
| 649 | $task = $this->get_task( $task_id ); |
| 650 | if ( empty( $task ) ) { |
| 651 | return false; |
| 652 | } |
| 653 | return $this->is_task_complete( $task ); |
| 654 | } |
| 655 | |
| 656 | /** |
| 657 | * Validate a Launchpad Task List |
| 658 | * |
| 659 | * @param Task_List $task_list Task List. |
| 660 | * |
| 661 | * @return null|WP_Error Null if valid, WP_Error if not. |
| 662 | */ |
| 663 | public static function validate_task_list( $task_list ) { |
| 664 | $error_code = 'validate_task_list'; |
| 665 | $error_messages = array(); |
| 666 | |
| 667 | if ( ! is_array( $task_list ) ) { |
| 668 | // Ensure we have a valid task list array. |
| 669 | $msg = 'Invalid task list'; |
| 670 | _doing_it_wrong( 'validate_task_list', esc_html( $msg ), '6.1' ); |
| 671 | return new WP_Error( $error_code, $msg ); |
| 672 | } |
| 673 | |
| 674 | if ( ! isset( $task_list['id'] ) ) { |
| 675 | // Ensure we have an id. |
| 676 | $msg = 'The Launchpad task list being registered requires a "id" attribute'; |
| 677 | _doing_it_wrong( 'validate_task_list', esc_html( $msg ), '6.1' ); |
| 678 | $error_messages[] = $msg; |
| 679 | } |
| 680 | |
| 681 | if ( ! isset( $task_list['task_ids'] ) ) { |
| 682 | // Ensure we have task_ids. |
| 683 | $msg = 'The Launchpad task list being registered requires a "task_ids" attribute'; |
| 684 | _doing_it_wrong( 'validate_task_list', esc_html( $msg ), '6.1' ); |
| 685 | $error_messages[] = $msg; |
| 686 | } elseif ( isset( $task_list['required_task_ids'] ) ) { |
| 687 | // Ensure we have a valid array. |
| 688 | if ( ! is_array( $task_list['required_task_ids'] ) ) { |
| 689 | $msg = 'The required_task_ids attribute must be an array'; |
| 690 | _doing_it_wrong( 'validate_task_list', esc_html( $msg ), '6.1' ); |
| 691 | $error_messages[] = $msg; |
| 692 | // Ensure all required tasks actually exist in the task list - we need the value to be an array for this to work. |
| 693 | } elseif ( array_intersect( $task_list['required_task_ids'], $task_list['task_ids'] ) !== $task_list['required_task_ids'] ) { |
| 694 | $msg = 'The required_task_ids must be a subset of the task_ids'; |
| 695 | _doing_it_wrong( 'validate_task_list', esc_html( $msg ), '6.1' ); |
| 696 | $error_messages[] = $msg; |
| 697 | } |
| 698 | } |
| 699 | |
| 700 | if ( isset( $task_list['visible_tasks_callback'] ) && ! is_callable( $task_list['visible_tasks_callback'] ) ) { |
| 701 | $msg = 'The visible_tasks_callback attribute must be callable'; |
| 702 | _doing_it_wrong( 'validate_task_list', esc_html( $msg ), '6.1' ); |
| 703 | $error_messages[] = $msg; |
| 704 | } |
| 705 | |
| 706 | if ( isset( $task_list['require_last_task_completion'] ) && ! is_bool( $task_list['require_last_task_completion'] ) ) { |
| 707 | $msg = 'The require_last_task_completion attribute must be a boolean'; |
| 708 | _doing_it_wrong( 'validate_task_list', esc_html( $msg ), '6.1' ); |
| 709 | $error_messages[] = $msg; |
| 710 | } |
| 711 | |
| 712 | if ( array() !== $error_messages ) { |
| 713 | $wp_error = new WP_Error(); |
| 714 | |
| 715 | foreach ( $error_messages as $error_message ) { |
| 716 | $wp_error->add( $error_code, $error_message ); |
| 717 | } |
| 718 | |
| 719 | return $wp_error; |
| 720 | } |
| 721 | |
| 722 | return null; |
| 723 | } |
| 724 | |
| 725 | /** |
| 726 | * Get currently active tasks. |
| 727 | * |
| 728 | * @param string $task_list_id Optional. Will default to `site_intent` option. |
| 729 | * @return Task[] Array of active tasks. |
| 730 | */ |
| 731 | private function get_active_tasks( $task_list_id = null ) { |
| 732 | $task_list_id = $task_list_id ? $task_list_id : get_option( 'site_intent' ); |
| 733 | if ( ! $task_list_id ) { |
| 734 | return array(); |
| 735 | } |
| 736 | $task_list = $this->get_task_list( $task_list_id ); |
| 737 | if ( empty( $task_list ) ) { |
| 738 | return array(); |
| 739 | } |
| 740 | $built_tasks = $this->build( $task_list_id ); |
| 741 | // filter for incomplete tasks |
| 742 | return wp_list_filter( $built_tasks, array( 'completed' => false ) ); |
| 743 | } |
| 744 | |
| 745 | /** |
| 746 | * Gets a list of completed tasks. |
| 747 | * |
| 748 | * @param string $task_list_id Optional. Will default to `site_intent` option. |
| 749 | * @return Task[] Array of completed tasks. |
| 750 | */ |
| 751 | private function get_completed_tasks( $task_list_id = null ) { |
| 752 | $task_list_id = $task_list_id ? $task_list_id : get_option( 'site_intent' ); |
| 753 | if ( ! $task_list_id ) { |
| 754 | return array(); |
| 755 | } |
| 756 | $task_list = $this->get_task_list( $task_list_id ); |
| 757 | if ( empty( $task_list ) ) { |
| 758 | return array(); |
| 759 | } |
| 760 | $built_tasks = $this->build( $task_list_id ); |
| 761 | // filter for incomplete tasks |
| 762 | return wp_list_filter( $built_tasks, array( 'completed' => true ) ); |
| 763 | } |
| 764 | |
| 765 | /** |
| 766 | * Checks if there are any active tasks. |
| 767 | * |
| 768 | * @param string|null $task_list_id Optional. Will default to `site_intent` option. |
| 769 | * @return boolean True if there are active tasks, false if not. |
| 770 | */ |
| 771 | private function has_active_tasks( $task_list_id = null ) { |
| 772 | return ! empty( $this->get_active_tasks( $task_list_id ) ); |
| 773 | } |
| 774 | |
| 775 | /** |
| 776 | * Adds task-defined `add_listener_callback` hooks for incomplete tasks. |
| 777 | * |
| 778 | * @param string $task_list_id Optional. Will default to `site_intent` option. |
| 779 | * @return void |
| 780 | */ |
| 781 | public function add_hooks_for_active_tasks( $task_list_id = null ) { |
| 782 | // leave things alone if Launchpad is not enabled. |
| 783 | if ( ! $this->is_fullscreen_launchpad_enabled() ) { |
| 784 | return; |
| 785 | } |
| 786 | |
| 787 | $task_list_id = $task_list_id ? $task_list_id : get_option( 'site_intent' ); |
| 788 | // Sites without a `site_intent` option will not have any tasks. |
| 789 | if ( ! $task_list_id ) { |
| 790 | return; |
| 791 | } |
| 792 | |
| 793 | $task_list = $this->get_task_list( $task_list_id ); |
| 794 | if ( empty( $task_list ) || ! isset( $task_list['task_ids'] ) ) { |
| 795 | return; |
| 796 | } |
| 797 | |
| 798 | foreach ( $task_list['task_ids'] as $task_id ) { |
| 799 | $task_definition = $this->get_task( $task_id ); |
| 800 | if ( isset( $task_definition['add_listener_callback'] ) && is_callable( $task_definition['add_listener_callback'] ) ) { |
| 801 | // We only need to know the built completion status if the task has an `add_listener_callback` property. |
| 802 | // Small optimization to not run `is_complete_callback` as often. |
| 803 | $task = $this->build_task( $task_definition ); |
| 804 | if ( ! $task['completed'] && is_callable( $task_definition['add_listener_callback'] ) ) { |
| 805 | call_user_func_array( $task_definition['add_listener_callback'], array( $task, $task_definition ) ); |
| 806 | } |
| 807 | } |
| 808 | } |
| 809 | } |
| 810 | |
| 811 | /** |
| 812 | * Marks a task as complete. |
| 813 | * |
| 814 | * @param string $task_id The task ID. |
| 815 | * @return bool True if successful, false if not. |
| 816 | */ |
| 817 | public function mark_task_complete( $task_id ) { |
| 818 | $result = wpcom_mark_launchpad_task_complete( $task_id ); |
| 819 | |
| 820 | $this->maybe_disable_fullscreen_launchpad(); |
| 821 | |
| 822 | return $result; |
| 823 | } |
| 824 | |
| 825 | /** |
| 826 | * Marks a task as complete if it is active for this site. This is a bit of a hacky way to be able to share a callback |
| 827 | * among several tasks, calling several completion IDs from the same callback. |
| 828 | * |
| 829 | * @param string $task_id The task ID. |
| 830 | * @return bool True if successful, false if not. |
| 831 | */ |
| 832 | public function mark_task_complete_if_active( $task_id ) { |
| 833 | // Ensure that the task is an active one |
| 834 | $active_tasks_by_task_id = wp_list_filter( $this->get_active_tasks(), array( 'id' => $task_id ) ); |
| 835 | if ( empty( $active_tasks_by_task_id ) ) { |
| 836 | return false; |
| 837 | } |
| 838 | |
| 839 | return $this->mark_task_complete( $task_id ); |
| 840 | } |
| 841 | |
| 842 | /** |
| 843 | * Disables fullscreen Launchpad if all tasks are complete. |
| 844 | * |
| 845 | * @return void |
| 846 | */ |
| 847 | public function maybe_disable_fullscreen_launchpad() { |
| 848 | $completed_site_launched_task = wp_list_filter( |
| 849 | $this->get_completed_tasks(), |
| 850 | array( |
| 851 | 'isLaunchTask' => true, |
| 852 | ) |
| 853 | ); |
| 854 | |
| 855 | $site_launched = ! empty( $completed_site_launched_task ); |
| 856 | |
| 857 | if ( $site_launched || ! $this->has_active_tasks() ) { |
| 858 | $this->disable_fullscreen_launchpad(); |
| 859 | } |
| 860 | } |
| 861 | |
| 862 | /** |
| 863 | * Validate a Launchpad Task |
| 864 | * |
| 865 | * @param Task $task Task. |
| 866 | * |
| 867 | * @return bool True if valid, false if not. |
| 868 | */ |
| 869 | public static function validate_task( $task ) { |
| 870 | if ( ! is_array( $task ) ) { |
| 871 | return false; |
| 872 | } |
| 873 | |
| 874 | if ( ! isset( $task['id'] ) ) { |
| 875 | _doing_it_wrong( 'validate_task', 'The Launchpad task being registered requires a "id" attribute', '6.1' ); |
| 876 | return false; |
| 877 | } |
| 878 | |
| 879 | // For now, allow the 'title' attribute. |
| 880 | $has_valid_title = isset( $task['title'] ) || ( isset( $task['get_title'] ) && is_callable( $task['get_title'] ) ); |
| 881 | |
| 882 | if ( ! $has_valid_title ) { |
| 883 | _doing_it_wrong( 'validate_task', 'The Launchpad task being registered requires a "title" attribute or a "get_title" callback', '6.2' ); |
| 884 | return false; |
| 885 | } |
| 886 | |
| 887 | $has_any_repetition_properties = isset( $task['target_repetitions'] ) || isset( $task['repetition_count_callback'] ); |
| 888 | $has_both_repetition_properties = isset( $task['target_repetitions'] ) && isset( $task['repetition_count_callback'] ); |
| 889 | |
| 890 | if ( $has_any_repetition_properties && ! $has_both_repetition_properties ) { |
| 891 | _doing_it_wrong( 'validate_task', 'The Launchpad task being registered requires both a "target_repetitions" attribute and a "repetition_count_callback" callback', '6.3' ); |
| 892 | return false; |
| 893 | } |
| 894 | |
| 895 | if ( isset( $task['target_repetitions'] ) && ! is_int( $task['target_repetitions'] ) ) { |
| 896 | _doing_it_wrong( 'validate_task', 'The Launchpad task being registered requires a "target_repetitions" attribute that is an integer', '6.4' ); |
| 897 | return false; |
| 898 | } |
| 899 | |
| 900 | return true; |
| 901 | } |
| 902 | |
| 903 | /** |
| 904 | * Checks if Launchpad is enabled. |
| 905 | * |
| 906 | * @return boolean |
| 907 | */ |
| 908 | public function is_fullscreen_launchpad_enabled() { |
| 909 | $launchpad_screen = get_option( 'launchpad_screen' ); |
| 910 | if ( 'full' !== $launchpad_screen ) { |
| 911 | return false; |
| 912 | } |
| 913 | |
| 914 | return $this->has_active_tasks(); |
| 915 | } |
| 916 | /** |
| 917 | * Disables Launchpad by setting the `launchpad_screen` option to `off`. |
| 918 | * |
| 919 | * @return bool True if successful, false if not. |
| 920 | */ |
| 921 | private function disable_fullscreen_launchpad() { |
| 922 | return update_option( 'launchpad_screen', 'off' ); |
| 923 | } |
| 924 | |
| 925 | /** |
| 926 | * Gets the title for a task list. |
| 927 | * |
| 928 | * @param string $id Task list id. |
| 929 | * @return string|null The title for the task list. |
| 930 | */ |
| 931 | public function get_task_list_title( $id ) { |
| 932 | $task_list = $this->get_task_list( $id ); |
| 933 | |
| 934 | return $this->load_value_from_callback( $task_list, 'get_title', null ); |
| 935 | } |
| 936 | } |