Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Settings_Form
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 4
110
0.00% covered (danger)
0.00%
0 / 1
 render_fields
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 posted_sections
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 attach
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * The one form every setting on Settings > Sharing saves through.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack\Sharing_Likes\Settings;
11
12/**
13 * One Save button for the whole screen.
14 *
15 * Sections cannot sit inside a single `<form>`: the services list nests the
16 * legacy forms its script submits over AJAX. So the form renders empty at the
17 * end of the screen, and each section's fields join it through the HTML `form`
18 * attribute instead, wherever they sit.
19 */
20final class Settings_Form {
21
22    /**
23     * `id` of the form element, which fields name in their `form` attribute.
24     */
25    public const ID = 'jetpack-sharing-settings';
26
27    /**
28     * Nonce action for the form.
29     *
30     * Deliberately not sharedaddy's `sharing-options`: `Services_Config::process_requests()`
31     * answers to that one, and on Simple so does a Likes save that turns Likes back
32     * on when its own fields are missing from the request.
33     */
34    public const NONCE_ACTION = 'jetpack-sharing-settings';
35
36    /**
37     * Field listing which sections put fields on the form, so a save leaves the rest alone.
38     */
39    public const SECTIONS_FIELD = 'jetpack_sharing_sections';
40
41    /**
42     * The services list's own settings: button style, label, and what hangs off them.
43     */
44    public const SECTION_SHARING = 'sharing';
45
46    /**
47     * The Like buttons settings.
48     */
49    public const SECTION_LIKES = 'likes';
50
51    /**
52     * Comment Likes alone, once a Simple site's post Likes moved to the block.
53     */
54    public const SECTION_COMMENT_LIKES = 'comment-likes';
55
56    /**
57     * Where the buttons appear.
58     */
59    public const SECTION_PLACEMENT = 'placement';
60
61    /**
62     * The rows that close the services table, whenever that table is hidden.
63     */
64    public const SECTION_EXTRAS = 'extras';
65
66    /**
67     * Sections that have put fields on the form during this render.
68     *
69     * @var string[]
70     */
71    private static $sections = array();
72
73    /**
74     * Print a section's fields, attached to the form.
75     *
76     * @param string $section One of the SECTION_* constants.
77     * @param string $markup  The fields, escaped by whoever rendered them.
78     */
79    public static function render_fields( string $section, string $markup ): void {
80        self::$sections[] = $section;
81
82        $markup .= sprintf(
83            '<input type="hidden" name="%1$s[]" value="%2$s" />',
84            esc_attr( self::SECTIONS_FIELD ),
85            esc_attr( $section )
86        );
87
88        echo self::attach( $markup ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped by whoever rendered it; attach() only adds an attribute.
89    }
90
91    /**
92     * Print the form and its Save button, if any section put fields on it.
93     */
94    public static function render(): void {
95        if ( array() === self::$sections ) {
96            return;
97        }
98
99        self::$sections = array();
100        ?>
101        <form method="post" action="" id="<?php echo esc_attr( self::ID ); ?>">
102            <p class="submit">
103                <input type="submit" name="submit" class="button-primary" value="<?php esc_attr_e( 'Save Changes', 'jetpack-sharing-likes' ); ?>" />
104                <?php
105                Post_Handler::render_action_field( 'save-settings' );
106                wp_nonce_field( self::NONCE_ACTION );
107                ?>
108            </p>
109        </form>
110        <?php
111    }
112
113    /**
114     * Sections the submitted form carried fields for. Callers verify the nonce.
115     *
116     * @return string[]
117     */
118    public static function posted_sections(): array {
119        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified by the caller.
120        if ( ! isset( $_POST[ self::SECTIONS_FIELD ] ) || ! is_array( $_POST[ self::SECTIONS_FIELD ] ) ) {
121            return array();
122        }
123
124        $known = array(
125            self::SECTION_SHARING,
126            self::SECTION_LIKES,
127            self::SECTION_COMMENT_LIKES,
128            self::SECTION_PLACEMENT,
129            self::SECTION_EXTRAS,
130        );
131
132        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- verified by the caller; checked against an allowlist.
133        $posted = array_filter( wp_unslash( $_POST[ self::SECTIONS_FIELD ] ), 'is_string' );
134
135        return array_values( array_intersect( $known, $posted ) );
136    }
137
138    /**
139     * Point every field in the markup at the form, leaving any that already name one.
140     *
141     * @param string $markup Field markup.
142     */
143    private static function attach( string $markup ): string {
144        $tags = new \WP_HTML_Tag_Processor( $markup );
145
146        while ( $tags->next_tag() ) {
147            if ( in_array( $tags->get_tag(), array( 'INPUT', 'SELECT', 'TEXTAREA' ), true ) && null === $tags->get_attribute( 'form' ) ) {
148                $tags->set_attribute( 'form', self::ID );
149            }
150        }
151
152        return $tags->get_updated_html();
153    }
154}