Make WordPress Core

Ticket #40702: 40702.11.diff

File 40702.11.diff, 26.4 KB (added by adamsilverstein, 7 years ago)
Line 
1diff --git src/wp-admin/admin-ajax.php src/wp-admin/admin-ajax.php
2index 3213d55028..e0f4464d94 100644
3--- src/wp-admin/admin-ajax.php
4+++ src/wp-admin/admin-ajax.php
5@@ -64,7 +64,7 @@ $core_actions_post = array(
6        'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post',
7        'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin',
8        'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme',
9-       'install-theme', 'get-post-thumbnail-html', 'get-community-events',
10+       'install-theme', 'get-post-thumbnail-html',
11 );
12
13 // Deprecated
14diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php
15index 029e20e62b..3342cad375 100644
16--- src/wp-admin/includes/ajax-actions.php
17+++ src/wp-admin/includes/ajax-actions.php
18@@ -297,40 +297,6 @@ function wp_ajax_autocomplete_user() {
19 }
20
21 /**
22- * Handles AJAX requests for community events
23- *
24- * @since 4.8.0
25- */
26-function wp_ajax_get_community_events() {
27-       require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' );
28-
29-       check_ajax_referer( 'community_events' );
30-
31-       $search         = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : '';
32-       $timezone       = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : '';
33-       $user_id        = get_current_user_id();
34-       $saved_location = get_user_option( 'community-events-location', $user_id );
35-       $events_client  = new WP_Community_Events( $user_id, $saved_location );
36-       $events         = $events_client->get_events( $search, $timezone );
37-
38-       if ( is_wp_error( $events ) ) {
39-               wp_send_json_error( array(
40-                       'error' => $events->get_error_message(),
41-               ) );
42-       } else {
43-               if ( isset( $events['location'] ) ) {
44-                       // Send only the data that the client will use.
45-                       $events['location'] = $events['location']['description'];
46-
47-                       // Store the location network-wide, so the user doesn't have to set it on each site.
48-                       update_user_option( $user_id, 'community-events-location', $events['location'], true );
49-               }
50-
51-               wp_send_json_success( $events );
52-       }
53-}
54-
55-/**
56  * Ajax handler for dashboard widgets.
57  *
58  * @since 3.4.0
59diff --git src/wp-admin/includes/dashboard.php src/wp-admin/includes/dashboard.php
60index b25426e753..514ff58af4 100644
61--- src/wp-admin/includes/dashboard.php
62+++ src/wp-admin/includes/dashboard.php
63@@ -144,7 +144,8 @@ function wp_get_community_events_script_data() {
64        $events_client = new WP_Community_Events( $user_id, $user_location );
65
66        $script_data = array(
67-               'nonce' => wp_create_nonce( 'community_events' ),
68+               'rest_url' => rest_url( '/' ),
69+               'rest_nonce' => wp_create_nonce( 'wp_rest' ),
70                'cache' => $events_client->get_cached_events(),
71
72                'l10n' => array(
73@@ -1243,7 +1244,7 @@ function wp_print_community_events_templates() {
74                <?php printf(
75                        /* translators: %s is a placeholder for the name of a city. */
76                        __( 'Attend an upcoming event near %s.' ),
77-                       '<strong>{{ data.location }}</strong>'
78+                       '<strong>{{ data.location.description }}</strong>'
79                ); ?>
80        </script>
81
82@@ -1265,12 +1266,14 @@ function wp_print_community_events_templates() {
83                                        </div>
84                                </div>
85
86-                               <div class="event-date-time">
87-                                       <span class="event-date">{{ event.formatted_date }}</span>
88-                                       <# if ( 'meetup' === event.type ) { #>
89-                                               <span class="event-time">{{ event.formatted_time }}</span>
90-                                       <# } #>
91-                               </div>
92+                               <# if ( event.date && event.date.formatted ) { #>
93+                                       <div class="event-date-time">
94+                                               <span class="event-date">{{ event.date.formatted.date }}</span>
95+                                               <# if ( 'meetup' === event.type ) { #>
96+                                                       <span class="event-time">{{ event.date.formatted.time }}</span>
97+                                               <# } #>
98+                                       </div>
99+                               <# } #>
100                        </li>
101                <# } ) #>
102        </script>
103@@ -1280,7 +1283,7 @@ function wp_print_community_events_templates() {
104                        <?php printf(
105                                /* translators: 1: the city the user searched for, 2: meetup organization documentation URL */
106                                __( 'There aren&#8217;t any events scheduled near %1$s at the moment. Would you like to <a href="%2$s">organize one</a>?' ),
107-                               '{{data.location}}',
108+                               '{{data.location.description}}',
109                                __( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
110                        ); ?>
111                </li>
112diff --git src/wp-admin/js/dashboard.js src/wp-admin/js/dashboard.js
113index 6a9b5033d4..d3d2bcc644 100644
114--- src/wp-admin/js/dashboard.js
115+++ src/wp-admin/js/dashboard.js
116@@ -191,7 +191,7 @@ jQuery(document).ready( function($) {
117
118 jQuery( function( $ ) {
119        'use strict';
120-
121+
122        var communityEventsData = window.communityEventsData || {};
123
124        var app = window.wp.communityEvents = {
125@@ -288,45 +288,71 @@ jQuery( function( $ ) {
126                getEvents: function( requestParams ) {
127                        var initiatedBy,
128                            app = this,
129-                           $spinner = $( '.community-events-form' ).children( '.spinner' );
130+                           $spinner = $( '.community-events-form' ).children( '.spinner' ),
131+                           dashboardLoadPromise = wp.api.init( { 'versionString': 'wp/dashboard/v1/' } );
132+;
133
134                        requestParams          = requestParams || {};
135-                       requestParams._wpnonce = communityEventsData.nonce;
136                        requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : '';
137+                       requestParams._embed   = 1;
138
139                        initiatedBy = requestParams.location ? 'user' : 'app';
140
141                        $spinner.addClass( 'is-active' );
142
143-                       wp.ajax.post( 'get-community-events', requestParams )
144-                               .always( function() {
145-                                       $spinner.removeClass( 'is-active' );
146-                               })
147-
148-                               .done( function( response ) {
149-                                       if ( 'no_location_available' === response.error ) {
150-                                               if ( requestParams.location ) {
151-                                                       response.unknownCity = requestParams.location;
152-                                               } else {
153-                                                       /*
154-                                                        * No location was passed, which means that this was an automatic query
155-                                                        * based on IP, locale, and timezone. Since the user didn't initiate it,
156-                                                        * it should fail silently. Otherwise, the error could confuse and/or
157-                                                        * annoy them.
158-                                                        */
159-
160-                                                       delete response.error;
161+                       dashboardLoadPromise.done( function( endpoint ) {
162+                               if ( ! app.model ) {
163+                                       app.model = new wp.api.collections.CommunityEventsMyLocation();
164+                               }
165+                               requestParams          = requestParams || {};
166+                               requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : '';
167+                               requestParams._embed   = 1;
168+                               initiatedBy = requestParams.location ? 'user' : 'app';
169+
170+                               $spinner.addClass( 'is-active' );
171+
172+
173+                               var meModel = app.model.fetch( {
174+                                       'data': requestParams,
175+                                       'success': function( model, response ) {
176+                                               var events   = response._embedded && response._embedded.events ? response._embedded.events[0] : [];
177+                                               var location = response;
178+
179+                                               delete response._embedded;
180+                                               delete response._links;
181+
182+                                               app.renderEventsTemplate({
183+                                                       location: location,
184+                                                       events: events
185+                                               }, initiatedBy );
186+
187+                                       },
188+                                       'error': function( model, response ) {
189+
190+                                               if ( 'rest_cannot_retrieve_user_location' === response.code && requestParams.location ) {
191+                                                       app.renderEventsTemplate({
192+                                                               unknownCity: requestParams.location,
193+                                                               location: false,
194+                                                               events: []
195+                                                       }, initiatedBy );
196+
197+                                                       return;
198                                                }
199+
200+                                               app.renderEventsTemplate( {
201+                                                       'location' : false,
202+                                                       'error'    : true
203+                                               }, initiatedBy );
204                                        }
205-                                       app.renderEventsTemplate( response, initiatedBy );
206-                               })
207-
208-                               .fail( function() {
209-                                       app.renderEventsTemplate( {
210-                                               'location' : false,
211-                                               'error'    : true
212-                                       }, initiatedBy );
213-                               });
214+                               } );
215+
216+                               meModel
217+                                       .always( function() {
218+                                               $spinner.removeClass( 'is-active' );
219+                                       });
220+                       });
221                },
222
223                /**
224diff --git src/wp-includes/js/wp-api.js src/wp-includes/js/wp-api.js
225index 3f950a47f5..bf3257b80b 100644
226--- src/wp-includes/js/wp-api.js
227+++ src/wp-includes/js/wp-api.js
228@@ -1282,14 +1282,20 @@
229
230                                                // Function that returns a constructed url passed on the parent.
231                                                url: function() {
232+                                                       var hasParent = ! _.isEmpty( this.parent );
233                                                        return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
234-                                                                       parentName + '/' + this.parent + '/' +
235+                                                                       parentName + '/' +
236+                                                                       ( hasParent ? ( this.parent + '/' ) : '' ) +
237                                                                        routeName;
238                                                },
239
240                                                // Specify the model that this collection contains.
241                                                model: function( attrs, options ) {
242-                                                       return new loadingObjects.models[ modelClassName ]( attrs, options );
243+                                                       if ( loadingObjects.models[ modelClassName ] ) {
244+                                                               return new loadingObjects.models[ modelClassName ]( attrs, options );
245+                                                       } else {
246+                                                               return new Backbone.Model();
247+                                                       }
248                                                },
249
250                                                // Include a reference to the original class name.
251diff --git src/wp-includes/rest-api.php src/wp-includes/rest-api.php
252index ec7c50d27b..891be7ceb6 100644
253--- src/wp-includes/rest-api.php
254+++ src/wp-includes/rest-api.php
255@@ -237,6 +237,14 @@ function create_initial_rest_routes() {
256        // Settings.
257        $controller = new WP_REST_Settings_Controller;
258        $controller->register_routes();
259+
260+       // Community events.
261+       $controller = new WP_REST_Community_Events_Events_Controller;
262+       $controller->register_routes();
263+
264+       // Community events location.
265+       $controller = new WP_REST_Community_Events_Location_Controller;
266+       $controller->register_routes();
267 }
268
269 /**
270diff --git src/wp-includes/rest-api/endpoints/dashboard/class-wp-rest-community-events-events-controller.php src/wp-includes/rest-api/endpoints/dashboard/class-wp-rest-community-events-events-controller.php
271new file mode 100644
272index 0000000000..570abd2adc
273--- /dev/null
274+++ src/wp-includes/rest-api/endpoints/dashboard/class-wp-rest-community-events-events-controller.php
275@@ -0,0 +1,281 @@
276+<?php
277+/**
278+ * REST API: WP_REST_Community_Events_Events_Controller class
279+ *
280+ * @package WordPress
281+ * @subpackage REST_API
282+ * @since 4.8.0
283+ */
284+
285+/**
286+ * Core class to access community events via the REST API.
287+ *
288+ * @since 4.8.0
289+ *
290+ * @see WP_REST_Controller
291+ */
292+class WP_REST_Community_Events_Events_Controller extends WP_REST_Controller {
293+
294+       /**
295+        * Constructor.
296+        *
297+        * @since 4.8.0
298+        * @access public
299+        */
300+       public function __construct() {
301+               $this->namespace = 'wp/dashboard/v1';
302+               $this->rest_base = 'community-events/events';
303+       }
304+
305+       /**
306+        * Registers the routes for the objects of the controller.
307+        *
308+        * @since 4.8.0
309+        * @access public
310+        *
311+        * @see register_rest_route()
312+        */
313+       public function register_routes() {
314+
315+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array(
316+                       array(
317+                               'methods'             => WP_REST_Server::READABLE,
318+                               'callback'            => array( $this, 'get_current_items' ),
319+                               'permission_callback' => array( $this, 'get_current_items_permissions_check' ),
320+                               'args'                => $this->get_collection_params(),
321+                       ),
322+                       'schema' => array( $this, 'get_public_item_schema' ),
323+               ) );
324+       }
325+
326+       /**
327+        * Checks whether a given request has permission to read community events.
328+        *
329+        * @since 4.8.0
330+        * @access public
331+        *
332+        * @param WP_REST_Request $request Full details about the request.
333+        * @return WP_Error|true True if the request has read access, WP_Error object otherwise.
334+        */
335+       public function get_current_items_permissions_check( $request ) {
336+               if ( ! is_user_logged_in() ) {
337+                       return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
338+               }
339+
340+               return true;
341+       }
342+
343+       /**
344+        * Retrieves community events.
345+        *
346+        * @since 4.8.0
347+        * @access public
348+        *
349+        * @param WP_REST_Request $request Full details about the request.
350+        * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
351+        */
352+       public function get_current_items( $request ) {
353+               require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' );
354+
355+               $user_id = get_current_user_id();
356+
357+               $location = $request->get_param( 'location' );
358+               $timezone = $request->get_param( 'timezone' );
359+
360+               $saved_location = get_user_option( 'community-events-location', $user_id );
361+               $events_client  = new WP_Community_Events( $user_id, $saved_location );
362+               $events         = $events_client->get_events( $location, $timezone );
363+
364+               $data = array();
365+
366+               // Store the location network-wide, so the user doesn't have to set it on each site.
367+               if ( ! is_wp_error( $events ) ) {
368+                       if ( isset( $events['location'] ) ) {
369+                               update_user_option( $user_id, 'community-events-location', $events['location'], true );
370+                       }
371+
372+                       if ( isset( $events['events'] ) ) {
373+                               foreach ( $events['events'] as $event ) {
374+                                       $data[] = $this->prepare_item_for_response( $event, $request );
375+                               }
376+                       }
377+               }
378+
379+               return rest_ensure_response( $data );
380+       }
381+
382+       /**
383+        * Prepares a single event output for response.
384+        *
385+        * @since 4.8.0
386+        * @access public
387+        *
388+        * @param array           $event   Event data array from the API.
389+        * @param WP_REST_Request $request Request object.
390+        * @return array Item prepared for response.
391+        */
392+       public function prepare_item_for_response( $event, $request ) {
393+               $data = array();
394+
395+               $keys_to_copy = array( 'type', 'title', 'url', 'meetup', 'meetup_url' );
396+               foreach ( $keys_to_copy as $key ) {
397+                       if ( isset( $event[ $key ] ) ) {
398+                               $data[ $key ] = $event[ $key ];
399+                       } else {
400+                               $data[ $key ] = null;
401+                       }
402+               }
403+
404+               $data['date'] = array(
405+                       'raw'       => isset( $event['date'] ) ? $event['date'] : null,
406+                       'formatted' => array(
407+                               'date' => isset( $event['formatted_date'] ) ? $event['formatted_date'] : null,
408+                               'time' => isset( $event['formatted_time'] ) ? $event['formatted_time'] : null,
409+                       ),
410+               );
411+
412+               $data['location'] = isset( $event['location'] ) ? $event['location'] : null;
413+
414+               return $data;
415+       }
416+
417+       /**
418+        * Retrieves a community event's schema, conforming to JSON Schema.
419+        *
420+        * @since 4.8.0
421+        * @access public
422+        *
423+        * @return array Item schema data.
424+        */
425+       public function get_item_schema() {
426+               return array(
427+                       '$schema'              => 'http://json-schema.org/schema#',
428+                       'title'                => 'community_event',
429+                       'type'                 => 'object',
430+                       'properties'           => array(
431+                               'type'       => array(
432+                                       'description' => __( 'Type for the event.' ),
433+                                       'type'        => 'string',
434+                                       'enum'        => array( 'meetup', 'wordcamp' ),
435+                                       'context'     => array( 'view', 'edit', 'embed' ),
436+                                       'readonly'    => true,
437+                               ),
438+                               'title'      => array(
439+                                       'description' => __( 'Title for the event.' ),
440+                                       'type'        => 'string',
441+                                       'context'     => array( 'view', 'edit', 'embed' ),
442+                                       'readonly'    => true,
443+                               ),
444+                               'url'        => array(
445+                                       'description' => __( 'Website URL for the event.' ),
446+                                       'type'        => 'string',
447+                                       'context'     => array( 'view', 'edit', 'embed' ),
448+                                       'readonly'    => true,
449+                               ),
450+                               'meetup'     => array(
451+                                       'description' => __( 'Name of the meetup, if the event is a meetup.' ),
452+                                       'type'        => 'string',
453+                                       'context'     => array( 'view', 'edit', 'embed' ),
454+                                       'readonly'    => true,
455+                               ),
456+                               'meetup_url' => array(
457+                                       'description' => __( 'URL for the meetup on meetup.com, if the event is a meetup.' ),
458+                                       'type'        => 'string',
459+                                       'context'     => array( 'view', 'edit', 'embed' ),
460+                                       'readonly'    => true,
461+                               ),
462+                               'date'       => array(
463+                                       'description' => __( 'Date and time information for the event.' ),
464+                                       'type'        => 'object',
465+                                       'context'     => array( 'view', 'edit', 'embed' ),
466+                                       'readonly'    => true,
467+                                       'properties'  => array(
468+                                               'raw'       => array(
469+                                                       'description' => __( 'Unformatted date and time string.' ),
470+                                                       'type'        => 'string',
471+                                                       'format'      => 'date-time',
472+                                                       'context'     => array( 'view', 'edit', 'embed' ),
473+                                                       'readonly'    => true,
474+                                               ),
475+                                               'formatted' => array(
476+                                                       'description' => __( 'Formatted date and time information for the event.' ),
477+                                                       'type'        => 'object',
478+                                                       'context'     => array( 'view', 'edit', 'embed' ),
479+                                                       'readonly'    => true,
480+                                                       'properties'  => array(
481+                                                               'date' => array(
482+                                                                       'description' => __( 'Formatted event date.' ),
483+                                                                       'type'        => 'string',
484+                                                                       'context'     => array( 'view', 'edit', 'embed' ),
485+                                                                       'readonly'    => true,
486+                                                               ),
487+                                                               'time' => array(
488+                                                                       'description' => __( 'Formatted event time.' ),
489+                                                                       'type'        => 'string',
490+                                                                       'context'     => array( 'view', 'edit', 'embed' ),
491+                                                                       'readonly'    => true,
492+                                                               ),
493+                                                       ),
494+                                               ),
495+                                       ),
496+                               ),
497+                               'location'   => array(
498+                                       'description' => __( 'Location information for the event.' ),
499+                                       'type'        => 'object',
500+                                       'context'     => array( 'view', 'edit', 'embed' ),
501+                                       'readonly'    => true,
502+                                       'properties'  => array(
503+                                               'location'  => array(
504+                                                       'description' => __( 'Location name for the event.' ),
505+                                                       'type'        => 'string',
506+                                                       'context'     => array( 'view', 'edit', 'embed' ),
507+                                                       'readonly'    => true,
508+                                               ),
509+                                               'country'   => array(
510+                                                       'description' => __( 'Two-letter country code for the event.' ),
511+                                                       'type'        => 'string',
512+                                                       'context'     => array( 'view', 'edit', 'embed' ),
513+                                                       'readonly'    => true,
514+                                               ),
515+                                               'latitude'  => array(
516+                                                       'description' => __( 'Latitude for the event.' ),
517+                                                       'type'        => 'number',
518+                                                       'context'     => array( 'view', 'edit', 'embed' ),
519+                                                       'readonly'    => true,
520+                                               ),
521+                                               'longitude' => array(
522+                                                       'description' => __( 'Longitude for the event.' ),
523+                                                       'type'        => 'number',
524+                                                       'context'     => array( 'view', 'edit', 'embed' ),
525+                                                       'readonly'    => true,
526+                                               ),
527+                                       ),
528+                               ),
529+                       ),
530+               );
531+       }
532+
533+       /**
534+        * Retrieves the query params for collections.
535+        *
536+        * @since 4.8.0
537+        * @access public
538+        *
539+        * @return array Collection parameters.
540+        */
541+       public function get_collection_params() {
542+               return array(
543+                       'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
544+                       'location' => array(
545+                               'description' => __( 'Optional city name to help determine the location for the events.' ),
546+                               'type'        => 'string',
547+                               'default'     => '',
548+                       ),
549+                       'timezone' => array(
550+                               'description' => __( 'Optional timezone to help determine the location for the events.' ),
551+                               'type'        => 'string',
552+                               'default'     => '',
553+                       ),
554+               );
555+       }
556+}
557diff --git src/wp-includes/rest-api/endpoints/dashboard/class-wp-rest-community-events-location-controller.php src/wp-includes/rest-api/endpoints/dashboard/class-wp-rest-community-events-location-controller.php
558new file mode 100644
559index 0000000000..3763885731
560--- /dev/null
561+++ src/wp-includes/rest-api/endpoints/dashboard/class-wp-rest-community-events-location-controller.php
562@@ -0,0 +1,217 @@
563+<?php
564+/**
565+ * REST API: WP_REST_Community_Events_Location_Controller class
566+ *
567+ * @package WordPress
568+ * @subpackage REST_API
569+ * @since 4.8.0
570+ */
571+
572+/**
573+ * Core class to access community events user locations via the REST API.
574+ *
575+ * @since 4.8.0
576+ *
577+ * @see WP_REST_Controller
578+ */
579+class WP_REST_Community_Events_Location_Controller extends WP_REST_Controller {
580+
581+       /**
582+        * Constructor.
583+        *
584+        * @since 4.8.0
585+        * @access public
586+        */
587+       public function __construct() {
588+               $this->namespace = 'wp/dashboard/v1';
589+               $this->rest_base = 'community-events';
590+       }
591+
592+       /**
593+        * Registers the routes for the objects of the controller.
594+        *
595+        * @since 4.8.0
596+        * @access public
597+        *
598+        * @see register_rest_route()
599+        */
600+       public function register_routes() {
601+
602+               register_rest_route( $this->namespace, '/' . $this->rest_base . '/my-location', array(
603+                       array(
604+                               'methods'             => WP_REST_Server::READABLE,
605+                               'callback'            => array( $this, 'get_current_item' ),
606+                               'permission_callback' => array( $this, 'get_current_item_permissions_check' ),
607+                               'args'                => $this->get_item_params(),
608+                       ),
609+                       'schema' => array( $this, 'get_public_item_schema' ),
610+               ) );
611+       }
612+
613+       /**
614+        * Checks whether a given request has permission to read the current user's community events location.
615+        *
616+        * @since 4.8.0
617+        * @access public
618+        *
619+        * @param WP_REST_Request $request Full details about the request.
620+        * @return WP_Error|true True if the request has read access, WP_Error object otherwise.
621+        */
622+       public function get_current_item_permissions_check( $request ) {
623+               if ( ! is_user_logged_in() ) {
624+                       return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
625+               }
626+
627+               return true;
628+       }
629+
630+       /**
631+        * Retrieves the community events location for the current user.
632+        *
633+        * @since 4.8.0
634+        * @access public
635+        *
636+        * @param WP_REST_Request $request Full details about the request.
637+        * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
638+        */
639+       public function get_current_item( $request ) {
640+               require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' );
641+
642+               $user_id = get_current_user_id();
643+
644+               $location = $request->get_param( 'location' );
645+               $timezone = $request->get_param( 'timezone' );
646+
647+               $saved_location = get_user_option( 'community-events-location', $user_id );
648+               $events_client  = new WP_Community_Events( $user_id, $saved_location );
649+               $events         = $events_client->get_events( $location, $timezone );
650+
651+               // Store the location network-wide, so the user doesn't have to set it on each site.
652+               if ( ! is_wp_error( $events ) ) {
653+                       if ( isset( $events['error'] ) && 'no_location_available' === $events['error'] ) {
654+                               return new WP_Error( 'rest_cannot_retrieve_user_location', __( 'The user location could not be retrieved.' ) );
655+                       }
656+
657+                       if ( isset( $events['location'] ) ) {
658+                               update_user_option( $user_id, 'community-events-location', $events['location'], true );
659+
660+                               $data = $this->prepare_item_for_response( $events['location'], $request );
661+
662+                               return rest_ensure_response( $data );
663+                       }
664+               }
665+
666+               return $events;
667+       }
668+
669+       /**
670+        * Prepares a single location output for response.
671+        *
672+        * @since 4.8.0
673+        * @access public
674+        *
675+        * @param array           $location Location data array from the API.
676+        * @param WP_REST_Request $request  Request object.
677+        * @return WP_REST_Response Response object.
678+        */
679+       public function prepare_item_for_response( $location, $request ) {
680+               $data = array(
681+                       'description' => isset( $location['description'] ) ? $location['description']       : null,
682+                       'country'     => isset( $location['country'] )     ? $location['country']           : null,
683+                       'latitude'    => isset( $location['latitude'] )    ? (float) $location['latitude']  : null,
684+                       'longitude'   => isset( $location['longitude'] )   ? (float) $location['longitude'] : null,
685+               );
686+
687+               $response = rest_ensure_response( $data );
688+
689+               $url = rest_url( 'wp/dashboard/v1/community-events/events/me' );
690+
691+               $url_args = array();
692+
693+               if ( ! empty( $request['location'] ) ) {
694+                       $url_args['location'] = $request['location'];
695+               }
696+               if ( ! empty( $request['timezone'] ) ) {
697+                       $url_args['timezone'] = $request['timezone'];
698+               }
699+
700+               if ( ! empty( $url_args ) ) {
701+                       $url = add_query_arg( $url_args, $url );
702+               }
703+
704+               $response->add_links( array(
705+                       'events' => array(
706+                               'href'       => $url,
707+                               'embeddable' => true,
708+                       ),
709+               ) );
710+
711+               return $response;
712+       }
713+
714+       /**
715+        * Retrieves a community events location schema, conforming to JSON Schema.
716+        *
717+        * @since 4.8.0
718+        * @access public
719+        *
720+        * @return array Item schema data.
721+        */
722+       public function get_item_schema() {
723+               return array(
724+                       '$schema'              => 'http://json-schema.org/schema#',
725+                       'title'                => 'community_events_location',
726+                       'type'                 => 'object',
727+                       'properties'           => array(
728+                               'description' => array(
729+                                       'description' => __( 'Location description.' ),
730+                                       'type'        => 'string',
731+                                       'context'     => array( 'view', 'edit', 'embed' ),
732+                                       'readonly'    => true,
733+                               ),
734+                               'country'     => array(
735+                                       'description' => __( 'Two-letter country code.' ),
736+                                       'type'        => 'string',
737+                                       'context'     => array( 'view', 'edit', 'embed' ),
738+                                       'readonly'    => true,
739+                               ),
740+                               'latitude'    => array(
741+                                       'description' => __( 'Latitude.' ),
742+                                       'type'        => 'number',
743+                                       'context'     => array( 'view', 'edit', 'embed' ),
744+                                       'readonly'    => true,
745+                               ),
746+                               'longitude'   => array(
747+                                       'description' => __( 'Longitude.' ),
748+                                       'type'        => 'number',
749+                                       'context'     => array( 'view', 'edit', 'embed' ),
750+                                       'readonly'    => true,
751+                               ),
752+                       ),
753+               );
754+       }
755+
756+       /**
757+        * Retrieves the params for a single item.
758+        *
759+        * @since 4.8.0
760+        * @access public
761+        *
762+        * @return array Item parameters.
763+        */
764+       public function get_item_params() {
765+               return array(
766+                       'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
767+                       'location' => array(
768+                               'description' => __( 'Optional city name to help determine the location.' ),
769+                               'type'        => 'string',
770+                               'default'     => '',
771+                       ),
772+                       'timezone' => array(
773+                               'description' => __( 'Optional timezone to help determine the location.' ),
774+                               'type'        => 'string',
775+                               'default'     => '',
776+                       ),
777+               );
778+       }
779+}
780diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
781index f1eda9ee38..c02fe73588 100644
782--- src/wp-includes/script-loader.php
783+++ src/wp-includes/script-loader.php
784@@ -732,7 +732,7 @@ function wp_default_scripts( &$scripts ) {
785                        'current' => __( 'Current Color' ),
786                ) );
787
788-               $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y' ), false, 1 );
789+               $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y', 'wp-api' ), false, 1 );
790
791                $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
792
793diff --git src/wp-settings.php src/wp-settings.php
794index d1505c502d..060ab4c782 100644
795--- src/wp-settings.php
796+++ src/wp-settings.php
797@@ -234,6 +234,8 @@ require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.p
798 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php' );
799 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php' );
800 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' );
801+require( ABSPATH . WPINC . '/rest-api/endpoints/dashboard/class-wp-rest-community-events-events-controller.php' );
802+require( ABSPATH . WPINC . '/rest-api/endpoints/dashboard/class-wp-rest-community-events-location-controller.php' );
803 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' );
804 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php' );
805 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php' );