From b3951af23443d60895bb7f872466623734244d2b Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:02:07 +0000 Subject: [PATCH 1/8] WordPress.com is WordPress too. --- api.wordpress.org/public_html/events/1.0/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.wordpress.org/public_html/events/1.0/index.php b/api.wordpress.org/public_html/events/1.0/index.php index dcbd6c6eae..f343ea9823 100644 --- a/api.wordpress.org/public_html/events/1.0/index.php +++ b/api.wordpress.org/public_html/events/1.0/index.php @@ -302,7 +302,7 @@ function build_response( $location, $location_args ) { */ function is_client_core( $user_agent ) { // This doesn't simply return the value of `strpos()` because `0` means `true` in this context - if ( false === strpos( $user_agent, 'WordPress/' ) ) { + if ( false === strpos( $user_agent, 'WordPress/' ) && false === strpos( $user_agent, 'WordPress.com/' ) ) { return false; } From fe2f6086cd93a0e79296d31869ffaebf3d76b13a Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:03:36 +0000 Subject: [PATCH 2/8] Add Click Tracking. --- .../public_html/events/1.0/index.php | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/api.wordpress.org/public_html/events/1.0/index.php b/api.wordpress.org/public_html/events/1.0/index.php index f343ea9823..115fb8e631 100644 --- a/api.wordpress.org/public_html/events/1.0/index.php +++ b/api.wordpress.org/public_html/events/1.0/index.php @@ -908,7 +908,7 @@ function get_events( $args = array() ) { $raw_events = $wpdb->get_results( $wpdb->prepare( "SELECT - `type`, `title`, `url`, + `type`, `source_id`, `title`, `url`, `meetup`, `meetup_url`, `date_utc`, `date_utc_offset`, `end_date`, `location`, `country`, `latitude`, `longitude` @@ -933,7 +933,7 @@ function get_events( $args = array() ) { $events[] = array( 'type' => $event->type, 'title' => $event->title, - 'url' => $event->url, + 'url' => add_click_tracking( $event->url, $event ), 'meetup' => $event->meetup, 'meetup_url' => $event->meetup_url, @@ -1326,6 +1326,10 @@ function maybe_add_regional_wordcamps( $local_events, $region_data, $user_agent, // before the event until it's over). } + foreach ( $regional_wordcamps as &$event ) { + $event['url'] = add_click_tracking( $event['url'], $event ); + } + return array_merge( $regional_wordcamps, $local_events ); } @@ -1452,7 +1456,7 @@ function pin_next_online_wordcamp( $events, $user_agent, $current_time, $user_co if ( false === $next_online_camp ) { $raw_camp = $wpdb->get_row( " SELECT - `title`, `url`, `meetup`, `meetup_url`, `date_utc`, `date_utc_offset`, `end_date`, `country`, `latitude`, `longitude` + `type`, `source_id`, `title`, `url`, `meetup`, `meetup_url`, `date_utc`, `date_utc_offset`, `end_date`, `country`, `latitude`, `longitude` FROM `wporg_events` WHERE type = 'wordcamp' AND @@ -1465,7 +1469,8 @@ function pin_next_online_wordcamp( $events, $user_agent, $current_time, $user_co if ( isset( $raw_camp->url ) ) { $next_online_camp = array( - 'type' => 'wordcamp', + 'type' => $raw_camp->type, + 'source_id' => $raw_camp->source_id, 'title' => $raw_camp->title, 'url' => $raw_camp->url, 'meetup' => $raw_camp->meetup, @@ -1522,6 +1527,9 @@ function pin_next_online_wordcamp( $events, $user_agent, $current_time, $user_co * potentially-interesting event, and crowding out local events. */ if ( $camp_is_in_users_country || $camp_is_in_next_two_weeks ) { + + $next_online_camp['url'] = add_click_tracking( $next_online_camp['url'], $next_online_camp ); + array_unshift( $events, $next_online_camp ); } } @@ -1565,7 +1573,7 @@ function pin_next_workshop_discussion_group( $events, $user_agent ) { $next_discussion_group = array( 'type' => 'meetup', 'title' => $raw_discussion_group->title, - 'url' => $raw_discussion_group->url, + 'url' => add_click_tracking( $raw_discussion_group->url, $raw_discussion_group ), 'meetup' => $raw_discussion_group->meetup, 'meetup_url' => $raw_discussion_group->meetup_url, @@ -1628,6 +1636,7 @@ function pin_one_off_events( $events, $current_time ) { ); if ( $current_time > strtotime( 'December 11, 2024' ) && $current_time < strtotime( 'December 17, 2024' ) ) { + $sotw['url'] = add_click_tracking( $sotw['url'], $sotw ); array_unshift( $events, $sotw ); } @@ -1724,4 +1733,31 @@ function get_bounded_coordinates( $lat, $lon, $distance_in_km = 50 ) { ); } +/** + * Add click tracking through a redirect. + * + * @param string $url The original URL. + * @param object $event The event object. + * @return string The tracked URL. + */ +function add_click_tracking( $url, $event ) { + // Inconsistent in API, sometimes arrays. + if ( is_array( $event ) ) { + $event = (object) $event; + $event->location = (object) $event->location; + } + + // Need both type and source_id to build the tracked link. + if ( empty( $event->type ) || empty( $event->source_id ) ) { + return $url; + } + + $tracked_link = 'https://api.wordpress.org/events/redirect/'; + $tracked_link .= '?' . urlencode( $event->type ) . '=' . urlencode( $event->source_id ); + $tracked_link .= '&url=' . urlencode( $url ); + $tracked_link .= '&source=' . ( is_client_core( $_SERVER['HTTP_USER_AGENT'] ) ? 'core' : 'api' ); + + return $tracked_link; +} + main(); From c01354aa7b04af74af695e01da8dcde8aeab295c Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:03:53 +0000 Subject: [PATCH 3/8] Limit to Australia first. --- api.wordpress.org/public_html/events/1.0/index.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api.wordpress.org/public_html/events/1.0/index.php b/api.wordpress.org/public_html/events/1.0/index.php index 115fb8e631..e72c12ae58 100644 --- a/api.wordpress.org/public_html/events/1.0/index.php +++ b/api.wordpress.org/public_html/events/1.0/index.php @@ -1747,6 +1747,11 @@ function add_click_tracking( $url, $event ) { $event->location = (object) $event->location; } + // Start down under. + if ( empty( $event->country ) || 'AU' !== strtoupper( $event->country ) ) { + return $url; + } + // Need both type and source_id to build the tracked link. if ( empty( $event->type ) || empty( $event->source_id ) ) { return $url; From 36b10499ceaf95ac4f0f12c7485516018c7743b3 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:05:19 +0000 Subject: [PATCH 4/8] Add the events/redirect endpoint. --- .../public_html/events/redirect/index.php | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 api.wordpress.org/public_html/events/redirect/index.php diff --git a/api.wordpress.org/public_html/events/redirect/index.php b/api.wordpress.org/public_html/events/redirect/index.php new file mode 100644 index 0000000000..4417928719 --- /dev/null +++ b/api.wordpress.org/public_html/events/redirect/index.php @@ -0,0 +1,84 @@ +get_row( + $wpdb->prepare( + "SELECT url, country + FROM wporg_events + WHERE type = %s AND source_id = %d + LIMIT 1", + $type, + $source_id + ) + ); +} + +if ( ! empty( $event->url ) ) { + $url = $event->url; // We trust the URL we've stored. +} else { + // If no event, validate the provided $url and redirect there. + $type = 'unknown-' . $type; + // Only allow redirects to known domains. + if ( ! preg_match( '#^https?://([^/]+\.)?(meetup.com|wordpress.org|wordcamp.org|doaction.org)/#i', $url ) ) { + $url = 'https://events.wordpress.org/'; + } + + // We could just sign the request, but for simplicity, we'll just use the above validation. +} + +// Redirect +header( 'Location: ' . $url, true, 302 ); + +if ( function_exists( 'fastcgi_finish_request' ) ) { + fastcgi_finish_request(); +} + +if ( function_exists( 'bump_stats_extra' ) ) { + bump_stats_extra( 'events-clicks', $type ); + if ( $event ) { + bump_stats_extra( 'events-clicks-country', strtoupper( $event->country ) ); + } + if ( isset( $_GET['ref'] ) && in_array( $_GET['ref'], [ 'core', 'api', 'events', 'email' ], true ) ) { + bump_stats_extra( 'events-clicks-ref', $_GET['ref'] ); + } +} + +/* +if ( $type && $source_id && $event ) { + $wpdb->query( + $wpdb->prepare( + "UPDATE wporg_events SET clicks = clicks + 1 WHERE type = %s AND source_id = %d", + $type, + $source_id + ) + ); +} +*/ \ No newline at end of file From 3a2b6b35e10e83bf6250253195eee7ce17d7dd79 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:31:46 +0000 Subject: [PATCH 5/8] Switch Events plugin from using REPLACE to using INSERT ON DUPLICATE KEY --- .../official-wordpress-events.php | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php b/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php index c288416d4f..ce3506523f 100644 --- a/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php +++ b/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php @@ -101,8 +101,6 @@ protected function get_meetup_client() { * be careful to maintain consistency when making any changes to this. */ public function prime_events_cache() { - global $wpdb; - $this->log( 'started call #' . did_action( 'owpe_prime_events_cache' ) ); if ( did_action( 'owpe_prime_events_cache' ) > 1 ) { @@ -116,7 +114,6 @@ public function prime_events_cache() { foreach ( $events as $event ) { $row_values = array( - 'id' => null, 'type' => $event->type, 'source_id' => $event->source_id, 'status' => $event->status, @@ -140,20 +137,54 @@ public function prime_events_cache() { continue; } - /* - * Insert the events into the table, without creating duplicates - * - * Note: Since replace() is matching against a unique key rather than the primary `id` key, it's - * expected for each row to be deleted and re-inserted, making the IDs increment each time. - * - * See http://stackoverflow.com/a/12205366/450127 - */ - $wpdb->replace( self::EVENTS_TABLE, $row_values ); + $this->insert_on_duplicate_key_update( + $wpdb->prefix . self::EVENTS_TABLE, + $row_values, + array_keys( $row_values ) + ); } $this->log( "finished job\n\n" ); } + /** + * INSERT INTO ... ON DUPLICATE KEY UPDATE ... helper + * + * @param string $table + * @param array $data + * @param array $update_keys + */ + protected function insert_on_duplicate_key_update( $table, $data, $update_keys ) { + global $wpdb; + + $sql = 'INSERT INTO %i ('; + $args = array( $table ); + $fields_sql = ''; + $values_sql = ''; + $values_args = []; + foreach ( $data as $field => $value ) { + $fields_sql .= '%i, '; + $values_sql .= '%s, '; + + $args[] = $field; + $values_args[] = $value; + } + $sql .= rtrim( $fields_sql, ', ' ) . ') VALUES ( ' . rtrim( $values_sql, ', ' ) . ') '; + + $args = array_merge( $args, $values_args ); + unset( $values_args ); + + $sql .= 'ON DUPLICATE KEY UPDATE '; + foreach ( $update_keys as $field ) { + $sql .= '%i = VALUES(%i), '; + $args[] = $field; + $args[] = $field; + } + $sql = rtrim( $sql, ', ' ); + + $wpdb->query( $wpdb->prepare( $sql, ...$args ) ); + } + /** * Enqueue scripts and styles */ From 25bbae84a0170bebcb84b64d2d898fad58150e4a Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:33:15 +0000 Subject: [PATCH 6/8] Add a click tracker counter to the events store. --- api.wordpress.org/public_html/events/redirect/index.php | 4 +--- .../official-wordpress-events.php | 9 ++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/api.wordpress.org/public_html/events/redirect/index.php b/api.wordpress.org/public_html/events/redirect/index.php index 4417928719..56a6ee218b 100644 --- a/api.wordpress.org/public_html/events/redirect/index.php +++ b/api.wordpress.org/public_html/events/redirect/index.php @@ -71,7 +71,6 @@ } } -/* if ( $type && $source_id && $event ) { $wpdb->query( $wpdb->prepare( @@ -80,5 +79,4 @@ $source_id ) ); -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php b/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php index ce3506523f..d7875f1cc4 100644 --- a/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php +++ b/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php @@ -130,6 +130,8 @@ public function prime_events_cache() { 'country' => $event->country_code, 'latitude' => $event->latitude, 'longitude' => $event->longitude, + 'clicks' => 0, + 'created_at' => gmdate( 'Y-m-d H:i:s' ), ); // Latitude and longitude are required by the database, so skip events that don't have one. @@ -137,10 +139,15 @@ public function prime_events_cache() { continue; } + $keys_not_to_update = array( + 'clicks', + 'created_at', + ); + $this->insert_on_duplicate_key_update( $wpdb->prefix . self::EVENTS_TABLE, $row_values, - array_keys( $row_values ) + array_diff( array_keys( $row_values ), $keys_not_to_update ) ); } From 432d760148ef00ea9f072b631dd39282cc874ab5 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:44:59 +0000 Subject: [PATCH 7/8] The location field isn't present here. --- api.wordpress.org/public_html/events/1.0/index.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api.wordpress.org/public_html/events/1.0/index.php b/api.wordpress.org/public_html/events/1.0/index.php index e72c12ae58..6ec64486d8 100644 --- a/api.wordpress.org/public_html/events/1.0/index.php +++ b/api.wordpress.org/public_html/events/1.0/index.php @@ -1742,10 +1742,7 @@ function get_bounded_coordinates( $lat, $lon, $distance_in_km = 50 ) { */ function add_click_tracking( $url, $event ) { // Inconsistent in API, sometimes arrays. - if ( is_array( $event ) ) { - $event = (object) $event; - $event->location = (object) $event->location; - } + $event = (object) $event; // Start down under. if ( empty( $event->country ) || 'AU' !== strtoupper( $event->country ) ) { From c64dc65293b827c008b7425c8bdf4525526ae7db Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 14 Nov 2025 03:45:55 +0000 Subject: [PATCH 8/8] Correct a comment. --- api.wordpress.org/public_html/events/redirect/index.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api.wordpress.org/public_html/events/redirect/index.php b/api.wordpress.org/public_html/events/redirect/index.php index 56a6ee218b..3ea174013f 100644 --- a/api.wordpress.org/public_html/events/redirect/index.php +++ b/api.wordpress.org/public_html/events/redirect/index.php @@ -12,8 +12,9 @@ // for bump_stats_extra(). include_once WPORGPATH . 'wp-content/mu-plugins/1-stats-extra.php'; +// Cache in browsers for a day, but not server. +header( 'Cache-Control: private, max-age=86400' ); allow_cors_requests(); -header( 'Cache-Control: private, max-age=86400' ); // Cache in browsers for an hour, but not server. $event = false; $url = urldecode( $_REQUEST['url'] ); // Fallback incase of unexpected DB failure.