Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.80% covered (warning)
63.80%
178 / 279
34.04% covered (danger)
34.04%
16 / 47
CRAP
0.00% covered (danger)
0.00%
0 / 1
Launchpad_Task_Lists
63.80% covered (warning)
63.80%
178 / 279
34.04% covered (danger)
34.04%
16 / 47
1113.12
0.00% covered (danger)
0.00%
0 / 1
 get_instance
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 register_task_list
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 register_task
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 unregister_task_list
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 unregister_task
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 get_task_list
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_task_list_enabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 is_task_list_dismissed
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 is_task_list_dismissible
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 set_task_list_dismissed
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 set_task_list_dismissed_until
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 is_temporally_dismissed
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 get_task_list_dismissed_status
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 get_task_list_dismissed_until
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 has_task_lists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get_all_task_lists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_task
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 get_required_task_ids
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_require_last_task_completion
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 is_task_list_completed
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_all_tasks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 build
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 is_visible
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 build_task
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
7
 load_value_from_callback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 load_extra_data
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 load_title
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 load_subtitle
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 load_repetition_count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 load_calypso_path
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 is_valid_admin_url_or_absolute_path
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 is_task_disabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_task_complete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_task_key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_task_id_complete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 validate_task_list
83.78% covered (warning)
83.78%
31 / 37
0.00% covered (danger)
0.00%
0 / 1
13.72
 get_active_tasks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 get_completed_tasks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 has_active_tasks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_hooks_for_active_tasks
14.29% covered (danger)
14.29%
2 / 14
0.00% covered (danger)
0.00%
0 / 1
87.20
 mark_task_complete
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 mark_task_complete_if_active
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 maybe_disable_fullscreen_launchpad
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 validate_task
61.11% covered (warning)
61.11%
11 / 18
0.00% covered (danger)
0.00%
0 / 1
20.47
 is_fullscreen_launchpad_enabled
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 disable_fullscreen_launchpad
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_task_list_title
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
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}
13PHAN;
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 */
23class 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}