Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.91% covered (warning)
73.91%
68 / 92
66.67% covered (warning)
66.67%
10 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Feedback_Source
73.91% covered (warning)
73.91%
68 / 92
66.67% covered (warning)
66.67%
10 / 15
117.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
13.38
 from_submission
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 get_source_title
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 get_current
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
5.93
 from_serialized
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 get_permalink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get_edit_form_url
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
19.71
 get_relative_permalink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_page_number
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_source_type
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_test
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_is_test
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serialize
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Feedback Entry
4 *
5 * @package automattic/jetpack-forms
6 */
7
8namespace Automattic\Jetpack\Forms\ContactForm;
9
10/**
11 * Class Feedback_Source
12 *
13 * Represents where a feedback was created from, feedback entry with an ID, title, permalink, and page number.
14 */
15class Feedback_Source {
16
17    /**
18     * Source types whose forms can only be placed by a user with the administrator-tier
19     * `edit_theme_options` capability. Destinations declared in these surfaces are trusted
20     * even though they have no numeric post author to check.
21     *
22     * @var string[]
23     */
24    const ADMIN_TIER_SOURCE_TYPES = array( 'block_template', 'block_template_part', 'widget' );
25
26    /**
27     * The ID of the post or page that the feedback was created on.
28     *
29     * @var string
30     */
31    private $id = '';
32
33    /**
34     * The title of the  post or page that the feedback was created on.
35     *
36     * @var string
37     */
38    private $title = '';
39
40    /**
41     * The permalink of the feedback entry.
42     *
43     * @var string
44     */
45    private $permalink = '';
46
47    /**
48     * The page number of the feedback post or page that the feedback was created on.
49     * This is used to determine the page number in a paginated view of page or post.
50     *
51     * @var int
52     */
53    private $page_number = 1;
54
55    /**
56     * The source type of the feedback entry.
57     * Possible values: single, widget, block_template, block_template_part
58     *
59     * @var string
60     */
61    private $source_type = 'single';
62
63    /**
64     * The request URL of the feedback entry.
65     *
66     * @var string
67     */
68    private $request_url = '';
69
70    /**
71     * Whether this feedback was created from a form preview submission.
72     *
73     * Test feedback is stored normally in the inbox but is distinguished in the
74     * notification email, excluded from the default CSV export, and skips the
75     * spam/Akismet pipeline.
76     *
77     * @var bool
78     */
79    private $is_test = false;
80
81    /**
82     * Constructor for Feedback_Source.
83     *
84     * @param string|int $id          The Source ID = post ID, widget ID, block template ID, or 0 for homepage or non-post/page.
85     * @param string     $title       The title of the feedback entry.
86     * @param int        $page_number The page number of the feedback entry, default is 1.
87     * @param string     $source_type The source type of the feedback entry, default is 'single'.
88     * @param string     $request_url The request URL of the feedback entry.
89     * @param bool       $is_test     Whether the feedback was submitted from a form preview.
90     */
91    public function __construct( $id = 0, $title = '', $page_number = 1, $source_type = 'single', $request_url = '', $is_test = false ) {
92
93        if ( is_numeric( $id ) ) {
94            $this->id = $id > 0 ? $id : 0;
95        } else {
96            $this->id = $id;
97        }
98
99        if ( is_numeric( $page_number ) ) {
100            $this->page_number = $page_number > 0 ? $page_number : 1;
101        } else {
102            $this->page_number = 1;
103        }
104
105        $this->title       = html_entity_decode( $title, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
106        $this->permalink   = empty( $request_url ) ? home_url() : $request_url;
107        $this->source_type = $source_type; // possible source types: single, widget, block_template, block_template_part
108        $this->request_url = $request_url;
109        $this->is_test     = (bool) $is_test;
110
111        if ( is_numeric( $id ) && ! empty( $id ) ) {
112            $entry_post = get_post( (int) $id );
113            if ( $entry_post && $entry_post->post_status === 'publish' ) {
114                $this->permalink = get_permalink( $entry_post );
115                $this->title     = html_entity_decode( get_the_title( $entry_post ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
116            } elseif ( $entry_post ) {
117                $this->permalink = '';
118
119                if ( $entry_post->post_status === 'trash' ) {
120                    /* translators: %s is the post title */
121                    $this->title = sprintf( __( '(trashed) %s', 'jetpack-forms' ), $this->title );
122                }
123            }
124            if ( empty( $entry_post ) ) {
125                /* translators: %s is the post title */
126                $this->title     = sprintf( __( '(deleted) %s', 'jetpack-forms' ), $this->title );
127                $this->permalink = '';
128            }
129        }
130    }
131
132    /**
133     * Creates a Feedback_Source instance from a submission.
134     *
135     * @param \WP_Post|null $current_post The current post object.
136     * @param int           $current_page_number The current page number, default is 1.
137     * @return Feedback_Source Returns an instance of Feedback_Source.
138     */
139    public static function from_submission( $current_post, int $current_page_number = 1 ) {
140        $id = isset( $current_post->ID ) ? (int) $current_post->ID : 0;
141
142        if ( ! $current_post instanceof \WP_Post || $id === 0 ) {
143            return new self( 0, '', $current_page_number );
144        }
145
146        $title = isset( $current_post->post_title ) ? html_entity_decode( $current_post->post_title, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) : __( '(no title)', 'jetpack-forms' );
147
148        return new self( $id, $title, $current_page_number );
149    }
150
151    /**
152     * Get the title of the current page. That we can then use to display in the feedback entry.
153     *
154     * @return string The title of the current page. That we want to show to the user. To tell them where the feedback was left.
155     */
156    private static function get_source_title() {
157        if ( is_front_page() ) {
158            return get_bloginfo( 'name' );
159        }
160        if ( is_home() ) {
161            return get_the_title( get_option( 'page_for_posts', true ) );
162        }
163        if ( is_singular() ) {
164            return get_the_title();
165        }
166        if ( is_archive() ) {
167            return get_the_archive_title();
168        }
169        if ( is_search() ) {
170            /* translators: %s is the search term */
171            return sprintf( __( 'Search results for: %s', 'jetpack-forms' ), get_search_query() );
172        }
173        if ( is_404() ) {
174            return __( '404 Not Found', 'jetpack-forms' );
175        }
176        return get_bloginfo( 'name' );
177    }
178
179    /**
180     * Creates a Feedback_Source instance for a block template.
181     *
182     * @param array $attributes Form Shortcode attributes.
183     *
184     * @return Feedback_Source Returns an instance of Feedback_Source.
185     */
186    public static function get_current( $attributes ) {
187        global $wp, $page;
188        $current_url = home_url( add_query_arg( array(), $wp->request ) );
189
190        // When a form is rendered inside the server-side preview
191        // (Form_Preview::maybe_render_preview), flag the source as a test
192        // submission. The flag travels with the signed JWT and is read back
193        // at submission time to branch the response into the test pipeline.
194        $is_test = Form_Preview::is_preview_mode();
195
196        // The widget context is resolved server-side in Contact_Form::parse() (it overwrites the
197        // widget attribute before the form is built), so the value seen here is trustworthy.
198        if ( isset( $attributes['widget'] ) && ! empty( $attributes['widget'] ) ) {
199            return new self( $attributes['widget'], self::get_source_title(), 1, 'widget', $current_url, $is_test );
200        }
201
202        // Block template part and block template sources are determined from render-scoped
203        // globals that are set only while the actual template part / template renders â€” never
204        // from a content attribute. A content attribute can be supplied by a post author (who
205        // need not hold edit_theme_options), so trusting it would let post-content forms
206        // masquerade as admin-authored template sources. See Util for the globals' lifecycle.
207        if ( ! empty( $GLOBALS['grunion_block_template_part_id'] ) ) {
208            return new self( $GLOBALS['grunion_block_template_part_id'], self::get_source_title(), $page, 'block_template_part', $current_url, $is_test );
209        }
210
211        if ( ! empty( $GLOBALS['grunion_block_template_id'] ) ) {
212            return new self( $GLOBALS['grunion_block_template_id'], self::get_source_title(), $page, 'block_template', $current_url, $is_test );
213        }
214
215        return new Feedback_Source( \get_the_ID(), \get_the_title(), $page, 'single', $current_url, $is_test );
216    }
217
218    /**
219     * Creates a Feedback_Source instance from serialized data.
220     *
221     * @param array $data The serialized data.
222     * @return Feedback_Source Returns an instance of Feedback_Source.
223     */
224    public static function from_serialized( $data ) {
225        $id          = $data['source_id'] ?? 0;
226        $title       = $data['entry_title'] ?? '';
227        $page_number = $data['entry_page'] ?? 1;
228        $source_type = $data['source_type'] ?? 'single';
229        $request_url = $data['request_url'] ?? '';
230        $is_test     = ! empty( $data['is_test'] );
231
232        return new self( $id, $title, $page_number, $source_type, $request_url, $is_test );
233    }
234
235    /**
236     * Get the permalink of the feedback entry.
237     *
238     * @return string The permalink of the feedback entry.
239     */
240    public function get_permalink() {
241        if ( $this->page_number > 1 && ! empty( $this->permalink ) ) {
242            return add_query_arg( 'page', $this->page_number, $this->permalink );
243        }
244        return wp_validate_redirect( $this->permalink, home_url() );
245    }
246
247    /**
248     * Get the edit URL of the form or page where the feedback was submitted from.
249     *
250     * @return string The edit URL of the form or page.
251     */
252    public function get_edit_form_url() {
253
254        if ( current_user_can( 'edit_theme_options' ) ) {
255            if ( $this->source_type === 'block_template' && \wp_is_block_theme() ) {
256                return admin_url( 'site-editor.php?p=' . esc_attr( '/wp_template/' . addslashes( $this->id ) ) . '&canvas=edit' );
257            }
258
259            if ( $this->source_type === 'block_template_part' && \wp_is_block_theme() ) {
260                return admin_url( 'site-editor.php?p=' . esc_attr( '/wp_template_part/' . addslashes( $this->id ) ) . '&canvas=edit' );
261            }
262
263            if ( $this->source_type === 'widget' && current_theme_supports( 'widgets' ) ) {
264                return admin_url( 'widgets.php' );
265            }
266        }
267
268        if ( $this->id && is_numeric( $this->id ) && $this->id > 0 && current_user_can( 'edit_post', (int) $this->id ) ) {
269            $entry_post = get_post( (int) $this->id );
270            if ( $entry_post && $entry_post->post_status === 'trash' ) {
271                return ''; // No edit link is possible for trashed posts. They need to be restored first.
272            }
273            return \get_edit_post_link( (int) $this->id, 'url' );
274        }
275
276        return '';
277    }
278
279    /**
280     * Get the relative permalink of the feedback entry.
281     *
282     * @return string The relative permalink of the feedback entry.
283     */
284    public function get_relative_permalink() {
285        if ( ! empty( $this->permalink ) ) {
286            return wp_make_link_relative( $this->get_permalink() );
287        }
288        return '';
289    }
290
291    /**
292     * Get the page number of the feedback entry.
293     *
294     * @return int The page number of the feedback entry.
295     */
296    public function get_page_number() {
297        return $this->page_number;
298    }
299    /**
300     * Get the title of the feedback entry.
301     *
302     * @return string The title of the feedback entry.
303     */
304    public function get_title() {
305        return $this->title;
306    }
307    /**
308     * Get the post id of the feedback entry.
309     *
310     * @return int|string The ID of the feedback entry.
311     */
312    public function get_id() {
313        return $this->id;
314    }
315
316    /**
317     * Get the source type of the feedback entry.
318     *
319     * Possible values: single, widget, block_template, block_template_part.
320     *
321     * @return string The source type.
322     */
323    public function get_source_type() {
324        return $this->source_type;
325    }
326
327    /**
328     * Whether this feedback was submitted from a form preview (test submission).
329     *
330     * @return bool
331     */
332    public function is_test() {
333        return $this->is_test;
334    }
335
336    /**
337     * Flag this feedback as a test submission coming from form preview.
338     *
339     * @param bool $is_test Whether the feedback is a test submission.
340     * @return void
341     */
342    public function set_is_test( $is_test ) {
343        $this->is_test = (bool) $is_test;
344    }
345
346    /**
347     * Get the page number of the entry title.
348     *
349     * @return array
350     */
351    public function serialize() {
352        $data = array(
353            'entry_title' => $this->title,
354            'entry_page'  => $this->page_number,
355            'source_id'   => $this->id,
356            'source_type' => $this->source_type,
357            'request_url' => $this->request_url,
358        );
359
360        if ( $this->is_test ) {
361            $data['is_test'] = true;
362        }
363
364        return $data;
365    }
366}