Ticket #40702: 40702-ajax.diff
File 40702-ajax.diff, 56.4 KB (added by , 7 years ago) |
---|
-
src/wp-admin/admin-ajax.php
diff --git src/wp-admin/admin-ajax.php src/wp-admin/admin-ajax.php index e0f4464d94..3213d55028 100644
$core_actions_post = array( 64 64 'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post', 65 65 'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin', 66 66 'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme', 67 'install-theme', 'get-post-thumbnail-html', 67 'install-theme', 'get-post-thumbnail-html', 'get-community-events', 68 68 ); 69 69 70 70 // Deprecated -
src/wp-admin/css/dashboard.css
diff --git src/wp-admin/css/dashboard.css src/wp-admin/css/dashboard.css index 98fb99378d..6a35594302 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: 15px 0 5px; 341 } 342 343 .community-events-form .regular-text { 344 width: 40%; 345 height: 28px; 346 } 347 348 .community-events li.event-none { 349 border-left: 4px solid #0070AE; 350 } 351 352 .community-events-form label { 353 display: inline-block; 354 padding-bottom: 3px; 355 } 356 357 .community-events .activity-block > p { 358 margin-bottom: 0; 359 display: inline; 360 } 361 362 #community-events-submit { 363 margin-left: 2px; 364 } 365 366 .community-events .button-link:hover, 367 .community-events .button-link:active { 368 color: #00a0d2; 369 } 370 371 .community-events-cancel.button.button-link { 372 color: #0073aa; 373 text-decoration: underline; 374 margin-left: 2px; 375 } 376 377 .community-events ul { 378 background-color: #fafafa; 379 padding-left: 0; 380 padding-right: 0; 381 padding-bottom: 0; 382 } 383 384 .community-events li { 385 margin: 0; 386 padding: 8px 12px; 387 color: #72777c; 388 } 389 .community-events li:first-child { 390 border-top: 1px solid #eee; 391 } 392 393 .community-events li ~ li { 394 border-top: 1px solid #eee; 395 } 396 397 .community-events .activity-block.last { 398 border-bottom: 1px solid #eee; 399 padding-top: 0; 400 margin-top: -1px; 401 } 402 403 .community-events .event-info { 404 display: block; 405 } 406 407 .event-icon { 408 height: 18px; 409 padding-right: 10px; 410 width: 18px; 411 display: none; /* Hide on smaller screens */ 412 } 413 414 .event-icon:before { 415 color: #82878C; 416 font-size: 18px; 417 } 418 .event-meetup .event-icon:before { 419 content: "\f484"; 420 } 421 .event-wordcamp .event-icon:before { 422 content: "\f486"; 423 } 424 425 .community-events .event-title { 426 font-weight: 600; 427 display: block; 428 } 429 430 .community-events .event-date, 431 .community-events .event-time { 432 display: block; 433 } 434 435 .community-events-footer { 436 margin-top: 0; 437 margin-bottom: 0; 438 padding: 12px; 439 border-top: 1px solid #eee; 440 color: #ddd; 441 } 442 304 443 /* Dashboard WordPress news */ 305 444 306 445 #dashboard_primary .inside { … … body #dashboard-widgets .postbox form .submit { 333 472 } 334 473 335 474 #dashboard_primary .rss-widget { 336 border-bottom: 1px solid #eee;337 475 font-size: 13px; 338 padding: 8px 12px 10px;476 padding: 0 12px 0; 339 477 } 340 478 341 479 #dashboard_primary .rss-widget:last-child { … … body #dashboard-widgets .postbox form .submit { 357 495 } 358 496 359 497 #dashboard_primary .rss-widget ul li { 360 margin-bottom: 8px; 498 padding: 4px 0; 499 margin: 0; 361 500 } 362 501 363 502 /* Dashboard right now */ … … form.initial-form.quickpress-open input#title { 874 1013 } 875 1014 876 1015 a.rsswidget { 877 font-size: 1 4px;1016 font-size: 13px; 878 1017 font-weight: 600; 879 line-height: 1. 7em;1018 line-height: 1.4em; 880 1019 } 881 1020 882 1021 .rss-widget ul li { … … a.rsswidget { 1087 1226 width: 30px; 1088 1227 margin: 4px 10px 5px 0; 1089 1228 } 1229 1230 .community-events-toggle-location { 1231 height: 38px; 1232 } 1233 1234 .community-events-form .regular-text { 1235 height: 31px; 1236 } 1090 1237 } 1091 1238 1092 1239 /* Smartphone */ … … a.rsswidget { 1110 1257 left: -35px; 1111 1258 } 1112 1259 } 1260 1261 @media screen and (min-width: 355px) { 1262 .community-events .event-info { 1263 display: table-row; 1264 float: left; 1265 max-width: 59%; 1266 } 1267 1268 .event-icon, 1269 .event-icon[aria-hidden="true"] { 1270 display: table-cell; 1271 } 1272 1273 .event-info-inner { 1274 display: table-cell; 1275 } 1276 1277 .community-events .event-date-time { 1278 float: right; 1279 max-width: 39%; 1280 } 1281 1282 .community-events .event-date, 1283 .community-events .event-time { 1284 text-align: right; 1285 } 1286 } -
src/wp-admin/includes/ajax-actions.php
diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php index 3342cad375..a2e829bf65 100644
function wp_ajax_autocomplete_user() { 297 297 } 298 298 299 299 /** 300 * Handles AJAX requests for community events 301 * 302 * @since 4.8.0 303 */ 304 function wp_ajax_get_community_events() { 305 require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' ); 306 307 check_ajax_referer( 'community_events' ); 308 309 $search = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : ''; 310 $timezone = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : ''; 311 $user_id = get_current_user_id(); 312 $saved_location = get_user_option( 'community-events-location', $user_id ); 313 $events_client = new WP_Community_Events( $user_id, $saved_location ); 314 $events = $events_client->get_events( $search, $timezone ); 315 316 if ( is_wp_error( $events ) ) { 317 wp_send_json_error( array( 318 'error' => $events->get_error_message(), 319 ) ); 320 } else { 321 if ( isset( $events['location'] ) ) { 322 // Store the location network-wide, so the user doesn't have to set it on each site. 323 update_user_option( $user_id, 'community-events-location', $events['location'], true ); 324 } 325 326 wp_send_json_success( $events ); 327 } 328 } 329 330 /** 300 331 * Ajax handler for dashboard widgets. 301 332 * 302 333 * @since 3.4.0 -
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..6d85524457
- + 1 <?php 2 /** 3 * Administration: Community Events class 4 * 5 * @package WordPress 6 * @subpackage Administration 7 * @since 4.8.0 8 */ 9 10 /** 11 * Class WP_Community_Events 12 * 13 * A client for api.wordpress.org/events. 14 * 15 * @since 4.8.0 16 */ 17 class WP_Community_Events { 18 /** 19 * ID for a WordPress user account. 20 * 21 * @access protected 22 * @since 4.8.0 23 * 24 * @var int 25 */ 26 protected $user_id = 0; 27 28 /** 29 * Stores location data for the user. 30 * 31 * @access protected 32 * @since 4.8.0 33 * 34 * @var bool|array 35 */ 36 protected $user_location = false; 37 38 /** 39 * Constructor for WP_Community_Events. 40 * 41 * @access public 42 * @since 4.8.0 43 * 44 * @param int $user_id WP user ID. 45 * @param bool|array $user_location Stored location data for the user. 46 * `false` to pass no location; 47 * `array` to pass a location { 48 * @type string $description The name of the location 49 * @type string $latitude The latitude in decimal degrees notation, without the degree 50 * symbol. e.g., `47.615200`. 51 * @type string $longitude The longitude in decimal degrees notation, without the degree 52 * symbol. e.g., `-122.341100`. 53 * @type string $country The ISO 3166-1 alpha-2 country code. e.g., `BR` 54 * } 55 */ 56 public function __construct( $user_id, $user_location = false ) { 57 $this->user_id = absint( $user_id ); 58 $this->user_location = $user_location; 59 } 60 61 /** 62 * Gets data about events near a particular location. 63 * 64 * Cached events will be immediately returned if the `user_location` property 65 * is set for the current user, and cached events exist for that location. 66 * 67 * Otherwise, this method sends a request to the w.org Events API with location 68 * data. The API will send back a recognized location based on the data, along 69 * with nearby events. 70 * 71 * @access public 72 * @since 4.8.0 73 * 74 * @param string $location_search Optional city name to help determine the location. 75 * e.g., "Seattle". Default empty string. 76 * @param string $timezone Optional timezone to help determine the location. 77 * Default empty string. 78 * @return array|WP_Error A WP_Error on failure; an array with location and events on 79 * success. 80 */ 81 public function get_events( $location_search = '', $timezone = '' ) { 82 $cached_events = $this->get_cached_events(); 83 84 if ( ! $location_search && $cached_events ) { 85 return $cached_events; 86 } 87 88 $request_url = $this->get_request_url( $location_search, $timezone ); 89 $response = wp_remote_get( $request_url ); 90 $response_code = wp_remote_retrieve_response_code( $response ); 91 $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); 92 $response_error = null; 93 $debugging_info = compact( 'request_url', 'response_code', 'response_body' ); 94 95 if ( is_wp_error( $response ) ) { 96 $response_error = $response; 97 } elseif ( 200 !== $response_code ) { 98 $response_error = new WP_Error( 99 'api-error', 100 /* translators: %s is a numeric HTTP status code; e.g., 400, 403, 500, 504, etc. */ 101 sprintf( __( 'Invalid API response code (%d)' ), $response_code ) 102 ); 103 } elseif ( ! isset( $response_body['location'], $response_body['events'] ) ) { 104 $response_error = new WP_Error( 105 'api-invalid-response', 106 isset( $response_body['error'] ) ? $response_body['error'] : __( 'Unknown API error.' ) 107 ); 108 } 109 110 if ( is_wp_error( $response_error ) ) { 111 $this->maybe_log_events_response( $response_error->get_error_message(), $debugging_info ); 112 113 return $response_error; 114 } else { 115 $expiration = false; 116 117 if ( isset( $response_body['ttl'] ) ) { 118 $expiration = $response_body['ttl']; 119 unset( $response_body['ttl'] ); 120 } 121 122 $this->cache_events( $response_body, $expiration ); 123 124 $response_body = $this->trim_events( $response_body ); 125 $response_body = $this->format_event_data_time( $response_body ); 126 127 // Avoid bloating the log with all the event data, but keep the count. 128 $debugging_info['response_body']['events'] = count( $debugging_info['response_body']['events'] ) . ' events trimmed.'; 129 130 $this->maybe_log_events_response( 'Valid response received', $debugging_info ); 131 132 return $response_body; 133 } 134 } 135 136 /** 137 * Builds a URL for requests to the w.org Events API 138 * 139 * @access protected 140 * @since 4.8.0 141 * 142 * @param string $search City search string. Default empty string. 143 * @param string $timezone Timezone string. Default empty string. 144 * @return string The request URL. 145 */ 146 protected function get_request_url( $search = '', $timezone = '' ) { 147 $api_url = 'https://api.wordpress.org/events/1.0/'; 148 $args = array( 'number' => 5 ); // Get more than three in case some get trimmed out. 149 150 /* 151 * Send the minimal set of necessary arguments, in order to increase the 152 * chances of a cache-hit on the API side. 153 */ 154 if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) { 155 $args['latitude'] = $this->user_location['latitude']; 156 $args['longitude'] = $this->user_location['longitude']; 157 } else { 158 $args['locale'] = get_user_locale( $this->user_id ); 159 160 if ( $timezone ) { 161 $args['timezone'] = $timezone; 162 } 163 164 if ( $search ) { 165 $args['location'] = $search; 166 } else { 167 /* 168 * Protect the user's privacy by anonymizing their IP before sending 169 * it to w.org, and only send it when necessary. 170 * 171 * The w.org API endpoint only uses the IP address when a location 172 * query is not provided, so we can safely avoid sending it when 173 * there is a query. 174 */ 175 $args['ip'] = $this->maybe_anonymize_ip_address( $this->get_unsafe_client_ip() ); 176 } 177 } 178 179 return add_query_arg( $args, $api_url ); 180 } 181 182 /** 183 * Determines the user's actual IP address, if possible 184 * 185 * $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user 186 * is making their request through a proxy, or when the web server is behind 187 * a proxy. In those cases, `REMOTE_ADDR` is set to the proxy address rather 188 * than the user's actual address. 189 * 190 * Modified from http://stackoverflow.com/a/2031935/450127, MIT license. 191 * 192 * SECURITY WARNING: This function is _NOT_ intended to be used in 193 * circumstances where the authenticity of the IP address matters. This does 194 * _NOT_ guarantee that the returned address is valid or accurate, and it can 195 * be easily spoofed. 196 * 197 * @access protected 198 * @since 4.8.0 199 * 200 * @return false|string `false` on failure, the `string` address on success 201 */ 202 protected function get_unsafe_client_ip() { 203 $client_ip = false; 204 205 // In order of preference, with the best ones for this purpose first. 206 $address_headers = array( 207 'HTTP_CLIENT_IP', 208 'HTTP_X_FORWARDED_FOR', 209 'HTTP_X_FORWARDED', 210 'HTTP_X_CLUSTER_CLIENT_IP', 211 'HTTP_FORWARDED_FOR', 212 'HTTP_FORWARDED', 213 'REMOTE_ADDR', 214 ); 215 216 foreach ( $address_headers as $header ) { 217 if ( array_key_exists( $header, $_SERVER ) ) { 218 /* 219 * HTTP_X_FORWARDED_FOR can contain a chain of comma-separated 220 * addresses. The first one is the original client. It can't be 221 * trusted for authenticity, but we don't need to for this purpose. 222 */ 223 $address_chain = explode( ',', $_SERVER[ $header ] ); 224 $client_ip = trim( $address_chain[0] ); 225 226 break; 227 } 228 } 229 230 return $client_ip; 231 } 232 233 /** 234 * Attempts to partially anonymize an IP address by converting it a network ID 235 * 236 * Geolocating the network ID usually returns a similar location as the 237 * actual IP, but provides some privacy for the user. 238 * 239 * Modified from https://github.com/geertw/php-ip-anonymizer, MIT license. 240 * 241 * @access protected 242 * @since 4.8.0 243 * 244 * @param string $address The IP address that should be anonymized. 245 * @return bool|string The anonymized address on success; 246 * the given address or false on failure. 247 */ 248 protected function maybe_anonymize_ip_address( $address ) { 249 // These functions are not available on Windows until PHP 5.3. 250 if ( ! function_exists( 'inet_pton' ) || ! function_exists( 'inet_ntop' ) ) { 251 return $address; 252 } 253 254 if ( 4 === strlen( inet_pton( $address ) ) ) { 255 $netmask = '255.255.255.0'; // ipv4. 256 } else { 257 $netmask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; // ipv6. 258 } 259 260 return inet_ntop( inet_pton( $address ) & inet_pton( $netmask ) ); 261 } 262 263 /** 264 * Generates a transient key based on user location 265 * 266 * This could be reduced to a one-liner in the calling functions, but it's 267 * intentionally a separate function because it's called from multiple 268 * functions, and having it abstracted keeps the logic consistent and DRY, 269 * which is less prone to errors. 270 * 271 * @access protected 272 * @since 4.8.0 273 * 274 * @param array $location Should contain 'latitude' and 'longitude' indexes. 275 * @return bool|string `false` on failure, or a string on success 276 */ 277 protected function get_events_transient_key( $location ) { 278 $key = false; 279 280 if ( isset( $location['latitude'], $location['longitude'] ) ) { 281 $key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] ); 282 } 283 284 return $key; 285 } 286 287 /** 288 * Caches an array of events data from the Events API. 289 * 290 * @access protected 291 * @since 4.8.0 292 * 293 * @param array $events Response body from the API request. 294 * @param int|bool $expiration Optional. Amount of time to cache the events. Defaults to false. 295 * @return bool `true` if events were cached; `false` if not. 296 */ 297 protected function cache_events( $events, $expiration = false ) { 298 $set = false; 299 $transient_key = $this->get_events_transient_key( $events['location'] ); 300 $cache_expiration = $expiration ? absint( $expiration ) : HOUR_IN_SECONDS * 12; 301 302 if ( $transient_key ) { 303 $set = set_site_transient( $transient_key, $events, $cache_expiration ); 304 } 305 306 return $set; 307 } 308 309 /** 310 * Gets cached events 311 * 312 * @access public 313 * @since 4.8.0 314 * 315 * @return false|array `false` on failure; an array containing `location` 316 * and `events` items on success. 317 */ 318 public function get_cached_events() { 319 $cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) ); 320 $cached_response = $this->trim_events( $cached_response ); 321 322 return $this->format_event_data_time( $cached_response ); 323 } 324 325 /** 326 * Adds formatted date and time items for each event in an API response 327 * 328 * This has to be called after the data is pulled from the cache, because 329 * the cached events are shared by all users. If it was called before storing 330 * the cache, then all users would see the events in the localized data/time 331 * of the user who triggered the cache refresh, rather than their own. 332 * 333 * @access protected 334 * @since 4.8.0 335 * 336 * @param array $response_body The response which contains the events. 337 * @return array The response with dates and times formatted 338 */ 339 protected function format_event_data_time( $response_body ) { 340 if ( isset( $response_body['events'] ) ) { 341 foreach ( $response_body['events'] as $key => $event ) { 342 $timestamp = strtotime( $event['date'] ); 343 344 /* 345 * The `date_format` option is not used because it's important 346 * in this context to keep the day of the week in the formatted date, 347 * so that users can tell at a glance if the event is on a day they 348 * are available, without having to open the link. 349 */ 350 /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://secure.php.net/date. */ 351 $response_body['events'][ $key ]['formatted_date'] = date_i18n( __( 'l, M j, Y' ), $timestamp ); 352 $response_body['events'][ $key ]['formatted_time'] = date_i18n( get_option( 'time_format' ), $timestamp ); 353 } 354 } 355 356 return $response_body; 357 } 358 359 /** 360 * Discards expired events, and reduces the remaining list 361 * 362 * @access protected 363 * @since 4.8.0 364 * 365 * @param array $response_body The response body which contains the events. 366 * @return array The response body with events trimmed. 367 */ 368 protected function trim_events( $response_body ) { 369 if ( isset( $response_body['events'] ) ) { 370 $current_timestamp = current_time('timestamp' ); 371 372 foreach ( $response_body['events'] as $key => $event ) { 373 // Skip WordCamps, because they might be multi-day events. 374 if ( 'meetup' !== $event['type'] ) { 375 continue; 376 } 377 378 $event_timestamp = strtotime( $event['date'] ); 379 380 if ( $current_timestamp > $event_timestamp && ( $current_timestamp - $event_timestamp ) > DAY_IN_SECONDS ) { 381 unset( $response_body['events'][ $key ] ); 382 } 383 } 384 385 $response_body['events'] = array_slice( $response_body['events'], 0, 3 ); 386 } 387 388 return $response_body; 389 } 390 391 /** 392 * Logs responses to Events API requests 393 * 394 * All responses are logged when debugging, even if they're not WP_Errors. 395 * Debugging info is still needed for "successful" responses, because 396 * the API might have returned a different location than the one the user 397 * intended to receive. In those cases, knowing the exact `request_url` is 398 * critical. 399 * 400 * Errors are logged instead of being triggered, to avoid breaking the JSON 401 * response when called from AJAX handlers and `display_errors` is enabled. 402 * 403 * @access protected 404 * @since 4.8.0 405 * 406 * @param string $message A description of what occurred 407 * @param array $debugging_info Details that provide more context for the 408 * log entry 409 */ 410 protected function maybe_log_events_response( $message, $details ) { 411 if ( ! WP_DEBUG_LOG ) { 412 return; 413 } 414 415 error_log( sprintf( 416 '%s: %s. Details: %s', 417 __METHOD__, 418 trim( $message, '.' ), 419 wp_json_encode( $details ) 420 ) ); 421 } 422 } -
src/wp-admin/includes/dashboard.php
diff --git src/wp-admin/includes/dashboard.php src/wp-admin/includes/dashboard.php index be0b201c9f..270b633cbd 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 * Gets 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 wp_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 'nonce' => wp_create_nonce( 'community_events' ), 148 'cache' => $events_client->get_cached_events(), 149 150 'l10n' => array( 151 'enter_closest_city' => __( 'Enter your closest city to find nearby events.' ), 152 'error_occurred_please_try_again' => __( 'An error occured. Please try again.' ), 153 154 /* 155 * These specific examples were chosen to highlight the fact that a 156 * state is not needed, even for cities whose name is not unique. 157 * It would be too cumbersome to include that in the instructions 158 * to the user, so it's left as an implication. 159 */ 160 /* 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. */ 161 'could_not_locate_city' => __( "We couldn't locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland." ), 162 163 // This one is only used with wp.a11y.speak(), so it can/should be more brief. 164 /* translators: %s is the name of a city. */ 165 'city_updated' => __( 'City updated. Listing events near %s.' ), 166 ) 167 ); 168 169 return $script_data; 170 } 171 172 /** 133 173 * Adds a new dashboard widget. 134 174 * 135 175 * @since 2.7.0 … … function wp_dashboard_rss_control( $widget_id, $form_inputs = array() ) { 1069 1109 wp_widget_rss_form( $widget_options[$widget_id], $form_inputs ); 1070 1110 } 1071 1111 1112 1113 /** 1114 * Renders the Events and News dashboard widget 1115 * 1116 * @since 4.8.0 1117 */ 1118 function wp_dashboard_events_news() { 1119 wp_print_community_events_markup(); 1120 1121 ?> 1122 1123 <div class="wordpress-news hide-if-no-js"> 1124 <?php wp_dashboard_primary(); ?> 1125 </div> 1126 1127 <p class="community-events-footer"> 1128 <a href="https://make.wordpress.org/community/meetups-landing-page" target="_blank"> 1129 <?php _e( 'Meetups' ); ?> <span class="dashicons dashicons-external"></span> 1130 </a> 1131 1132 | 1133 1134 <a href="https://central.wordcamp.org/schedule/" target="_blank"> 1135 <?php _e( 'WordCamps' ); ?> <span class="dashicons dashicons-external"></span> 1136 </a> 1137 1138 | 1139 1140 <?php // translators: If a Rosetta site exists (e.g. https://es.wordpress.org/news/), then use that. Otherwise, leave untranslated. ?> 1141 <a href="<?php _e( 'https://wordpress.org/news/' ); ?>" target="_blank"> 1142 <?php _e( 'News' ); ?> <span class="dashicons dashicons-external"></span> 1143 </a> 1144 </p> 1145 1146 <?php 1147 } 1148 1149 /** 1150 * Prints the markup for the Community Events section of the Events and News Dashboard widget 1151 * 1152 * @since 4.8.0 1153 */ 1154 function wp_print_community_events_markup() { 1155 $script_data = wp_get_community_events_script_data(); 1156 1157 ?> 1158 1159 <div class="community-events-errors notice notice-error inline hide-if-js"> 1160 <p class="hide-if-js"> 1161 <?php _e( 'This widget requires JavaScript.'); ?> 1162 </p> 1163 1164 <p class="community-events-error-occurred" aria-hidden="true"> 1165 <?php echo $script_data['l10n']['error_occurred_please_try_again']; ?> 1166 </p> 1167 1168 <p class="community-events-could-not-locate" aria-hidden="true"></p> 1169 </div> 1170 1171 <div class="community-events-loading hide-if-no-js"> 1172 <?php _e( 'Loading…'); ?> 1173 </div> 1174 1175 <?php 1176 /* 1177 * Hide the main element when the page first loads, because the content 1178 * won't be ready until wp.communityEvents.renderEventsTemplate() has run. 1179 */ 1180 ?> 1181 <div id="community-events" class="community-events" aria-hidden="true"> 1182 <div class="activity-block"> 1183 <p> 1184 <span id="community-events-location-message"></span> 1185 1186 <button class="button-link community-events-toggle-location" aria-label="<?php _e( 'Edit city'); ?>" aria-expanded="false"> 1187 <span class="dashicons dashicons-edit"></span> 1188 </button> 1189 </p> 1190 1191 <form class="community-events-form" aria-hidden="true" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" method="post"> 1192 <label for="community-events-location"> 1193 <?php _e( 'City:' ); ?> 1194 </label> 1195 <?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. */ ?> 1196 <input id="community-events-location" class="regular-text" type="text" name="community-events-location" placeholder="<?php _e( 'Cincinnati' ); ?>" /> 1197 1198 <?php submit_button( __( 'Submit' ), 'secondary', 'community-events-submit', false ); ?> 1199 1200 <button class="community-events-cancel button button-link" type="button" aria-expanded="false"> 1201 <?php _e( 'Cancel' ); ?> 1202 </button> 1203 1204 <span class="spinner"></span> 1205 </form> 1206 </div> 1207 1208 <ul class="community-events-results activity-block last"></ul> 1209 </div> 1210 1211 <?php 1212 } 1213 1214 /** 1215 * Renders the events templates for the Event and News widget 1216 * 1217 * @since 4.8.0 1218 */ 1219 function wp_print_community_events_templates() { 1220 $script_data = wp_get_community_events_script_data(); 1221 1222 ?> 1223 1224 <script id="tmpl-community-events-attend-event-near" type="text/template"> 1225 <?php printf( 1226 /* translators: %s is a placeholder for the name of a city. */ 1227 __( 'Attend an upcoming event near %s.' ), 1228 '<strong>{{ data.location.description }}</strong>' 1229 ); ?> 1230 </script> 1231 1232 <script id="tmpl-community-events-could-not-locate" type="text/template"> 1233 <?php printf( 1234 $script_data['l10n']['could_not_locate_city'], 1235 '<em>{{data.unknownCity}}</em>' 1236 ); ?> 1237 </script> 1238 1239 <script id="tmpl-community-events-event-list" type="text/template"> 1240 <# _.each( data.events, function( event ) { #> 1241 <li class="event event-{{ event.type }} wp-clearfix"> 1242 <div class="event-info"> 1243 <div class="dashicons event-icon" aria-hidden="true"></div> 1244 <div class="event-info-inner"> 1245 <a class="event-title" href="{{ event.url }}">{{ event.title }}</a> 1246 <span class="event-city">{{ event.location.location }}</span> 1247 </div> 1248 </div> 1249 1250 <div class="event-date-time"> 1251 <span class="event-date">{{ event.formatted_date }}</span> 1252 <# if ( 'meetup' === event.type ) { #> 1253 <span class="event-time">{{ event.formatted_time }}</span> 1254 <# } #> 1255 </div> 1256 </li> 1257 <# } ) #> 1258 </script> 1259 1260 <script id="tmpl-community-events-no-upcoming-events" type="text/template"> 1261 <li class="event-none"> 1262 <?php printf( 1263 /* translators: Replace the URL if a locale-specific one exists */ 1264 __( '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>?' ), 1265 '{{data.location.description}}' 1266 ); ?> 1267 </li> 1268 </script> 1269 1270 <?php 1271 } 1272 1072 1273 /** 1073 1274 * WordPress News dashboard widget. 1074 1275 * … … function wp_dashboard_primary() { 1105 1306 */ 1106 1307 'title' => apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ), 1107 1308 'items' => 1, 1108 'show_summary' => 1,1309 'show_summary' => 0, 1109 1310 'show_author' => 0, 1110 'show_date' => 1,1311 'show_date' => 0, 1111 1312 ), 1112 1313 'planet' => array( 1113 1314 … … function wp_dashboard_primary() { 1152 1353 ) 1153 1354 ); 1154 1355 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 1356 wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds ); 1170 1357 } 1171 1358 … … function wp_dashboard_primary_output( $widget_id, $feeds ) { 1181 1368 foreach ( $feeds as $type => $args ) { 1182 1369 $args['type'] = $type; 1183 1370 echo '<div class="rss-widget">'; 1184 if ( $type === 'plugins' ) {1185 wp_dashboard_plugins_output( $args['url'], $args );1186 } else {1187 1371 wp_widget_rss_output( $args['url'], $args ); 1188 }1189 1372 echo "</div>"; 1190 1373 } 1191 1374 } 1192 1375 1193 1376 /** 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 1377 * Display file upload quota on dashboard. 1274 1378 * 1275 1379 * 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..eb34231168 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's 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/includes/upgrade.php
diff --git src/wp-admin/includes/upgrade.php src/wp-admin/includes/upgrade.php index 94ad771761..d4be87dfaa 100644
function upgrade_all() { 565 565 if ( $wp_current_db_version < 37965 ) 566 566 upgrade_460(); 567 567 568 if ( $wp_current_db_version < 40500 ) { //todo update to commit for #40702 569 upgrade_480(); 570 } 571 568 572 maybe_disable_link_manager(); 569 573 570 574 maybe_disable_automattic_widgets(); … … function upgrade_460() { 1733 1737 } 1734 1738 1735 1739 /** 1740 * Executes changes made in WordPress 4.8.0. 1741 * 1742 * @ignore 1743 * @since 4.8.0 1744 * 1745 * @global int $wp_current_db_version Current database version. 1746 */ 1747 function upgrade_480() { 1748 global $wp_current_db_version; 1749 1750 if ( $wp_current_db_version < 40500 ) { // todo update to commit for #40702 1751 // This feature plugin was merged for #40702, so the plugin itself is no longer needed 1752 deactivate_plugins( array( 'nearby-wp-events/nearby-wordpress-events.php' ), true ); 1753 1754 // The markup stored in this transient changed for #40702 1755 delete_transient( 'dash_' . md5( 'dashboard_primary' . '_' . get_locale() ) ); 1756 } 1757 } 1758 1759 /** 1736 1760 * Executes network-level upgrade routines. 1737 1761 * 1738 1762 * @since 3.0.0 -
src/wp-admin/index.php
diff --git src/wp-admin/index.php src/wp-admin/index.php index d2d7ec889d..76628abbd7 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', wp_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..e4488693f1 100644
1 /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true */1 /* global pagenow, ajaxurl, postboxes, wpActiveEditor:true, communityEventsData */ 2 2 var ajaxWidgets, ajaxPopulateWidgets, quickPressLoad; 3 3 4 4 jQuery(document).ready( function($) { … … 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 * Initializes the wp.communityEvents object 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 * Toggles 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 * Sends REST API requests 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 294 requestParams = requestParams || {}; 295 requestParams._wpnonce = communityEventsData.nonce; 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 wp.ajax.post( 'get-community-events', requestParams ) 303 .always( function() { 304 $spinner.removeClass( 'is-active' ); 305 }) 306 307 .done( function( response ) { 308 if ( 'no_location_available' === response.error ) { 309 if ( requestParams.location ) { 310 response.unknownCity = requestParams.location; 311 } else { 312 /* 313 * No location was passed, which means that this was an automatic query 314 * based on IP, locale, and timezone. Since the user didn't initiate it, 315 * it should fail silently. Otherwise, the error could confuse and/or 316 * annoy them. 317 */ 318 319 delete response.error; 320 } 321 } 322 app.renderEventsTemplate( response, initiatedBy ); 323 }) 324 325 .fail( function() { 326 app.renderEventsTemplate( { 327 'location' : false, 328 'error' : true 329 }, initiatedBy ); 330 }); 331 }, 332 333 /** 334 * Renders the template for the Events section of the Events & News widget 335 * 336 * @since 4.8.0 337 * 338 * @param {Object} templateParams The various parameters that will get passed to wp.template 339 * @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user; 340 * 'app' to indicate it was triggered automatically by the app itself. 341 */ 342 renderEventsTemplate: function( templateParams, initiatedBy ) { 343 var template, 344 elementVisibility, 345 l10nPlaceholder = /%(?:\d\$)?s/g, // Match `%s`, `%1$s`, `%2$s`, etc. 346 $locationMessage = $( '#community-events-location-message' ), 347 $results = $( '.community-events-results' ); 348 349 /* 350 * Hide all toggleable elements by default, to keep the logic simple. 351 * Otherwise, each block below would have to turn hide everything that 352 * could have been shown at an earlier point. 353 * 354 * The exception to that is that the .community-events container. It's hidden 355 * when the page is first loaded, because the content isn't ready yet, 356 * but once we've reached this point, it should always be shown. 357 */ 358 elementVisibility = { 359 '.community-events' : true, 360 '.community-events-loading' : false, 361 '.community-events-errors' : false, 362 '.community-events-error-occurred' : false, 363 '.community-events-could-not-locate' : false, 364 '#community-events-location-message' : false, 365 '.community-events-toggle-location' : false, 366 '.community-events-results' : false 367 }; 368 369 /* 370 * Determine which templates should be rendered and which elements 371 * should be displayed 372 */ 373 if ( templateParams.location && templateParams.location.description ) { 374 template = wp.template( 'community-events-attend-event-near' ); 375 $locationMessage.html( template( templateParams ) ); 376 377 if ( templateParams.events.length ) { 378 template = wp.template( 'community-events-event-list' ); 379 $results.html( template( templateParams ) ); 380 } else { 381 template = wp.template( 'community-events-no-upcoming-events' ); 382 $results.html( template( templateParams ) ); 383 } 384 wp.a11y.speak( communityEventsData.l10n.city_updated.replace( l10nPlaceholder, templateParams.location.description ) ); 385 386 elementVisibility['#community-events-location-message'] = true; 387 elementVisibility['.community-events-toggle-location'] = true; 388 elementVisibility['.community-events-results'] = true; 389 390 } else if ( templateParams.unknownCity ) { 391 template = wp.template( 'community-events-could-not-locate' ); 392 $( '.community-events-could-not-locate' ).html( template( templateParams ) ); 393 wp.a11y.speak( communityEventsData.l10n.could_not_locate_city.replace( l10nPlaceholder, templateParams.unknownCity ) ); 394 395 elementVisibility['.community-events-errors'] = true; 396 elementVisibility['.community-events-could-not-locate'] = true; 397 398 } else if ( templateParams.error && 'user' === initiatedBy ) { 399 /* 400 * Errors messages are only shown for requests that were initiated 401 * by the user, not for ones that were initiated by the app itself. 402 * Showing error messages for an event that user isn't aware of 403 * could be confusing or unnecessarily distracting. 404 */ 405 wp.a11y.speak( communityEventsData.l10n.error_occurred_please_try_again ); 406 407 elementVisibility['.community-events-errors'] = true; 408 elementVisibility['.community-events-error-occurred'] = true; 409 410 } else { 411 $locationMessage.text( communityEventsData.l10n.enter_closest_city ); 412 413 elementVisibility['#community-events-location-message'] = true; 414 elementVisibility['.community-events-toggle-location'] = true; 415 } 416 417 // Set the visibility of toggleable elements. 418 _.each( elementVisibility, function( isVisible, element ) { 419 $( element ).attr( 'aria-hidden', ! isVisible ); 420 }); 421 422 $( '.community-events-toggle-location' ).attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] ); 423 424 /* 425 * During the initial page load, the location form should be hidden 426 * by default if the user has saved a valid location during a previous 427 * session. It's safe to assume that they want to continue using that 428 * location, and displaying the form would unnecessarily clutter the 429 * widget. 430 */ 431 if ( 'app' === initiatedBy && templateParams.location.description ) { 432 app.toggleLocationForm( 'hide' ); 433 } else { 434 app.toggleLocationForm( 'show' ); 435 } 436 } 437 }; 438 439 if ( $( '#dashboard_primary' ).is( ':visible' ) ) { 440 app.init(); 441 } else { 442 $( document ).on( 'postbox-toggled', function( event, postbox ) { 443 var $postbox = $( postbox ); 444 445 if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) { 446 app.init(); 447 } 448 }); 449 } 450 }); -
src/wp-admin/network/index.php
diff --git src/wp-admin/network/index.php src/wp-admin/network/index.php index 81ededbe37..38acec4b6e 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', wp_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/script-loader.php
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php index def438c260..43209721f5 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-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..09f8685d62
- + 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 * Performs setup tasks 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 * Simulates 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 * Simulates 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 * Simulates 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 * Simulates 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 }