Make WordPress Core

Ticket #40702: 40702-ajax.2.diff

File 40702-ajax.2.diff, 56.0 KB (added by iandunn, 7 years ago)

Removed deprecated noop, unused AJAX response properties; i18n improvements; minor cleanup

  • 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( 
    6464        'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post',
    6565        'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin',
    6666        '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',
    6868);
    6969
    7070// 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
     
    301301        content: "\f153";
    302302}
    303303
     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
    304443/* Dashboard WordPress news */
    305444
    306445#dashboard_primary .inside {
    body #dashboard-widgets .postbox form .submit { 
    333472}
    334473
    335474#dashboard_primary .rss-widget {
    336         border-bottom: 1px solid #eee;
    337475        font-size: 13px;
    338         padding: 8px 12px 10px;
     476        padding: 0 12px 0;
    339477}
    340478
    341479#dashboard_primary .rss-widget:last-child {
    body #dashboard-widgets .postbox form .submit { 
    357495}
    358496
    359497#dashboard_primary .rss-widget ul li {
    360         margin-bottom: 8px;
     498        padding: 4px 0;
     499        margin: 0;
    361500}
    362501
    363502/* Dashboard right now */
    form.initial-form.quickpress-open input#title { 
    8741013}
    8751014
    8761015a.rsswidget {
    877         font-size: 14px;
     1016        font-size: 13px;
    8781017        font-weight: 600;
    879         line-height: 1.7em;
     1018        line-height: 1.4em;
    8801019}
    8811020
    8821021.rss-widget ul li {
    a.rsswidget { 
    10871226                width: 30px;
    10881227                margin: 4px 10px 5px 0;
    10891228        }
     1229
     1230        .community-events-toggle-location {
     1231                height: 38px;
     1232        }
     1233
     1234        .community-events-form .regular-text {
     1235                height: 31px;
     1236        }
    10901237}
    10911238
    10921239/* Smartphone */
    a.rsswidget { 
    11101257                left: -35px;
    11111258        }
    11121259}
     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() { 
    297297}
    298298
    299299/**
     300 * Handles AJAX requests for community events
     301 *
     302 * @since 4.8.0
     303 */
     304function 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/**
    300334 * Ajax handler for dashboard widgets.
    301335 *
    302336 * @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 */
     17class 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() { 
    5252                wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' );
    5353        }
    5454
    55         // WordPress News
    56         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' );
    5757
    5858        if ( is_network_admin() ) {
    5959
    function wp_dashboard_setup() { 
    130130}
    131131
    132132/**
     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 */
     139function 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/**
    133173 * Adds a new dashboard widget.
    134174 *
    135175 * @since 2.7.0
    function wp_dashboard_rss_control( $widget_id, $form_inputs = array() ) { 
    10691109        wp_widget_rss_form( $widget_options[$widget_id], $form_inputs );
    10701110}
    10711111
     1112
     1113/**
     1114 * Renders the Events and News dashboard widget.
     1115 *
     1116 * @since 4.8.0
     1117 */
     1118function 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 */
     1154function 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&hellip;'); ?>
     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 */
     1219function 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&#8217;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
    10721274/**
    10731275 * WordPress News dashboard widget.
    10741276 *
    10751277 * @since 2.7.0
     1278 * @since 4.8.0 Removed popular plugins feed.
    10761279 */
    10771280function wp_dashboard_primary() {
    10781281        $feeds = array(
    function wp_dashboard_primary() { 
    11051308                         */
    11061309                        'title'        => apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ),
    11071310                        'items'        => 1,
    1108                         'show_summary' => 1,
     1311                        'show_summary' => 0,
    11091312                        'show_author'  => 0,
    1110                         'show_date'    => 1,
     1313                        'show_date'    => 0,
    11111314                ),
    11121315                'planet' => array(
    11131316
    function wp_dashboard_primary() { 
    11521355                )
    11531356        );
    11541357
    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 
    11691358        wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds );
    11701359}
    11711360
    function wp_dashboard_primary() { 
    11731362 * Display the WordPress news feeds.
    11741363 *
    11751364 * @since 3.8.0
     1365 * @since 4.8.0 Removed popular plugins feed.
    11761366 *
    11771367 * @param string $widget_id Widget ID.
    11781368 * @param array  $feeds     Array of RSS feeds.
    function wp_dashboard_primary_output( $widget_id, $feeds ) { 
    11811371        foreach ( $feeds as $type => $args ) {
    11821372                $args['type'] = $type;
    11831373                echo '<div class="rss-widget">';
    1184                 if ( $type === 'plugins' ) {
    1185                         wp_dashboard_plugins_output( $args['url'], $args );
    1186                 } else {
    11871374                        wp_widget_rss_output( $args['url'], $args );
    1188                 }
    11891375                echo "</div>";
    11901376        }
    11911377}
    11921378
    11931379/**
    1194  * Display plugins text for the WordPress news widget.
    1195  *
    1196  * @since 2.5.0
    1197  *
    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 them
    1203         $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 plugin
    1219                 while ( true ) {
    1220                         // Abort this foreach loop iteration if there's no plugins left of this type
    1221                         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 descriptions
    1251                 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) . '&amp;TB_iframe=true&amp;width=600&amp;height=800';
    1260                 echo '<li class="dashboard-news-plugin"><span>' . __( 'Popular Plugin' ) . ':</span> ' . esc_html( $raw_title ) .
    1261                         '&nbsp;<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 /**
    12731380 * Display file upload quota on dashboard.
    12741381 *
    12751382 * 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() {} 
    12951295function wp_dashboard_secondary_control() {}
    12961296
    12971297/**
     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 */
     1306function 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) . '&amp;TB_iframe=true&amp;width=600&amp;height=800';
     1367                echo '<li class="dashboard-news-plugin"><span>' . __( 'Popular Plugin' ) . ':</span> ' . esc_html( $raw_title ) .
     1368                        '&nbsp;<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/**
    12981380 * This was once used to move child posts to a new parent.
    12991381 *
    13001382 * @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() { 
    565565        if ( $wp_current_db_version < 37965 )
    566566                upgrade_460();
    567567
     568        if ( $wp_current_db_version < 40500 ) { //todo update to commit for #40702
     569                upgrade_480();
     570        }
     571
    568572        maybe_disable_link_manager();
    569573
    570574        maybe_disable_automattic_widgets();
    function upgrade_460() { 
    17331737}
    17341738
    17351739/**
     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 */
     1747function 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/**
    17361760 * Executes network-level upgrade routines.
    17371761 *
    17381762 * @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'); 
    1515wp_dashboard_setup();
    1616
    1717wp_enqueue_script( 'dashboard' );
     18wp_localize_script( 'dashboard', 'communityEventsData', wp_get_community_events_script_data() );
     19
    1820if ( current_user_can( 'edit_theme_options' ) )
    1921        wp_enqueue_script( 'customize-loader' );
    2022if ( current_user_can( 'install_plugins' ) ) {
    include( ABSPATH . 'wp-admin/admin-header.php' ); 
    138140</div><!-- wrap -->
    139141
    140142<?php
     143wp_print_community_events_templates();
     144
    141145require( 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 */
    22var ajaxWidgets, ajaxPopulateWidgets, quickPressLoad;
    33
    44jQuery(document).ready( function($) {
    jQuery(document).ready( function($) { 
    187187        }
    188188
    189189} );
     190
     191
     192wp.communityEvents = wp.communityEvents || {};
     193
     194jQuery( 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( 
    5454wp_dashboard_setup();
    5555
    5656wp_enqueue_script( 'dashboard' );
     57wp_localize_script( 'dashboard', 'communityEventsData', wp_get_community_events_script_data() );
    5758wp_enqueue_script( 'plugin-install' );
    5859add_thickbox();
    5960
    require_once( ABSPATH . 'wp-admin/admin-header.php' ); 
    7374
    7475</div><!-- wrap -->
    7576
    76 <?php include( ABSPATH . 'wp-admin/admin-footer.php' ); ?>
     77<?php
     78wp_print_community_events_templates();
     79include( 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 ) { 
    724724                        'current' => __( 'Current Color' ),
    725725                ) );
    726726
    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 );
    728728
    729729                $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
    730730
  • 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 */
     18class 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}