diff --git a/public_html/wp-content/mu-plugins/latest-site-hints.php b/public_html/wp-content/mu-plugins/latest-site-hints.php
index bca31578d9..f60988facd 100644
--- a/public_html/wp-content/mu-plugins/latest-site-hints.php
+++ b/public_html/wp-content/mu-plugins/latest-site-hints.php
@@ -7,6 +7,7 @@
defined( 'WPINC' ) || die();
add_action( 'wp', __NAMESPACE__ . '\maybe_add_latest_site_hints' );
+add_filter( 'jetpack_contact_form_html', __NAMESPACE__ . '\maybe_disable_contact_form' );
/**
* If user or bot visits WordCamp site that has newer site for the same city,
@@ -135,6 +136,86 @@ function show_notification_about_latest_site() {
'
';
}
+/**
+ * Check if contact forms should be disabled on this site.
+ *
+ * Forms are disabled when:
+ * 1. There's a newer event site for this city, OR
+ * 2. The event ended more than 18 months ago.
+ *
+ * This reduces spam sent to organizers of past events.
+ *
+ * See https://github.com/WordPress/wordcamp.org/issues/1468
+ *
+ * @param string $html The contact form HTML.
+ *
+ * @return string The original form HTML or a replacement message.
+ */
+function maybe_disable_contact_form( $html ) {
+ global $current_blog;
+
+ $wordcamp = get_wordcamp_post();
+ $end_date = absint( $wordcamp->meta['End Date (YYYY-mm-dd)'][0] ?? 0 );
+
+ // Check if the event is still ongoing.
+ if ( $end_date && time() < ( (int) $end_date + DAY_IN_SECONDS ) ) {
+ return $html;
+ }
+
+ $has_newer_site = false;
+ $expired_by_time = false;
+
+ // Check if the event ended more than 18 months ago.
+ if ( $end_date && time() > ( (int) $end_date + 18 * MONTH_IN_SECONDS ) ) {
+ $expired_by_time = true;
+ }
+
+ // Optimization: skip the newer-site database query during the first 3 months,
+ // since a new edition is unlikely to exist that soon.
+ if ( ! $expired_by_time && $end_date && time() < ( (int) $end_date + 3 * MONTH_IN_SECONDS ) ) {
+ return $html;
+ }
+
+ // Check if there's a newer site for this city.
+ $latest_domain = get_latest_home_url( $current_blog->domain, $current_blog->path );
+ if ( $latest_domain && trailingslashit( get_site_url() ) !== $latest_domain ) {
+ $has_newer_site = true;
+ }
+
+ if ( ! $has_newer_site && ! $expired_by_time ) {
+ return $html;
+ }
+
+ $event_name = esc_html( get_blog_details( $current_blog->blog_id )->blogname );
+
+ if ( $has_newer_site ) {
+ $message = wp_kses_post( sprintf(
+ // translators: %1$s is the event name, %2$s is the URL of the latest edition.
+ __( '%1$s is over. Please visit the next edition for more details and to get in touch.', 'wordcamporg' ),
+ $event_name,
+ esc_url( $latest_domain )
+ ) );
+ } else {
+ $message = wp_kses_post( sprintf(
+ // translators: %s is the event name.
+ __( '%s is over and this contact form is no longer active.', 'wordcamporg' ),
+ $event_name
+ ) );
+ }
+
+ $coc_message = wp_kses_post( sprintf(
+ // translators: %s is the mailto link for incident reports.
+ __( 'If you have a Code of Conduct concern, you can report it to the WordPress Incident Response Team.', 'wordcamporg' ),
+ 'reports@wordpress.org'
+ ) );
+
+ return sprintf(
+ '
%s
%s
',
+ $message,
+ $coc_message
+ );
+}
+
/**
* Get the home URL of the most recent event in a given city.
*
diff --git a/public_html/wp-content/mu-plugins/tests/test-disable-contact-form.php b/public_html/wp-content/mu-plugins/tests/test-disable-contact-form.php
new file mode 100644
index 0000000000..6075fb8961
--- /dev/null
+++ b/public_html/wp-content/mu-plugins/tests/test-disable-contact-form.php
@@ -0,0 +1,245 @@
+';
+
+ /**
+ * Create WordCamp posts on the central site for our test sites.
+ *
+ * @param WP_UnitTest_Factory $factory
+ */
+ public static function wpSetUpBeforeClass( $factory ) {
+ parent::wpSetUpBeforeClass( $factory );
+
+ switch_to_blog( WORDCAMP_ROOT_BLOG_ID );
+
+ // Register the `wordcamp` post type if not already registered.
+ if ( ! post_type_exists( 'wordcamp' ) ) {
+ register_post_type( 'wordcamp' );
+ }
+
+ // Past event (2018 Seattle) - has a newer site (2019), ended long ago.
+ $past_event_id = wp_insert_post( array(
+ 'post_type' => 'wordcamp',
+ 'post_title' => 'WordCamp Seattle 2018',
+ 'post_status' => 'publish',
+ ) );
+ update_post_meta( $past_event_id, '_site_id', self::$year_dot_2018_site_id );
+ update_post_meta( $past_event_id, 'End Date (YYYY-mm-dd)', strtotime( '2018-06-15' ) );
+ self::$wordcamp_post_ids[ self::$year_dot_2018_site_id ] = $past_event_id;
+
+ // Current/future event (2019 Seattle) - newest site, end date in the future.
+ $current_event_id = wp_insert_post( array(
+ 'post_type' => 'wordcamp',
+ 'post_title' => 'WordCamp Seattle 2019',
+ 'post_status' => 'publish',
+ ) );
+ update_post_meta( $current_event_id, '_site_id', self::$year_dot_2019_site_id );
+ update_post_meta( $current_event_id, 'End Date (YYYY-mm-dd)', time() + YEAR_IN_SECONDS );
+ self::$wordcamp_post_ids[ self::$year_dot_2019_site_id ] = $current_event_id;
+
+ // Recent past event (Vancouver 2020) - newest site for city, ended 6 months ago.
+ $recent_past_id = wp_insert_post( array(
+ 'post_type' => 'wordcamp',
+ 'post_title' => 'WordCamp Vancouver 2020',
+ 'post_status' => 'publish',
+ ) );
+ update_post_meta( $recent_past_id, '_site_id', self::$slash_year_2020_site_id );
+ update_post_meta( $recent_past_id, 'End Date (YYYY-mm-dd)', time() - ( 6 * MONTH_IN_SECONDS ) );
+ self::$wordcamp_post_ids[ self::$slash_year_2020_site_id ] = $recent_past_id;
+
+ // Old event with no newer site (Vancouver 2016) - ended more than 18 months ago.
+ $old_event_id = wp_insert_post( array(
+ 'post_type' => 'wordcamp',
+ 'post_title' => 'WordCamp Vancouver 2016',
+ 'post_status' => 'publish',
+ ) );
+ update_post_meta( $old_event_id, '_site_id', self::$slash_year_2016_site_id );
+ update_post_meta( $old_event_id, 'End Date (YYYY-mm-dd)', strtotime( '2016-06-15' ) );
+ self::$wordcamp_post_ids[ self::$slash_year_2016_site_id ] = $old_event_id;
+
+ // Event with no end date (Japan yearless site).
+ $no_date_id = wp_insert_post( array(
+ 'post_type' => 'wordcamp',
+ 'post_title' => 'WordCamp Japan',
+ 'post_status' => 'publish',
+ ) );
+ update_post_meta( $no_date_id, '_site_id', self::$yearless_site_id );
+ self::$wordcamp_post_ids[ self::$yearless_site_id ] = $no_date_id;
+
+ restore_current_blog();
+ }
+
+ /**
+ * Clean up WordCamp posts created during setup.
+ */
+ public static function wpTearDownAfterClass() {
+ switch_to_blog( WORDCAMP_ROOT_BLOG_ID );
+
+ foreach ( self::$wordcamp_post_ids as $post_id ) {
+ wp_delete_post( $post_id, true );
+ }
+
+ restore_current_blog();
+
+ parent::wpTearDownAfterClass();
+ }
+
+ /**
+ * Switch to the given blog and update the $current_blog global.
+ *
+ * `switch_to_blog()` does not update `$current_blog`, but
+ * `maybe_disable_contact_form()` reads from it directly.
+ *
+ * @param int $blog_id The blog ID to switch to.
+ */
+ protected function switch_to_blog_with_globals( $blog_id ) {
+ global $current_blog;
+
+ switch_to_blog( $blog_id );
+
+ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional for test setup.
+ $current_blog = get_site( $blog_id );
+ }
+
+ /**
+ * Restore the previous blog and its $current_blog global.
+ */
+ protected function restore_blog_with_globals() {
+ global $current_blog;
+
+ restore_current_blog();
+
+ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional for test teardown.
+ $current_blog = get_site( get_current_blog_id() );
+ }
+
+ /**
+ * Test that contact forms are disabled when the event has a newer site.
+ *
+ * @covers WordCamp\Latest_Site_Hints\maybe_disable_contact_form
+ */
+ public function test_past_event_with_newer_site_disables_form() {
+ $this->switch_to_blog_with_globals( self::$year_dot_2018_site_id );
+
+ $result = maybe_disable_contact_form( self::$sample_form_html );
+
+ $this->assertStringContainsString( 'wordcamp-contact-form-disabled', $result );
+ $this->assertStringContainsString( 'is over', $result );
+ $this->assertStringContainsString( 'the next edition', $result );
+ $this->assertStringNotContainsString( '