Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.93% covered (warning)
71.93%
82 / 114
33.33% covered (danger)
33.33%
4 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Quiz_Shortcode
73.87% covered (warning)
73.87%
82 / 111
33.33% covered (danger)
33.33%
4 / 12
50.26
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_scripts
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 is_javascript_unavailable
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 noscript_info
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 is_wpcom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 shortcode
61.29% covered (warning)
61.29%
19 / 31
0.00% covered (danger)
0.00%
0 / 1
15.80
 shortcode_wrapper
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 do_shortcode
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
1
 question_shortcode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 answer_shortcode
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 wrong_shortcode
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 explanation_shortcode
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
1<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileNam
2
3use Automattic\Jetpack\Assets;
4
5if ( ! defined( 'ABSPATH' ) ) {
6    exit( 0 );
7}
8
9/**
10 * Quiz shortcode.
11 *
12 * Usage:
13 *
14 * [quiz]
15 * [question]What's the right answer?[/question]
16 * [wrong]This one?[explanation]Nope[/explanation][/wrong]
17 * [answer]Yes, this is the one![explanation]Yay![/explanation][/answer]
18 * [wrong]Maybe this one[explanation]Keep trying[/explanation][/wrong]
19 * [wrong]How about this one?[explanation]Try again[/explanation][/wrong]
20 * [/quiz]
21 *
22 * Can also be wrapped in [quiz-wrapper] to display all quizzes together.
23 */
24class Quiz_Shortcode {
25
26    /**
27     * Parameters admitted by [quiz] shortcode.
28     *
29     * @since 4.5.0
30     *
31     * @var array
32     */
33    private static $quiz_params = array();
34
35    /**
36     * Whether the [quiz-wrapper] shortcode is used.
37     *
38     * @since 10.1
39     *
40     * @var bool
41     */
42    private static $quiz_wrapper = false;
43
44    /**
45     * Whether the scripts were enqueued.
46     *
47     * @since 4.5.0
48     *
49     * @var bool
50     */
51    private static $scripts_enqueued = false;
52
53    /**
54     * In a8c training, store user currently logged in.
55     *
56     * @since 4.5.0
57     *
58     * @var null
59     */
60    private static $username = null;
61
62    /**
63     * Whether the noscript tag was already printed.
64     *
65     * @since 4.5.0
66     *
67     * @var bool
68     */
69    private static $noscript_info_printed = false;
70
71    /**
72     * Whether JavaScript is available.
73     *
74     * @since 4.5.0
75     *
76     * @var null
77     */
78    private static $javascript_unavailable = null;
79
80    /**
81     * Register all shortcodes.
82     *
83     * @since 4.5.0
84     */
85    public static function init() {
86        add_shortcode( 'quiz-wrapper', array( __CLASS__, 'shortcode_wrapper' ) );
87        add_shortcode( 'quiz', array( __CLASS__, 'shortcode' ) );
88        add_shortcode( 'question', array( __CLASS__, 'question_shortcode' ) );
89        add_shortcode( 'answer', array( __CLASS__, 'answer_shortcode' ) );
90        add_shortcode( 'wrong', array( __CLASS__, 'wrong_shortcode' ) );
91        add_shortcode( 'explanation', array( __CLASS__, 'explanation_shortcode' ) );
92    }
93
94    /**
95     * Enqueue assets needed by the quiz,
96     *
97     * @since 4.5.0
98     */
99    private static function enqueue_scripts() {
100        wp_enqueue_style( 'quiz', plugins_url( 'css/quiz.css', __FILE__ ), array(), JETPACK__VERSION );
101        wp_enqueue_script(
102            'quiz',
103            Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/quiz.min.js', 'modules/shortcodes/js/quiz.js' ),
104            array( 'jquery' ),
105            JETPACK__VERSION,
106            true
107        );
108    }
109
110    /**
111     * Check if this is a feed and thus JS is unavailable.
112     *
113     * @since 4.5.0
114     *
115     * @return bool|null
116     */
117    private static function is_javascript_unavailable() {
118        if ( self::$javascript_unavailable !== null ) {
119            return self::$javascript_unavailable;
120        }
121
122        if ( is_feed() ) {
123            self::$javascript_unavailable = true;
124            return self::$javascript_unavailable;
125        }
126
127        self::$javascript_unavailable = false;
128        return self::$javascript_unavailable;
129    }
130
131    /**
132     * Display message when JS is not available.
133     *
134     * @since 4.5.0
135     *
136     * @return string
137     */
138    private static function noscript_info() {
139        if ( self::$noscript_info_printed ) {
140            return '';
141        }
142        self::$noscript_info_printed = true;
143        return '<noscript><div><i>' . esc_html__( 'Please view this post in your web browser to complete the quiz.', 'jetpack' ) . '</i></div></noscript>';
144    }
145
146    /**
147     * Check if we're in WordPress.com.
148     *
149     * @since 4.5.0
150     *
151     * @return bool
152     */
153    public static function is_wpcom() {
154        return defined( 'IS_WPCOM' ) && IS_WPCOM;
155    }
156
157    /**
158     * Parse shortcode arguments and render its output.
159     *
160     * @since 4.5.0
161     *
162     * @param array  $atts    Shortcode parameters.
163     * @param string $content Content enclosed by shortcode tags.
164     *
165     * @return string
166     */
167    public static function shortcode( $atts, $content = null ) {
168
169        // There's nothing to do if there's nothing enclosed.
170        if ( empty( $content ) ) {
171            return '';
172        }
173
174        $id = '';
175
176        if ( self::is_javascript_unavailable() ) {
177            // in an e-mail print the question and the info sentence once per question, too.
178            self::$noscript_info_printed = false;
179        } else {
180
181            if ( ! self::$scripts_enqueued ) {
182                // lazy enqueue cannot use the wp_enqueue_scripts action anymore.
183                self::enqueue_scripts();
184                self::$scripts_enqueued = true;
185            }
186
187            $default_atts = self::is_wpcom()
188                ? array(
189                    'trackid'     => '',
190                    'a8ctraining' => '',
191                )
192                : array(
193                    'trackid' => '',
194                );
195
196            self::$quiz_params = shortcode_atts( $default_atts, $atts );
197
198            if ( ! empty( self::$quiz_params['trackid'] ) ) {
199                $id .= ' data-trackid="' . esc_attr( self::$quiz_params['trackid'] ) . '"';
200            }
201            if ( self::is_wpcom() && ! empty( self::$quiz_params['a8ctraining'] ) ) {
202                if ( self::$username === null ) {
203                    self::$username = wp_get_current_user()->user_login;
204                }
205                $id .= ' data-a8ctraining="' . esc_attr( self::$quiz_params['a8ctraining'] ) . '" data-username="' . esc_attr( self::$username ) . '"';
206            }
207        }
208
209        $quiz         = self::do_shortcode( $content );
210        $quiz_options = '';
211
212        if ( self::$quiz_wrapper ) {
213            $quiz_options = '<div class="jetpack-quiz-options">
214            <span class="jetpack-quiz-count"></span>
215            <a class="jetpack-quiz-option-button" data-quiz-option="previous" role="button" aria-label="' . esc_attr__( 'Previous quiz', 'jetpack' ) . '">
216            <svg viewBox="0 0 24 24" class="quiz-gridicon">
217            <g><path d="M14 20l-8-8 8-8 1.414 1.414L8.828 12l6.586 6.586"></path></g></svg></a>
218            <a class="jetpack-quiz-option-button" data-quiz-option="next" role="button" aria-label="' . esc_attr__( 'Next quiz', 'jetpack' ) . '">
219            <svg viewBox="0 0 24 24" class="quiz-gridicon">
220            <g><path d="M10 20l8-8-8-8-1.414 1.414L15.172 12l-6.586 6.586"></path></g></svg></a>
221            </div>';
222        }
223
224        return '<div class="jetpack-quiz quiz"' . $id . '>' . $quiz . $quiz_options . '</div>';
225    }
226
227    /**
228     * Wrap shortcode contents.
229     *
230     * @since 10.1
231     *
232     * @param array  $atts    Shortcode parameters.
233     * @param string $content Content enclosed by shortcode tags.
234     *
235     * @return string
236     */
237    public static function shortcode_wrapper( $atts, $content = null ) {
238        self::$quiz_wrapper = true;
239        return '<div class="jetpack-quiz-wrapper">' . self::do_shortcode( $content ) . '</div>';
240    }
241
242    /**
243     * Strip line breaks, restrict allowed HTML to a few allowed tags and execute nested shortcodes.
244     *
245     * @since 4.5.0
246     *
247     * @param string $content Post content.
248     *
249     * @return mixed|string
250     */
251    private static function do_shortcode( $content ) {
252        // strip autoinserted line breaks.
253        $content = preg_replace( '#(<(?:br /|/?p)>\n?)*(\[/?[a-z]+\])(<(?:br /|/?p)>\n?)*#', '$2', $content );
254
255        // Add internal parameter so it's only rendered when it has it.
256        $content = preg_replace( '/\[(question|answer|wrong|explanation)\]/i', '[$1 quiz_item="true"]', $content );
257        $content = do_shortcode( $content );
258        $content = wp_kses(
259            $content,
260            array(
261                'tt'     => array(),
262                'a'      => array(
263                    'href'             => true,
264                    'class'            => true,
265                    'data-quiz-option' => true,
266                    'aria-label'       => true,
267                    'role'             => 'button',
268                ),
269                'pre'    => array(),
270                'strong' => array(),
271                'i'      => array(),
272                'svg'    => array(),
273                'g'      => array(),
274                'path'   => array( 'd' => true ),
275                'br'     => array(),
276                'span'   => array( 'class' => true ),
277                'img'    => array( 'src' => true ),
278                'div'    => array(
279                    'class'            => true,
280                    'data-correct'     => 1,
281                    'data-track-id'    => 1,
282                    'data-a8ctraining' => 1,
283                    'data-username'    => 1,
284                    'tabindex'         => false,
285                ),
286            )
287        );
288        return $content;
289    }
290
291    /**
292     * Render question.
293     *
294     * @since 4.5.0
295     *
296     * @param array $atts    Shortcode attributes.
297     * @param null  $content Post content.
298     *
299     * @return string
300     */
301    public static function question_shortcode( $atts, $content = null ) {
302        return isset( $atts['quiz_item'] )
303            ? '<div class="jetpack-quiz-question question" tabindex="-1">' . self::do_shortcode( $content ) . '</div>'
304            : '';
305    }
306
307    /**
308     * Render correct answer.
309     *
310     * @since 4.5.0
311     *
312     * @param array $atts    Shortcode attributes.
313     * @param null  $content Post content.
314     *
315     * @return string
316     */
317    public static function answer_shortcode( $atts, $content = null ) {
318        if ( self::is_javascript_unavailable() ) {
319            return self::noscript_info();
320        }
321
322        return isset( $atts['quiz_item'] )
323            ? '<div class="jetpack-quiz-answer answer" data-correct="1">' . self::do_shortcode( $content ) . '</div>'
324            : '';
325    }
326
327    /**
328     * Render wrong response.
329     *
330     * @since 4.5.0
331     *
332     * @param array $atts    Shortcode attributes.
333     * @param null  $content Post content.
334     *
335     * @return string
336     */
337    public static function wrong_shortcode( $atts, $content = null ) {
338        if ( self::is_javascript_unavailable() ) {
339            return self::noscript_info();
340        }
341
342        return isset( $atts['quiz_item'] )
343            ? '<div class="jetpack-quiz-answer answer">' . self::do_shortcode( $content ) . '</div>'
344            : '';
345    }
346
347    /**
348     * Render explanation for wrong or right answer.
349     *
350     * @since 4.5.0
351     *
352     * @param array $atts    Shortcode attributes.
353     * @param null  $content Post content.
354     *
355     * @return string
356     */
357    public static function explanation_shortcode( $atts, $content = null ) {
358        if ( self::is_javascript_unavailable() ) {
359            return self::noscript_info();
360        }
361
362        return isset( $atts['quiz_item'] )
363            ? '<div class="jetpack-quiz-explanation explanation">' . self::do_shortcode( $content ) . '</div>'
364            : '';
365    }
366}
367
368Quiz_Shortcode::init();