Index: wp-content/plugins/theme-directory/jobs/class-manager.php
===================================================================
--- wp-content/plugins/theme-directory/jobs/class-manager.php	(revision 0)
+++ wp-content/plugins/theme-directory/jobs/class-manager.php	(working copy)
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Manager to wrap up all the logic for Cron tasks.
+ *
+ * @package WordPressdotorg\Theme_Directory\Jobs
+ */
+
+namespace WordPressdotorg\Theme_Directory\Jobs;
+
+/**
+ * Class Manager
+ *
+ * @package WordPressdotorg\Theme_Directory\Jobs
+ */
+class Manager {
+
+	/**
+	 * Add all the actions for cron tasks and schedules.
+	 */
+	public function __construct() {
+		// Register all the cron task handlers.
+		add_action( 'admin_init', [ $this, 'register_cron_tasks' ] );
+		add_filter( 'cron_schedules', [ $this, 'register_schedules' ] );
+
+		// The actual cron hooks.
+		add_action( 'theme_directory_trac_sync', [ __NAMESPACE__ . '\Trac_Sync', 'cron_trigger' ] );
+
+		// A cronjob to check cronjobs.
+		add_action( 'theme_directory_check_cronjobs', [ $this, 'register_cron_tasks' ] );
+	}
+
+	/**
+	 * Register any cron schedules needed.
+	 *
+	 * @see wp_get_schedules() for core-registered schedules.
+	 *
+	 * @param array $schedules Registered schedule intervals.
+	 * @return array
+	 */
+	public function register_schedules( $schedules ) {
+		$schedules['every_15m']  = [
+			'interval' => 15 * MINUTE_IN_SECONDS,
+			'display'  => 'Every 15 minutes',
+		];
+		$schedules['every_120s'] = [
+			'interval' => 2 * MINUTE_IN_SECONDS,
+			'display'  => 'Every 120 seconds',
+		];
+
+		return $schedules;
+	}
+
+	/**
+	 * Queue all of our cron tasks.
+	 *
+	 * The jobs are queued for 1 minutes time to avoid recurring job failures from repeating too soon.
+	 */
+	public function register_cron_tasks() {
+		if ( ! wp_next_scheduled( 'theme_directory_trac_sync' ) ) {
+			wp_schedule_event( time() + 60, 'every_15m', 'theme_directory_trac_sync' );
+		}
+
+		if ( ! wp_next_scheduled( 'theme_directory_check_cronjobs' ) ) {
+			wp_schedule_event( time() + 60, 'every_120s', 'theme_directory_check_cronjobs' );
+		}
+	}
+}

Property changes on: wp-content/plugins/theme-directory/jobs/class-manager.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: wp-content/plugins/theme-directory/jobs/class-trac-sync.php
===================================================================
--- wp-content/plugins/theme-directory/jobs/class-trac-sync.php	(revision 0)
+++ wp-content/plugins/theme-directory/jobs/class-trac-sync.php	(working copy)
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Watch Trac reviews and queue up jobs to update themes with review results.
+ *
+ * @package WordPressdotorg\Theme_Directory\Jobs
+ */
+
+namespace WordPressdotorg\Theme_Directory\Jobs;
+
+/**
+ * Class Trac_Sync
+ *
+ * @package WordPressdotorg\Theme_Directory\Jobs
+ */
+class Trac_Sync {
+
+	/**
+	 * Trac statuses.
+	 *
+	 * @var array
+	 */
+	protected static $stati = [
+		'new'  => [
+			'status' => 'reopened',
+		],
+		'live' => [
+			'status'     => 'closed',
+			'resolution' => 'live',
+		],
+		'old'  => [
+			'status'     => 'closed',
+			'resolution' => 'not-approved',
+		],
+	];
+
+	/**
+	 * The cron trigger for the svn import job.
+	 */
+	public static function cron_trigger() {
+		if ( ! class_exists( 'Trac' ) ) {
+			require_once ABSPATH . WPINC . '/class-IXR.php';
+			require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php';
+			require_once WP_PLUGIN_DIR . '/theme-directory/lib/class-trac.php';
+		}
+
+		$trac         = new \Trac( 'themetracbot', THEME_TRACBOT_PASSWORD, 'https://themes.trac.wordpress.org/login/xmlrpc' );
+		$last_request = get_option( 'wporg-themes-last-trac-sync', strtotime( '-2 days' ) );
+		update_option( 'wporg-themes-last-trac-sync', time() );
+
+		foreach ( self::$stati as $new_status => $args ) {
+			// Get array of tickets.
+			$tickets = (array) $trac->ticket_query( add_query_arg( wp_parse_args( $args, [
+				'order'      => 'changetime',
+				'changetime' => date( 'c', $last_request ),
+				'desc'       => 1,
+			] ) ) );
+
+			foreach ( $tickets as $ticket_id ) {
+				// Get the theme associated with that ticket.
+				$theme_id = self::get_theme_id( $ticket_id );
+				if ( ! $theme_id ) {
+					continue;
+				}
+
+				// If there was a newer-version-uploaded, we have more than one version per ticket.
+				$versions = array_keys( (array) get_post_meta( $theme_id, '_ticket_id', true ), $ticket_id, true );
+				usort( $versions, 'version_compare' );
+				$version = end( $versions );
+
+				// There should always be a version associated with a ticket.
+				if ( ! $version ) {
+					continue;
+				}
+
+				/*
+				 * Bail if the the theme has the wrong status.
+				 *
+				 * For approved and rejected themes, we bail if the current status is not
+				 * 'new' That can happen when there are additional ticket updates (like
+				 * comments) after the ticket was closed.
+				 *
+				 * For reopened tickets we bail if the version is already marked as 'new'.
+				 * This should only be the case if the ticket was closed and reopened before
+				 * this script was able to sync the closed status.
+				 */
+				$current_status = wporg_themes_get_version_status( $theme_id, $version );
+				if ( ( 'new' !== $new_status && 'new' !== $current_status ) || ( 'new' === $new_status && 'new' === $current_status ) ) {
+					continue;
+				}
+
+				// We don't need to set an already approved live version to live again.
+				if ( 'live' === $current_status && 'live' === $new_status ) {
+					continue;
+				}
+
+				wporg_themes_update_version_status( $theme_id, $version, $new_status );
+			}
+		}
+	}
+
+	/**
+	 * Returns the ID of a theme associated with the passed ticket number.
+	 *
+	 * @param string $ticket_id Trac ticket number.
+	 * @return int The post ID, or 0 if none can be found.
+	 */
+	public static function get_theme_id( $ticket_id ) {
+		$post_id = 0;
+
+		$post_ids = new WP_Query( [
+			'fields'         => 'ids',
+			'post_status'    => 'any',
+			'post_type'      => 'repopackage',
+
+			// phpcs:ignore WordPress.VIP.SlowDBQuery.slow_db_query_meta_query
+			'meta_query'     => [
+				[
+					'value'   => $ticket_id,
+					'compare' => 'IN',
+				],
+			],
+
+			// phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_posts_per_page
+			'posts_per_page' => - 1,
+		] );
+
+		if ( ! empty( $post_ids ) ) {
+			$post_id = current( $post_ids );
+		}
+
+		return $post_id;
+	}
+}

Property changes on: wp-content/plugins/theme-directory/jobs/class-trac-sync.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: wp-content/plugins/theme-directory/theme-directory.php
===================================================================
--- wp-content/plugins/theme-directory/theme-directory.php	(revision 6259)
+++ wp-content/plugins/theme-directory/theme-directory.php	(working copy)
@@ -29,6 +29,11 @@
 // Load the query modifications needed for the directory.
 include __DIR__ . '/query-modifications.php';
 
+// Load repo jobs.
+include __DIR__ . '/jobs/class-manager.php';
+include __DIR__ . '/jobs/class-trac-sync.php';
+new WordPressdotorg\Theme_Directory\Jobs\Manager();
+
 /**
  * Things to change on activation.
  */
