PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-forms / src / service / class-post-to-url.php

class-post-to-url.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3, at jetpack_vendor/automattic/jetpack-forms/src/service/class-post-to-url.php

197 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post to URL using Jetpack Contact Forms.
4 *
5 * @package automattic/jetpack
6 */
7
8 namespace Automattic\Jetpack\Forms\Service;
9
10 use Automattic\Jetpack\Forms\ContactForm\Feedback;
11 use WP_Error;
12
13 /**
14 * Class Post_To_Url
15 *
16 * Hooks on Jetpack's Contact form to post form data to some URL.
17 */
18 class Post_To_Url {
19 /**
20 * Singleton instance
21 *
22 * @var Post_To_Url
23 */
24 private static $instance = null;
25
26 /**
27 * Initialize and return singleton instance.
28 *
29 * @return Post_To_Url
30 */
31 public static function init() {
32 if ( null === self::$instance ) {
33 self::$instance = new self();
34 }
35
36 return self::$instance;
37 }
38
39 /**
40 * Post_To_Url class constructor.
41 * Hooks on `grunion_after_feedback_post_inserted` action to send form data to specified URL.
42 * NOTE: As a singleton, this constructor is private and only callable from ::init, which will return the singleton instance,
43 * effectively preventing multiple instances of this class (hence, multiple hooks triggering the POST request).
44 */
45 private function __construct() {
46 add_action( 'grunion_after_feedback_post_inserted', array( $this, 'feedback_post_hook' ), 10, 4 );
47 }
48
49 /**
50 * Get the setup for the post to URL.
51 *
52 * Salesforce-only: posts to the fixed Salesforce Web-to-Lead endpoint when
53 * the form has a salesforceData.organizationId attribute. The legacy
54 * postToUrl override is intentionally NOT honored here — postToUrl is
55 * deprecated and the new pipeline (Form_Webhooks) already handles it with
56 * proper URL validation. Honoring it here too would let an Editor with
57 * Salesforce enabled override the destination to an arbitrary URL,
58 * including internal/cloud-metadata endpoints (SSRF).
59 *
60 * @param array $attributes - the attributes of the contact form.
61 * @return array|bool Array setup, or false if Salesforce isn't configured.
62 */
63 private function get_setup( $attributes = array() ) {
64 if ( empty( $attributes['salesforceData']['organizationId'] ) ) {
65 return false;
66 }
67
68 return array(
69 'url' => 'https://webto.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8',
70 'format' => 'urlencoded',
71 );
72 }
73
74 /**
75 * Hook on `grunion_after_feedback_post_inserted` action to send form data to specified URL.
76 *
77 * @param int $post_id - the post_id for the CPT that is created.
78 * @param array $fields - a collection of Automattic\Jetpack\Forms\ContactForm\Contact_Form_Field instances.
79 * @param bool $is_spam - marked as spam by Akismet(?).
80 * @param array $entry_values - extra fields added to from the contact form.
81 *
82 * @return null|void
83 */
84 public function feedback_post_hook( $post_id, $fields, $is_spam, $entry_values ) {
85 // Try and get the form from any of the fields
86 $form = null;
87 foreach ( $fields as $field ) {
88 if ( ! empty( $field->form ) ) {
89 $form = $field->form;
90 break;
91 }
92 }
93 if ( ! $form || ! is_a( $form, 'Automattic\Jetpack\Forms\ContactForm\Contact_Form' ) ) {
94 return;
95 }
96
97 // if spam (hinted by akismet?), don't process
98 if ( $is_spam ) {
99 return;
100 }
101
102 $setup = $this->get_setup( $form->attributes );
103
104 if ( ! $setup ) {
105 return;
106 }
107
108 $form_data = $this->get_form_data( $form, $fields, $entry_values );
109
110 $result = $this->post_to_url( $form_data, $setup );
111
112 if ( is_wp_error( $result ) ) {
113 // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- figuring out what to do with the error.
114 $message = sprintf(
115 'JETPACK %s - Jetpack Forms: POSTing to URL failed: "%s" at %s',
116 constant( 'JETPACK__VERSION' ),
117 $result->get_error_message(),
118 $entry_values['entry_permalink']
119 );
120 // TODO: not sure what to do with the error. Is not useful at frontend and it would be difficult to
121 // solve for a non tech-savvy user. We should log it somewhere, but it could turn messy.
122 // Maybe email the owner?
123 }
124 }
125
126 /**
127 * POST to URL
128 *
129 * @param array $data The data key/value pairs to send in POST.
130 * @param array $options Options for POST.
131 *
132 * @return array|WP_Error The result value from wp_safe_remote_post
133 *
134 * TODO: do complex fields (MC, etc) need to be handled differently? JSON should be fine, but URLencoded might need to be serialized.
135 */
136 private function post_to_url( $data, $options = array() ) {
137 global $wp_version;
138
139 $user_agent = "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' ) . '; ' . get_bloginfo( 'url' );
140 $format = $options['format'] === 'urlencoded' ? 'application/x-www-form-urlencoded' : 'application/json';
141 $args = array(
142 'body' => $data,
143 'headers' => array(
144 'Content-Type' => $format,
145 'user-agent' => $user_agent,
146 ),
147 );
148 return wp_safe_remote_post( $options['url'], $args );
149 }
150
151 /**
152 * Gather fields key/value pairs from the form
153 * Sanitizes the hidden fields values
154 *
155 * @param \Automattic\Jetpack\Forms\ContactForm\Contact_Form $form The form instance being processed/submitted.
156 * @param array $visible_fields Visible submitted fields.
157 * @param array $entry_values The feedback entry values.
158 */
159 private function get_form_data( $form, $visible_fields, $entry_values ) {
160 $fields = array();
161 foreach ( $visible_fields as $field ) {
162 $fields[ $field->get_attribute( 'id' ) ] = Feedback::encode_special_chars( $field->value );
163 }
164
165 // Right in the middle, backwards compatibility for salesforceData implementation.
166 $salesforce_data = (array) ( $form->attributes['salesforceData'] ?? array() );
167 if ( ! empty( $salesforce_data['organizationId'] ) ) {
168 $fields['oid'] = sanitize_text_field( $salesforce_data['organizationId'] );
169 $fields['lead_source'] = $entry_values['entry_permalink'];
170 }
171
172 // `hiddenFields` is a legacy attribute that may appear in a few shapes on forms
173 // in the wild: an array of `{ name, value }` objects (its original design), an
174 // associative `name => value` map, or a JSON-encoded string. Iterating it blindly
175 // and accessing `['name']`/`['value']` on a non-array element fatals on PHP 8 with
176 // "Cannot access offset of type string on string", so normalize defensively.
177 $hidden_fields = $form->attributes['hiddenFields'] ?? array();
178 if ( is_string( $hidden_fields ) ) {
179 $decoded = json_decode( $hidden_fields, true );
180 $hidden_fields = is_array( $decoded ) ? $decoded : array();
181 }
182 foreach ( (array) $hidden_fields as $key => $hidden_field ) {
183 if ( is_array( $hidden_field ) ) {
184 // Original `{ name, value }` object shape.
185 if ( isset( $hidden_field['name'] ) ) {
186 $fields[ $hidden_field['name'] ] = sanitize_text_field( $hidden_field['value'] ?? '' );
187 }
188 } elseif ( ! is_int( $key ) ) {
189 // Associative `name => value` shape.
190 $fields[ $key ] = sanitize_text_field( (string) $hidden_field );
191 }
192 }
193
194 return $fields;
195 }
196 }
197