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..6ec64486d8 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;
}
@@ -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,33 @@ 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.
+ $event = (object) $event;
+
+ // 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;
+ }
+
+ $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();
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..3ea174013f
--- /dev/null
+++ b/api.wordpress.org/public_html/events/redirect/index.php
@@ -0,0 +1,83 @@
+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
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..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
@@ -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,
@@ -133,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.
@@ -140,20 +139,59 @@ 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 );
+ $keys_not_to_update = array(
+ 'clicks',
+ 'created_at',
+ );
+
+ $this->insert_on_duplicate_key_update(
+ $wpdb->prefix . self::EVENTS_TABLE,
+ $row_values,
+ array_diff( array_keys( $row_values ), $keys_not_to_update )
+ );
}
$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
*/