Index: wordpress.org/public_html/wp-content/plugins/wporg-markdown/README.md
===================================================================
--- wordpress.org/public_html/wp-content/plugins/wporg-markdown/README.md	(nonexistent)
+++ wordpress.org/public_html/wp-content/plugins/wporg-markdown/README.md	(working copy)
@@ -0,0 +1,47 @@
+# WPORG Markdown Importer
+
+Imports Markdown from a remote site (like GitHub) into WordPress as pages.
+
+## Configuration
+
+Each importer needs to override the abstract methods:
+
+* `get_base()` - Base URL for imported pages. This will be stripped from the key before comparing.
+* `get_manifest_url()` - URL pointing to the manifest.
+* `get_post_type()` - Post type to import as.
+
+## Manifest Format
+
+The manifest should be a JSON object, with the keys set to the desired permalink (excluding the base path). Each item should also be a JSON object, containing the following keys:
+
+* `slug` - Post name to insert. (Must match the final path-part of the key.)
+* `markdown_source` - URL for the Markdown file to parse into content.
+* `parent` - Key for the parent to store under. (Must correspond to the non-final path-parts of the key.)
+* `title` - Title to use when creating post. Used temporarily, will be updated from the Markdown file. If not specified, defaults to `slug` (but will be updated from Markdown source).
+
+**Note:** The Handbook index should have the slug `index`.
+
+Example:
+
+```json
+{
+	"foo": {
+		"title": "Temporary Foo Title",
+		"slug": "foo",
+		"markdown_source": "https://raw.githubusercontent.com/WordPress/doc-repo/master/foo.md",
+		"parent": null
+	},
+	"foo/bar": {
+		"title": "Temporary Bar Title",
+		"slug": "bar",
+		"markdown_source": "https://raw.githubusercontent.com/WordPress/doc-repo/master/foo/bar.md",
+		"parent": "foo"
+	},
+	"foo/bar/quux": {
+		"title": "Temporary Quux Title",
+		"slug": "quux",
+		"markdown_source": "https://raw.githubusercontent.com/WordPress/doc-repo/master/foo/bar/quux.md",
+		"parent": "foo/bar"
+	}
+}
+```
Index: wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-editor.php
===================================================================
--- wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-editor.php	(nonexistent)
+++ wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-editor.php	(working copy)
@@ -0,0 +1,186 @@
+<?php
+
+namespace WordPressdotorg\Markdown;
+
+use WP_Post;
+
+class Editor {
+	public function __construct( Importer $importer ) {
+		$this->importer = $importer;
+	}
+
+	public function init() {
+		add_filter( 'the_title', array( $this, 'filter_the_title_edit_link' ), 10, 2 );
+		add_filter( 'get_edit_post_link', array( $this, 'redirect_edit_link_to_github' ), 10, 3 );
+		add_filter( 'o2_filter_post_actions', array( $this, 'redirect_o2_edit_link_to_github' ), 11, 2 );
+		add_action( 'wp_head', array( $this, 'render_edit_button_style' ) );
+		add_action( 'edit_form_top', array( $this, 'render_editor_warning' ) );
+	}
+
+	public function render_edit_button_style() {
+		?>
+		<style>
+			a.github-edit {
+				margin-left: .5em;
+				font-size: .5em;
+				vertical-align: top;
+				display: inline-block;
+				border: 1px solid #eeeeee;
+				border-radius: 2px;
+				background: #eeeeee;
+				padding: .5em .6em .4em;
+				color: black;
+				margin-top: 0.1em;
+			}
+			a.github-edit > * {
+				opacity: 0.6;
+			}
+			a.github-edit:hover > * {
+				opacity: 1;
+				color: black;
+			}
+			a.github-edit img {
+				height: .8em;
+			}
+		</style>
+		<?php
+	}
+
+	/**
+	 * Render a warning for editors accessing the edit page via the admin.
+	 *
+	 * @param WP_Post $post Post being edited.
+	 */
+	public function render_editor_warning( WP_Post $post ) {
+		if ( $post->post_type !== $this->importer->get_post_type() ) {
+			return;
+		}
+
+		printf(
+			'<div class="notice notice-warning"><p>%s</p><p><a href="%s">%s</a></p></div>',
+			'This page is maintained on GitHub. Content, title, and slug edits here will be discarded on next sync.',
+			$this->get_markdown_edit_link( $post->ID ),
+			'Edit on GitHub'
+		);
+	}
+
+	/**
+	 * Append a "Edit on GitHub" link to Handbook document titles
+	 */
+	public function filter_the_title_edit_link( $title, $id = null ) {
+		// Only apply to the main title for the document
+		if ( ! is_singular( $this->importer->get_post_type() )
+			|| ! is_main_query()
+			|| ! in_the_loop()
+			|| $id !== get_queried_object_id() ) {
+			return $title;
+		}
+
+		$markdown_source = $this->get_markdown_edit_link( get_the_ID() );
+		if ( ! $markdown_source ) {
+			return $title;
+		}
+
+		$src = plugins_url( 'assets/images/github-mark.svg', dirname( dirname( __DIR__ ) ) . '/wporg-cli/wporg-cli.php' );
+
+		return $title . ' <a class="github-edit" href="' . esc_url( $markdown_source ) . '"><img src="' . esc_url( $src ) . '"> <span>Edit</span></a>';
+	}
+
+	/**
+	 * WP-CLI Handbook pages are maintained in the GitHub repo, so the edit
+	 * link should ridirect to there.
+	 */
+	public function redirect_edit_link_to_github( $link, $post_id, $context ) {
+		if ( is_admin() ) {
+			return $link;
+		}
+		$post = get_post( $post_id );
+		if ( ! $post ) {
+			return $link;
+		}
+
+		if ( $this->importer->get_post_type() !== $post->post_type ) {
+			return $link;
+		}
+
+		$markdown_source = $this->get_markdown_edit_link( $post_id );
+		if ( ! $markdown_source ) {
+			return $link;
+		}
+
+		if ( 'display' === $context ) {
+			$markdown_source = esc_url( $markdown_source );
+		}
+
+		return $markdown_source;
+	}
+
+	/**
+	 * o2 does inline editing, so we also need to remove the class name that it looks for.
+	 *
+	 * o2 obeys the edit_post capability for displaying the edit link, so we also need to manually
+	 * add the edit link if it isn't there - it always redirects to GitHub, so it doesn't need to
+	 * obey the edit_post capability in this instance.
+	 */
+	public static function redirect_o2_edit_link_to_github( $actions, $post_id ) {
+		$post = get_post( $post_id );
+		if ( ! $post ) {
+			return $actions;
+		}
+
+		if ( $this->importer->get_post_type() !== $post->post_type ) {
+			return $actions;
+		}
+
+		$markdown_source = $this->get_markdown_edit_link( $post_id );
+		if ( ! $markdown_source ) {
+			return $actions;
+		}
+
+		/*
+		 * Define our own edit post action for o2.
+		 *
+		 * Notable differences from the original are:
+		 * - the 'href' parameter always goes to the GitHub source.
+		 * - the 'o2-edit' class is missing, so inline editing is disabled.
+		 */
+		$edit_action = array(
+			'action' => 'edit',
+			'href' => $markdown_source,
+			'classes' => array( 'edit-post-link' ),
+			'rel' => $post_id,
+			'initialState' => 'default'
+		);
+
+		// Find and replace the existing edit action.
+		$replaced = false;
+		foreach( $actions as &$action ) {
+			if ( 'edit' === $action['action'] ) {
+				$action = $edit_action;
+				$replaced = true;
+				break;
+			}
+		}
+		unset( $action );
+
+		// If there was no edit action replaced, add it in manually.
+		if ( ! $replaced ) {
+			$actions[30] = $edit_action;
+		}
+
+		return $actions;
+	}
+
+	protected function get_markdown_edit_link( $post_id ) {
+		$markdown_source = $this->importer->get_markdown_source( $post_id );
+		if ( is_wp_error( $markdown_source ) ) {
+			return '';
+		}
+		if ( 'github.com' !== parse_url( $markdown_source, PHP_URL_HOST )
+			|| false !== stripos( $markdown_source, '/edit/master/' ) ) {
+			return $markdown_source;
+		}
+		$markdown_source = str_replace( '/blob/master/', '/edit/master/', $markdown_source );
+		return $markdown_source;
+	}
+}
Index: wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-importer.php
===================================================================
--- wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-importer.php	(nonexistent)
+++ wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-importer.php	(working copy)
@@ -0,0 +1,333 @@
+<?php
+
+namespace WordPressdotorg\Markdown;
+
+use WP_CLI;
+use WP_Error;
+use WP_Post;
+use WP_Query;
+use WPCom_GHF_Markdown_Parser;
+
+abstract class Importer {
+	/**
+	 * Meta key to store source in.
+	 *
+	 * @var string
+	 */
+	protected $meta_key = 'wporg_markdown_source';
+
+	/**
+	 * Meta key to store request ETag in.
+	 *
+	 * @var string
+	 */
+	protected $etag_meta_key = 'wporg_markdown_etag';
+
+	/**
+	 * Posts per page to query for.
+	 *
+	 * This needs to be set at least as high as the number of pages being
+	 * imported, but should not be unbounded (-1).
+	 *
+	 * @var int
+	 */
+	protected $posts_per_page = 350;
+
+	/**
+	 * Get base URL for all pages.
+	 *
+	 * This is used for generating the keys for the existing pages.
+	 *
+	 * @see static::get_existing_for_post()
+	 *
+	 * @return string Base URL to strip from page permalink.
+	 */
+	abstract protected function get_base();
+
+	/**
+	 * Get manifest URL.
+	 *
+	 * This URL should point to a JSON file containing the manifest for the
+	 * site's content. (Typically raw.githubusercontent.com)
+	 *
+	 * @return string URL for the manifest file.
+	 */
+	abstract protected function get_manifest_url();
+
+	/**
+	 * Get post type for the type being imported.
+	 *
+	 * @return string Post type slug to import as.
+	 */
+	abstract public function get_post_type();
+
+	/**
+	 * Get existing data for a given post.
+	 *
+	 * @param WP_Post $post Post to get existing data for.
+	 * @return array 2-tuple of array key and data.
+	 */
+	protected function get_existing_for_post( WP_Post $post ) {
+		$key = rtrim( str_replace( $this->get_base(), '', get_permalink( $post->ID ) ), '/' );
+		if ( empty( $key ) ) {
+			$key = 'index';
+		}
+
+		$data = array(
+			'post_id' => $post->ID,
+		);
+		return array( $key, $data );
+	}
+
+	/**
+	 * Import the manifest.
+	 *
+	 * Fetches the manifest, parses, and creates pages as needed.
+	 */
+	public function import_manifest() {
+		$response = wp_remote_get( $this->get_manifest_url() );
+		if ( is_wp_error( $response ) ) {
+			if ( class_exists( 'WP_CLI' ) ) {
+				WP_CLI::error( $response->get_error_message() );
+			}
+			return $response;
+		} elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+			if ( class_exists( 'WP_CLI' ) ) {
+				WP_CLI::error( 'Non-200 from Markdown source' );
+			}
+			return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
+		}
+		$manifest = json_decode( wp_remote_retrieve_body( $response ), true );
+		if ( ! $manifest ) {
+			if ( class_exists( 'WP_CLI' ) ) {
+				WP_CLI::error( 'Invalid manifest' );
+			}
+			return new WP_Error( 'invalid-manifest', 'Manifest did not unfurl properly.' );;
+		}
+		// Fetch all handbook posts for comparison
+		$q = new WP_Query( array(
+			'post_type'      => $this->get_post_type(),
+			'post_status'    => 'publish',
+			'posts_per_page' => $this->posts_per_page,
+		) );
+		$existing = array();
+		foreach ( $q->posts as $post ) {
+			list( $key, $data ) = $this->get_existing_for_post( $post );
+			$existing[ $key ] = $data;
+		}
+		$created = $updated = 0;
+		foreach ( $manifest as $key => $doc ) {
+			// Already exists, update.
+			if ( ! empty( $existing[ $key ] ) ) {
+				$existing_id = $existing[ $key ]['post_id'];
+				if ( $this->update_post_from_manifest_doc( $existing_id, $doc ) ) {
+					$updated++;
+				}
+
+				continue;
+			}
+			if ( $this->process_manifest_doc( $doc, $existing, $manifest ) ) {
+				$created++;
+			}
+		}
+		if ( class_exists( 'WP_CLI' ) ) {
+			WP_CLI::success( "Successfully created {$created} and updated {$updated} handbook pages." );
+		}
+	}
+
+	/**
+	 * Process a document from the manifest.
+	 *
+	 * @param array $doc Document to process.
+	 * @param array $existing List of existing posts, will be added to.
+	 * @param array $manifest Manifest data.
+	 * @return boolean True if processing succeeded, false otherwise.
+	 */
+	protected function process_manifest_doc( $doc, &$existing, $manifest ) {
+		$post_parent = null;
+		if ( ! empty( $doc['parent'] ) ) {
+			// Find the parent in the existing set
+			if ( empty( $existing[ $doc['parent'] ] ) ) {
+				if ( ! $this->process_manifest_doc( $manifest[ $doc['parent'] ], $existing, $manifest ) ) {
+					return false;
+				}
+			}
+			if ( ! empty( $existing[ $doc['parent'] ] ) ) {
+				$parent = $existing[ $doc['parent'] ];
+				$post_parent = $parent['post_id'];
+			}
+		}
+		$post = $this->create_post_from_manifest_doc( $doc, $post_parent );
+		if ( $post ) {
+			list( $key, $data ) = $this->get_existing_for_post( $post );
+			$existing[ $key ] = $data;
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Create a new handbook page from the manifest document
+	 */
+	protected function create_post_from_manifest_doc( $doc, $post_parent = null ) {
+		if ( $doc['slug'] === 'index' ) {
+			$doc['slug'] = $this->get_post_type();
+		}
+		$post_data = array(
+			'post_type'   => $this->get_post_type(),
+			'post_status' => 'publish',
+			'post_parent' => $post_parent,
+			'post_title'  => wp_slash( $doc['slug'] ),
+			'post_name'   => sanitize_title_with_dashes( $doc['slug'] ),
+		);
+		if ( isset( $doc['title'] ) ) {
+			$doc['post_title'] = sanitize_text_field( wp_slash( $doc['title'] ) );
+		}
+		$post_id = wp_insert_post( $post_data );
+		if ( ! $post_id ) {
+			return false;
+		}
+		if ( class_exists( 'WP_CLI' ) ) {
+			WP_CLI::log( "Created post {$post_id} for {$doc['slug']}." );
+		}
+		update_post_meta( $post_id, $this->meta_key, esc_url_raw( $doc['markdown_source'] ) );
+		return get_post( $post_id );
+	}
+
+	/**
+	 * Update an existing post from the manifest.
+	 *
+	 * @param int $post_id Existing post ID.
+	 * @param array $doc Document details from the manifest.
+	 * @return boolean True if updated, false otherwise.
+	 */
+	protected function update_post_from_manifest_doc( $post_id, $doc ) {
+		$did_update = update_post_meta( $post_id, $this->meta_key, esc_url_raw( $doc['markdown_source'] ) );
+		return $did_update;
+	}
+
+	/**
+	 * Update existing posts from Markdown source.
+	 *
+	 * Reparses the Markdown for every page.
+	 */
+	public function import_all_markdown() {
+		$q = new WP_Query( array(
+			'post_type'      => $this->get_post_type(),
+			'post_status'    => 'publish',
+			'fields'         => 'ids',
+			'posts_per_page' => $this->posts_per_page,
+		) );
+		$ids = $q->posts;
+		$success = 0;
+		foreach( $ids as $id ) {
+			$ret = $this->update_post_from_markdown_source( $id );
+			if ( class_exists( 'WP_CLI' ) ) {
+				if ( is_wp_error( $ret ) ) {
+					WP_CLI::warning( $ret->get_error_message() );
+				} elseif ( false === $ret ) {
+					WP_CLI::log( "No updates for {$id}" );
+					$success++;
+				} else {
+					WP_CLI::log( "Updated {$id} from markdown source" );
+					$success++;
+				}
+			}
+		}
+		if ( class_exists( 'WP_CLI' ) ) {
+			$total = count( $ids );
+			WP_CLI::success( "Successfully updated {$success} of {$total} pages." );
+		}
+	}
+
+	/**
+	 * Update a post from its Markdown source.
+	 *
+	 * @param int $post_id Post ID to update.
+	 * @return boolean|WP_Error True if updated, false if no update needed, error otherwise.
+	 */
+	protected function update_post_from_markdown_source( $post_id ) {
+		$markdown_source = $this->get_markdown_source( $post_id );
+		if ( is_wp_error( $markdown_source ) ) {
+			return $markdown_source;
+		}
+		if ( ! function_exists( 'jetpack_require_lib' ) ) {
+			return new WP_Error( 'missing-jetpack-require-lib', 'jetpack_require_lib() is missing on system.' );
+		}
+
+		// Transform GitHub repo HTML pages into their raw equivalents
+		$markdown_source = preg_replace( '#https?://github\.com/([^/]+/[^/]+)/blob/(.+)#', 'https://raw.githubusercontent.com/$1/$2', $markdown_source );
+		$markdown_source = add_query_arg( 'v', time(), $markdown_source );
+
+		// Grab the stored ETag, and use it to deduplicate.
+		$args = array(
+			'headers' => array(),
+		);
+		$last_etag = get_post_meta( $post_id, $this->etag_meta_key, true );
+		if ( ! empty( $last_etag ) ) {
+			$args['headers']['If-None-Match'] = $last_etag;
+		}
+
+		$response = wp_remote_get( $markdown_source, $args );
+		if ( is_wp_error( $response ) ) {
+			return $response;
+		} elseif ( 304 === wp_remote_retrieve_response_code( $response ) ) {
+			// No update required!
+			return false;
+		} elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+			return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
+		}
+
+		$etag = wp_remote_retrieve_header( $response, 'etag' );
+
+		$markdown = wp_remote_retrieve_body( $response );
+		// Strip YAML doc from the header
+		$markdown = preg_replace( '#^---(.+)---#Us', '', $markdown );
+
+		$title = null;
+		if ( preg_match( '/^#\s(.+)/', $markdown, $matches ) ) {
+			$title = $matches[1];
+			$markdown = preg_replace( '/^#\swp\s(.+)/', '', $markdown );
+		}
+		$markdown = trim( $markdown );
+
+		// Steal the first sentence as the excerpt
+		$excerpt = '';
+		if ( preg_match( '/^(.+)/', $markdown, $matches ) ) {
+			$excerpt = $matches[1];
+			$markdown = preg_replace( '/^(.+)/', '', $markdown );
+		}
+
+		// Transform to HTML and save the post
+		jetpack_require_lib( 'markdown' );
+		$parser = new WPCom_GHF_Markdown_Parser();
+		$parser->preserve_shortcodes = false;
+		$html = $parser->transform( $markdown );
+		$post_data = array(
+			'ID'           => $post_id,
+			'post_content' => wp_filter_post_kses( wp_slash( $html ) ),
+			'post_excerpt' => sanitize_text_field( wp_slash( $excerpt ) ),
+		);
+		if ( ! is_null( $title ) ) {
+			$post_data['post_title'] = sanitize_text_field( wp_slash( $title ) );
+		}
+		wp_update_post( $post_data );
+
+		// Set ETag for future updates.
+		update_post_meta( $post_id, $this->etag_meta_key, wp_slash( $etag ) );
+
+		return true;
+	}
+
+	/**
+	 * Retrieve the markdown source URL for a given post.
+	 */
+	public function get_markdown_source( $post_id ) {
+		$markdown_source = get_post_meta( $post_id, $this->meta_key, true );
+		if ( ! $markdown_source ) {
+			return new WP_Error( 'missing-markdown-source', 'Markdown source is missing for post.' );
+		}
+
+		return $markdown_source;
+	}
+}
Index: wordpress.org/public_html/wp-content/plugins/wporg-markdown/plugin.php
===================================================================
--- wordpress.org/public_html/wp-content/plugins/wporg-markdown/plugin.php	(nonexistent)
+++ wordpress.org/public_html/wp-content/plugins/wporg-markdown/plugin.php	(working copy)
@@ -0,0 +1,9 @@
+<?php
+/**
+ * Plugin Name: WPORG Markdown Importer
+ * Description: Automatic Markdown imports for handbooks and DevHub (CLI/API handbooks)
+ * Author: Daniel Bachhuber and Ryan McCue
+ */
+
+require __DIR__ . '/inc/class-editor.php';
+require __DIR__ . '/inc/class-importer.php';
Index: wordpress.org/public_html/wp-content/themes/pub/wporg-developer/functions.php
===================================================================
--- wordpress.org/public_html/wp-content/themes/pub/wporg-developer/functions.php	(revision 5719)
+++ wordpress.org/public_html/wp-content/themes/pub/wporg-developer/functions.php	(working copy)
@@ -61,6 +61,11 @@
 require __DIR__ . '/inc/cli.php';
 
 /**
+ * REST API handbook.
+ */
+require __DIR__ . '/inc/rest-api.php';
+
+/**
  * Explanations for functions. hooks, classes, and methods.
  */
 require( __DIR__ . '/inc/explanations.php' );
Index: wordpress.org/public_html/wp-content/themes/pub/wporg-developer/inc/rest-api.php
===================================================================
--- wordpress.org/public_html/wp-content/themes/pub/wporg-developer/inc/rest-api.php	(nonexistent)
+++ wordpress.org/public_html/wp-content/themes/pub/wporg-developer/inc/rest-api.php	(working copy)
@@ -0,0 +1,59 @@
+<?php
+
+use WordPressdotorg\Markdown\Editor;
+use WordPressdotorg\Markdown\Importer;
+
+class DevHub_REST_API extends Importer {
+	/**
+	 * Singleton instance.
+	 *
+	 * @var static
+	 */
+	protected static $instance;
+
+	/**
+	 * Get the singleton instance, or create if needed.
+	 *
+	 * @return static
+	 */
+	public static function instance() {
+		if ( empty( static::$instance ) ) {
+			static::$instance = new static();
+		}
+
+		return static::$instance;
+	}
+
+	protected function get_base() {
+		return home_url( 'rest-api/' );
+	}
+
+	protected function get_manifest_url() {
+		return 'https://raw.githubusercontent.com/WP-API/docs/master/bin/manifest.json';
+	}
+
+	public function get_post_type() {
+		return 'rest-api-handbook';
+	}
+
+	public function init() {
+		add_action( 'init', array( $this, 'register_cron_jobs' ) );
+		add_action( 'restapi_import_manifest', array( $this, 'import_manifest' ) );
+		add_action( 'restapi_import_all_markdown', array( $this, 'import_all_markdown' ) );
+
+		$editor = new Editor( $this );
+		$editor->init();
+	}
+
+	public function register_cron_jobs() {
+		if ( ! wp_next_scheduled( 'restapi_import_manifest' ) ) {
+			wp_schedule_event( time(), '15_minutes', 'restapi_import_manifest' );
+		}
+		if ( ! wp_next_scheduled( 'restapi_import_all_markdown' ) ) {
+			wp_schedule_event( time(), '15_minutes', 'restapi_import_all_markdown' );
+		}
+	}
+}
+
+DevHub_REST_API::instance()->init();
+
