Ticket #40702: 40702-ajax.2.diff
File 40702-ajax.2.diff, 56.0 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..029e20e62b 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 // Send only the data that the client will use. 323 $events['location'] = $events['location']['description']; 324 325 // Store the location network-wide, so the user doesn't have to set it on each site. 326 update_user_option( $user_id, 'community-events-location', $events['location'], true ); 327 } 328 329 wp_send_json_success( $events ); 330 } 331 } 332 333 /** 300 334 * Ajax handler for dashboard widgets. 301 335 * 302 336 * @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..2b76bd6bc3
- + 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 * @since 4.8.0 42 * 43 * @param int $user_id WP user ID. 44 * @param bool|array $user_location Stored location data for the user. 45 * false to pass no location; 46 * array to pass a location { 47 * @type string $description The name of the location 48 * @type string $latitude The latitude in decimal degrees notation, without the degree 49 * symbol. e.g.: 47.615200. 50 * @type string $longitude The longitude in decimal degrees notation, without the degree 51 * symbol. e.g.: -122.341100. 52 * @type string $country The ISO 3166-1 alpha-2 country code. e.g.: BR 53 * } 54 */ 55 public function __construct( $user_id, $user_location = false ) { 56 $this->user_id = absint( $user_id ); 57 $this->user_location = $user_location; 58 } 59 60 /** 61 * Gets data about events near a particular location. 62 * 63 * Cached events will be immediately returned if the `user_location` property 64 * is set for the current user, and cached events exist for that location. 65 * 66 * Otherwise, this method sends a request to the w.org Events API with location 67 * data. The API will send back a recognized location based on the data, along 68 * with nearby events. 69 * 70 * @since 4.8.0 71 * 72 * @param string $location_search Optional city name to help determine the location. 73 * e.g., "Seattle". Default empty string. 74 * @param string $timezone Optional timezone to help determine the location. 75 * Default empty string. 76 * @return array|WP_Error A WP_Error on failure; an array with location and events on 77 * success. 78 */ 79 public function get_events( $location_search = '', $timezone = '' ) { 80 $cached_events = $this->get_cached_events(); 81 82 if ( ! $location_search && $cached_events ) { 83 return $cached_events; 84 } 85 86 $request_url = $this->get_request_url( $location_search, $timezone ); 87 $response = wp_remote_get( $request_url ); 88 $response_code = wp_remote_retrieve_response_code( $response ); 89 $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); 90 $response_error = null; 91 $debugging_info = compact( 'request_url', 'response_code', 'response_body' ); 92 93 if ( is_wp_error( $response ) ) { 94 $response_error = $response; 95 } elseif ( 200 !== $response_code ) { 96 $response_error = new WP_Error( 97 'api-error', 98 /* translators: %s is a numeric HTTP status code; e.g., 400, 403, 500, 504, etc. */ 99 sprintf( __( 'Invalid API response code (%d)' ), $response_code ) 100 ); 101 } elseif ( ! isset( $response_body['location'], $response_body['events'] ) ) { 102 $response_error = new WP_Error( 103 'api-invalid-response', 104 isset( $response_body['error'] ) ? $response_body['error'] : __( 'Unknown API error.' ) 105 ); 106 } 107 108 if ( is_wp_error( $response_error ) ) { 109 $this->maybe_log_events_response( $response_error->get_error_message(), $debugging_info ); 110 111 return $response_error; 112 } else { 113 $expiration = false; 114 115 if ( isset( $response_body['ttl'] ) ) { 116 $expiration = $response_body['ttl']; 117 unset( $response_body['ttl'] ); 118 } 119 120 $this->cache_events( $response_body, $expiration ); 121 122 $response_body = $this->trim_events( $response_body ); 123 $response_body = $this->format_event_data_time( $response_body ); 124 125 // Avoid bloating the log with all the event data, but keep the count. 126 $debugging_info['response_body']['events'] = count( $debugging_info['response_body']['events'] ) . ' events trimmed.'; 127 128 $this->maybe_log_events_response( 'Valid response received', $debugging_info ); 129 130 return $response_body; 131 } 132 } 133 134 /** 135 * Builds a URL for requests to the w.org Events API. 136 * 137 * @access protected 138 * @since 4.8.0 139 * 140 * @param string $search City search string. Default empty string. 141 * @param string $timezone Timezone string. Default empty string. 142 * @return string The request URL. 143 */ 144 protected function get_request_url( $search = '', $timezone = '' ) { 145 $api_url = 'https://api.wordpress.org/events/1.0/'; 146 $args = array( 'number' => 5 ); // Get more than three in case some get trimmed out. 147 148 /* 149 * Send the minimal set of necessary arguments, in order to increase the 150 * chances of a cache-hit on the API side. 151 */ 152 if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) { 153 $args['latitude'] = $this->user_location['latitude']; 154 $args['longitude'] = $this->user_location['longitude']; 155 } else { 156 $args['locale'] = get_user_locale( $this->user_id ); 157 158 if ( $timezone ) { 159 $args['timezone'] = $timezone; 160 } 161 162 if ( $search ) { 163 $args['location'] = $search; 164 } else { 165 /* 166 * Protect the user's privacy by anonymizing their IP before sending 167 * it to w.org, and only send it when necessary. 168 * 169 * The w.org API endpoint only uses the IP address when a location 170 * query is not provided, so we can safely avoid sending it when 171 * there is a query. 172 */ 173 $args['ip'] = $this->maybe_anonymize_ip_address( $this->get_unsafe_client_ip() ); 174 } 175 } 176 177 return add_query_arg( $args, $api_url ); 178 } 179 180 /** 181 * Determines the user's actual IP address, if possible. 182 * 183 * $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user 184 * is making their request through a proxy, or when the web server is behind 185 * a proxy. In those cases, $_SERVER['REMOTE_ADDR'] is set to the proxy address rather 186 * than the user's actual address. 187 * 188 * Modified from http://stackoverflow.com/a/2031935/450127, MIT license. 189 * 190 * SECURITY WARNING: This function is _NOT_ intended to be used in 191 * circumstances where the authenticity of the IP address matters. This does 192 * _NOT_ guarantee that the returned address is valid or accurate, and it can 193 * be easily spoofed. 194 * 195 * @access protected 196 * @since 4.8.0 197 * 198 * @return false|string false on failure, the string address on success. 199 */ 200 protected function get_unsafe_client_ip() { 201 $client_ip = false; 202 203 // In order of preference, with the best ones for this purpose first. 204 $address_headers = array( 205 'HTTP_CLIENT_IP', 206 'HTTP_X_FORWARDED_FOR', 207 'HTTP_X_FORWARDED', 208 'HTTP_X_CLUSTER_CLIENT_IP', 209 'HTTP_FORWARDED_FOR', 210 'HTTP_FORWARDED', 211 'REMOTE_ADDR', 212 ); 213 214 foreach ( $address_headers as $header ) { 215 if ( array_key_exists( $header, $_SERVER ) ) { 216 /* 217 * HTTP_X_FORWARDED_FOR can contain a chain of comma-separated 218 * addresses. The first one is the original client. It can't be 219 * trusted for authenticity, but we don't need to for this purpose. 220 */ 221 $address_chain = explode( ',', $_SERVER[ $header ] ); 222 $client_ip = trim( $address_chain[0] ); 223 224 break; 225 } 226 } 227 228 return $client_ip; 229 } 230 231 /** 232 * Attempts to partially anonymize an IP address by converting it to a network ID. 233 * 234 * Geolocating the network ID usually returns a similar location as the 235 * actual IP, but provides some privacy for the user. 236 * 237 * Modified from https://github.com/geertw/php-ip-anonymizer, MIT license. 238 * 239 * @access protected 240 * @since 4.8.0 241 * 242 * @param string $address The IP address that should be anonymized. 243 * @return bool|string The anonymized address on success; the given address 244 * or false on failure. 245 */ 246 protected function maybe_anonymize_ip_address( $address ) { 247 // These functions are not available on Windows until PHP 5.3. 248 if ( ! function_exists( 'inet_pton' ) || ! function_exists( 'inet_ntop' ) ) { 249 return $address; 250 } 251 252 if ( 4 === strlen( inet_pton( $address ) ) ) { 253 $netmask = '255.255.255.0'; // ipv4. 254 } else { 255 $netmask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; // ipv6. 256 } 257 258 return inet_ntop( inet_pton( $address ) & inet_pton( $netmask ) ); 259 } 260 261 /** 262 * Generates a transient key based on user location. 263 * 264 * This could be reduced to a one-liner in the calling functions, but it's 265 * intentionally a separate function because it's called from multiple 266 * functions, and having it abstracted keeps the logic consistent and DRY, 267 * which is less prone to errors. 268 * 269 * @access protected 270 * @since 4.8.0 271 * 272 * @param array $location Should contain 'latitude' and 'longitude' indexes. 273 * @return bool|string false on failure, or a string on success. 274 */ 275 protected function get_events_transient_key( $location ) { 276 $key = false; 277 278 if ( isset( $location['latitude'], $location['longitude'] ) ) { 279 $key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] ); 280 } 281 282 return $key; 283 } 284 285 /** 286 * Caches an array of events data from the Events API. 287 * 288 * @access protected 289 * @since 4.8.0 290 * 291 * @param array $events Response body from the API request. 292 * @param int|bool $expiration Optional. Amount of time to cache the events. Defaults to false. 293 * @return bool true if events were cached; false if not. 294 */ 295 protected function cache_events( $events, $expiration = false ) { 296 $set = false; 297 $transient_key = $this->get_events_transient_key( $events['location'] ); 298 $cache_expiration = $expiration ? absint( $expiration ) : HOUR_IN_SECONDS * 12; 299 300 if ( $transient_key ) { 301 $set = set_site_transient( $transient_key, $events, $cache_expiration ); 302 } 303 304 return $set; 305 } 306 307 /** 308 * Gets cached events. 309 * 310 * @since 4.8.0 311 * 312 * @return false|array false on failure; an array containing `location` 313 * and `events` items on success. 314 */ 315 public function get_cached_events() { 316 $cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) ); 317 $cached_response = $this->trim_events( $cached_response ); 318 319 return $this->format_event_data_time( $cached_response ); 320 } 321 322 /** 323 * Adds formatted date and time items for each event in an API response. 324 * 325 * This has to be called after the data is pulled from the cache, because 326 * the cached events are shared by all users. If it was called before storing 327 * the cache, then all users would see the events in the localized data/time 328 * of the user who triggered the cache refresh, rather than their own. 329 * 330 * @access protected 331 * @since 4.8.0 332 * 333 * @param array $response_body The response which contains the events. 334 * @return array The response with dates and times formatted. 335 */ 336 protected function format_event_data_time( $response_body ) { 337 if ( isset( $response_body['events'] ) ) { 338 foreach ( $response_body['events'] as $key => $event ) { 339 $timestamp = strtotime( $event['date'] ); 340 341 /* 342 * The `date_format` option is not used because it's important 343 * in this context to keep the day of the week in the formatted date, 344 * so that users can tell at a glance if the event is on a day they 345 * are available, without having to open the link. 346 */ 347 /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://secure.php.net/date. */ 348 $response_body['events'][ $key ]['formatted_date'] = date_i18n( __( 'l, M j, Y' ), $timestamp ); 349 $response_body['events'][ $key ]['formatted_time'] = date_i18n( get_option( 'time_format' ), $timestamp ); 350 } 351 } 352 353 return $response_body; 354 } 355 356 /** 357 * Discards expired events, and reduces the remaining list. 358 * 359 * @access protected 360 * @since 4.8.0 361 * 362 * @param array $response_body The response body which contains the events. 363 * @return array The response body with events trimmed. 364 */ 365 protected function trim_events( $response_body ) { 366 if ( isset( $response_body['events'] ) ) { 367 $current_timestamp = current_time('timestamp' ); 368 369 foreach ( $response_body['events'] as $key => $event ) { 370 // Skip WordCamps, because they might be multi-day events. 371 if ( 'meetup' !== $event['type'] ) { 372 continue; 373 } 374 375 $event_timestamp = strtotime( $event['date'] ); 376 377 if ( $current_timestamp > $event_timestamp && ( $current_timestamp - $event_timestamp ) > DAY_IN_SECONDS ) { 378 unset( $response_body['events'][ $key ] ); 379 } 380 } 381 382 $response_body['events'] = array_slice( $response_body['events'], 0, 3 ); 383 } 384 385 return $response_body; 386 } 387 388 /** 389 * Logs responses to Events API requests. 390 * 391 * All responses are logged when debugging, even if they're not WP_Errors. 392 * Debugging info is still needed for "successful" responses, because 393 * the API might have returned a different location than the one the user 394 * intended to receive. In those cases, knowing the exact `request_url` is 395 * critical. 396 * 397 * Errors are logged instead of being triggered, to avoid breaking the JSON 398 * response when called from AJAX handlers and `display_errors` is enabled. 399 * 400 * @access protected 401 * @since 4.8.0 402 * 403 * @param string $message A description of what occurred. 404 * @param array $debugging_info Details that provide more context for the 405 * log entry. 406 */ 407 protected function maybe_log_events_response( $message, $details ) { 408 if ( ! WP_DEBUG_LOG ) { 409 return; 410 } 411 412 error_log( sprintf( 413 '%s: %s. Details: %s', 414 __METHOD__, 415 trim( $message, '.' ), 416 wp_json_encode( $details ) 417 ) ); 418 } 419 } -
src/wp-admin/includes/dashboard.php
diff --git src/wp-admin/includes/dashboard.php src/wp-admin/includes/dashboard.php index be0b201c9f..de0eef3447 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 }}</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: 1: the city the user searched for, 2: meetup organization documentation URL */ 1264 __( 'There aren’t any events scheduled near %1$s at the moment. Would you like to <a href="%2$s">organize one</a>?' ), 1265 '{{data.location}}', 1266 __( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' ) 1267 ); ?> 1268 </li> 1269 </script> 1270 1271 <?php 1272 } 1273 1072 1274 /** 1073 1275 * WordPress News dashboard widget. 1074 1276 * 1075 1277 * @since 2.7.0 1278 * @since 4.8.0 Removed popular plugins feed. 1076 1279 */ 1077 1280 function wp_dashboard_primary() { 1078 1281 $feeds = array( … … function wp_dashboard_primary() { 1105 1308 */ 1106 1309 'title' => apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ), 1107 1310 'items' => 1, 1108 'show_summary' => 1,1311 'show_summary' => 0, 1109 1312 'show_author' => 0, 1110 'show_date' => 1,1313 'show_date' => 0, 1111 1314 ), 1112 1315 'planet' => array( 1113 1316 … … function wp_dashboard_primary() { 1152 1355 ) 1153 1356 ); 1154 1357 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 1358 wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds ); 1170 1359 } 1171 1360 … … function wp_dashboard_primary() { 1173 1362 * Display the WordPress news feeds. 1174 1363 * 1175 1364 * @since 3.8.0 1365 * @since 4.8.0 Removed popular plugins feed. 1176 1366 * 1177 1367 * @param string $widget_id Widget ID. 1178 1368 * @param array $feeds Array of RSS feeds. … … function wp_dashboard_primary_output( $widget_id, $feeds ) { 1181 1371 foreach ( $feeds as $type => $args ) { 1182 1372 $args['type'] = $type; 1183 1373 echo '<div class="rss-widget">'; 1184 if ( $type === 'plugins' ) {1185 wp_dashboard_plugins_output( $args['url'], $args );1186 } else {1187 1374 wp_widget_rss_output( $args['url'], $args ); 1188 }1189 1375 echo "</div>"; 1190 1376 } 1191 1377 } 1192 1378 1193 1379 /** 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 1380 * Display file upload quota on dashboard. 1274 1381 * 1275 1382 * 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..a9e0e6f9d1 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 /** 1298 1380 * This was once used to move child posts to a new parent. 1299 1381 * 1300 1382 * @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..e4e8952909 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, .community-events-cancel', app.toggleLocationForm ); 234 235 $container.on( 'submit', '.community-events-form', function( event ) { 236 event.preventDefault(); 237 238 app.getEvents( { 239 location: $( '#community-events-location' ).val() 240 }); 241 }); 242 243 if ( communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events ) { 244 app.renderEventsTemplate( communityEventsData.cache, 'app' ); 245 } else { 246 app.getEvents(); 247 } 248 249 app.initialized = true; 250 }, 251 252 /** 253 * Toggles the visibility of the Edit Location form. 254 * 255 * @since 4.8.0 256 * 257 * @param {event|string} action 'show' or 'hide' to specify a state; 258 * Or an event object to flip between states 259 */ 260 toggleLocationForm: function( action ) { 261 var $toggleButton = $( '.community-events-toggle-location' ), 262 $cancelButton = $( '.community-events-cancel' ), 263 $form = $( '.community-events-form' ); 264 265 if ( 'object' === typeof action ) { 266 // Strict comparison doesn't work in this case. 267 action = 'true' == $toggleButton.attr( 'aria-expanded' ) ? 'hide' : 'show'; 268 } 269 270 if ( 'hide' === action ) { 271 $toggleButton.attr( 'aria-expanded', false ); 272 $cancelButton.attr( 'aria-expanded', false ); 273 $form.attr( 'aria-hidden', true ); 274 } else { 275 $toggleButton.attr( 'aria-expanded', true ); 276 $cancelButton.attr( 'aria-expanded', true ); 277 $form.attr( 'aria-hidden', false ); 278 } 279 }, 280 281 /** 282 * Sends REST API requests to fetch events for the widget. 283 * 284 * @since 4.8.0 285 * 286 * @param {object} requestParams 287 */ 288 getEvents: function( requestParams ) { 289 var initiatedBy, 290 app = this, 291 $spinner = $( '.community-events-form' ).children( '.spinner' ); 292 293 requestParams = requestParams || {}; 294 requestParams._wpnonce = communityEventsData.nonce; 295 requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : ''; 296 297 initiatedBy = requestParams.location ? 'user' : 'app'; 298 299 $spinner.addClass( 'is-active' ); 300 301 wp.ajax.post( 'get-community-events', requestParams ) 302 .always( function() { 303 $spinner.removeClass( 'is-active' ); 304 }) 305 306 .done( function( response ) { 307 if ( 'no_location_available' === response.error ) { 308 if ( requestParams.location ) { 309 response.unknownCity = requestParams.location; 310 } else { 311 /* 312 * No location was passed, which means that this was an automatic query 313 * based on IP, locale, and timezone. Since the user didn't initiate it, 314 * it should fail silently. Otherwise, the error could confuse and/or 315 * annoy them. 316 */ 317 318 delete response.error; 319 } 320 } 321 app.renderEventsTemplate( response, initiatedBy ); 322 }) 323 324 .fail( function() { 325 app.renderEventsTemplate( { 326 'location' : false, 327 'error' : true 328 }, initiatedBy ); 329 }); 330 }, 331 332 /** 333 * Renders the template for the Events section of the Events & News widget. 334 * 335 * @since 4.8.0 336 * 337 * @param {Object} templateParams The various parameters that will get passed to wp.template 338 * @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user; 339 * 'app' to indicate it was triggered automatically by the app itself. 340 */ 341 renderEventsTemplate: function( templateParams, initiatedBy ) { 342 var template, 343 elementVisibility, 344 l10nPlaceholder = /%(?:\d\$)?s/g, // Match `%s`, `%1$s`, `%2$s`, etc. 345 $locationMessage = $( '#community-events-location-message' ), 346 $results = $( '.community-events-results' ); 347 348 /* 349 * Hide all toggleable elements by default, to keep the logic simple. 350 * Otherwise, each block below would have to turn hide everything that 351 * could have been shown at an earlier point. 352 * 353 * The exception to that is that the .community-events container. It's hidden 354 * when the page is first loaded, because the content isn't ready yet, 355 * but once we've reached this point, it should always be shown. 356 */ 357 elementVisibility = { 358 '.community-events' : true, 359 '.community-events-loading' : false, 360 '.community-events-errors' : false, 361 '.community-events-error-occurred' : false, 362 '.community-events-could-not-locate' : false, 363 '#community-events-location-message' : false, 364 '.community-events-toggle-location' : false, 365 '.community-events-results' : false 366 }; 367 368 /* 369 * Determine which templates should be rendered and which elements 370 * should be displayed. 371 */ 372 if ( templateParams.location ) { 373 template = wp.template( 'community-events-attend-event-near' ); 374 $locationMessage.html( template( templateParams ) ); 375 376 if ( templateParams.events.length ) { 377 template = wp.template( 'community-events-event-list' ); 378 $results.html( template( templateParams ) ); 379 } else { 380 template = wp.template( 'community-events-no-upcoming-events' ); 381 $results.html( template( templateParams ) ); 382 } 383 wp.a11y.speak( communityEventsData.l10n.city_updated.replace( l10nPlaceholder, templateParams.location ) ); 384 385 elementVisibility['#community-events-location-message'] = true; 386 elementVisibility['.community-events-toggle-location'] = true; 387 elementVisibility['.community-events-results'] = true; 388 389 } else if ( templateParams.unknownCity ) { 390 template = wp.template( 'community-events-could-not-locate' ); 391 $( '.community-events-could-not-locate' ).html( template( templateParams ) ); 392 wp.a11y.speak( communityEventsData.l10n.could_not_locate_city.replace( l10nPlaceholder, templateParams.unknownCity ) ); 393 394 elementVisibility['.community-events-errors'] = true; 395 elementVisibility['.community-events-could-not-locate'] = true; 396 397 } else if ( templateParams.error && 'user' === initiatedBy ) { 398 /* 399 * Errors messages are only shown for requests that were initiated 400 * by the user, not for ones that were initiated by the app itself. 401 * Showing error messages for an event that user isn't aware of 402 * could be confusing or unnecessarily distracting. 403 */ 404 wp.a11y.speak( communityEventsData.l10n.error_occurred_please_try_again ); 405 406 elementVisibility['.community-events-errors'] = true; 407 elementVisibility['.community-events-error-occurred'] = true; 408 409 } else { 410 $locationMessage.text( communityEventsData.l10n.enter_closest_city ); 411 412 elementVisibility['#community-events-location-message'] = true; 413 elementVisibility['.community-events-toggle-location'] = true; 414 } 415 416 // Set the visibility of toggleable elements. 417 _.each( elementVisibility, function( isVisible, element ) { 418 $( element ).attr( 'aria-hidden', ! isVisible ); 419 }); 420 421 $( '.community-events-toggle-location' ).attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] ); 422 423 /* 424 * During the initial page load, the location form should be hidden 425 * by default if the user has saved a valid location during a previous 426 * session. It's safe to assume that they want to continue using that 427 * location, and displaying the form would unnecessarily clutter the 428 * widget. 429 */ 430 if ( 'app' === initiatedBy && templateParams.location ) { 431 app.toggleLocationForm( 'hide' ); 432 } else { 433 app.toggleLocationForm( 'show' ); 434 } 435 } 436 }; 437 438 if ( $( '#dashboard_primary' ).is( ':visible' ) ) { 439 app.init(); 440 } else { 441 $( document ).on( 'postbox-toggled', function( event, postbox ) { 442 var $postbox = $( postbox ); 443 444 if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) { 445 app.init(); 446 } 447 }); 448 } 449 }); -
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..ed5cc2caee
- + 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 * @since 4.8.0 33 */ 34 public function setUp() { 35 parent::setUp(); 36 37 require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' ); 38 39 $this->instance = new WP_Community_Events( 1, $this->get_user_location() ); 40 } 41 42 /** 43 * Simulates a stored user location. 44 * 45 * @access private 46 * @since 4.8.0 47 * 48 * @return array The mock location. 49 */ 50 private function get_user_location() { 51 return array( 52 'description' => 'San Francisco', 53 'latitude' => '37.7749300', 54 'longitude' => '-122.4194200', 55 'country' => 'US', 56 ); 57 } 58 59 /** 60 * Test: get_events() should return an instance of WP_Error if the response code is not 200. 61 * 62 * @since 4.8.0 63 */ 64 public function test_get_events_bad_response_code() { 65 add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 66 67 $this->assertWPError( $this->instance->get_events() ); 68 69 remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 70 } 71 72 /** 73 * Test: The response body should not be cached if the response code is not 200. 74 * 75 * @since 4.8.0 76 */ 77 public function test_get_cached_events_bad_response_code() { 78 add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 79 80 $this->instance->get_events(); 81 82 $this->assertFalse( $this->instance->get_cached_events() ); 83 84 remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); 85 } 86 87 /** 88 * Simulates an HTTP response with a non-200 response code. 89 * 90 * @since 4.8.0 91 * 92 * @return array A mock response with a 404 HTTP status code 93 */ 94 public function _http_request_bad_response_code() { 95 return array( 96 'headers' => '', 97 'body' => '', 98 'response' => array( 99 'code' => 404, 100 ), 101 'cookies' => '', 102 'filename' => '', 103 ); 104 } 105 106 /** 107 * Test: get_events() should return an instance of WP_Error if the response body does not have 108 * the required properties. 109 * 110 * @since 4.8.0 111 */ 112 public function test_get_events_invalid_response() { 113 add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 114 115 $this->assertWPError( $this->instance->get_events() ); 116 117 remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 118 } 119 120 /** 121 * Test: The response body should not be cached if it does not have the required properties. 122 * 123 * @since 4.8.0 124 */ 125 public function test_get_cached_events_invalid_response() { 126 add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 127 128 $this->instance->get_events(); 129 130 $this->assertFalse( $this->instance->get_cached_events() ); 131 132 remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); 133 } 134 135 /** 136 * Simulates an HTTP response with a body that does not have the required properties. 137 * 138 * @since 4.8.0 139 * 140 * @return array A mock response that's missing required properties. 141 */ 142 public function _http_request_invalid_response() { 143 return array( 144 'headers' => '', 145 'body' => wp_json_encode( array() ), 146 'response' => array( 147 'code' => 200, 148 ), 149 'cookies' => '', 150 'filename' => '', 151 ); 152 } 153 154 /** 155 * Test: With a valid response, get_events() should return an associated array containing a location array and 156 * an events array with individual events that have formatted time and date. 157 * 158 * @since 4.8.0 159 */ 160 public function test_get_events_valid_response() { 161 add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 162 163 $response = $this->instance->get_events(); 164 165 $this->assertNotWPError( $response ); 166 $this->assertEqualSetsWithIndex( $this->get_user_location(), $response['location'] ); 167 $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $response['events'][0]['formatted_date'] ); 168 $this->assertEquals( '1:00 pm', $response['events'][0]['formatted_time'] ); 169 170 remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 171 } 172 173 /** 174 * Test: get_cached_events() should return the same data as get_events(), including formatted time 175 * and date values for each event. 176 * 177 * @since 4.8.0 178 */ 179 public function test_get_cached_events_valid_response() { 180 add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 181 182 $this->instance->get_events(); 183 184 $cached_events = $this->instance->get_cached_events(); 185 186 $this->assertNotWPError( $cached_events ); 187 $this->assertEqualSetsWithIndex( $this->get_user_location(), $cached_events['location'] ); 188 $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $cached_events['events'][0]['formatted_date'] ); 189 $this->assertEquals( '1:00 pm', $cached_events['events'][0]['formatted_time'] ); 190 191 remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); 192 } 193 194 /** 195 * Simulates an HTTP response with valid location and event data. 196 * 197 * @since 4.8.0 198 * 199 * @return array A mock HTTP response with valid data. 200 */ 201 public function _http_request_valid_response() { 202 return array( 203 'headers' => '', 204 'body' => wp_json_encode( array( 205 'location' => $this->get_user_location(), 206 'events' => array( 207 array( 208 'type' => 'meetup', 209 'title' => 'Flexbox + CSS Grid: Magic for Responsive Layouts', 210 'url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/', 211 'meetup' => 'The East Bay WordPress Meetup Group', 212 'meetup_url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/', 213 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Sunday 1pm' ) ), 214 'location' => array( 215 'location' => 'Oakland, CA, USA', 216 'country' => 'us', 217 'latitude' => 37.808453, 218 'longitude' => -122.26593, 219 ), 220 ), 221 array( 222 'type' => 'meetup', 223 'title' => 'Part 3- Site Maintenance - Tools to Make It Easy', 224 'url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/', 225 'meetup' => 'WordPress Bay Area Foothills Group', 226 'meetup_url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/', 227 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Wednesday 1:30pm' ) ), 228 'location' => array( 229 'location' => 'Milpitas, CA, USA', 230 'country' => 'us', 231 'latitude' => 37.432813, 232 'longitude' => -121.907095, 233 ), 234 ), 235 array( 236 'type' => 'wordcamp', 237 'title' => 'WordCamp Kansas City', 238 'url' => 'https://2017.kansascity.wordcamp.org', 239 'meetup' => null, 240 'meetup_url' => null, 241 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Saturday' ) ), 242 'location' => array( 243 'location' => 'Kansas City, MO', 244 'country' => 'US', 245 'latitude' => 39.0392325, 246 'longitude' => -94.577076, 247 ), 248 ), 249 ), 250 ) ), 251 'response' => array( 252 'code' => 200, 253 ), 254 'cookies' => '', 255 'filename' => '', 256 ); 257 } 258 }