PluginProbe
Friends / 4.3.2
Friends v4.3.2
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
← All changes | includes/class-rest.php +455 -271 2.7.64.3.2 View file →
@@ -40,13 +40,25 @@
40 40 * Register the WordPress hooks
41 41 */
42 42 private function register_hooks() {
43 43 add_action( 'rest_api_init', array( $this, 'add_rest_routes' ) );
44 - add_action( 'wp_trash_post', array( $this, 'notify_remote_friend_post_deleted' ) );
45 - add_action( 'before_delete_post', array( $this, 'notify_remote_friend_post_deleted' ) );
46 - add_action( 'set_user_role', array( $this, 'notify_remote_friend_request_accepted' ), 20, 3 );
44 + add_action( 'rest_pre_serve_request', array( $this, 'send_rest_origin' ), 20, 3 );
47 45 }
48 46
47 + public function send_rest_origin( $ret, $response, $request ) {
48 + if ( strpos( $request->get_route(), '/' . self::PREFIX . '/extension' ) !== 0 ) {
49 + return $ret;
50 + }
51 +
52 + if ( $request->get_header( 'origin' ) ) {
53 + $scheme = wp_parse_url( $request->get_header( 'origin' ), PHP_URL_SCHEME );
54 + if ( 'moz-extension' === $scheme ) {
55 + header( 'access-control-allow-origin: ' . $request->get_header( 'origin' ) );
56 + }
57 + }
58 + return $ret;
59 + }
60 +
49 61 /**
50 62 * Add the REST API to send and receive friend requests
51 63 */
52 64 public function add_rest_routes() {
@@ -51,288 +63,532 @@
51 63 */
52 64 public function add_rest_routes() {
53 65 register_rest_route(
54 66 self::PREFIX,
55 - 'friend-request',
67 + 'embed',
56 68 array(
69 + 'methods' => 'GET',
70 + 'callback' => array( $this, 'rest_embed_friend_post' ),
71 + 'permission_callback' => function () {
72 + return current_user_can( Friends::REQUIRED_ROLE );
73 + },
74 + )
75 + );
76 +
77 + register_rest_route(
78 + self::PREFIX,
79 + 'get-feeds',
80 + array(
81 + 'methods' => 'GET',
82 + 'callback' => array( $this, 'rest_get_feeds' ),
83 + 'permission_callback' => function () {
84 + return current_user_can( Friends::REQUIRED_ROLE );
85 + },
86 + )
87 + );
88 +
89 + register_rest_route(
90 + self::PREFIX,
91 + 'refresh-feed',
92 + array(
57 93 'methods' => 'POST',
58 - 'callback' => array( $this, 'rest_friend_request' ),
59 - 'permission_callback' => '__return_true',
94 + 'callback' => array( $this, 'rest_refresh_feed' ),
95 + 'params' => array(
96 + 'id' => array(
97 + 'type' => 'integer',
98 + 'required' => true,
99 + ),
100 + ),
101 + 'permission_callback' => function () {
102 + return current_user_can( Friends::REQUIRED_ROLE );
103 + },
60 104 )
61 105 );
106 +
62 107 register_rest_route(
63 108 self::PREFIX,
64 - 'accept-friend-request',
109 + 'link-preview',
65 110 array(
66 111 'methods' => 'POST',
67 - 'callback' => array( $this, 'rest_accept_friend_request' ),
68 - 'permission_callback' => '__return_true',
112 + 'callback' => array( $this, 'rest_link_preview' ),
113 + 'permission_callback' => function () {
114 + return current_user_can( Friends::REQUIRED_ROLE );
115 + },
116 + 'args' => array(
117 + 'id' => array(
118 + 'type' => 'integer',
119 + 'required' => true,
120 + ),
121 + ),
69 122 )
70 123 );
124 +
71 125 register_rest_route(
72 126 self::PREFIX,
73 - 'post-deleted',
127 + 'extension',
74 128 array(
75 - 'methods' => 'POST',
76 - 'callback' => array( $this, 'rest_friend_post_deleted' ),
77 - 'permission_callback' => '__return_true',
129 + 'methods' => array( 'GET', 'POST' ),
130 + 'callback' => array( $this, 'rest_extension' ),
131 + 'permission_callback' => '__return_true', // Public.
132 + 'params' => array(
133 + 'key' => array(
134 + 'type' => 'string',
135 + 'required' => false,
136 + ),
137 + ),
78 138 )
79 139 );
140 +
80 141 register_rest_route(
81 142 self::PREFIX,
82 - 'embed',
143 + 'extension/action',
83 144 array(
84 - 'methods' => 'GET',
85 - 'callback' => array( $this, 'rest_embed_friend_post' ),
86 - 'permission_callback' => '__return_true',
145 + 'methods' => 'POST',
146 + 'callback' => array( $this, 'rest_extension_action' ),
147 + 'permission_callback' => array( $this, 'browser_extension_action_permission_callback' ),
148 + 'args' => array(
149 + 'action' => array(
150 + 'type' => 'string',
151 + 'required' => true,
152 + ),
153 + 'key' => array(
154 + 'type' => 'string',
155 + 'required' => true,
156 + ),
157 + ),
87 158 )
88 159 );
89 160 }
90 161
91 162 /**
92 - * Receive a notification via REST that a friend request was accepted
163 + * Translate a REST error message
93 164 *
94 - * @param \WP_REST_Request $request The incoming request.
95 - * @return array The array to be returned via the REST API.
165 + * @param string $message The message to translate.
166 + * @return string The translated message.
96 167 */
97 - public function rest_accept_friend_request( \WP_REST_Request $request ) {
98 - $request_id = $request->get_param( 'request' );
99 - $friend_user_id = get_option( 'friends_request_' . sha1( $request_id ) );
100 - $friend_user = false;
101 - if ( $friend_user_id ) {
102 - $friend_user = new User( $friend_user_id );
168 + public static function translate_error_message( $message ) {
169 + $messages = self::get_error_messages( true );
170 + if ( isset( $messages[ $message ] ) ) {
171 + return $messages[ $message ];
103 172 }
173 + return $message;
174 + }
104 175
105 - if ( ! $request_id || ! $friend_user || is_wp_error( $friend_user ) || ! $friend_user->user_url ) {
106 - return new \WP_Error(
107 - 'friends_invalid_parameters',
108 - 'Not all necessary parameters were provided.',
109 - array(
110 - 'status' => 403,
111 - )
112 - );
176 + /**
177 + * Get the error messages for REST
178 + *
179 + * @return array The error messages.
180 + */
181 + public static function get_error_messages() {
182 + $english = function () {
183 + return 'en_US';
184 + };
185 +
186 + // In the first pass never translate these messages.
187 + add_filter( 'locale', $english );
188 +
189 + $messages = array(
190 + 'friends_invalid_parameters' => __( 'Not all necessary parameters were provided.', 'friends' ),
191 + 'friends_invalid_url' => __( 'An invalid URL was provided.', 'friends' ),
192 + 'friends_no_request' => __( 'No request was found.', 'friends' ),
193 + 'friends_invalid_site' => __( 'An invalid site was provided.', 'friends' ),
194 + 'unknown' => __( 'An unknown error occurred.', 'friends' ),
195 + );
196 +
197 + remove_filter( 'locale', $english );
198 +
199 + // Add mapping for English text to translations.
200 + foreach ( $messages as $key => $message ) {
201 + $messages[ $message ] = __( $message, 'friends' ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
113 202 }
114 203
115 - $future_in_token = get_user_option( 'friends_future_in_token_' . sha1( $request_id ), $friend_user_id );
116 - $proof = $request->get_param( 'proof' );
117 - if ( ! $proof || sha1( $future_in_token . $request_id ) !== $proof ) {
118 - return new \WP_Error(
119 - 'friends_invalid_proof',
120 - 'An invalid proof was provided.',
121 - array(
122 - 'status' => 403,
123 - )
124 - );
204 + return $messages;
205 + }
206 +
207 + /**
208 + * Standardize the error message texts
209 + *
210 + * @param string $code The error code.
211 + * @param string $message The message to return, if not provided the default message will be used.
212 + * @param int $status The status code to return.
213 + *
214 + * @return \WP_Error The error object.
215 + */
216 + public static function error( $code, $message = '', $status = 403 ) {
217 + if ( ! $message ) {
218 + // Return English error messages.
219 + $messages = self::get_error_messages();
220 + if ( isset( $messages[ $code ] ) ) {
221 + $message = $messages[ $code ];
222 + } else {
223 + $message = $messages['unknown'];
224 + }
125 225 }
126 226
127 - $friend_user_login = User::get_user_login_for_url( $friend_user->user_url );
128 - if ( ! $friend_user->has_cap( 'pending_friend_request' ) ) {
129 - return new \WP_Error(
130 - 'friends_offer_no_longer_valid',
131 - 'The friendship offer is no longer valid.',
132 - array(
133 - 'status' => 403,
134 - )
135 - );
227 + return new \WP_Error(
228 + $code,
229 + $message,
230 + array(
231 + 'status' => $status,
232 + )
233 + );
234 + }
235 +
236 + public function rest_embed_friend_post( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
237 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
238 + if ( empty( $_GET['url'] ) ) {
239 + return false;
136 240 }
241 + $post_id = $this->friends->feed->url_to_postid( sanitize_text_field( wp_unslash( $_GET['url'] ) ) );
242 + if ( empty( $post_id ) ) {
243 + return false;
244 + }
245 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
137 246
138 - $future_out_token = $request->get_param( 'key' );
139 - if ( ! is_string( $future_out_token ) || empty( $future_out_token ) ) {
140 - return new \WP_Error(
141 - 'friends_invalid_key',
142 - 'The key must be a non-empty string.',
143 - array(
144 - 'status' => 403,
145 - )
146 - );
247 + if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ) ) ) {
248 + return false;
147 249 }
148 - $friend_user->make_friend( $future_out_token, $future_in_token );
149 250
150 - $friend_user->update_user_icon_url( $request->get_param( 'icon_url' ) );
151 - if ( $request->get_param( 'name' ) ) {
152 - wp_update_user(
153 - array(
154 - 'ID' => $friend_user->ID,
155 - 'nickname' => $request->get_param( 'name' ),
156 - 'first_name' => $request->get_param( 'name' ),
157 - 'display_name' => $request->get_param( 'name' ),
158 - )
159 - );
251 + enqueue_embed_scripts();
252 + $post = get_post( $post_id );
253 + $args = compact( 'post' );
254 + setup_postdata( $post );
255 +
256 + header( 'Content-type: text/html' );
257 + Friends::template_loader()->get_template_part( 'embed/header-embed', null, $args );
258 + Friends::template_loader()->get_template_part( 'embed/embed-content', null, $args );
259 + exit;
260 + }
261 +
262 + public function rest_get_feeds( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
263 + $feeds = User_Feed::get_all_due( true );
264 + $feeds = array_map(
265 + function ( $feed ) {
266 + return array(
267 + 'id' => $feed->get_id(),
268 + 'url' => $feed->get_url(),
269 + 'parser' => $feed->get_parser(),
270 + 'last_log' => $feed->get_last_log(),
271 + 'next_poll' => $feed->get_next_poll(),
272 + );
273 + },
274 + $feeds
275 + );
276 +
277 + return $feeds;
278 + }
279 +
280 + public function rest_refresh_feed( $request ) {
281 + $feed_id = $request->get_param( 'id' );
282 + $feed = new User_Feed( get_term( intval( $feed_id ) ) );
283 + add_filter( 'notify_about_new_friend_post', '__return_false', 999 );
284 + add_action(
285 + 'wp_feed_options',
286 + function ( &$feed ) {
287 + $feed->enable_cache( false );
288 + }
289 + );
290 + $new_posts = array();
291 +
292 + $friend_user = $feed->get_friend_user();
293 + $was_polled = false;
294 + if ( $friend_user && $feed->can_be_polled_now() ) {
295 + $feed->set_polling_now();
296 + $new_posts = $this->friends->feed->retrieve_feed( $feed );
297 + $feed->was_polled();
298 + if ( is_wp_error( $new_posts ) ) {
299 + return $new_posts;
300 + }
301 + $was_polled = true;
302 + $friend_user->delete_outdated_posts();
160 303 }
161 304
162 - delete_user_option( $friend_user_id, 'friends_future_in_token_' . sha1( $request_id ) );
163 -
164 - do_action( 'notify_accepted_friend_request', $friend_user );
165 305 return array(
166 - 'signature' => sha1( $future_out_token . $future_in_token ),
306 + 'new_posts' => count( $new_posts ),
307 + 'url' => $feed->get_url(),
308 + 'was_polled' => $was_polled,
167 309 );
168 310 }
169 311
170 312 /**
171 - * Receive a friend request via REST
313 + * Fetch a link preview on demand.
172 314 *
173 - * @param \WP_REST_Request $request The incoming request.
174 - * @return array The array to be returned via the REST API.
315 + * @param \WP_REST_Request $request The REST request.
316 + * @return array|\WP_Error
175 317 */
176 - public function rest_friend_request( \WP_REST_Request $request ) {
177 - $version = $request->get_param( 'version' );
178 - if ( 2 !== intval( $version ) ) {
179 - return new \WP_Error(
180 - 'friends_unsupported_protocol_version',
181 - 'Incompatible Friends protocol version.',
182 - array(
183 - 'status' => 403,
184 - )
185 - );
318 + public function rest_link_preview( $request ) {
319 + $post_id = intval( $request->get_param( 'id' ) );
320 + $post = get_post( $post_id );
321 +
322 + if ( ! $post || ! in_array( $post->post_type, apply_filters( 'friends_frontend_post_types', array() ), true ) ) {
323 + return new \WP_Error( 'friends_link_preview_invalid_post', __( 'The requested post is not available.', 'friends' ), array( 'status' => 404 ) );
186 324 }
187 325
188 - $codeword = $request->get_param( 'codeword' );
189 - if ( get_option( 'friends_require_codeword' ) && get_option( 'friends_codeword', 'friends' ) !== $codeword ) {
190 - return new \WP_Error(
191 - 'friends_invalid_codeword',
192 - get_option( 'friends_wrong_codeword_message', 'An invalid codeword was provided.' ),
193 - array(
194 - 'status' => 403,
195 - )
196 - );
326 + $preview = Link_Preview::update_link_preview( $post_id );
327 +
328 + return array(
329 + 'preview' => $preview ? $preview : false,
330 + );
331 + }
332 +
333 + public function rest_extension( $request ) {
334 + $return = array(
335 + 'version' => Friends::VERSION,
336 + 'friends_url' => home_url( '/friends/' ),
337 + 'settings_url' => admin_url( 'admin.php?page=friends-browser-extension' ),
338 + );
339 +
340 + if ( 'POST' === $request->get_method() && $request->get_param( 'key' ) ) {
341 + $current_user = self::get_browser_extension_user( $request->get_param( 'key' ) );
342 + if ( ! is_wp_error( $current_user ) ) {
343 + $context = $this->get_browser_extension_request_context( $request, $current_user );
344 +
345 + /**
346 + * Allows plugins to register actions for the Friends browser extension.
347 + *
348 + * Each action is an associative array with:
349 + * - `id` (string, optional) — stable action identifier for clients that persist action state.
350 + * - `name` (string, required) — label shown in the extension popup.
351 + * - `url` (string, required) — target URL; may contain `{current_url}` which the extension substitutes with the current page URL (URL-encoded).
352 + * - `method` (string, optional) — if `"POST"`, the extension submits a form instead of opening a link.
353 + * - `fields` (object, optional) — for POST actions, key/value pairs of form fields; values may contain `{current_url}` (raw) and `{page_html}` placeholders.
354 + * - `run` (string, optional) — if `"inline"`, the extension handles the response in place instead of opening a new tab.
355 + * - `inputs` (array, optional) — user-editable fields for inline actions.
356 + * - `submit_label` (string, optional) — label for the inline action submit button.
357 + * - `category` (string, optional) — groups actions under a named header; actions without a category appear under the default "Actions" header.
358 + *
359 + * Inline action responses may include `message`, `edit_url`, and `link_label`. They may also
360 + * include `fields`, `values`, and `submit_label` to let the browser extension update the same
361 + * inline form for follow-up edits after the first action has created server-side state.
362 + *
363 + * Example:
364 + * ```php
365 + * add_filter( 'friends_browser_extension_actions', function ( $actions, $current_user, $context ) {
366 + * $actions[] = array(
367 + * 'name' => 'Save to Collection',
368 + * 'url' => home_url( '/collect/?url={current_url}' ),
369 + * );
370 + * return $actions;
371 + * }, 10, 3 );
372 + * ```
373 + *
374 + * @param array $actions The array of actions.
375 + * @param \WP_User $current_user The current user.
376 + * @param array $context Browser extension request context: key, version, user, and request.
377 + * @return array The modified array of actions.
378 + */
379 + $previous_user_id = get_current_user_id();
380 + wp_set_current_user( $current_user->ID );
381 + try {
382 + $actions = apply_filters( 'friends_browser_extension_actions', array(), $current_user, $context );
383 + } finally {
384 + wp_set_current_user( $previous_user_id );
385 + }
386 +
387 + $return['actions'] = array_values(
388 + array_filter(
389 + $actions,
390 + function ( $action ) {
391 + return is_array( $action )
392 + && ! empty( $action['name'] )
393 + && is_string( $action['name'] )
394 + && ! empty( $action['url'] )
395 + && is_string( $action['url'] );
396 + }
397 + )
398 + );
399 + } else {
400 + $return['error'] = 'Invalid API key';
401 + }
197 402 }
198 403
199 - $url = trim( $request->get_param( 'url' ) );
200 - if ( ! is_string( $url ) || ! Friends::check_url( $url ) || 0 === strcasecmp( home_url(), $url ) ) {
201 - return new \WP_Error(
202 - 'friends_invalid_site',
203 - 'An invalid site was provided.',
204 - array(
205 - 'status' => 403,
206 - )
207 - );
208 - }
404 + return $return;
405 + }
209 406
210 - $future_out_token = $request->get_param( 'key' );
211 - if ( ! is_string( $future_out_token ) || empty( $future_out_token ) ) {
407 + /**
408 + * Validate a browser extension inline action request.
409 + *
410 + * @param \WP_REST_Request $request The REST request.
411 + * @return true|\WP_Error True if the request is allowed, otherwise an error.
412 + */
413 + public function browser_extension_action_permission_callback( $request ) {
414 + $current_user = self::get_browser_extension_user( $request->get_param( 'key' ) );
415 + if ( is_wp_error( $current_user ) ) {
212 416 return new \WP_Error(
213 - 'friends_invalid_key',
214 - 'The key must be a non-empty string.',
215 - array(
216 - 'status' => 403,
217 - )
417 + $current_user->get_error_code(),
418 + $current_user->get_error_message(),
419 + array( 'status' => 401 )
218 420 );
219 421 }
220 - $user_login = User::get_user_login_for_url( $url );
221 - $friend_user = User::create( $user_login, 'friend_request', $url, $request->get_param( 'name' ), $request->get_param( 'icon_url' ) );
222 - if ( $friend_user->has_cap( 'friend' ) ) {
223 - if ( get_user_option( 'friends_out_token', $friend_user->ID ) && ! get_user_option( 'friends_out_token', $friend_user->ID ) ) {
224 - // TODO: trigger an accept friend request right away?
225 - }
226 - $friend_user->set_role( 'friend_request' );
227 - }
228 - $friend_user->update_user_icon_url( $request->get_param( 'icon_url' ) );
229 - $friend_user->update_user_option( 'friends_future_out_token', $request->get_param( 'key' ) );
230 - $message = $request->get_param( 'message' );
231 - if ( is_string( $message ) ) {
232 - $friend_user->update_user_option( 'friends_request_message', mb_substr( $message, 0, 2000 ) );
233 - }
234 422
235 - $request_id = wp_generate_password( 128, false );
236 - $friend_user->update_user_option( 'friends_request_id', $request_id );
423 + $attributes = $request->get_attributes();
424 + $attributes['friends_browser_extension_user'] = $current_user;
425 + $attributes['friends_browser_extension_context'] = $this->get_browser_extension_request_context( $request, $current_user );
426 + $request->set_attributes( $attributes );
237 427
238 - return array(
239 - 'request' => $request_id,
240 - );
428 + return true;
241 429 }
242 430
243 431 /**
244 - * Notify friends of a deleted post
432 + * Handle a browser extension inline action.
245 433 *
246 - * @param int $post_id The post id of the post that is deleted.
434 + * @param \WP_REST_Request $request The REST request.
435 + * @return \WP_REST_Response The REST response.
247 436 */
248 - public function notify_remote_friend_post_deleted( $post_id ) {
249 - $post = \WP_Post::get_instance( $post_id );
250 - if ( 'post' !== $post->post_type ) {
251 - return;
437 + public function rest_extension_action( $request ) {
438 + $url_params = $request->get_url_params();
439 + $action = isset( $url_params['action'] ) ? $url_params['action'] : $request->get_param( 'action' );
440 + $action = sanitize_key( (string) wp_unslash( $action ) );
441 + if ( ! $action ) {
442 + return self::browser_extension_action_error(
443 + new \WP_Error( 'friends_missing_browser_extension_action', __( 'No browser extension action was provided.', 'friends' ) ),
444 + 400
445 + );
252 446 }
253 447
254 - $friends = User_Query::all_friends();
255 - $friends = $friends->get_results();
448 + $attributes = $request->get_attributes();
449 + $current_user = isset( $attributes['friends_browser_extension_user'] ) ? $attributes['friends_browser_extension_user'] : self::get_browser_extension_user( $request->get_param( 'key' ) );
450 + if ( is_wp_error( $current_user ) ) {
451 + return self::browser_extension_action_error( $current_user, 401 );
452 + }
256 453
257 - foreach ( $friends as $friend_user ) {
258 - $friend_rest_url = $friend_user->get_rest_url();
454 + $context = isset( $attributes['friends_browser_extension_context'] ) ? $attributes['friends_browser_extension_context'] : $this->get_browser_extension_request_context( $request, $current_user );
455 + $context['action'] = $action;
259 456
260 - $response = wp_safe_remote_post(
261 - $friend_rest_url . '/post-deleted',
262 - array(
263 - 'body' => array(
264 - 'post_id' => $post_id,
265 - 'auth' => $friend_user->get_friend_auth(),
266 - ),
267 - 'timeout' => 20,
268 - 'redirection' => 5,
269 - )
457 + $previous_user_id = get_current_user_id();
458 + wp_set_current_user( $current_user->ID );
459 +
460 + try {
461 + /**
462 + * Handles a browser extension inline action.
463 + *
464 + * Return a \WP_REST_Response, \WP_Error, array, or scalar value. Returning null means the action
465 + * was not handled.
466 + *
467 + * @param mixed $response The action response.
468 + * @param string $action The browser extension action name.
469 + * @param \WP_REST_Request $request The REST request.
470 + * @param \WP_User $current_user The user authenticated by the browser extension key.
471 + * @param array $context Browser extension request context.
472 + */
473 + $response = apply_filters( 'friends_browser_extension_action', null, $action, $request, $current_user, $context );
474 +
475 + /**
476 + * Handles a specific browser extension inline action.
477 + *
478 + * The dynamic portion of the hook name, `$action`, is the sanitized action name from the
479 + * request's `action` parameter.
480 + *
481 + * @param mixed $response The action response.
482 + * @param \WP_REST_Request $request The REST request.
483 + * @param \WP_User $current_user The user authenticated by the browser extension key.
484 + * @param array $context Browser extension request context.
485 + */
486 + $response = apply_filters( "friends_browser_extension_action_{$action}", $response, $request, $current_user, $context );
487 + } finally {
488 + wp_set_current_user( $previous_user_id );
489 + }
490 +
491 + if ( null === $response ) {
492 + return self::browser_extension_action_error(
493 + new \WP_Error( 'friends_unknown_browser_extension_action', __( 'Unknown browser extension action.', 'friends' ) ),
494 + 404
270 495 );
271 496 }
497 +
498 + return self::prepare_browser_extension_action_response( $response );
272 499 }
273 500
274 501 /**
275 - * Receive a REST message that a post was deleted.
502 + * Get the user authenticated by a browser extension key.
276 503 *
277 - * @param \WP_REST_Request $request The incoming request.
278 - * @return array The array to be returned via the REST API.
504 + * @param string $key The browser extension API key.
505 + * @return \WP_User|\WP_Error The authenticated user or an error.
279 506 */
280 - public function rest_friend_post_deleted( $request ) {
281 - $tokens = explode( '-', $request->get_param( 'auth' ) );
282 - $user_id = $this->friends->access_control->verify_token( $tokens[0], isset( $tokens[1] ) ? $tokens[1] : null, isset( $tokens[2] ) ? $tokens[2] : null );
283 - if ( ! $user_id ) {
284 - return new \WP_Error(
285 - 'friends_request_failed',
286 - __( 'Could not respond to the request.', 'friends' ),
287 - array(
288 - 'status' => 403,
289 - )
290 - );
507 + private static function get_browser_extension_user( $key ) {
508 + $key = sanitize_text_field( (string) wp_unslash( $key ) );
509 + if ( ! $key ) {
510 + return new \WP_Error( 'friends_invalid_browser_extension_key', __( 'Invalid API key', 'friends' ) );
291 511 }
292 - $friend_user = new User( $user_id );
293 - $remote_post_id = $request->get_param( 'post_id' );
294 - $remote_post_ids = $friend_user->get_remote_post_ids();
295 512
296 - if ( ! isset( $remote_post_ids[ $remote_post_id ] ) ) {
297 - return array(
298 - 'deleted' => false,
299 - );
513 + $user = Admin::get_browser_api_key_user( $key );
514 + if ( ! $user ) {
515 + return new \WP_Error( 'friends_invalid_browser_extension_key', __( 'Invalid API key', 'friends' ) );
300 516 }
301 517
302 - $post_id = $remote_post_ids[ $remote_post_id ];
303 - $post = \WP_Post::get_instance( $post_id );
304 - if ( Friends::CPT === $post->post_type ) {
305 - wp_delete_post( $post_id );
518 + return $user;
519 + }
520 +
521 + /**
522 + * Build browser extension request context for plugin filters.
523 + *
524 + * @param \WP_REST_Request $request The REST request.
525 + * @param \WP_User $current_user The user authenticated by the browser extension key.
526 + * @return array Browser extension request context.
527 + */
528 + private function get_browser_extension_request_context( $request, $current_user ) {
529 + $key = sanitize_text_field( (string) wp_unslash( $request->get_param( 'key' ) ) );
530 + $version = sanitize_text_field( (string) wp_unslash( $request->get_param( 'version' ) ) );
531 +
532 + if ( ! $version ) {
533 + $version = sanitize_text_field( (string) wp_unslash( $request->get_param( 'extension_version' ) ) );
306 534 }
307 535
308 536 return array(
309 - 'deleted' => true,
537 + 'key' => $key,
538 + 'browser_extension_key' => $key,
539 + 'version' => $version,
540 + 'extension_version' => $version,
541 + 'user' => $current_user,
542 + 'request' => $request,
310 543 );
311 544 }
312 545
313 - public function rest_embed_friend_post( $request ) {
314 - if ( empty( $_GET['url'] ) ) {
315 - return false;
546 + /**
547 + * Prepare a browser extension action response.
548 + *
549 + * @param mixed $response The handler response.
550 + * @return \WP_REST_Response The REST response.
551 + */
552 + private static function prepare_browser_extension_action_response( $response ) {
553 + if ( is_wp_error( $response ) ) {
554 + return self::browser_extension_action_error( $response, 400 );
316 555 }
317 - $post_id = $this->friends->feed->url_to_postid( $_GET['url'] );
318 - if ( empty( $post_id ) ) {
319 - return false;
556 +
557 + if ( $response instanceof \WP_REST_Response ) {
558 + return $response;
320 559 }
321 560
322 - if ( ! in_array( get_post_type( $post_id ), apply_filters( 'friends_frontend_post_types', array() ) ) ) {
323 - return false;
561 + if ( true === $response ) {
562 + $response = array(
563 + 'success' => true,
564 + );
324 565 }
325 566
326 - enqueue_embed_scripts();
327 - $post = get_post( $post_id );
328 - $args = compact( 'post' );
329 - setup_postdata( $post );
567 + return rest_ensure_response( $response );
568 + }
330 569
331 - header( 'Content-type: text/html' );
332 - Friends::template_loader()->get_template_part( 'embed/header-embed', null, $args );
333 - Friends::template_loader()->get_template_part( 'embed/embed-content', null, $args );
334 - exit;
570 + /**
571 + * Format a browser extension action error response.
572 + *
573 + * @param \WP_Error $error The error.
574 + * @param int $status The default HTTP status.
575 + * @return \WP_REST_Response The REST response.
576 + */
577 + private static function browser_extension_action_error( \WP_Error $error, $status ) {
578 + $error_data = $error->get_error_data();
579 + if ( is_array( $error_data ) && ! empty( $error_data['status'] ) ) {
580 + $status = absint( $error_data['status'] );
581 + }
582 +
583 + return new \WP_REST_Response(
584 + array(
585 + 'success' => false,
586 + 'code' => $error->get_error_code(),
587 + 'message' => $error->get_error_message(),
588 + ),
589 + $status
590 + );
335 591 }
336 592
337 593 /**
338 594 * Discover the REST URL for a friend site
@@ -357,9 +613,9 @@
357 613 * @return string|\WP_Error The REST URL or an error.
358 614 */
359 615 public function discover_rest_url( $url ) {
360 616 if ( ! is_string( $url ) || ! Friends::check_url( $url ) ) {
361 - return new \WP_Error( 'invalid-url-given', 'An invalid URL was given.' );
617 + return self::error( 'friends_invalid_url' );
362 618 }
363 619
364 620 $response = wp_safe_remote_get(
365 621 $url,
@@ -374,9 +630,9 @@
374 630 }
375 631
376 632 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
377 633 $dom = new \DOMDocument();
378 - set_error_handler( '__return_null' );
634 + set_error_handler( '__return_null' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
379 635 $dom->loadHTML( wp_remote_retrieve_body( $response ) );
380 636 restore_error_handler();
381 637
382 638 $xpath = new \DOMXpath( $dom );
@@ -390,78 +646,6 @@
390 646 }
391 647 }
392 648
393 649 return null;
394 - }
395 -
396 - /**
397 - * Notify the friend's site via REST about the accepted friend request.
398 - *
399 - * Accepting a friend request is simply setting the role to "friend".
400 - *
401 - * @param int $user_id The user id.
402 - * @param string $new_role The new role.
403 - * @param string $old_roles The old roles.
404 - */
405 - public function notify_remote_friend_request_accepted( $user_id, $new_role, $old_roles ) {
406 - if ( 'friend' !== $new_role && 'acquaintance' !== $new_role ) {
407 - return;
408 - }
409 -
410 - $request_token = get_user_option( 'friends_request_id', $user_id );
411 - if ( ! $request_token ) {
412 - // We were accepted, so no need to notify the other.
413 - return;
414 - }
415 -
416 - $friend_user = new User( $user_id );
417 -
418 - $friend_rest_url = $friend_user->get_rest_url();
419 - $request_id = $friend_user->get_user_option( 'friends_request_id' );
420 - $future_out_token = $friend_user->get_user_option( 'friends_future_out_token' );
421 - $future_in_token = wp_generate_password( 128, false );
422 -
423 - $current_user = wp_get_current_user();
424 - $response = wp_safe_remote_post(
425 - $friend_rest_url . '/accept-friend-request',
426 - array(
427 - 'body' => array(
428 - 'request' => $request_id,
429 - 'proof' => sha1( $future_out_token . $request_id ),
430 - 'key' => $future_in_token,
431 - 'name' => $current_user->display_name,
432 - 'icon_url' => get_avatar_url( $current_user->ID ),
433 - ),
434 - 'timeout' => 20,
435 - 'redirection' => 5,
436 - )
437 - );
438 -
439 - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
440 - // TODO find a way to message the user.
441 - return;
442 - }
443 -
444 - $json = json_decode( wp_remote_retrieve_body( $response ) );
445 - if ( ! isset( $json->signature ) || sha1( $future_in_token . $future_out_token ) !== $json->signature ) {
446 - $friend_user->set_role( 'friend_request' );
447 - // TODO find a way to message the user.
448 - return;
449 - }
450 -
451 - $friend_user->make_friend( $future_out_token, $future_in_token );
452 - $friend_user->delete_user_option( 'friends_request_id' );
453 - $friend_user->delete_user_option( 'friends_future_out_token' );
454 -
455 - /*
456 - TODO
457 - if ( isset( $json->user_icon_url ) ) {
458 - $this->friends->access_control->update_user_icon_url( $friend_user->ID, $json->user_icon_url );
459 - }
460 - When their friend request is no longer valid
461 - $friend_user->set_role( 'pending_friend_request' );
462 - if ( isset( $json->friend_request_pending ) ) {
463 - }
464 - }
465 - */
466 650 }
467 651 }