Ticket #40702: 40702.diff
File 40702.diff, 53.9 KB (added by , 7 years ago) |
---|
-
src/wp-admin/css/dashboard.css
diff --git src/wp-admin/css/dashboard.css src/wp-admin/css/dashboard.css index 98fb99378d..2b80450a4a 100644
301 301 content: "\f153"; 302 302 } 303 303 304 /* Dashboard WordPress events */ 305 306 .community-events-errors { 307 margin: 0; 308 } 309 310 .community-events-loading { 311 padding: 10px 12px 8px; 312 } 313 314 .community-events { 315 margin-bottom: 6px; 316 padding: 0 12px; 317 } 318 319 .community-events .spinner { 320 float: none; 321 margin: 0; 322 padding-bottom: 3px; 323 } 324 325 .community-events-errors[aria-hidden="true"], 326 .community-events-errors *[aria-hidden="true"], 327 .community-events-loading[aria-hidden="true"], 328 .community-events[aria-hidden="true"], 329 .community-events *[aria-hidden="true"] { 330 display: none; 331 } 332 333 .community-events .activity-block:first-child, 334 .community-events h2 { 335 padding-top: 12px; 336 padding-bottom: 10px; 337 } 338 339 .community-events-form { 340 margin: 5px 0; 341 } 342 343 .community-events-form .regular-text { 344 margin-top: 2px; 345 width: 40%; 346 } 347 348 .community-events-form label { 349 display: inline-block; 350 padding-bottom: 3px; 351 } 352 353 .community-events .activity-block > p { 354 margin-bottom: 0; 355 display: inline; 356 } 357 358 #community-events-submit { 359 margin-left: 2px; 360 } 361 362 .community-events .button-link:hover, 363 .community-events .button-link:active { 364 color: #00a0d2; 365 } 366 367 .community-events-cancel.button.button-link { 368 color: #0073aa; 369 text-decoration: underline; 370 margin-left: 2px; 371 } 372 373 .community-events ul { 374 background-color: #fafafa; 375 padding-left: 0; 376 padding-right: 0; 377 padding-bottom: 0; 378 } 379 380 .community-events li { 381 margin: 0; 382 padding: 8px 12px; 383 color: #72777c; 384 } 385 .community-events li:first-child { 386 border-top: 1px solid #eee; 387 } 388 389 .community-events li ~ li { 390 border-top: 1px solid #eee; 391 } 392 393 .community-events .activity-block { 394 border-bottom: 0; 395 } 396 .community-events .activity-block.last { 397 border-bottom: 1px solid #eee; 398 padding-top: 0; 399 } 400 401 .community-events .event-info { 402 display: block; 403 } 404 405 .event-icon { 406 height: 18px; 407 padding-right: 10px; 408 width: 18px; 409 display: none; /* Hide on smaller screens */ 410 } 411 .rtl .event-icon { 412 padding-right: 0; 413 padding-left: 10px; 414 } 415 416 .event-icon:before { 417 color: #82878C; 418 font-size: 18px; 419 } 420 .event-meetup .event-icon:before { 421 content: "\f484"; 422 } 423 .event-wordcamp .event-icon:before { 424 content: "\f486"; 425 } 426 427 .community-events .event-title { 428 font-weight: 600; 429 display: block; 430 } 431 432 .community-events .event-date, 433 .community-events .event-time { 434 display: block; 435 } 436 437 .community-events-footer { 438 margin-top: 0; 439 margin-bottom: 0; 440 padding: 12px; 441 border-top: 1px solid #eee; 442 color: #ddd; 443 } 444 304 445 /* Dashboard WordPress news */ 305 446 306 447 #dashboard_primary .inside { … … body #dashboard-widgets .postbox form .submit { 333 474 } 334 475 335 476 #dashboard_primary .rss-widget { 336 border-bottom: 1px solid #eee;337 477 font-size: 13px; 338 padding: 8px 12px 10px;478 padding: 0 12px 0; 339 479 } 340 480 341 481 #dashboard_primary .rss-widget:last-child { … … body #dashboard-widgets .postbox form .submit { 357 497 } 358 498 359 499 #dashboard_primary .rss-widget ul li { 360 margin-bottom: 8px; 500 padding: 4px 0; 501 margin: 0; 361 502 } 362 503 363 504 /* Dashboard right now */ … … form.initial-form.quickpress-open input#title { 874 1015 } 875 1016 876 1017 a.rsswidget { 877 font-size: 1 4px;1018 font-size: 13px; 878 1019 font-weight: 600; 879 line-height: 1. 7em;1020 line-height: 1.4em; 880 1021 } 881 1022 882 1023 .rss-widget ul li { … … a.rsswidget { 1087 1228 width: 30px; 1088 1229 margin: 4px 10px 5px 0; 1089 1230 } 1231 1232 .community-events-toggle-location { 1233 height: 38px; 1234 } 1090 1235 } 1091 1236 1092 1237 /* Smartphone */ … … a.rsswidget { 1110 1255 left: -35px; 1111 1256 } 1112 1257 } 1258 1259 @media screen and (min-width: 355px) { 1260 .community-events .event-info { 1261 display: table-row; 1262 float: left; 1263 max-width: 59%; 1264 } 1265 .rtl .community-events .event-info { 1266 float: right; 1267 } 1268 1269 .event-icon, 1270 .event-icon[aria-hidden="true"] { 1271 display: table-cell; 1272 } 1273 1274 .event-info-inner { 1275 display: table-cell; 1276 } 1277 1278 .community-events .event-date-time { 1279 float: right; 1280 max-width: 39%; 1281 } 1282 .rtl .community-events .event-date-time { 1283 float: left; 1284 } 1285 1286 .community-events .event-date, 1287 .community-events .event-time { 1288 text-align: right; 1289 } 1290 .rtl .community-events .event-date, 1291 .rtl .community-events .event-time { 1292 text-align: left; 1293 } 1294 } -
new file src/wp-admin/includes/class-wp-community-events.php
diff --git src/wp-admin/includes/class-wp-community-events.php src/wp-admin/includes/class-wp-community-events.php new file mode 100644 index 0000000000..6cf2d72bfb
- + 1 <?php 2 /** 3 * Administration: Community Events class 4 * 5 * @package WordPress 6 * @subpackage Administration 7 * @since 4.8.0 8 */ 9 10 11 defined( 'WPINC' ) || die(); 12 13 /** 14 * Class WP_Community_Events 15 * 16 * A client for api.wordpress.org/events. 17 * 18 * @since 4.8.0 19 */ 20 class WP_Community_Events { 21 /** 22 * WP user ID. 23 * 24 * @access protected 25 * @since 4.8.0 26 * 27 * @var int 28 */ 29 protected $user_id = 0; 30 31 /** 32 * Stored location data for the user. 33 * 34 * @access protected 35 * @since 4.8.0 36 * 37 * @var bool|array 38 */ 39 protected $user_location = false; 40 41 /** 42 * WP_Community_Events constructor. 43 * 44 * @access public 45 * @since 4.8.0 46 * 47 * @param int $user_id WP user ID. 48 * @param bool|array $user_location Stored location data for the user. 49 * `false` to pass no location; 50 * `array` to pass a location { 51 * @type string $description The name of the location 52 * @type string $latitude The latitude in decimal degrees notation, without the degree 53 * symbol. e.g., `47.615200`. 54 * @type string $longitude The longitude in decimal degrees notation, without the degree 55 * symbol. e.g., `-122.341100`. 56 * @type string $country The ISO 3166-1 alpha-2 country code. e.g., `BR` 57 * } 58 */ 59 public function __construct( $user_id, $user_location = false ) { 60 $this->user_id = absint( $user_id ); 61 $this->user_location = $user_location; 62 } 63 64 /** 65 * Get data about events near a particular location. 66 * 67 * If the `user_location` property is set and there are cached events for this 68 * location, these will be immediately returned. 69 * 70 * If not, this method will send a request to the Events API with location data. 71 * The API will send back a recognized location based on the data, along with 72 * nearby events. 73 * 74 * @access public 75 * @since 4.8.0 76 * 77 * @param string $location_search Optional search string to help determine the location. 78 * Default empty string. 79 * @param string $timezone Optional timezone to help determine the location. 80 * Default empty string. 81 * @return array|WP_Error A WP_Error on failure; an array with location and events on 82 * success. 83 */ 84 public function get_events( $location_search = '', $timezone = '' ) { 85 $cached_events = $this->get_cached_events(); 86 87 if ( ! $location_search && $cached_events ) { 88 return $cached_events; 89 } 90 91 $request_url = $this->get_request_url( $location_search, $timezone ); 92 $response = wp_remote_get( $request_url ); 93 $response_code = wp_remote_retrieve_response_code( $response ); 94 $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); 95 $response_error = null; 96 $debugging_info = compact( 'request_url', 'response_code', 'response_body' ); 97 98 if ( is_wp_error( $response ) ) { 99 $response_error = $response; 100 } elseif ( 200 !== $response_code ) { 101 $response_error = new WP_Error( 102 'api-error', 103 /* translators: %s is a numeric HTTP status code; e.g., 400, 403, 500, 504, etc. */ 104 esc_html( sprintf( __( 'Invalid API response code (%d)' ), $response_code ) ) 105 ); 106 } elseif ( ! isset( $response_body['location'], $response_body['events'] ) ) { 107 $response_error = new WP_Error( 108 'api-invalid-response', 109 isset( $response_body['error'] ) ? $response_body['error'] : __( 'Unknown API error.' ) 110 ); 111 } 112 113 if ( is_wp_error( $response_error ) ) { 114 $this->maybe_log_events_response( $response_error->get_error_message(), $debugging_info ); 115 116 return $response_error; 117 } else { 118 $expiration = false; 119 120 if ( isset( $response_body['ttl'] ) ) { 121 $expiration = $response_body['ttl']; 122 unset( $response_body['ttl'] ); 123 } 124 125 $this->cache_events( $response_body, $expiration ); 126 127 $response_body = $this->trim_events( $response_body ); 128 $response_body = $this->format_event_data_time( $response_body ); 129 130 // Avoid bloating the log with all the event data, but keep the count. 131 $debugging_info['response_body']['events'] = count( $debugging_info['response_body']['events'] ) . ' events trimmed.'; 132 133 $this->maybe_log_events_response( 'Valid response received', $debugging_info ); 134 135 return $response_body; 136 } 137 } 138 139 /** 140 * Build a URL for requests to the Events API 141 * 142 * @access protected 143 * @since 4.8.0 144 * 145 * @param string $search City search string. Default empty string. 146 * @param string $timezone Timezone string. Default empty string. 147 * @return string The request URL. 148 */ 149 protected function get_request_url( $search = '', $timezone = '' ) { 150 $api_url = 'https://api.wordpress.org/events/1.0/'; 151 $args = array( 'number' => 5 ); // Get more than three in case some get trimmed out. 152 153 /* 154 * Send the minimal set of necessary arguments in order to increase the 155 * chances of a cache-hit on the API side. 156 */ 157 if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) { 158 $args['latitude'] = $this->user_location['latitude']; 159 $args['longitude'] = $this->user_location['longitude']; 160 } else { 161 $args['ip'] = $this->get_unsafe_client_ip(); 162 $args['locale'] = get_user_locale( $this->user_id ); 163 164 if ( $timezone ) { 165 $args['timezone'] = $timezone; 166 } 167 168 if ( $search ) { 169 $args['location'] = $search; 170 } 171 } 172 173 return add_query_arg( $args, $api_url ); 174 } 175 176 /** 177 * Determine the user's actual IP if possible 178 * 179 * If the user is making their request through a proxy, or if the web server 180 * is behind a proxy, then $_SERVER['REMOTE_ADDR'] will be the proxy address 181 * rather than the user's actual address. 182 * 183 * Modified from http://stackoverflow.com/a/2031935/450127. 184 * 185 * SECURITY WARNING: This function is _NOT_ intended to be used in 186 * circumstances where the authenticity of the IP address matters. This does 187 * _NOT_ guarantee that the returned address is valid or accurate, and it can 188 * be easily spoofed. 189 * 190 * @access protected 191 * @since 4.8.0 192 * 193 * @return false|string `false` on failure, the `string` address on success 194 */ 195 protected function get_unsafe_client_ip() { 196 $client_ip = false; 197 198 // In order of preference, with the best ones for this purpose first. 199 $address_headers = array( 200 'HTTP_CLIENT_IP', 201 'HTTP_X_FORWARDED_FOR', 202 'HTTP_X_FORWARDED', 203 'HTTP_X_CLUSTER_CLIENT_IP', 204 'HTTP_FORWARDED_FOR', 205 'HTTP_FORWARDED', 206 'REMOTE_ADDR', 207 ); 208 209 foreach ( $address_headers as $header ) { 210 if ( array_key_exists( $header, $_SERVER ) ) { 211 /* 212 * HTTP_X_FORWARDED_FOR can contain a chain of comma-separated 213 * addresses. The first one is the original client. It can't be 214 * trusted for authenticity, but we don't need to for this purpose. 215 */ 216 $address_chain = explode( ',', $_SERVER[ $header ] ); 217 $client_ip = trim( $address_chain[0] ); 218 219 break; 220 } 221 } 222 223 return $client_ip; 224 } 225 226 /** 227 * Generate a transient key based on user location 228 * 229 * This could be reduced to a one-liner in the calling functions, but it's 230 * intentionally a separate function because it's called from multiple 231 * functions, and having it abstracted keeps the logic consistent and DRY, 232 * which is less prone to errors. 233 * 234 * @access protected 235 * @since 4.8.0 236 * 237 * @param array $location Should contain 'latitude' and 'longitude' indexes. 238 * @return bool|string `false` on failure, or a string on success 239 */ 240 protected function get_events_transient_key( $location ) { 241 $key = false; 242 243 if ( isset( $location['latitude'], $location['longitude'] ) ) { 244 $key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] ); 245 } 246 247 return $key; 248 } 249 250 /** 251 * Cache an array of events data from the Events API. 252 * 253 * @access protected 254 * @since 4.8.0 255 * 256 * @param array $events Response body from the API request. 257 * @param int|bool $expiration Optional. Amount of time to cache the events. Defaults to false. 258 * @return bool `true` if events were cached; `false` if not. 259 */ 260 protected function cache_events( $events, $expiration = false ) { 261 $set = false; 262 $transient_key = $this->get_events_transient_key( $events['location'] ); 263 $cache_expiration = $expiration ? absint( $expiration ) : HOUR_IN_SECONDS * 12; 264 265 if ( $transient_key ) { 266 $set = set_site_transient( $transient_key, $events, $cache_expiration ); 267 } 268 269 return $set; 270 } 271 272 /** 273 * Get cached events 274 * 275 * @access public 276 * @since 4.8.0 277 * 278 * @return false|array `false` on failure; an array containing `location` 279 * and `events` items on success. 280 */ 281 public function get_cached_events() { 282 $cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) ); 283 $cached_response = $this->trim_events( $cached_response ); 284 285 return $this->format_event_data_time( $cached_response ); 286 } 287 288 /** 289 * Add formatted date and time items for each event in an API response 290 * 291 * This has to be called after the data is pulled from the cache, because 292 * the cached events are shared by all users. If it was called before storing 293 * the cache, then all users would see the events in the localized data/time 294 * of the user who triggered the cache refresh, rather than their own. 295 * 296 * @access protected 297 * @since 4.8.0 298 * 299 * @param array $response_body The response which contains the events. 300 * @return array The response with dates and times formatted 301 */ 302 protected function format_event_data_time( $response_body ) { 303 if ( isset( $response_body['events'] ) ) { 304 foreach ( $response_body['events'] as $key => $event ) { 305 $timestamp = strtotime( $event['date'] ); 306 307 /* 308 * The `date_format` option is not used because it's important 309 * in this context to keep the day of the week in the formatted date, 310 * so that users can tell at a glance if the event is on a day they 311 * are available, without having to open the link. 312 */ 313 /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://secure.php.net/date. */ 314 $response_body['events'][ $key ]['formatted_date'] = date_i18n( __( 'l, M j, Y' ), $timestamp ); 315 $response_body['events'][ $key ]['formatted_time'] = date_i18n( get_option( 'time_format' ), $timestamp ); 316 } 317 } 318 319 return $response_body; 320 } 321 322 /** 323 * Discard events that occurred more than 24 hours ago, then reduce the remaining list down to three items. 324 * 325 * @access protected 326 * @since 4.8.0 327 * 328 * @param array $response_body The response body which contains the events. 329 * @return array The response body with events trimmed. 330 */ 331 protected function trim_events( $response_body ) { 332 if ( isset( $response_body['events'] ) ) { 333 $current_timestamp = current_time('timestamp' ); 334 335 foreach ( $response_body['events'] as $key => $event ) { 336 // Skip WordCamps, because they might be multi-day events. 337 if ( 'meetup' !== $event['type'] ) { 338 continue; 339 } 340 341 $event_timestamp = strtotime( $event['date'] ); 342 343 if ( $current_timestamp > $event_timestamp && ( $current_timestamp - $event_timestamp ) > DAY_IN_SECONDS ) { 344 unset( $response_body['events'][ $key ] ); 345 } 346 } 347 348 $response_body['events'] = array_slice( $response_body['events'], 0, 3 ); 349 } 350 351 return $response_body; 352 } 353 354 355 /** 356 * Log responses to Events API requests 357 * 358 * All responses are logged when debugging, even if they're not WP_Errors. 359 * Debugging info is still needed for "successful" responses, because 360 * the API might have returned a different location than the one the user 361 * intended to receive. In those cases, knowing the exact `request_url` is 362 * critical. 363 * 364 * Errors are logged instead of being triggered, to avoid breaking the JSON 365 * response when called from AJAX handlers and `display_errors` is enabled. 366 * 367 * @access protected 368 * @since 4.8.0 369 * 370 * @param string $message A description of what occurred 371 * @param array $debugging_info Details that provide more context for the 372 * log entry 373 */ 374 protected function maybe_log_events_response( $message, $details ) { 375 if ( ! WP_DEBUG_LOG ) { 376 return; 377 } 378 379 error_log( sprintf( 380 '%s: %s. Details: %s', 381 __METHOD__, 382 trim( $message, '.' ), 383 wp_json_encode( $details ) 384 ) ); 385 } 386 } -
src/wp-admin/includes/dashboard.php
diff --git src/wp-admin/includes/dashboard.php src/wp-admin/includes/dashboard.php index be0b201c9f..4e2b7245f3 100644
function wp_dashboard_setup() { 52 52 wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' ); 53 53 } 54 54 55 // WordPress News56 wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress News' ), 'wp_dashboard_primary' );55 // WordPress Events and News 56 wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' ); 57 57 58 58 if ( is_network_admin() ) { 59 59 … … function wp_dashboard_setup() { 130 130 } 131 131 132 132 /** 133 * Get the community events data that needs to be passed to dashboard.js 134 * 135 * @since 4.8.0 136 * 137 * @return array The script data. 138 */ 139 function get_community_events_script_data() { 140 require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' ); 141 142 $user_id = get_current_user_id(); 143 $user_location = get_user_option( 'community-events-location', $user_id ); 144 $events_client = new WP_Community_Events( $user_id, $user_location ); 145 146 $script_data = array( 147 'cache' => $events_client->get_cached_events(), 148 149 'l10n' => array( 150 'enter_closest_city' => __( 'Enter your closest city name to find nearby events' ), 151 'error_occurred_please_try_again' => __( 'An error occured. Please try again.' ), 152 153 /* 154 * These specific examples were chosen to highlight the fact that a 155 * state is not needed, even for cities whose name is not unique. 156 * It would be too cumbersome to include that in the instructions 157 * to the user, so it's left as an implication. 158 */ 159 /* translators: %s is the name of the city we couldn't locate. Replace the examples with cities in your locale, but test that they match the expected location before including them. Use endonyms (native locale names) whenever possible. */ 160 'could_not_locate_city' => __( "We couldn't locate <em>%s</em>. Please try another nearby city. For example: Kansas City; Springfield; Portland." ), 161 162 // This one is only used with wp.a11y.speak(), so it can/should be more brief. 163 /* translators: %s is the name of a city. */ 164 'city_updated' => __( 'City updated. Listing events near %s.' ), 165 ) 166 ); 167 168 return $script_data; 169 } 170 171 /** 133 172 * Adds a new dashboard widget. 134 173 * 135 174 * @since 2.7.0 … … function wp_dashboard_rss_control( $widget_id, $form_inputs = array() ) { 1069 1108 wp_widget_rss_form( $widget_options[$widget_id], $form_inputs ); 1070 1109 } 1071 1110 1111 1112 /** 1113 * Callback function to render the Events and News dashboard widget 1114 * 1115 * @since 4.8.0 1116 */ 1117 function wp_dashboard_events_news() { 1118 wp_print_community_events_markup(); 1119 1120 ?> 1121 1122 <div class="wordpress-news hide-if-no-js"> 1123 <?php wp_dashboard_primary(); ?> 1124 </div> 1125 1126 <p class="community-events-footer"> 1127 <a href="https://make.wordpress.org/community/meetups-landing-page" target="_blank"> 1128 <?php esc_html_e( 'Meetups' ); ?> <span class="dashicons dashicons-external"></span> 1129 </a> 1130 1131 | 1132 1133 <a href="https://central.wordcamp.org/schedule/" target="_blank"> 1134 <?php esc_html_e( 'WordCamps' ); ?> <span class="dashicons dashicons-external"></span> 1135 </a> 1136 1137 | 1138 1139 <?php // translators: If a Rosetta site exists (e.g. https://es.wordpress.org/news/), then use that. Otherwise, leave untranslated. ?> 1140 <a href="<?php esc_html_e( 'https://wordpress.org/news/' ); ?>" target="_blank"> 1141 <?php esc_html_e( 'News' ); ?> <span class="dashicons dashicons-external"></span> 1142 </a> 1143 </p> 1144 1145 <?php 1146 } 1147 1148 /** 1149 * Print the markup for the Community Events section of the Events and News Dashboard widget 1150 * 1151 * @since 4.8.0 1152 */ 1153 function wp_print_community_events_markup() { 1154 $script_data = get_community_events_script_data(); 1155 1156 ?> 1157 1158 <div class="community-events-errors notice notice-error inline hide-if-js"> 1159 <p class="hide-if-js"> 1160 <?php esc_html_e( 'This widget requires JavaScript.'); ?> 1161 </p> 1162 1163 <p class="community-events-error-occurred" aria-hidden="true"> 1164 <?php echo esc_html( $script_data['l10n']['error_occurred_please_try_again'] ); ?> 1165 </p> 1166 1167 <p class="community-events-could-not-locate" aria-hidden="true"></p> 1168 </div> 1169 1170 <div class="community-events-loading hide-if-no-js"> 1171 <?php esc_html_e( 'Loading…'); ?> 1172 </div> 1173 1174 <?php 1175 /* 1176 * Hide the main element when the page first loads, because the content 1177 * won't be ready until wp.communityEvents.renderEventsTemplate() has run. 1178 */ 1179 ?> 1180 <div id="community-events" class="community-events" aria-hidden="true"> 1181 <div class="activity-block"> 1182 <p> 1183 <span id="community-events-location-message"></span> 1184 1185 <button class="button-link community-events-toggle-location" aria-label="<?php esc_attr_e( 'Edit city'); ?>" aria-expanded="false"> 1186 <span class="dashicons dashicons-edit"></span> 1187 </button> 1188 </p> 1189 1190 <form class="community-events-form" aria-hidden="true" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" method="post"> 1191 <label for="community-events-location"> 1192 <?php esc_html_e( 'City name:' ); ?> 1193 </label> 1194 <?php /* translators: Replace with the name of a city in your locale that shows events. Use only the city name itself, without any region or country. Use the endonym instead of the English name. */ ?> 1195 <input id="community-events-location" class="regular-text" type="text" name="community-events-location" placeholder="<?php esc_attr_e( 'Cincinnati' ); ?>" /> 1196 1197 <?php submit_button( esc_html__( 'Submit' ), 'secondary', 'community-events-submit', false ); ?> 1198 1199 <button class="community-events-cancel button button-link" type="button" aria-expanded="false"> 1200 <?php esc_html_e( 'Cancel' ); ?> 1201 </button> 1202 1203 <span class="spinner"></span> 1204 </form> 1205 </div> 1206 1207 <ul class="community-events-results activity-block last"></ul> 1208 </div> 1209 1210 <?php 1211 } 1212 1213 /** 1214 * Render the events templates for the Event and News widget 1215 * 1216 * @since 4.8.0 1217 */ 1218 function wp_print_community_events_templates() { 1219 $script_data = get_community_events_script_data(); 1220 1221 ?> 1222 1223 <script id="tmpl-community-events-attend-event-near" type="text/template"> 1224 <?php printf( 1225 /* translators: %s is a placeholder for the name of a city. */ 1226 __( 'Attend an upcoming event near <strong>%s</strong>' ), 1227 '{{ data.location.description }}' 1228 ); ?> 1229 </script> 1230 1231 <script id="tmpl-community-events-could-not-locate" type="text/template"> 1232 <?php printf( 1233 $script_data['l10n']['could_not_locate_city'], 1234 '{{data.unknownCity}}' 1235 ); ?> 1236 </script> 1237 1238 <script id="tmpl-community-events-event-list" type="text/template"> 1239 <# _.each( data.events, function( event ) { #> 1240 <li class="event event-{{ event.type }} wp-clearfix"> 1241 <div class="event-info"> 1242 <div class="dashicons event-icon" aria-hidden="true"></div> 1243 <div class="event-info-inner"> 1244 <a class="event-title" href="{{ event.url }}">{{ event.title }}</a> 1245 <span class="event-city">{{ event.location.location }}</span> 1246 </div> 1247 </div> 1248 1249 <div class="event-date-time"> 1250 <span class="event-date">{{ event.formatted_date }}</span> 1251 <# if ( 'meetup' === event.type ) { #> 1252 <span class="event-time">{{ event.formatted_time }}</span> 1253 <# } #> 1254 </div> 1255 </li> 1256 <# } ) #> 1257 </script> 1258 1259 <script id="tmpl-community-events-no-upcoming-events" type="text/template"> 1260 <li class="event-none"> 1261 <?php printf( 1262 /* translators: Replace the URL if a locale-specific one exists */ 1263 __( 'There aren\'t any events scheduled near %s at the moment. Would you like to <a href="https://make.wordpress.org/community/handbook/meetup-organizer/welcome/">organize one</a>?' ), 1264 '{{data.location.description}}' 1265 ); ?> 1266 </li> 1267 </script> 1268 1269 <?php 1270 } 1271 1072 1272 /** 1073 1273 * WordPress News dashboard widget. 1074 1274 * … … function wp_dashboard_primary() { 1105 1305 */ 1106 1306 'title' => apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ), 1107 1307 'items' => 1, 1108 'show_summary' => 1,1308 'show_summary' => 0, 1109 1309 'show_author' => 0, 1110 'show_date' => 1,1310 'show_date' => 0, 1111 1311 ), 1112 1312 'planet' => array( 1113 1313 … … function wp_dashboard_primary() { 1152 1352 ) 1153 1353 ); 1154 1354 1155 if ( ( ! wp_disallow_file_mods( 'dashboard_widget' ) ) && ( ! is_multisite() && is_blog_admin() && current_user_can( 'install_plugins' ) ) || ( is_network_admin() && current_user_can( 'manage_network_plugins' ) && current_user_can( 'install_plugins' ) ) ) {1156 $feeds['plugins'] = array(1157 'link' => '',1158 'url' => array(1159 'popular' => 'http://wordpress.org/plugins/rss/browse/popular/',1160 ),1161 'title' => '',1162 'items' => 1,1163 'show_summary' => 0,1164 'show_author' => 0,1165 'show_date' => 0,1166 );1167 }1168 1169 1355 wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds ); 1170 1356 } 1171 1357 … … function wp_dashboard_primary_output( $widget_id, $feeds ) { 1181 1367 foreach ( $feeds as $type => $args ) { 1182 1368 $args['type'] = $type; 1183 1369 echo '<div class="rss-widget">'; 1184 if ( $type === 'plugins' ) {1185 wp_dashboard_plugins_output( $args['url'], $args );1186 } else {1187 1370 wp_widget_rss_output( $args['url'], $args ); 1188 }1189 1371 echo "</div>"; 1190 1372 } 1191 1373 } 1192 1374 1193 1375 /** 1194 * Display plugins text for the WordPress news widget.1195 *1196 * @since 2.5.01197 *1198 * @param string $rss The RSS feed URL.1199 * @param array $args Array of arguments for this RSS feed.1200 */1201 function wp_dashboard_plugins_output( $rss, $args = array() ) {1202 // Plugin feeds plus link to install them1203 $popular = fetch_feed( $args['url']['popular'] );1204 1205 if ( false === $plugin_slugs = get_transient( 'plugin_slugs' ) ) {1206 $plugin_slugs = array_keys( get_plugins() );1207 set_transient( 'plugin_slugs', $plugin_slugs, DAY_IN_SECONDS );1208 }1209 1210 echo '<ul>';1211 1212 foreach ( array( $popular ) as $feed ) {1213 if ( is_wp_error( $feed ) || ! $feed->get_item_quantity() )1214 continue;1215 1216 $items = $feed->get_items(0, 5);1217 1218 // Pick a random, non-installed plugin1219 while ( true ) {1220 // Abort this foreach loop iteration if there's no plugins left of this type1221 if ( 0 == count($items) )1222 continue 2;1223 1224 $item_key = array_rand($items);1225 $item = $items[$item_key];1226 1227 list($link, $frag) = explode( '#', $item->get_link() );1228 1229 $link = esc_url($link);1230 if ( preg_match( '|/([^/]+?)/?$|', $link, $matches ) )1231 $slug = $matches[1];1232 else {1233 unset( $items[$item_key] );1234 continue;1235 }1236 1237 // Is this random plugin's slug already installed? If so, try again.1238 reset( $plugin_slugs );1239 foreach ( $plugin_slugs as $plugin_slug ) {1240 if ( $slug == substr( $plugin_slug, 0, strlen( $slug ) ) ) {1241 unset( $items[$item_key] );1242 continue 2;1243 }1244 }1245 1246 // If we get to this point, then the random plugin isn't installed and we can stop the while().1247 break;1248 }1249 1250 // Eliminate some common badly formed plugin descriptions1251 while ( ( null !== $item_key = array_rand($items) ) && false !== strpos( $items[$item_key]->get_description(), 'Plugin Name:' ) )1252 unset($items[$item_key]);1253 1254 if ( !isset($items[$item_key]) )1255 continue;1256 1257 $raw_title = $item->get_title();1258 1259 $ilink = wp_nonce_url('plugin-install.php?tab=plugin-information&plugin=' . $slug, 'install-plugin_' . $slug) . '&TB_iframe=true&width=600&height=800';1260 echo '<li class="dashboard-news-plugin"><span>' . __( 'Popular Plugin' ) . ':</span> ' . esc_html( $raw_title ) .1261 ' <a href="' . $ilink . '" class="thickbox open-plugin-details-modal" aria-label="' .1262 /* translators: %s: plugin name */1263 esc_attr( sprintf( __( 'Install %s' ), $raw_title ) ) . '">(' . __( 'Install' ) . ')</a></li>';1264 1265 $feed->__destruct();1266 unset( $feed );1267 }1268 1269 echo '</ul>';1270 }1271 1272 /**1273 1376 * Display file upload quota on dashboard. 1274 1377 * 1275 1378 * Runs on the {@see 'activity_box_end'} hook in wp_dashboard_right_now(). -
src/wp-admin/includes/deprecated.php
diff --git src/wp-admin/includes/deprecated.php src/wp-admin/includes/deprecated.php index 2bf25d3336..76f7709ff2 100644
function wp_dashboard_secondary() {} 1295 1295 function wp_dashboard_secondary_control() {} 1296 1296 1297 1297 /** 1298 * Display plugins text for the WordPress news widget. 1299 * 1300 * @since 2.5.0 1301 * @deprecated 4.8.0 1302 * 1303 * @param string $rss The RSS feed URL. 1304 * @param array $args Array of arguments for this RSS feed. 1305 */ 1306 function wp_dashboard_plugins_output( $rss, $args = array() ) { 1307 _deprecated_function( __FUNCTION__, '4.8.0' ); 1308 1309 // Plugin feeds plus link to install them 1310 $popular = fetch_feed( $args['url']['popular'] ); 1311 1312 if ( false === $plugin_slugs = get_transient( 'plugin_slugs' ) ) { 1313 $plugin_slugs = array_keys( get_plugins() ); 1314 set_transient( 'plugin_slugs', $plugin_slugs, DAY_IN_SECONDS ); 1315 } 1316 1317 echo '<ul>'; 1318 1319 foreach ( array( $popular ) as $feed ) { 1320 if ( is_wp_error( $feed ) || ! $feed->get_item_quantity() ) 1321 continue; 1322 1323 $items = $feed->get_items(0, 5); 1324 1325 // Pick a random, non-installed plugin 1326 while ( true ) { 1327 // Abort this foreach loop iteration if there's no plugins left of this type 1328 if ( 0 == count($items) ) 1329 continue 2; 1330 1331 $item_key = array_rand($items); 1332 $item = $items[$item_key]; 1333 1334 list($link, $frag) = explode( '#', $item->get_link() ); 1335 1336 $link = esc_url($link); 1337 if ( preg_match( '|/([^/]+?)/?$|', $link, $matches ) ) 1338 $slug = $matches[1]; 1339 else { 1340 unset( $items[$item_key] ); 1341 continue; 1342 } 1343 1344 // Is this random plugin's slug already installed? If so, try again. 1345 reset( $plugin_slugs ); 1346 foreach ( $plugin_slugs as $plugin_slug ) { 1347 if ( $slug == substr( $plugin_slug, 0, strlen( $slug ) ) ) { 1348 unset( $items[$item_key] ); 1349 continue 2; 1350 } 1351 } 1352 1353 // If we get to this point, then the random plugin isn't installed and we can stop the while(). 1354 break; 1355 } 1356 1357 // Eliminate some common badly formed plugin descriptions 1358 while ( ( null !== $item_key = array_rand($items) ) && false !== strpos( $items[$item_key]->get_description(), 'Plugin Name:' ) ) 1359 unset($items[$item_key]); 1360 1361 if ( !isset($items[$item_key]) ) 1362 continue; 1363 1364 $raw_title = $item->get_title(); 1365 1366 $ilink = wp_nonce_url('plugin-install.php?tab=plugin-information&plugin=' . $slug, 'install-plugin_' . $slug) . '&TB_iframe=true&width=600&height=800'; 1367 echo '<li class="dashboard-news-plugin"><span>' . __( 'Popular Plugin' ) . ':</span> ' . esc_html( $raw_title ) . 1368 ' <a href="' . $ilink . '" class="thickbox open-plugin-details-modal" aria-label="' . 1369 /* translators: %s: plugin name */ 1370 esc_attr( sprintf( __( 'Install %s' ), $raw_title ) ) . '">(' . __( 'Install' ) . ')</a></li>'; 1371 1372 $feed->__destruct(); 1373 unset( $feed ); 1374 } 1375 1376 echo '</ul>'; 1377 } 1378 1379 /** 1380 * noop the Nearby WordPress Events feature plugin bootstrap process 1381 * 1382 * This function was never used in Core, but it's necessary to prevent the 1383 * feature plugin from bootstrapping now that its functionality has been merged 1384 * into Core. 1385 * 1386 * See https://plugins.trac.wordpress.org/browser/nearby-wp-events/tags/0.7/nearby-wordpress-events.php?marks=58-87#L55 1387 * 1388 * @deprecated 4.8.0 1389 */ 1390 function wp_get_nearby_events() {} 1391 1392 /** 1298 1393 * This was once used to move child posts to a new parent. 1299 1394 * 1300 1395 * @since 2.3.0 -
src/wp-admin/index.php
diff --git src/wp-admin/index.php src/wp-admin/index.php index d2d7ec889d..e2dde48aaa 100644
require_once(ABSPATH . 'wp-admin/includes/dashboard.php'); 15 15 wp_dashboard_setup(); 16 16 17 17 wp_enqueue_script( 'dashboard' ); 18 wp_localize_script( 'dashboard', 'communityEventsData', get_community_events_script_data() ); 19 18 20 if ( current_user_can( 'edit_theme_options' ) ) 19 21 wp_enqueue_script( 'customize-loader' ); 20 22 if ( current_user_can( 'install_plugins' ) ) { … … include( ABSPATH . 'wp-admin/admin-header.php' ); 138 140 </div><!-- wrap --> 139 141 140 142 <?php 143 wp_print_community_events_templates(); 144 141 145 require( ABSPATH . 'wp-admin/admin-footer.php' ); -
src/wp-admin/js/dashboard.js
diff --git src/wp-admin/js/dashboard.js src/wp-admin/js/dashboard.js index fa100dd16c..a6a5662871 100644
jQuery(document).ready( function($) { 187 187 } 188 188 189 189 } ); 190 191 192 wp.communityEvents = wp.communityEvents || {}; 193 194 jQuery( function( $ ) { 195 'use strict'; 196 197 var app = wp.communityEvents = { 198 initialized: false, 199 model: null, 200 201 /** 202 * Main entry point 203 * 204 * @since 4.8.0 205 */ 206 init: function() { 207 if ( app.initialized ) { 208 return; 209 } 210 211 var $container = $( '#community-events' ); 212 213 /* 214 * When JavaScript is disabled, the errors container is shown, so 215 * that "This widget requires Javascript" message can be seen. 216 * 217 * When JS is enabled, the container is hidden at first, and then 218 * revealed during the template rendering, if there actually are 219 * errors to show. 220 * 221 * The display indicator switches from `hide-if-js` to `aria-hidden` 222 * here in order to maintain consistency with all the other fields 223 * that key off of `aria-hidden` to determine their visibility. 224 * `aria-hidden` can't be used initially, because there would be no 225 * way to set it to false when JavaScript is disabled, which would 226 * prevent people from seeing the "This widget requires JavaScript" 227 * message. 228 */ 229 $( '.community-events-errors' ) 230 .attr( 'aria-hidden', true ) 231 .removeClass( 'hide-if-js' ); 232 233 $container.on( 'click', '.community-events-toggle-location', app.toggleLocationForm ); 234 $container.on( 'click', '.community-events-cancel', app.toggleLocationForm ); 235 236 $container.on( 'submit', '.community-events-form', function( event ) { 237 event.preventDefault(); 238 239 app.getEvents( { 240 location: $( '#community-events-location' ).val() 241 }); 242 }); 243 244 if ( communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events ) { 245 app.renderEventsTemplate( communityEventsData.cache, 'app' ); 246 } else { 247 app.getEvents(); 248 } 249 250 app.initialized = true; 251 }, 252 253 /** 254 * Toggle the visibility of the Edit Location form 255 * 256 * @since 4.8.0 257 * 258 * @param {event|string} action 'show' or 'hide' to specify a state; 259 * Or an event object to flip between states 260 */ 261 toggleLocationForm: function( action ) { 262 var $toggleButton = $( '.community-events-toggle-location' ), 263 $cancelButton = $( '.community-events-cancel' ), 264 $form = $( '.community-events-form' ); 265 266 if ( 'object' === typeof action ) { 267 // Strict comparison doesn't work in this case. 268 action = 'true' == $toggleButton.attr( 'aria-expanded' ) ? 'hide' : 'show'; 269 } 270 271 if ( 'hide' === action ) { 272 $toggleButton.attr( 'aria-expanded', false ); 273 $cancelButton.attr( 'aria-expanded', false ); 274 $form.attr( 'aria-hidden', true ); 275 } else { 276 $toggleButton.attr( 'aria-expanded', true ); 277 $cancelButton.attr( 'aria-expanded', true ); 278 $form.attr( 'aria-hidden', false ); 279 } 280 }, 281 282 /** 283 * Send Ajax request to fetch events for the widget 284 * 285 * @since 4.8.0 286 * 287 * @param {object} requestParams 288 */ 289 getEvents: function( requestParams ) { 290 var initiatedBy, 291 app = this, 292 $spinner = $( '.community-events-form' ).children( '.spinner' ), 293 dashboardLoadPromise = wp.api.init( { 'versionString': 'wp/dashboard/v1/' } ); 294 295 requestParams = requestParams || {}; 296 requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : ''; 297 298 initiatedBy = requestParams.location ? 'user' : 'app'; 299 300 $spinner.addClass( 'is-active' ); 301 302 dashboardLoadPromise.done( function() { 303 if ( ! app.model ) { 304 app.model = new wp.api.models.CommunityEventsMe(); 305 } 306 307 $.when( app.model.fetch( { data: requestParams } ) ) 308 .always( function() { 309 $spinner.removeClass( 'is-active' ); 310 }) 311 312 .done( function( response ) { 313 if ( 'no_location_available' === response.error ) { 314 if ( requestParams.location ) { 315 response.unknownCity = requestParams.location; 316 } else { 317 /* 318 * No location was passed, which means that this was an automatic query 319 * based on IP, locale, and timezone. Since the user didn't initiate it, 320 * it should fail silently. Otherwise, the error could confuse and/or 321 * annoy them. 322 */ 323 delete response.error; 324 } 325 } 326 app.renderEventsTemplate( response, initiatedBy ); 327 }) 328 329 .fail( function() { 330 app.renderEventsTemplate( { 331 'location' : false, 332 'error' : true 333 }, initiatedBy ); 334 }); 335 }); 336 }, 337 338 /** 339 * Render the template for the Events section of the Events & News widget 340 * 341 * @since 4.8.0 342 * 343 * @param {Object} templateParams The various parameters that will get passed to wp.template 344 * @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user; 345 * 'app' to indicate it was triggered automatically by the app itself. 346 */ 347 renderEventsTemplate: function( templateParams, initiatedBy ) { 348 var template, 349 elementVisibility, 350 l10nPlaceholder = /%(?:\d\$)?s/g, // Match `%s`, `%1$s`, `%2$s`, etc. 351 $locationMessage = $( '#community-events-location-message' ), 352 $results = $( '.community-events-results' ); 353 354 /* 355 * Hide all toggleable elements by default, to keep the logic simple. 356 * Otherwise, each block below would have to turn hide everything that 357 * could have been shown at an earlier point. 358 * 359 * The exception to that is that the .community-events container. It's hidden 360 * when the page is first loaded, because the content isn't ready yet, 361 * but once we've reached this point, it should always be shown. 362 */ 363 elementVisibility = { 364 '.community-events' : true, 365 '.community-events-loading' : false, 366 '.community-events-errors' : false, 367 '.community-events-error-occurred' : false, 368 '.community-events-could-not-locate' : false, 369 '#community-events-location-message' : false, 370 '.community-events-toggle-location' : false, 371 '.community-events-results' : false 372 }; 373 374 /* 375 * Determine which templates should be rendered and which elements 376 * should be displayed 377 */ 378 if ( templateParams.location && templateParams.location.description ) { 379 template = wp.template( 'community-events-attend-event-near' ); 380 $locationMessage.html( template( templateParams ) ); 381 382 if ( templateParams.events.length ) { 383 template = wp.template( 'community-events-event-list' ); 384 $results.html( template( templateParams ) ); 385 } else { 386 template = wp.template( 'community-events-no-upcoming-events' ); 387 $results.html( template( templateParams ) ); 388 } 389 wp.a11y.speak( communityEventsData.l10n.city_updated.replace( l10nPlaceholder, templateParams.location.description ) ); 390 391 elementVisibility['#community-events-location-message'] = true; 392 elementVisibility['.community-events-toggle-location'] = true; 393 elementVisibility['.community-events-results'] = true; 394 395 } else if ( templateParams.unknownCity ) { 396 template = wp.template( 'community-events-could-not-locate' ); 397 $( '.community-events-could-not-locate' ).html( template( templateParams ) ); 398 wp.a11y.speak( communityEventsData.l10n.could_not_locate_city.replace( l10nPlaceholder, templateParams.unknownCity ) ); 399 400 elementVisibility['.community-events-errors'] = true; 401 elementVisibility['.community-events-could-not-locate'] = true; 402 403 } else if ( templateParams.error && 'user' === initiatedBy ) { 404 /* 405 * Errors messages are only shown for requests that were initiated 406 * by the user, not for ones that were initiated by the app itself. 407 * Showing error messages for an event that user isn't aware of 408 * could be confusing or unnecessarily distracting. 409 */ 410 wp.a11y.speak( communityEventsData.l10n.error_occurred_please_try_again ); 411 412 elementVisibility['.community-events-errors'] = true; 413 elementVisibility['.community-events-error-occurred'] = true; 414 415 } else { 416 $locationMessage.text( communityEventsData.l10n.enter_closest_city ); 417 418 elementVisibility['#community-events-location-message'] = true; 419 elementVisibility['.community-events-toggle-location'] = true; 420 } 421 422 // Set the visibility of toggleable elements. 423 _.each( elementVisibility, function( isVisible, element ) { 424 $( element ).attr( 'aria-hidden', ! isVisible ); 425 }); 426 427 $( '.community-events-toggle-location' ).attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] ); 428 429 /* 430 * During the initial page load, the location form should be hidden 431 * by default if the user has saved a valid location during a previous 432 * session. It's safe to assume that they want to continue using that 433 * location, and displaying the form would unnecessarily clutter the 434 * widget. 435 */ 436 if ( 'app' === initiatedBy && templateParams.location.description ) { 437 app.toggleLocationForm( 'hide' ); 438 } else { 439 app.toggleLocationForm( 'show' ); 440 } 441 } 442 }; 443 444 if ( $( '#dashboard_primary' ).is( ':visible' ) ) { 445 app.init(); 446 } else { 447 $( document ).on( 'postbox-toggled', function( event, postbox ) { 448 var $postbox = $( postbox ); 449 450 if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) { 451 app.init(); 452 } 453 }); 454 } 455 }); -
src/wp-admin/network/index.php
diff --git src/wp-admin/network/index.php src/wp-admin/network/index.php index 81ededbe37..e3a2c03ad7 100644
get_current_screen()->set_help_sidebar( 54 54 wp_dashboard_setup(); 55 55 56 56 wp_enqueue_script( 'dashboard' ); 57 wp_localize_script( 'dashboard', 'communityEventsData', get_community_events_script_data() ); 57 58 wp_enqueue_script( 'plugin-install' ); 58 59 add_thickbox(); 59 60 … … require_once( ABSPATH . 'wp-admin/admin-header.php' ); 73 74 74 75 </div><!-- wrap --> 75 76 76 <?php include( ABSPATH . 'wp-admin/admin-footer.php' ); ?> 77 <?php 78 wp_print_community_events_templates(); 79 include( ABSPATH . 'wp-admin/admin-footer.php' ); -
src/wp-includes/rest-api.php
diff --git src/wp-includes/rest-api.php src/wp-includes/rest-api.php index e6cdc3a959..bb7979af12 100644
function create_initial_rest_routes() { 237 237 // Settings. 238 238 $controller = new WP_REST_Settings_Controller; 239 239 $controller->register_routes(); 240 241 // Dashboard. 242 register_rest_route( 243 'wp/dashboard/v1', 244 '/community-events/me', 245 array( 246 'methods' => WP_REST_Server::READABLE, 247 'callback' => 'rest_get_community_events', 248 'permission_callback' => 'rest_get_community_events_permissions_check', 249 250 'args' => array( 251 'location' => array( 'validate_callback' => 'sanitize_text_field' ), 252 'timezone' => array( 'validate_callback' => 'sanitize_text_field' ), 253 ), 254 ) 255 ); 256 } 257 258 /** 259 * Checks if a given request has access to get community events. 260 * 261 * @return bool 262 */ 263 function rest_get_community_events_permissions_check() { 264 return current_user_can( 'read' ); 265 } 266 267 /** 268 * Retrieves nearby events. 269 * 270 * @since 4.8.0 271 * 272 * @param WP_REST_Request $request Full details about the request. 273 * @return array|WP_Error WP_REST_Response on success, or WP_Error object on failure. 274 */ 275 function rest_get_community_events( $request ) { 276 $user_id = get_current_user_id(); 277 278 if ( empty( $user_id ) ) { 279 return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) ); 280 } 281 282 require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' ); 283 284 $saved_location = get_user_option( 'community-events-location', $user_id ); 285 $events_client = new WP_Community_Events( $user_id, $saved_location ); 286 $events = $events_client->get_events( $request['location'], $request['timezone'] ); 287 288 // Store the location network-wide, so the user doesn't have to set it on each site. 289 if ( ! is_wp_error( $events ) && isset( $events['location'] ) ) { 290 update_user_option( $user_id, 'community-events-location', $events['location'], true ); 291 } 292 293 return $events; 240 294 } 241 295 242 296 /** -
src/wp-includes/script-loader.php
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php index def438c260..8bcfa03d93 100644
function wp_default_scripts( &$scripts ) { 724 724 'current' => __( 'Current Color' ), 725 725 ) ); 726 726 727 $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox' ), false, 1 );727 $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-api', 'wp-util', 'wp-a11y' ), false, 1 ); 728 728 729 729 $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" ); 730 730 -
new file tests/phpunit/tests/admin/includesCommunityEvents.php
diff --git tests/phpunit/tests/admin/includesCommunityEvents.php tests/phpunit/tests/admin/includesCommunityEvents.php new file mode 100644 index 0000000000..f0a0a7eafe
- + 1 <?php 2 /** 3 * Unit tests for methods in WP_Community_Events. 4 * 5 * @package WordPress 6 * @subpackage UnitTests 7 * @since 4.8.0 8 */ 9 10 /** 11 * Class Test_WP_Community_Events. 12 * 13 * @group admin 14 * @group community-events 15 * 16 * @since 4.8.0 17 */ 18 class Test_WP_Community_Events extends WP_UnitTestCase { 19 /** 20 * An instance of the class to test. 21 * 22 * @access private 23 * @since 4.8.0 24 * 25 * @var WP_Community_Events 26 */ 27 private $instance; 28 29 /** 30 * Perform for every test. 31 * 32 * @access public 33 * @since 4.8.0 34 */ 35 public function setUp() { 36 parent::setUp(); 37 38 require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' ); 39 40 $this->instance = new WP_Community_Events( 1, $this->get_user_location() ); 41 } 42 43 /** 44 * Simulate a stored user location. 45 * 46 * @access private 47 * @since 4.8.0 48 * 49 * @return array The mock location. 50 */ 51 private function get_user_location() { 52 return array( 53 'description' => 'San Francisco', 54 'latitude' => '37.7749300', 55 'longitude' => '-122.4194200', 56 'country' => 'US', 57 ); 58 } 59 60 /** 61 * Test: `get_events()` should return an instance of WP_Error if the response code is not 200. 62 * 63 * @access public 64 * @since 4.8.0 65 */ 66 public function test_get_events_bad_response_code() { 67 add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 68 69 $this->assertWPError( $this->instance->get_events() ); 70 71 remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 72 } 73 74 /** 75 * Test: The response body should not be cached if the response code is not 200. 76 * 77 * @access public 78 * @since 4.8.0 79 */ 80 public function test_get_cached_events_bad_response_code() { 81 add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 82 83 $this->instance->get_events(); 84 85 $this->assertFalse( $this->instance->get_cached_events() ); 86 87 remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 88 } 89 90 /** 91 * Simulate an HTTP response with a non-200 response code. 92 * 93 * @access public 94 * @since 4.8.0 95 * 96 * @return array A mock response with a 404 HTTP status code 97 */ 98 public function _http_request_bad_response_code() { 99 return array( 100 'headers' => '', 101 'body' => '', 102 'response' => array( 103 'code' => 404, 104 ), 105 'cookies' => '', 106 'filename' => '', 107 ); 108 } 109 110 /** 111 * Test: `get_events()` should return an instance of WP_Error if the response body does not have 112 * the required properties. 113 * 114 * @access public 115 * @since 4.8.0 116 */ 117 public function test_get_events_invalid_response() { 118 add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 119 120 $this->assertWPError( $this->instance->get_events() ); 121 122 remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 123 } 124 125 /** 126 * Test: The response body should not be cached if it does not have the required properties. 127 * 128 * @access public 129 * @since 4.8.0 130 */ 131 public function test_get_cached_events_invalid_response() { 132 add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 133 134 $this->instance->get_events(); 135 136 $this->assertFalse( $this->instance->get_cached_events() ); 137 138 remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 139 } 140 141 /** 142 * Simulate an HTTP response with a body that does not have the required properties. 143 * 144 * @access public 145 * @since 4.8.0 146 * 147 * @return array A mock response that's missing required properties. 148 */ 149 public function _http_request_invalid_response() { 150 return array( 151 'headers' => '', 152 'body' => wp_json_encode( array() ), 153 'response' => array( 154 'code' => 200, 155 ), 156 'cookies' => '', 157 'filename' => '', 158 ); 159 } 160 161 /** 162 * Test: With a valid response, `get_events()` should return an associated array containing a location array and 163 * an events array with individual events that have formatted time and date. 164 * 165 * @access public 166 * @since 4.8.0 167 */ 168 public function test_get_events_valid_response() { 169 add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 170 171 $response = $this->instance->get_events(); 172 173 $this->assertNotWPError( $response ); 174 $this->assertEqualSetsWithIndex( $this->get_user_location(), $response['location'] ); 175 $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $response['events'][0]['formatted_date'] ); 176 $this->assertEquals( '1:00 pm', $response['events'][0]['formatted_time'] ); 177 178 remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 179 } 180 181 /** 182 * Test: `get_cached_events()` should return the same data as `get_events()`, including formatted time 183 * and date values for each event. 184 * 185 * @access public 186 * @since 4.8.0 187 */ 188 public function test_get_cached_events_valid_response() { 189 add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 190 191 $this->instance->get_events(); 192 193 $cached_events = $this->instance->get_cached_events(); 194 195 $this->assertNotWPError( $cached_events ); 196 $this->assertEqualSetsWithIndex( $this->get_user_location(), $cached_events['location'] ); 197 $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $cached_events['events'][0]['formatted_date'] ); 198 $this->assertEquals( '1:00 pm', $cached_events['events'][0]['formatted_time'] ); 199 200 remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 201 } 202 203 /** 204 * Simulate an HTTP response with valid location and event data. 205 * 206 * @access public 207 * @since 4.8.0 208 * 209 * @return array A mock HTTP response with valid data. 210 */ 211 public function _http_request_valid_response() { 212 return array( 213 'headers' => '', 214 'body' => wp_json_encode( array( 215 'location' => $this->get_user_location(), 216 'events' => array( 217 array( 218 'type' => 'meetup', 219 'title' => 'Flexbox + CSS Grid: Magic for Responsive Layouts', 220 'url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/', 221 'meetup' => 'The East Bay WordPress Meetup Group', 222 'meetup_url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/', 223 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Sunday 1pm' ) ), 224 'location' => array( 225 'location' => 'Oakland, CA, USA', 226 'country' => 'us', 227 'latitude' => 37.808453, 228 'longitude' => -122.26593, 229 ), 230 ), 231 array( 232 'type' => 'meetup', 233 'title' => 'Part 3- Site Maintenance - Tools to Make It Easy', 234 'url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/', 235 'meetup' => 'WordPress Bay Area Foothills Group', 236 'meetup_url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/', 237 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Wednesday 1:30pm' ) ), 238 'location' => array( 239 'location' => 'Milpitas, CA, USA', 240 'country' => 'us', 241 'latitude' => 37.432813, 242 'longitude' => -121.907095, 243 ), 244 ), 245 array( 246 'type' => 'wordcamp', 247 'title' => 'WordCamp Kansas City', 248 'url' => 'https://2017.kansascity.wordcamp.org', 249 'meetup' => null, 250 'meetup_url' => null, 251 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Saturday' ) ), 252 'location' => array( 253 'location' => 'Kansas City, MO', 254 'country' => 'US', 255 'latitude' => 39.0392325, 256 'longitude' => -94.577076, 257 ), 258 ), 259 ), 260 ) ), 261 'response' => array( 262 'code' => 200, 263 ), 264 'cookies' => '', 265 'filename' => '', 266 ); 267 } 268 }