diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index e0f4464d94..3213d55028 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -64,7 +64,7 @@ $core_actions_post = array( 'parse-media-shortcode', 'destroy-sessions', 'install-plugin', 'update-plugin', 'press-this-save-post', 'press-this-add-category', 'crop-image', 'generate-password', 'save-wporg-username', 'delete-plugin', 'search-plugins', 'search-install-plugins', 'activate-plugin', 'update-theme', 'delete-theme', - 'install-theme', 'get-post-thumbnail-html', + 'install-theme', 'get-post-thumbnail-html', 'get-community-events', ); // Deprecated diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css index 98fb99378d..6a35594302 100644 --- a/src/wp-admin/css/dashboard.css +++ b/src/wp-admin/css/dashboard.css @@ -301,6 +301,145 @@ content: "\f153"; } +/* Dashboard WordPress events */ + +.community-events-errors { + margin: 0; +} + +.community-events-loading { + padding: 10px 12px 8px; +} + +.community-events { + margin-bottom: 6px; + padding: 0 12px; +} + +.community-events .spinner { + float: none; + margin: 0; + padding-bottom: 3px; +} + +.community-events-errors[aria-hidden="true"], +.community-events-errors *[aria-hidden="true"], +.community-events-loading[aria-hidden="true"], +.community-events[aria-hidden="true"], +.community-events *[aria-hidden="true"] { + display: none; +} + +.community-events .activity-block:first-child, +.community-events h2 { + padding-top: 12px; + padding-bottom: 10px; +} + +.community-events-form { + margin: 15px 0 5px; +} + +.community-events-form .regular-text { + width: 40%; + height: 28px; +} + +.community-events li.event-none { + border-left: 4px solid #0070AE; +} + +.community-events-form label { + display: inline-block; + padding-bottom: 3px; +} + +.community-events .activity-block > p { + margin-bottom: 0; + display: inline; +} + +#community-events-submit { + margin-left: 2px; +} + +.community-events .button-link:hover, +.community-events .button-link:active { + color: #00a0d2; +} + +.community-events-cancel.button.button-link { + color: #0073aa; + text-decoration: underline; + margin-left: 2px; +} + +.community-events ul { + background-color: #fafafa; + padding-left: 0; + padding-right: 0; + padding-bottom: 0; +} + +.community-events li { + margin: 0; + padding: 8px 12px; + color: #72777c; +} +.community-events li:first-child { + border-top: 1px solid #eee; +} + +.community-events li ~ li { + border-top: 1px solid #eee; +} + +.community-events .activity-block.last { + border-bottom: 1px solid #eee; + padding-top: 0; + margin-top: -1px; +} + +.community-events .event-info { + display: block; +} + +.event-icon { + height: 18px; + padding-right: 10px; + width: 18px; + display: none; /* Hide on smaller screens */ +} + +.event-icon:before { + color: #82878C; + font-size: 18px; +} +.event-meetup .event-icon:before { + content: "\f484"; +} +.event-wordcamp .event-icon:before { + content: "\f486"; +} + +.community-events .event-title { + font-weight: 600; + display: block; +} + +.community-events .event-date, +.community-events .event-time { + display: block; +} + +.community-events-footer { + margin-top: 0; + margin-bottom: 0; + padding: 12px; + border-top: 1px solid #eee; + color: #ddd; +} + /* Dashboard WordPress news */ #dashboard_primary .inside { @@ -333,9 +472,8 @@ body #dashboard-widgets .postbox form .submit { } #dashboard_primary .rss-widget { - border-bottom: 1px solid #eee; font-size: 13px; - padding: 8px 12px 10px; + padding: 0 12px 0; } #dashboard_primary .rss-widget:last-child { @@ -357,7 +495,8 @@ body #dashboard-widgets .postbox form .submit { } #dashboard_primary .rss-widget ul li { - margin-bottom: 8px; + padding: 4px 0; + margin: 0; } /* Dashboard right now */ @@ -874,9 +1013,9 @@ form.initial-form.quickpress-open input#title { } a.rsswidget { - font-size: 14px; + font-size: 13px; font-weight: 600; - line-height: 1.7em; + line-height: 1.4em; } .rss-widget ul li { @@ -1087,6 +1226,14 @@ a.rsswidget { width: 30px; margin: 4px 10px 5px 0; } + + .community-events-toggle-location { + height: 38px; + } + + .community-events-form .regular-text { + height: 31px; + } } /* Smartphone */ @@ -1110,3 +1257,30 @@ a.rsswidget { left: -35px; } } + +@media screen and (min-width: 355px) { + .community-events .event-info { + display: table-row; + float: left; + max-width: 59%; + } + + .event-icon, + .event-icon[aria-hidden="true"] { + display: table-cell; + } + + .event-info-inner { + display: table-cell; + } + + .community-events .event-date-time { + float: right; + max-width: 39%; + } + + .community-events .event-date, + .community-events .event-time { + text-align: right; + } +} diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 3342cad375..029e20e62b 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -296,6 +296,40 @@ function wp_ajax_autocomplete_user() { wp_die( wp_json_encode( $return ) ); } +/** + * Handles AJAX requests for community events + * + * @since 4.8.0 + */ +function wp_ajax_get_community_events() { + require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' ); + + check_ajax_referer( 'community_events' ); + + $search = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : ''; + $timezone = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : ''; + $user_id = get_current_user_id(); + $saved_location = get_user_option( 'community-events-location', $user_id ); + $events_client = new WP_Community_Events( $user_id, $saved_location ); + $events = $events_client->get_events( $search, $timezone ); + + if ( is_wp_error( $events ) ) { + wp_send_json_error( array( + 'error' => $events->get_error_message(), + ) ); + } else { + if ( isset( $events['location'] ) ) { + // Send only the data that the client will use. + $events['location'] = $events['location']['description']; + + // Store the location network-wide, so the user doesn't have to set it on each site. + update_user_option( $user_id, 'community-events-location', $events['location'], true ); + } + + wp_send_json_success( $events ); + } +} + /** * Ajax handler for dashboard widgets. * diff --git a/src/wp-admin/includes/class-wp-community-events.php b/src/wp-admin/includes/class-wp-community-events.php new file mode 100644 index 0000000000..2b76bd6bc3 --- /dev/null +++ b/src/wp-admin/includes/class-wp-community-events.php @@ -0,0 +1,419 @@ +user_id = absint( $user_id ); + $this->user_location = $user_location; + } + + /** + * Gets data about events near a particular location. + * + * Cached events will be immediately returned if the `user_location` property + * is set for the current user, and cached events exist for that location. + * + * Otherwise, this method sends a request to the w.org Events API with location + * data. The API will send back a recognized location based on the data, along + * with nearby events. + * + * @since 4.8.0 + * + * @param string $location_search Optional city name to help determine the location. + * e.g., "Seattle". Default empty string. + * @param string $timezone Optional timezone to help determine the location. + * Default empty string. + * @return array|WP_Error A WP_Error on failure; an array with location and events on + * success. + */ + public function get_events( $location_search = '', $timezone = '' ) { + $cached_events = $this->get_cached_events(); + + if ( ! $location_search && $cached_events ) { + return $cached_events; + } + + $request_url = $this->get_request_url( $location_search, $timezone ); + $response = wp_remote_get( $request_url ); + $response_code = wp_remote_retrieve_response_code( $response ); + $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); + $response_error = null; + $debugging_info = compact( 'request_url', 'response_code', 'response_body' ); + + if ( is_wp_error( $response ) ) { + $response_error = $response; + } elseif ( 200 !== $response_code ) { + $response_error = new WP_Error( + 'api-error', + /* translators: %s is a numeric HTTP status code; e.g., 400, 403, 500, 504, etc. */ + sprintf( __( 'Invalid API response code (%d)' ), $response_code ) + ); + } elseif ( ! isset( $response_body['location'], $response_body['events'] ) ) { + $response_error = new WP_Error( + 'api-invalid-response', + isset( $response_body['error'] ) ? $response_body['error'] : __( 'Unknown API error.' ) + ); + } + + if ( is_wp_error( $response_error ) ) { + $this->maybe_log_events_response( $response_error->get_error_message(), $debugging_info ); + + return $response_error; + } else { + $expiration = false; + + if ( isset( $response_body['ttl'] ) ) { + $expiration = $response_body['ttl']; + unset( $response_body['ttl'] ); + } + + $this->cache_events( $response_body, $expiration ); + + $response_body = $this->trim_events( $response_body ); + $response_body = $this->format_event_data_time( $response_body ); + + // Avoid bloating the log with all the event data, but keep the count. + $debugging_info['response_body']['events'] = count( $debugging_info['response_body']['events'] ) . ' events trimmed.'; + + $this->maybe_log_events_response( 'Valid response received', $debugging_info ); + + return $response_body; + } + } + + /** + * Builds a URL for requests to the w.org Events API. + * + * @access protected + * @since 4.8.0 + * + * @param string $search City search string. Default empty string. + * @param string $timezone Timezone string. Default empty string. + * @return string The request URL. + */ + protected function get_request_url( $search = '', $timezone = '' ) { + $api_url = 'https://api.wordpress.org/events/1.0/'; + $args = array( 'number' => 5 ); // Get more than three in case some get trimmed out. + + /* + * Send the minimal set of necessary arguments, in order to increase the + * chances of a cache-hit on the API side. + */ + if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) { + $args['latitude'] = $this->user_location['latitude']; + $args['longitude'] = $this->user_location['longitude']; + } else { + $args['locale'] = get_user_locale( $this->user_id ); + + if ( $timezone ) { + $args['timezone'] = $timezone; + } + + if ( $search ) { + $args['location'] = $search; + } else { + /* + * Protect the user's privacy by anonymizing their IP before sending + * it to w.org, and only send it when necessary. + * + * The w.org API endpoint only uses the IP address when a location + * query is not provided, so we can safely avoid sending it when + * there is a query. + */ + $args['ip'] = $this->maybe_anonymize_ip_address( $this->get_unsafe_client_ip() ); + } + } + + return add_query_arg( $args, $api_url ); + } + + /** + * Determines the user's actual IP address, if possible. + * + * $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user + * is making their request through a proxy, or when the web server is behind + * a proxy. In those cases, $_SERVER['REMOTE_ADDR'] is set to the proxy address rather + * than the user's actual address. + * + * Modified from http://stackoverflow.com/a/2031935/450127, MIT license. + * + * SECURITY WARNING: This function is _NOT_ intended to be used in + * circumstances where the authenticity of the IP address matters. This does + * _NOT_ guarantee that the returned address is valid or accurate, and it can + * be easily spoofed. + * + * @access protected + * @since 4.8.0 + * + * @return false|string false on failure, the string address on success. + */ + protected function get_unsafe_client_ip() { + $client_ip = false; + + // In order of preference, with the best ones for this purpose first. + $address_headers = array( + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_FORWARDED_FOR', + 'HTTP_FORWARDED', + 'REMOTE_ADDR', + ); + + foreach ( $address_headers as $header ) { + if ( array_key_exists( $header, $_SERVER ) ) { + /* + * HTTP_X_FORWARDED_FOR can contain a chain of comma-separated + * addresses. The first one is the original client. It can't be + * trusted for authenticity, but we don't need to for this purpose. + */ + $address_chain = explode( ',', $_SERVER[ $header ] ); + $client_ip = trim( $address_chain[0] ); + + break; + } + } + + return $client_ip; + } + + /** + * Attempts to partially anonymize an IP address by converting it to a network ID. + * + * Geolocating the network ID usually returns a similar location as the + * actual IP, but provides some privacy for the user. + * + * Modified from https://github.com/geertw/php-ip-anonymizer, MIT license. + * + * @access protected + * @since 4.8.0 + * + * @param string $address The IP address that should be anonymized. + * @return bool|string The anonymized address on success; the given address + * or false on failure. + */ + protected function maybe_anonymize_ip_address( $address ) { + // These functions are not available on Windows until PHP 5.3. + if ( ! function_exists( 'inet_pton' ) || ! function_exists( 'inet_ntop' ) ) { + return $address; + } + + if ( 4 === strlen( inet_pton( $address ) ) ) { + $netmask = '255.255.255.0'; // ipv4. + } else { + $netmask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; // ipv6. + } + + return inet_ntop( inet_pton( $address ) & inet_pton( $netmask ) ); + } + + /** + * Generates a transient key based on user location. + * + * This could be reduced to a one-liner in the calling functions, but it's + * intentionally a separate function because it's called from multiple + * functions, and having it abstracted keeps the logic consistent and DRY, + * which is less prone to errors. + * + * @access protected + * @since 4.8.0 + * + * @param array $location Should contain 'latitude' and 'longitude' indexes. + * @return bool|string false on failure, or a string on success. + */ + protected function get_events_transient_key( $location ) { + $key = false; + + if ( isset( $location['latitude'], $location['longitude'] ) ) { + $key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] ); + } + + return $key; + } + + /** + * Caches an array of events data from the Events API. + * + * @access protected + * @since 4.8.0 + * + * @param array $events Response body from the API request. + * @param int|bool $expiration Optional. Amount of time to cache the events. Defaults to false. + * @return bool true if events were cached; false if not. + */ + protected function cache_events( $events, $expiration = false ) { + $set = false; + $transient_key = $this->get_events_transient_key( $events['location'] ); + $cache_expiration = $expiration ? absint( $expiration ) : HOUR_IN_SECONDS * 12; + + if ( $transient_key ) { + $set = set_site_transient( $transient_key, $events, $cache_expiration ); + } + + return $set; + } + + /** + * Gets cached events. + * + * @since 4.8.0 + * + * @return false|array false on failure; an array containing `location` + * and `events` items on success. + */ + public function get_cached_events() { + $cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) ); + $cached_response = $this->trim_events( $cached_response ); + + return $this->format_event_data_time( $cached_response ); + } + + /** + * Adds formatted date and time items for each event in an API response. + * + * This has to be called after the data is pulled from the cache, because + * the cached events are shared by all users. If it was called before storing + * the cache, then all users would see the events in the localized data/time + * of the user who triggered the cache refresh, rather than their own. + * + * @access protected + * @since 4.8.0 + * + * @param array $response_body The response which contains the events. + * @return array The response with dates and times formatted. + */ + protected function format_event_data_time( $response_body ) { + if ( isset( $response_body['events'] ) ) { + foreach ( $response_body['events'] as $key => $event ) { + $timestamp = strtotime( $event['date'] ); + + /* + * The `date_format` option is not used because it's important + * in this context to keep the day of the week in the formatted date, + * so that users can tell at a glance if the event is on a day they + * are available, without having to open the link. + */ + /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://secure.php.net/date. */ + $response_body['events'][ $key ]['formatted_date'] = date_i18n( __( 'l, M j, Y' ), $timestamp ); + $response_body['events'][ $key ]['formatted_time'] = date_i18n( get_option( 'time_format' ), $timestamp ); + } + } + + return $response_body; + } + + /** + * Discards expired events, and reduces the remaining list. + * + * @access protected + * @since 4.8.0 + * + * @param array $response_body The response body which contains the events. + * @return array The response body with events trimmed. + */ + protected function trim_events( $response_body ) { + if ( isset( $response_body['events'] ) ) { + $current_timestamp = current_time('timestamp' ); + + foreach ( $response_body['events'] as $key => $event ) { + // Skip WordCamps, because they might be multi-day events. + if ( 'meetup' !== $event['type'] ) { + continue; + } + + $event_timestamp = strtotime( $event['date'] ); + + if ( $current_timestamp > $event_timestamp && ( $current_timestamp - $event_timestamp ) > DAY_IN_SECONDS ) { + unset( $response_body['events'][ $key ] ); + } + } + + $response_body['events'] = array_slice( $response_body['events'], 0, 3 ); + } + + return $response_body; + } + + /** + * Logs responses to Events API requests. + * + * All responses are logged when debugging, even if they're not WP_Errors. + * Debugging info is still needed for "successful" responses, because + * the API might have returned a different location than the one the user + * intended to receive. In those cases, knowing the exact `request_url` is + * critical. + * + * Errors are logged instead of being triggered, to avoid breaking the JSON + * response when called from AJAX handlers and `display_errors` is enabled. + * + * @access protected + * @since 4.8.0 + * + * @param string $message A description of what occurred. + * @param array $debugging_info Details that provide more context for the + * log entry. + */ + protected function maybe_log_events_response( $message, $details ) { + if ( ! WP_DEBUG_LOG ) { + return; + } + + error_log( sprintf( + '%s: %s. Details: %s', + __METHOD__, + trim( $message, '.' ), + wp_json_encode( $details ) + ) ); + } +} diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index be0b201c9f..de0eef3447 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -52,8 +52,8 @@ function wp_dashboard_setup() { wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' ); } - // WordPress News - wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress News' ), 'wp_dashboard_primary' ); + // WordPress Events and News + wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' ); if ( is_network_admin() ) { @@ -129,6 +129,46 @@ function wp_dashboard_setup() { do_action( 'do_meta_boxes', $screen->id, 'side', '' ); } +/** + * Gets the community events data that needs to be passed to dashboard.js. + * + * @since 4.8.0 + * + * @return array The script data. + */ +function wp_get_community_events_script_data() { + require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' ); + + $user_id = get_current_user_id(); + $user_location = get_user_option( 'community-events-location', $user_id ); + $events_client = new WP_Community_Events( $user_id, $user_location ); + + $script_data = array( + 'nonce' => wp_create_nonce( 'community_events' ), + 'cache' => $events_client->get_cached_events(), + + 'l10n' => array( + 'enter_closest_city' => __( 'Enter your closest city to find nearby events.' ), + 'error_occurred_please_try_again' => __( 'An error occured. Please try again.' ), + + /* + * These specific examples were chosen to highlight the fact that a + * state is not needed, even for cities whose name is not unique. + * It would be too cumbersome to include that in the instructions + * to the user, so it's left as an implication. + */ + /* 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. */ + 'could_not_locate_city' => __( "We couldn't locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland." ), + + // This one is only used with wp.a11y.speak(), so it can/should be more brief. + /* translators: %s is the name of a city. */ + 'city_updated' => __( 'City updated. Listing events near %s.' ), + ) + ); + + return $script_data; +} + /** * Adds a new dashboard widget. * @@ -1069,10 +1109,173 @@ function wp_dashboard_rss_control( $widget_id, $form_inputs = array() ) { wp_widget_rss_form( $widget_options[$widget_id], $form_inputs ); } + +/** + * Renders the Events and News dashboard widget. + * + * @since 4.8.0 + */ +function wp_dashboard_events_news() { + wp_print_community_events_markup(); + + ?> + +
+ +
+ + + + + +
+

+ +

+ + + + +
+ +
+ +
+ + + + + + + + + + + + + + + apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ), 'items' => 1, - 'show_summary' => 1, + 'show_summary' => 0, 'show_author' => 0, - 'show_date' => 1, + 'show_date' => 0, ), 'planet' => array( @@ -1152,20 +1355,6 @@ function wp_dashboard_primary() { ) ); - 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' ) ) ) { - $feeds['plugins'] = array( - 'link' => '', - 'url' => array( - 'popular' => 'http://wordpress.org/plugins/rss/browse/popular/', - ), - 'title' => '', - 'items' => 1, - 'show_summary' => 0, - 'show_author' => 0, - 'show_date' => 0, - ); - } - wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds ); } @@ -1173,6 +1362,7 @@ function wp_dashboard_primary() { * Display the WordPress news feeds. * * @since 3.8.0 + * @since 4.8.0 Removed popular plugins feed. * * @param string $widget_id Widget ID. * @param array $feeds Array of RSS feeds. @@ -1181,94 +1371,11 @@ function wp_dashboard_primary_output( $widget_id, $feeds ) { foreach ( $feeds as $type => $args ) { $args['type'] = $type; echo '
'; - if ( $type === 'plugins' ) { - wp_dashboard_plugins_output( $args['url'], $args ); - } else { wp_widget_rss_output( $args['url'], $args ); - } echo "
"; } } -/** - * Display plugins text for the WordPress news widget. - * - * @since 2.5.0 - * - * @param string $rss The RSS feed URL. - * @param array $args Array of arguments for this RSS feed. - */ -function wp_dashboard_plugins_output( $rss, $args = array() ) { - // Plugin feeds plus link to install them - $popular = fetch_feed( $args['url']['popular'] ); - - if ( false === $plugin_slugs = get_transient( 'plugin_slugs' ) ) { - $plugin_slugs = array_keys( get_plugins() ); - set_transient( 'plugin_slugs', $plugin_slugs, DAY_IN_SECONDS ); - } - - echo ''; -} - /** * Display file upload quota on dashboard. * diff --git a/src/wp-admin/includes/deprecated.php b/src/wp-admin/includes/deprecated.php index 2bf25d3336..a9e0e6f9d1 100644 --- a/src/wp-admin/includes/deprecated.php +++ b/src/wp-admin/includes/deprecated.php @@ -1294,6 +1294,88 @@ function wp_dashboard_secondary() {} */ function wp_dashboard_secondary_control() {} +/** + * Display plugins text for the WordPress news widget. + * + * @since 2.5.0 + * @deprecated 4.8.0 + * + * @param string $rss The RSS feed URL. + * @param array $args Array of arguments for this RSS feed. + */ +function wp_dashboard_plugins_output( $rss, $args = array() ) { + _deprecated_function( __FUNCTION__, '4.8.0' ); + + // Plugin feeds plus link to install them + $popular = fetch_feed( $args['url']['popular'] ); + + if ( false === $plugin_slugs = get_transient( 'plugin_slugs' ) ) { + $plugin_slugs = array_keys( get_plugins() ); + set_transient( 'plugin_slugs', $plugin_slugs, DAY_IN_SECONDS ); + } + + echo ''; +} + /** * This was once used to move child posts to a new parent. * diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index 94ad771761..d4be87dfaa 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -565,6 +565,10 @@ function upgrade_all() { if ( $wp_current_db_version < 37965 ) upgrade_460(); + if ( $wp_current_db_version < 40500 ) { //todo update to commit for #40702 + upgrade_480(); + } + maybe_disable_link_manager(); maybe_disable_automattic_widgets(); @@ -1732,6 +1736,26 @@ function upgrade_460() { } } +/** + * Executes changes made in WordPress 4.8.0. + * + * @ignore + * @since 4.8.0 + * + * @global int $wp_current_db_version Current database version. + */ +function upgrade_480() { + global $wp_current_db_version; + + if ( $wp_current_db_version < 40500 ) { // todo update to commit for #40702 + // This feature plugin was merged for #40702, so the plugin itself is no longer needed + deactivate_plugins( array( 'nearby-wp-events/nearby-wordpress-events.php' ), true ); + + // The markup stored in this transient changed for #40702 + delete_transient( 'dash_' . md5( 'dashboard_primary' . '_' . get_locale() ) ); + } +} + /** * Executes network-level upgrade routines. * diff --git a/src/wp-admin/index.php b/src/wp-admin/index.php index d2d7ec889d..76628abbd7 100644 --- a/src/wp-admin/index.php +++ b/src/wp-admin/index.php @@ -15,6 +15,8 @@ require_once(ABSPATH . 'wp-admin/includes/dashboard.php'); wp_dashboard_setup(); wp_enqueue_script( 'dashboard' ); +wp_localize_script( 'dashboard', 'communityEventsData', wp_get_community_events_script_data() ); + if ( current_user_can( 'edit_theme_options' ) ) wp_enqueue_script( 'customize-loader' ); if ( current_user_can( 'install_plugins' ) ) { @@ -138,4 +140,6 @@ include( ABSPATH . 'wp-admin/admin-header.php' ); set_help_sidebar( wp_dashboard_setup(); wp_enqueue_script( 'dashboard' ); +wp_localize_script( 'dashboard', 'communityEventsData', wp_get_community_events_script_data() ); wp_enqueue_script( 'plugin-install' ); add_thickbox(); @@ -73,4 +74,6 @@ require_once( ABSPATH . 'wp-admin/admin-header.php' ); - + __( 'Current Color' ), ) ); - $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox' ), false, 1 ); + $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y' ), false, 1 ); $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" ); diff --git a/tests/phpunit/tests/admin/includesCommunityEvents.php b/tests/phpunit/tests/admin/includesCommunityEvents.php new file mode 100644 index 0000000000..ed5cc2caee --- /dev/null +++ b/tests/phpunit/tests/admin/includesCommunityEvents.php @@ -0,0 +1,258 @@ +instance = new WP_Community_Events( 1, $this->get_user_location() ); + } + + /** + * Simulates a stored user location. + * + * @access private + * @since 4.8.0 + * + * @return array The mock location. + */ + private function get_user_location() { + return array( + 'description' => 'San Francisco', + 'latitude' => '37.7749300', + 'longitude' => '-122.4194200', + 'country' => 'US', + ); + } + + /** + * Test: get_events() should return an instance of WP_Error if the response code is not 200. + * + * @since 4.8.0 + */ + public function test_get_events_bad_response_code() { + add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); + + $this->assertWPError( $this->instance->get_events() ); + + remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); + } + + /** + * Test: The response body should not be cached if the response code is not 200. + * + * @since 4.8.0 + */ + public function test_get_cached_events_bad_response_code() { + add_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); + + $this->instance->get_events(); + + $this->assertFalse( $this->instance->get_cached_events() ); + + remove_filter( 'pre_http_request', array( $this, '_http_request_bad_response_code' ) ); + } + + /** + * Simulates an HTTP response with a non-200 response code. + * + * @since 4.8.0 + * + * @return array A mock response with a 404 HTTP status code + */ + public function _http_request_bad_response_code() { + return array( + 'headers' => '', + 'body' => '', + 'response' => array( + 'code' => 404, + ), + 'cookies' => '', + 'filename' => '', + ); + } + + /** + * Test: get_events() should return an instance of WP_Error if the response body does not have + * the required properties. + * + * @since 4.8.0 + */ + public function test_get_events_invalid_response() { + add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); + + $this->assertWPError( $this->instance->get_events() ); + + remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); + } + + /** + * Test: The response body should not be cached if it does not have the required properties. + * + * @since 4.8.0 + */ + public function test_get_cached_events_invalid_response() { + add_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); + + $this->instance->get_events(); + + $this->assertFalse( $this->instance->get_cached_events() ); + + remove_filter( 'pre_http_request', array( $this, '_http_request_invalid_response' ) ); + } + + /** + * Simulates an HTTP response with a body that does not have the required properties. + * + * @since 4.8.0 + * + * @return array A mock response that's missing required properties. + */ + public function _http_request_invalid_response() { + return array( + 'headers' => '', + 'body' => wp_json_encode( array() ), + 'response' => array( + 'code' => 200, + ), + 'cookies' => '', + 'filename' => '', + ); + } + + /** + * Test: With a valid response, get_events() should return an associated array containing a location array and + * an events array with individual events that have formatted time and date. + * + * @since 4.8.0 + */ + public function test_get_events_valid_response() { + add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); + + $response = $this->instance->get_events(); + + $this->assertNotWPError( $response ); + $this->assertEqualSetsWithIndex( $this->get_user_location(), $response['location'] ); + $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $response['events'][0]['formatted_date'] ); + $this->assertEquals( '1:00 pm', $response['events'][0]['formatted_time'] ); + + remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); + } + + /** + * Test: get_cached_events() should return the same data as get_events(), including formatted time + * and date values for each event. + * + * @since 4.8.0 + */ + public function test_get_cached_events_valid_response() { + add_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); + + $this->instance->get_events(); + + $cached_events = $this->instance->get_cached_events(); + + $this->assertNotWPError( $cached_events ); + $this->assertEqualSetsWithIndex( $this->get_user_location(), $cached_events['location'] ); + $this->assertEquals( date( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $cached_events['events'][0]['formatted_date'] ); + $this->assertEquals( '1:00 pm', $cached_events['events'][0]['formatted_time'] ); + + remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) ); + } + + /** + * Simulates an HTTP response with valid location and event data. + * + * @since 4.8.0 + * + * @return array A mock HTTP response with valid data. + */ + public function _http_request_valid_response() { + return array( + 'headers' => '', + 'body' => wp_json_encode( array( + 'location' => $this->get_user_location(), + 'events' => array( + array( + 'type' => 'meetup', + 'title' => 'Flexbox + CSS Grid: Magic for Responsive Layouts', + 'url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/', + 'meetup' => 'The East Bay WordPress Meetup Group', + 'meetup_url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/', + 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Sunday 1pm' ) ), + 'location' => array( + 'location' => 'Oakland, CA, USA', + 'country' => 'us', + 'latitude' => 37.808453, + 'longitude' => -122.26593, + ), + ), + array( + 'type' => 'meetup', + 'title' => 'Part 3- Site Maintenance - Tools to Make It Easy', + 'url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/', + 'meetup' => 'WordPress Bay Area Foothills Group', + 'meetup_url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/', + 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Wednesday 1:30pm' ) ), + 'location' => array( + 'location' => 'Milpitas, CA, USA', + 'country' => 'us', + 'latitude' => 37.432813, + 'longitude' => -121.907095, + ), + ), + array( + 'type' => 'wordcamp', + 'title' => 'WordCamp Kansas City', + 'url' => 'https://2017.kansascity.wordcamp.org', + 'meetup' => null, + 'meetup_url' => null, + 'date' => date( 'Y-m-d H:i:s', strtotime( 'next Saturday' ) ), + 'location' => array( + 'location' => 'Kansas City, MO', + 'country' => 'US', + 'latitude' => 39.0392325, + 'longitude' => -94.577076, + ), + ), + ), + ) ), + 'response' => array( + 'code' => 200, + ), + 'cookies' => '', + 'filename' => '', + ); + } +}