Index: handbook.php
===================================================================
--- handbook.php	(revision 9139)
+++ handbook.php	(working copy)
@@ -14,6 +14,7 @@
 require_once dirname( __FILE__ ) . '/inc/email-post-changes.php';
 require_once dirname( __FILE__ ) . '/inc/walker.php';
 require_once dirname( __FILE__ ) . '/inc/watchlist.php';
+require_once dirname( __FILE__ ) . '/inc/feedback.php';
 
 /**
  * Initialize our handbooks
@@ -46,7 +47,7 @@
 	}
 
 	static public function enqueue_scripts() {
-		wp_enqueue_script( 'wporg-handbook', plugins_url( '/scripts/handbook.js', __FILE__ ), array( 'jquery' ), '20150930' );
+		wp_enqueue_script( 'wporg-handbook', plugins_url( '/scripts/handbook.js', __FILE__ ), array( 'jquery', 'wp-api-fetch', 'utils'), '20190915' );
 	}
 
 }
Index: inc/feedback.php
===================================================================
--- inc/feedback.php	(nonexistent)
+++ inc/feedback.php	(working copy)
@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * Class providing a mechanism for users to submit feedback on posts.
+ *
+ * @package handbook
+ */
+class WPorg_Handbook_Feedback {
+	/**
+	 * Initialize the Feedback module.
+	 */
+	static function init() {
+		add_action( 'rest_api_init',   array( __CLASS__, 'register_rest_endpoints' ) );
+		add_action( 'wp_print_styles', array( __CLASS__, 'print_styles' ) );
+		add_filter( 'the_content',     array( __CLASS__, 'render_form' ) );
+	}
+
+	/**
+	 * Register REST API endpoints.
+	 */
+	static public function register_rest_endpoints() {
+		register_rest_route(
+			'wporg-handbook/v1',
+			'submit-feedback' ,
+			array(
+				'methods'  => array( 'PUT' ),
+				'callback' => array( __CLASS__, 'handle_form_submission' ),
+			) );
+	}
+
+	/**
+	 * Email feedback to the site admin.
+	 *
+	 * @param WP_REST_Request $request
+	 *
+	 * @return string|WP_Error
+	 */
+	static public function handle_form_submission( $request ) {
+		$admin_email = get_option( 'admin_email' );
+
+		// Removing tags for formatting purposes, but still need to practice late-escaping for security purposes.
+		$page_id          = absint( $request->get_param( 'pageID' ) );
+		$page_title       = strip_tags( get_the_title( $page_id ) );
+		$feedback         = strip_tags( $request->get_param( 'message' ) );
+		$valid_submission = $page_id && ! empty( $feedback );
+
+		$response = new WP_Error(
+			'email_failed',
+			sprintf(
+				__( "I'm sorry, but your feedback could not be submitted, please send an email to %s instead.", 'wporg' ),
+				$admin_email
+			)
+		);
+
+		if ( ! $valid_submission ) {
+			return $response;
+		}
+
+		$message = sprintf(
+			// translators: %s is a post title.
+			__( 'Somebody reported that "%s" did not help them, and suggested the following changes:', 'wporg' ) . "\n\n" .
+			'----' . "\n\n" .
+			'%s'   . "\n\n" .
+			'----' . "\n\n" .
+			__( 'If you think those would be helpful changes, please edit the page to reflect them.', 'wporg' ) . "\n\n" .
+			'%s',
+			esc_html( $page_title ),
+			esc_html( $feedback ),
+			esc_url_raw( get_edit_post_link( $page_id, 'raw' ) )
+		);
+
+		$mail_sent = wp_mail(
+			$admin_email,
+			// translators: %s is a post title.
+			sprintf( __( 'Feedback on "%s"', 'wporg' ), esc_html( $page_title ) ),
+			$message
+		);
+
+		if ( $mail_sent ) {
+			$response = __( 'Thanks for your feedback!', 'wporg' );
+		}
+
+		return $response;
+	}
+
+	/**
+	 * Print the styles needed for the feedback markup.
+	 */
+	static public function print_styles() {
+		wp_enqueue_style( 'dashicons' );
+
+		?>
+
+		<!-- handbook/feedback -->
+		<style>
+			.handbook-feedback {
+			}
+
+			.feedback-vote {
+				background-color: #B4B9BE;
+			}
+
+			.feedback-vote-helpful.has-voted {
+				background-color: #46B450;
+			}
+
+			.feedback-vote-unhelpful.has-voted {
+				background-color: #DC3232;
+			}
+
+			#feedback-message-container {
+				margin-top: 10px;
+			}
+
+			#feedback-message-container input[type="submit"] {
+				margin-top: 10px;
+			}
+
+			.handbook-spinner {
+				background: url( <?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?> ) no-repeat;
+				background-size: 20px 20px;
+				display: inline-block;
+				vertical-align: middle;
+				opacity: 0.7;
+				filter: alpha( opacity=70 );
+				width: 20px;
+				height: 20px;
+				margin: 0 0 0 5px;
+			}
+
+			.dashicons.feedback-success {
+				color: #46B450;
+			}
+
+			.dashicons.feedback-error {
+				color: #DC3232;
+			}
+		</style>
+
+		<?php
+	}
+
+	/**
+	 * Append the feedback form to the post's content.
+	 *
+	 * @param string $content
+	 *
+	 * @return string
+	 */
+	static public function render_form( $content ) {
+		if ( 'handbook' !== get_post_type() ) {
+			return $content;
+		}
+
+		/*
+		 * Prevent this function from being called multiple times, because shortcodes in the page content apply
+		 * `the_content`, etc.
+		 */
+		remove_filter( 'the_content', array( __CLASS__, 'render_form' ) );
+
+		ob_start();
+
+		?>
+
+		<div class="handbook-feedback callout callout-info">
+			<?php esc_html_e( 'Did this page help you?', 'wporg' ); ?>
+
+			<button class="feedback-vote feedback-vote-helpful">
+				<span class="dashicons dashicons-thumbs-up">
+					<span class="screen-reader-text">
+						<?php esc_html_e( 'Yes', 'wporg' ); ?>
+					</span>
+				</span>
+			</button>
+
+			<button class="feedback-vote feedback-vote-unhelpful">
+				<span class="dashicons dashicons-thumbs-down">
+					<span class="screen-reader-text">
+						<?php esc_html_e( 'No', 'wporg' ); ?>
+					</span>
+				</span>
+			</button>
+
+			<div id="feedback-message-container" style="display: none;">
+				<input id="handbook-page-id" type="hidden" value="<?php echo absint( get_the_ID() ); ?>" />
+
+				<label for="feedback-message">
+					<?php esc_html_e( 'How could it have been more helpful?', 'wporg' ); ?>
+				</label>
+				<textarea id="feedback-message" name="feedback-message"></textarea>
+
+				<input id="submit-feedback" type="submit" value="Send Feedback">
+			</div>
+		</div>
+
+		<script id="tmpl-handbook-feedback-spinner" type="text/template">
+			<?php esc_html_e( 'Sending feedback...', 'wporg' ); ?>
+			<span class="handbook-spinner"></span>
+		</script>
+
+		<script id="tmpl-handbook-feedback-submission-result" type="text/template">
+			<span class="dashicons dashicons-{{data.icon}} feedback-{{data.status}}"></span>
+
+			{{data.message}}
+		</script>
+
+		<?php
+
+		return $content . ob_get_clean();
+	}
+}
+
+WPorg_Handbook_Feedback::init();

Property changes on: inc/feedback.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: scripts/handbook.js
===================================================================
--- scripts/handbook.js	(revision 9139)
+++ scripts/handbook.js	(working copy)
@@ -1,3 +1,6 @@
+var wp = wp || {};
+wp.handbook = wp.handbook || {};
+
 /**
  * Enables toggling visibility of all the chapters in the Handbook Chapter widget.
  * 
@@ -32,3 +35,94 @@
 	}
 });
 
+
+jQuery( document ).ready( function() {
+	'use strict';
+
+	var app = wp.handbook.feedback = {
+		/**
+		 * Main entry point
+		 */
+		init: function () {
+			app.container        = document.getElementsByClassName( 'handbook-feedback' )[ 0 ];
+			app.voteUp           = app.container.getElementsByClassName( 'feedback-vote-helpful' )[ 0 ];
+			app.voteDown         = app.container.getElementsByClassName( 'feedback-vote-unhelpful' )[ 0 ];
+			app.messageContainer = document.getElementById( 'feedback-message-container' );
+
+			app.voteUp.addEventListener(   'click', app.vote );
+			app.voteDown.addEventListener( 'click', app.vote );
+			document.getElementById( 'submit-feedback' ).addEventListener( 'click', app.submitFeedback );
+		},
+
+		/**
+		 * Handle the event when a user clicks on a vote up/down button.
+		 *
+		 * @param {object} event
+		 */
+		vote: function( event ) {
+			var targetClass = event.target.className,
+			    // The class will be different depending on if they click on the button or the Dashicon inside the button.
+			    wasHelpful = -1 !== targetClass.indexOf( 'vote-helpful' ) || -1 !== targetClass.indexOf( 'thumbs-up' );
+
+
+			if ( wasHelpful ) {
+				app.voteUp.classList.add( 'has-voted' );
+				app.voteDown.classList.remove( 'has-voted' );
+				app.messageContainer.style.display = 'none';
+
+				// There's nothing else to do right now, but in the future we could track votes for stats, etc.
+				return;
+			}
+
+			app.voteUp.classList.remove( 'has-voted' );
+			app.voteDown.classList.add( 'has-voted' );
+			app.messageContainer.style.display = 'block';
+			app.messageContainer.scrollIntoView( { behavior : "smooth", block : "end", inline : "nearest" } );
+		},
+
+		/**
+		 * Handle the event when a user clicks on a Submit Feedback button.
+		 *
+		 * @param {object} event
+		 */
+		submitFeedback: function( event ) {
+			var template     = wp.template( 'handbook-feedback-spinner' ),
+			    templateData = {},
+				fetchOptions = {
+					path: 'wporg-handbook/v1/submit-feedback',
+					method: 'PUT',
+					data: {
+						pageID:  document.getElementById( 'handbook-page-id' ).value,
+						message: document.getElementById( 'feedback-message' ).value,
+					}
+				};
+
+			app.messageContainer.innerHTML = template();
+
+			wp.apiFetch( fetchOptions ).then( function( message ) {
+				templateData.status  = 'success';
+				templateData.icon    = 'yes-alt';
+				templateData.message = message;
+
+			} ).catch( function( error ) {
+				templateData.status  = 'error';
+				templateData.icon    = 'warning';
+				templateData.message = error.message;
+
+			} ).finally( function() {
+				template = wp.template( 'handbook-feedback-submission-result' );
+				app.messageContainer.innerHTML = template( templateData );
+			} );
+		},
+	};
+
+	/*
+	 * We have to wait until after the `ready.o2` event fires, because otherwise o2 will wipe out any registered
+	 * event handlers for elements inside the post content.
+	 *
+	 * See https://github.com/Automattic/o2/issues/175.
+	 */
+	jQuery( document ).on( 'ready.o2', function() {
+		app.init();
+	} );
+} );
