Index: wp-admin/includes/class-wp-plugin-readme-parser.php
===================================================================
--- wp-admin/includes/class-wp-plugin-readme-parser.php	(revision 0)
+++ wp-admin/includes/class-wp-plugin-readme-parser.php	(revision 0)
@@ -0,0 +1,464 @@
+<?php
+
+/**
+ * Parse a plugin's readme.txt file
+ * Based on http://code.svn.wordpress.org/plugin-readme-parser/parse-readme.php
+ * @link http://wordpress.org/extend/plugins/about/readme.txt
+ * @link http://wordpress.org/extend/plugins/about/validator/
+ * @pacakge WordPress
+ * @version 1.0
+ */
+class wp_plugin_readme_parser {
+
+	/**
+	 * Readme.txt file contents
+	 * This string will change with each pass in the process
+	 * @var string
+	 */
+	protected $_readme_contents = '';
+	
+	/**
+	 * Special section names
+	 * @var array
+	 */
+	protected $_special_sections = array(
+		'description',
+		'installation',
+		'frequently_asked_questions',
+		'screenshots',
+		'changelog',
+		'change_log',
+		'upgrade_notice'
+	);
+
+	/**
+	 * Minimum version of WordPress
+	 * @var string
+	 */
+	protected $_requires_at_least = '';
+
+	/**
+	 * Maximum compatible version of WordPress
+	 * @var string
+	 */
+	protected $_tested_up_to = '';
+
+	/**
+	 * Stable tag for the plugin
+	 * @var string
+	 */
+	protected $_stable_tag = '';
+
+	/**
+	 * Keywords for the plugin directory search
+	 * @var array
+	 */
+	protected $_tags = array();
+
+	/**
+	 * Authors who wrote the plugin
+	 * @var array
+	 */
+	protected $_contributors = array();
+
+	/**
+	 * Where to donate
+	 * @var string
+	 */
+	protected $_donate_link = '';
+
+	/**
+	 * License type
+	 * @var string
+	 */
+	protected $_license = '';
+
+	/**
+	 * Recognized keys for records
+	 * @var array
+	 */
+	protected $_keys = array(
+		'requires_at_least' => '/Requires at least:[ \t]*(.+)/i',
+		'tested_up_to'      => '/Tested up to:[ \t]*(.+)/i',
+		'stable_tag'        => '/Stable tag:[ \t]*(.+)/i',
+		'tags'              => '/Tags:[ \t]*(.+)/i',
+		'contributors'      => '/Contributors:[ \t]*(.+)/i',
+		'donate_link'       => '/Donate link:[ \t]*(.+)/i',
+		'license'           => '/License:[ \t]*(.+)/i'
+	);
+	
+	/**
+	 * Is the description a copy of the short description?
+	 * @var bool
+	 */
+	protected $_is_excerpt = false;
+
+	/**
+	 * Was the short description truncated to 150 characters?
+	 * @var bool
+	 */
+	protected $_is_truncated = false;
+
+	/**
+	 * Short description of the plugin
+	 * @var string
+	 */
+	protected $_short_description = '';
+
+	/**
+	 * List of the plugin's screenshots
+	 * @var array
+	 */
+	protected $_screenshots = array();
+	
+	/**
+	 * Content outside of the standard sections
+	 * @var string
+	 */
+	protected $_remaining_content = '';
+	
+	/**
+	 * Upgrade notices for users
+	 * Key = version, value = message.  Example:
+	 * 1.0 => Please upgrade
+	 * 1.1 => Dire security bug
+	 * 1.2 => Minor UI issue
+	 * @var array
+	 */
+	protected $_upgrade_notice = array();
+	
+	/**
+	 * Allowed tags in sections
+	 * @var array
+	 */
+	private $_allowed_tags = array(
+		'a'          => array(
+			'href'       => array(),
+			'title'      => array(),
+			'rel'        => array()
+		),
+		'blockquote' => array( 'cite' => array() ),
+		'br'         => array(),
+		'cite'       => array(),
+		'p'          => array(),
+		'code'       => array(),
+		'pre'        => array(),
+		'em'         => array(),
+		'strong'     => array(),
+		'ul'         => array(),
+		'ol'         => array(),
+		'li'         => array(),
+		'h3'         => array(),
+		'h4'         => array()
+	);
+
+	/**
+	 * Parse a plugin's readme.txt file.  Expects a path to the file.
+	 * @param string $file
+	 * @return string
+	 */
+	public function parse_readme_file ( $file ) {
+		if ( !file_exists( $file ) ) {
+			throw new Exception('File not found');
+		}
+		$this->_readme_contents = file_get_contents( $file );
+		return $this->_parse_readme();
+	}
+
+	/**
+	 * Parse a plugin's readme.txt.  Expect's the file contents as string.
+	 * @param type $file_contents
+	 * @return type 
+	 */
+	public function parse_readme_contents( $file_contents ) {
+		$this->_readme_contents = $file_contents;
+		return $this->_parse_readme();
+	}
+	
+	/**
+	 * Extract sections
+	 * @return array
+	 */
+	private function _exract_sections( ) {
+		$ret = array();
+
+		$sections = preg_split( '/^[\s]*==[\s]*(.+?)[\s]*==/m', $this->_readme_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY );
+
+		// Check if the first element is a short description
+		$this->_short_description = '';
+		$this->_is_truncated      = false;
+		$this->_is_excerpt        = true;
+		if ( '==' != substr($sections[0][0], 0, 2) ) {
+			$short_description = array_shift( $sections );
+			
+			// If the user doesn't include a full description, this will be used, not truncated, and converted to markdown.  If they
+			// do include a full description, this field will be overwritten
+			$ret['description'] = $this->_markdown( $short_description );
+
+			$short_description = $this->_sanitize_text( $short_description );
+			if ( strlen( $short_description ) > 150 )
+				$this->_is_truncated = true;
+			$this->_short_description = substr( $short_description, 0, 150 );
+		}
+
+		// Sanitize titles / sections
+		for ( $i = 0 ; $i < count( $sections ) ; $i += 2 ) {
+			$title    = $this->_sanitize_text( $sections[$i] );
+			$contents = preg_replace( '/^[\s]*=[\s]+(.+?)[\s]+=/m', '<h4>$1</h4>', $sections[$i+1] );
+			$contents = $this->_markdown( $contents );
+			$ret[$title] = $contents;			
+			if ( $this->_is_excerpt && 'description' == strtolower( strip_tags( $title ) ) )
+				$this->_is_excerpt = false;
+		}
+
+		// Remove the sections from the readme file
+		$this->_readme_contents = trim( str_replace( $sections, '', $this->_readme_contents ) );
+		
+		// Done
+		return $this->_process_sections( $ret );
+	}
+
+	/**
+	 * Post-process sections
+	 * Any special business logic (e.g. change_log -> changelog) is done here
+	 * @param array $sections
+	 * @return array
+	 */
+	private function _process_sections( $sections ) {
+		
+		// Rename sections to lower-case-underscore notation relegate non-special
+		// content to the "remaining content" section
+		$_sections = array();
+		$this->_remaining_content = '';
+		foreach ( (array) $sections as $k => $v ) {
+			$name = strtolower( preg_replace( '/[^a-zA-Z_]/', '_', $k ) );
+			$_sections[$name] = $v;
+
+			// Is this "remaining content" ?
+			if ( !in_array( $name, $this->_special_sections ) ) {
+				$title_id = esc_attr( $k );
+				$title_id = str_replace( ' ', '-', $title_id );
+				$this->_remaining_content .= sprintf("\n<h3 id=\"%s\">%s</h3>\n%s", $name, $k, $v);
+			}
+		}
+		$sections = $_sections;
+
+		// Upgrade notice section
+		$this->_upgrade_notice = array();
+		if ( array_key_exists( 'upgrade_notice', $sections ) ) {
+			$upgrade_notice = array();
+			$notices = preg_split( '/<h4>(.*?)<\/h4>/', $sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
+			if ( empty( $notices ) && count( $notices ) > 1 ) {
+				for ( $i = 0 ; $i < count( $notices ) ; $i += 2 )
+					$upgrade_notice[$notices[$i]] = substr( $this->_sanitize_text( @$notices[$i+1] ), 0, 300 );
+			} elseif ( is_array( $notices ) && 1 == count( $notices ) ) {
+				$upgrade_notice[ $this->_stable_tag ] = $notices[0];
+			}
+			$this->_upgrade_notice = $upgrade_notice;
+			unset( $sections['upgrade_notice'] );
+		}
+
+		// Screenshots
+		$this->_screenshots = array();
+		if ( array_key_exists( 'screenshots', $sections ) ) {
+			preg_match_all( '/<li>(.*?)<\/li>/s', $sections['screenshots'], $screenshots, PREG_SET_ORDER);
+			if ( $screenshots ) {
+				foreach ( (array) $screenshots as $screenshot )
+					$this->_screenshots[] = $screenshot[1];
+			}
+		}
+
+		// Fix change_log -> changelog
+		if ( array_key_exists( 'change_log', $sections ) )
+			$sections['changelog'] = $sections['change_log'];
+
+		return $sections;
+	}
+
+	/**
+	 * Post-process records
+	 * Any special business logic (e.g. splitting tags, contributors) is done here
+	 * @param array $records
+	 * @return array
+	 */
+	private function _process_records( $records ) {
+		
+		// Escape donate link URL
+		if ( !empty( $records['donate_link'] ) )
+			$records['donate_link'] = esc_url( $records['donate_link'] );
+		
+		// Split up tags
+		$tags = preg_split('/,[\s]*?/', trim( $records['tags'] ) );
+		foreach ( (array) $tags as $k => $v ) {
+			$v = $this->_sanitize_text( $v );
+			if ( !empty( $v ) )
+				$tags[$k] = $this->_sanitize_text( $v );
+			else
+				unset( $tags[$k] );
+		}
+		$records['tags'] = $tags;
+
+		// Split up contributors
+		$contributors = preg_split( '/,[\s]*/', trim( $records['contributors'] ) );
+		foreach ( (array) $contributors as $k => $v ) {
+			$v = $this->_sanitize_text( $v );
+			if ( !empty( $v ) )
+				$contributors[$k] = sanitize_user( $v );
+			else
+				unset( $contributors[$k] );
+		}
+		$records['contributors'] = $contributors;
+
+		// Assign records to object properties
+		foreach ( (array) $records as $k => $v )
+			if (in_array( $k , array_keys( $this->_keys ) ) )
+				$this->{"_$k"} = $v;
+
+		// Done
+		return $records;
+	}
+
+	/**
+	 * Extract key-value pairs
+	 * @return array
+	 */
+	private function _extract_records( ) {
+		$ret = array();
+		foreach ( (array) $this->_keys as $k => $v ) {
+			if ( preg_match( $v, $this->_readme_contents, $matches ) ) {
+				$ret[$k] = $this->_sanitize_text( $matches[1] );
+				$this->_readme_contents = trim( str_replace( $matches[0], '', $this->_readme_contents ) );
+			}
+		}
+		return $this->_process_records( $ret );
+	}
+	
+	/**
+	 * Parse a readme file into an associative array
+	 * @return string
+	 */
+	protected function _parse_readme() {
+
+		// Normalize white-space
+		$file_contents = str_replace( array( "\r\n", "\r" ), "\n", $this->_readme_contents );
+		$this->_readme_contents = trim( $this->_readme_contents );
+
+		// Remove UTF-8 byte-order-mark
+		if ( 0 === strpos( $this->_readme_contents, "\xEF\xBB\xBF" ) )
+			$this->_readme_contents = substr( $this->_readme_contents, 3 );
+
+		// === Plugin Name ===
+		// Must be the very first thing.		
+		if ( !preg_match( '/^===(.*)===/', $this->_readme_contents, $_name ) )
+			return array(); // require a name
+		$name = trim( $_name[1], '=' );
+		$name = $this->_sanitize_text( $name );
+		$this->_readme_contents = str_replace( $_name[0], '', $this->_readme_contents );
+		
+		// Extract the "key: value" records to the local scope
+		extract( $this->_extract_records() );
+		
+		// Extract the sections (e.g. == HEADING == .... text ...)
+		$sections = $this->_exract_sections();
+		extract( $sections );
+		
+		// Compile the final array
+		return array(
+			'name'              => isset( $name )              ? $name              : '',
+			'tags'              => isset( $tags )              ? $tags              : array(),
+			'requires_at_least' => isset( $requires_at_least ) ? $requires_at_least : '',
+			'tested_up_to'      => isset( $tested_up_to )      ? $tested_up_to      : '',
+			'stable_tag'        => isset( $stable_tag )        ? $stable_tag        : 'trunk',
+			'contributors'      => isset( $contributors )      ? $contributors      : array(),
+			'donate_link'       => isset( $donate_link )       ? $donate_link       : '',
+			'short_description' => $this->_short_description,
+			'screenshots'       => $this->_screenshots,
+			'is_excerpt'        => $this->_is_excerpt,
+			'is_truncated'      => $this->_is_truncated,
+			'sections'          => $sections,
+			'remaining_content' => $this->_remaining_content,
+			'upgrade_notice'    => $this->_upgrade_notice
+		);
+	}
+
+	/**
+	 * Make the text safe for use in a browser
+	 * @param string $text
+	 * @return string
+	 */
+	private function _sanitize_text( $text ) {
+		$text = strip_tags( $text );
+		$text = esc_html( $text );
+		$text = trim( $text );
+		return $text;
+	}
+
+	/**
+	 * Format text.  Use markdown, with some extra code -> backtick formatting.
+	 * @param type $text
+	 * @return type 
+	 */
+	private function _markdown( $text ) {
+		$text = trim( $text );
+		$text = $this->_convert_code( $text );
+		$text = Markdown( $text );		
+		$text = balanceTags( $text );
+		$text = wp_kses( $text, $this->_allowed_tags );
+		$text = trim( $text );
+		return $text;
+	}
+
+	/**
+	 * First take any user formatted code blocks and turn them into backticks so that
+	 * markdown will preserve things like underscores in code blocks
+	 * @param type $text
+	 * @param type $markdown
+	 * @return type 
+	 */
+	private function _convert_code( $text ) {
+		$text = preg_replace_callback( '/(<pre><code>|<code>)(.*?)(<\/code>\/pre>|<\/code>)/s', array( $this, '_decodeit' ), $text );
+		$text = str_replace( array("\r\n", "\r"), "\n", $text );
+		// Markdown seems to be choking on block level stuff too.  Let's just encode it and be done with it.
+		$text = preg_replace_callback( '/(^|\n)`(.*?)`/s', array( $this, '_encodeit' ), $text );
+		return $text;
+	}
+	
+	/**
+	 * Encode Markdown to HTML
+	 * @param array $matches Output from preg_replace_callback
+	 * @return string 
+	 */
+	private function _encodeit( $matches ) {
+		$text = trim( $matches[2] );
+		$text = htmlspecialchars( $text, ENT_QUOTES );
+		$text = str_replace( array( "\r\n", "\r" ), "\n", $text );
+		$text = preg_replace( "|\n\n\n+|", "\n\n", $text );
+		$text = str_replace(
+			array( '&amp;lt;', '&amp;&gt; ' ),
+			array( '&lt;'    , '&gt;'       ),
+			$text
+		);
+		$text = "<code>$text</code>";
+		if ( "`" != $matches[1] )
+			$text = "<pre>$text</pre>";
+		return $text;
+	}
+
+	/**
+	 * De-code HTML, turn it back into markdown
+	 * @param array $matches Output from preg_replace_callback
+	 * @return string 
+	 */
+	private function _decodeit( $matches ) {
+		$text = $matches[2];
+		$text = html_entity_decode( $text );
+		$text = str_replace(
+			array( '<br />', '&#38;', '&#39;'),
+			array( ''      , '&'    , "'"    ),
+			$text
+		);		
+		if ( '<pre><code>' == $matches[1] )
+			$text = "\n$text\n";
+		return "`$text`";
+	}
+}

Property changes on: wp-admin\includes\class-wp-plugin-readme-parser.php
___________________________________________________________________
Added: svn:eol-style
   + LF

Index: wp-admin/includes/class-wp-plugins-list-table.php
===================================================================
--- wp-admin/includes/class-wp-plugins-list-table.php	(revision 19730)
+++ wp-admin/includes/class-wp-plugins-list-table.php	(working copy)
@@ -427,6 +427,10 @@
 							$author = '<a href="' . $plugin_data['AuthorURI'] . '" title="' . esc_attr__( 'Visit author homepage' ) . '">' . $plugin_data['Author'] . '</a>';
 						$plugin_meta[] = sprintf( __( 'By %s' ), $author );
 					}
+					$slug = basename( $plugin_file, '.php' );
+					$plugin_meta[] = '<a href="' . self_admin_url( 'plugin-install.php?tab=plugin-readme&amp;readme=true&amp;plugin=' . $slug .
+								'&amp;TB_iframe=true&amp;width=600&amp;height=550' ) . '" class="thickbox" title="' .
+								esc_attr( sprintf( __( 'More information about %s' ), "{$plugin_data['Name']} {$plugin_data['Version']}" ) ) . '">' . __( 'Details' ) . '</a>';
 					if ( ! empty( $plugin_data['PluginURI'] ) )
 						$plugin_meta[] = '<a href="' . $plugin_data['PluginURI'] . '" title="' . esc_attr__( 'Visit plugin site' ) . '">' . __( 'Visit plugin site' ) . '</a>';
 
Index: wp-admin/css/colors-classic.dev.css
===================================================================
--- wp-admin/css/colors-classic.dev.css	(revision 19730)
+++ wp-admin/css/colors-classic.dev.css	(working copy)
@@ -583,7 +583,8 @@
 }
 
 div#media-upload-header,
-div#plugin-information-header {
+div#plugin-information-header,
+div#plugin-readme-header {
 	background-color: #f9f9f9;
 	border-bottom-color: #dfdfdf;
 }
@@ -1725,20 +1726,25 @@
 	border-right: 1px solid #fff;
 }
 
-#plugin-information .fyi ul {
+#plugin-information .fyi ul,
+#plugin-readme .fyi ul {
 	background-color: #eaf3fa;
 }
 
-#plugin-information .fyi h2.mainheader {
+#plugin-information .fyi h2.mainheader,
+#plugin-readme .fyi h2.mainheader {
 	background-color: #cee1ef;
 }
 
 #plugin-information pre,
-#plugin-information code {
+#plugin-information code,
+#plugin-readme pre,
+#plugin-readme code {
 	background-color: #ededff;
 }
 
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme pre {
 	border: 1px solid #ccc;
 }
 
Index: wp-admin/css/colors-fresh.dev.css
===================================================================
--- wp-admin/css/colors-fresh.dev.css	(revision 19730)
+++ wp-admin/css/colors-fresh.dev.css	(working copy)
@@ -591,7 +591,8 @@
 }
 
 div#media-upload-header,
-div#plugin-information-header {
+div#plugin-information-header,
+div#plugin-readme-header {
 	background-color: #f9f9f9;
 	border-bottom-color: #dfdfdf;
 }
@@ -1360,20 +1361,25 @@
 	border-right: 1px solid #f9f9f9;
 }
 
-#plugin-information .fyi ul {
+#plugin-information .fyi ul,
+#plugin-readme .fyi ul {
 	background-color: #eaf3fa;
 }
 
-#plugin-information .fyi h2.mainheader {
+#plugin-information .fyi h2.mainheader,
+#plugin-readme .fyi h2.mainheader {
 	background-color: #cee1ef;
 }
 
 #plugin-information pre,
-#plugin-information code {
+#plugin-information code,
+#plugin-readme pre,
+#plugin-readme code {
 	background-color: #ededff;
 }
 
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme pre {
 	border: 1px solid #ccc;
 }
 
Index: wp-admin/plugin-install.php
===================================================================
--- wp-admin/plugin-install.php	(revision 19730)
+++ wp-admin/plugin-install.php	(working copy)
@@ -6,7 +6,7 @@
  * @subpackage Administration
  */
 // TODO route this pages via a specific iframe handler instead of the do_action below
-if ( !defined( 'IFRAME_REQUEST' ) && isset( $_GET['tab'] ) && ( 'plugin-information' == $_GET['tab'] ) )
+if ( !defined( 'IFRAME_REQUEST' ) && isset( $_GET['tab'] ) && ( in_array( $_GET['tab'], array( 'plugin-information', 'plugin-readme' ) ) ) )
 	define( 'IFRAME_REQUEST', true );
 
 /** WordPress Administration Bootstrap */
@@ -28,7 +28,7 @@
 $parent_file = 'plugins.php';
 
 wp_enqueue_script( 'plugin-install' );
-if ( 'plugin-information' != $tab )
+if ( !in_array( $tab, array('plugin-information', 'plugin-readme') ) )
 	add_thickbox();
 
 $body_id = $tab;
Index: wp-admin/includes/plugin-install.php
===================================================================
--- wp-admin/includes/plugin-install.php	(revision 19730)
+++ wp-admin/includes/plugin-install.php	(working copy)
@@ -384,3 +384,216 @@
 	exit;
 }
 add_action('install_plugins_pre_plugin-information', 'install_plugin_information');
+
+/**
+ * Get information from a plugin locally
+ * @param string $slug
+ * @since 3.4.0
+ */
+function local_plugin_api( $slug ) {
+
+	// Try to find the plugin file from the slug
+	$plugin_data = array();
+	if ( file_exists( ABSPATH . PLUGINDIR . "/$slug.php" ) ) {
+		$plugin_data = get_plugin_data( ABSPATH . PLUGINDIR . "/$slug.php" );
+	} elseif ( file_exists( ABSPATH . PLUGINDIR . "/$slug/$slug.php" ) ) {
+		$plugin_data = get_plugin_data( ABSPATH . PLUGINDIR . "/$slug/$slug.php" );
+	}
+	
+	// Try to load data from readme.txt
+	$readme_data = false;
+	if ( file_exists( ABSPATH . PLUGINDIR . "/$slug/readme.txt" ) ) {
+
+		// Get markddown library, turn off plugin functionality
+		if ( !defined( 'MARKDOWN_WP_POSTS' ) )
+			define( 'MARKDOWN_WP_POSTS', false );
+		if ( !defined( 'MARKDOWN_WP_COMMENTS' ) )
+			define( 'MARKDOWN_WP_COMMENTS', false );
+		if ( !class_exists( 'Markdown_Parser' ) )
+			include_once( ABSPATH . 'wp-admin/includes/markdown.php' );
+		
+		include_once( ABSPATH . 'wp-admin/includes/class-wp-plugin-readme-parser.php' );
+		$readme_parser = new wp_plugin_readme_parser();
+		$readme_data = $readme_parser->parse_readme_file( ABSPATH . PLUGINDIR . "/$slug/readme.txt" );
+	}
+	
+	// If there's no readme (e.g. hello.php) create a fake structure
+	if ( empty( $readme_data ) ) {
+		$readme_data = array(
+			'contributors'      => '',
+			'requires_at_least' => '',
+			'tested_up_to'      => '',
+			'tags'              => array(),
+			'sections'          => array(
+				'description'   => !empty( $plugin_data['Description'] ) ? $plugin_data['Description'] : ''
+			)
+		);
+	}
+
+	// Convert to an API-style response
+	$api = array(
+		'name'           => !empty( $plugin_data['Name'] ) ? $plugin_data['Name'] : $slug,
+		'slug'           => $slug,
+		'version'        => !empty( $plugin_data['Version'] ) ? $plugin_data['Version'] : '',
+		'author'         => !empty( $plugin_data['AuthorName'] ) ? $plugin_data['AuthorName'] : '',
+		'author_profile' => null,
+		'contributors'   => $readme_data['contributors'],
+		'requires'       => $readme_data['requires_at_least'],
+		'tested'         => $readme_data['tested_up_to'],
+		'compatibility'  => null,
+		'rating'         => null,
+		'num_ratings'    => null,
+		'downloaded'     => null,
+		'last_updated'   => null,
+		'added'          => null,
+		'homepage'       => !empty( $plugin_data['PluginURI'] ) ? $plugin_data['PluginURI'] : '',
+		'sections'       => $readme_data['sections'],
+		'download_link'  => null,
+		'tags'           => $readme_data['tags']
+	);
+
+	// Done
+	return (object) $api;
+}
+
+/**
+ * Display local plugin information in dialog box form.
+ * Pull from the readme.txt file and the plugin header
+ * @since 3.4.0
+ */
+function plugin_readme_information() {
+	global $tab;
+
+	$api = local_plugin_api( stripslashes( $_REQUEST['plugin'] ) );
+
+	if ( is_wp_error($api) )
+		wp_die($api);
+
+	$plugins_allowedtags = array(
+		'a'    => array(
+			'href'   => array(),
+			'title'  => array(),
+			'target' => array()
+		),
+		'abbr' => array(
+			'title' => array()
+		),
+		'acronym' => array(
+			'title' => array()
+		),
+		'code'   => array(),
+		'pre'    => array(),
+		'em'     => array(),
+		'strong' => array(),
+		'div'    => array(),
+		'p'      => array(),
+		'ul'     => array(),
+		'ol'     => array(),
+		'li'     => array(),
+		'h1'     => array(),
+		'h2'     => array(),
+		'h3'     => array(),
+		'h4'     => array(),
+		'h5'     => array(),
+		'h6'     => array(),
+		'img'    => array(
+			'src'   => array(),
+			'class' => array(),
+			'alt'   => array()
+		)
+	);
+
+	$plugins_section_titles = array(
+		'description'  => _x('Description',  'Plugin installer section title'),
+		'installation' => _x('Installation', 'Plugin installer section title'),
+		'faq'          => _x('FAQ',          'Plugin installer section title'),
+		'changelog'    => _x('Changelog',    'Plugin installer section title'),
+		'other_notes'  => _x('Other Notes',  'Plugin installer section title')
+	);
+
+	// No screenshots at this time
+	if ( !empty( $api->sections['screenshots'] ) )
+		unset( $api->sections['screenshots'] );
+	
+	// Sanitize HTML
+	foreach ( (array)$api->sections as $section_name => $content )
+		$api->sections[$section_name] = wp_kses( $content, $plugins_allowedtags );
+	foreach ( array( 'version', 'author', 'requires', 'tested', 'homepage', 'downloaded', 'slug' ) as $key ) {
+		if ( isset( $api->$key ) )
+			$api->$key = wp_kses( $api->$key, $plugins_allowedtags );
+	}
+
+	// Default to the Description tab, Do not translate, API returns English.
+	$section = isset( $_REQUEST['section'] ) ? stripslashes( $_REQUEST['section'] ) : 'description';
+	if ( empty($section) || ! isset($api->sections[ $section ]) )
+		$section = array_shift( $section_titles = array_keys((array)$api->sections) );
+
+	iframe_header( __( 'Plugin Details' ) );
+	?>
+	<div id="<?php echo $tab; ?>-header">
+		<ul id="sidemenu">
+			<?php foreach ( (array) $api->sections as $section_name => $content ) : ?>
+				<?php				
+					if ( isset( $plugins_section_titles[ $section_name ] ) )
+						$title = $plugins_section_titles[ $section_name ];
+					else
+						$title = ucwords( str_replace( '_', ' ', $section_name ) );
+
+					$class = ( $section_name == $section ) ? ' class="current"' : '';
+					$href = add_query_arg( array('tab' => $tab, 'section' => $section_name) );
+					$href = esc_url($href);
+					$san_section = esc_attr( $section_name );
+				?>
+				<li><a name="<?php echo $san_section; ?>" href="<?php echo $href; ?>" <?php echo $class; ?>><?php echo $title; ?></a></li>
+			<?php endforeach ; ?>
+		</ul>
+	</div>
+	<div class="alignright fyi">
+		<h2 class="mainheader"><?php /* translators: For Your Information */ _e('FYI') ?></h2>
+		<ul>
+			<?php if ( !empty( $api->version ) ) : ?>
+				<li><strong><?php _e('Version:') ?></strong> <?php echo $api->version ?></li>
+			<?php endif; ?>
+			<?php if ( !empty( $api->author ) ) : ?>
+				<li><strong><?php _e('Author:') ?></strong> <?php echo $api->author ?></li>
+			<?php endif; ?>
+			<?php if ( !empty( $api->requires ) ) : ?>
+				<li><strong><?php _e('Requires WordPress Version:') ?></strong> <?php printf(__('%s or higher'), $api->requires) ?></li>
+			<?php endif; ?>
+			<?php if ( !empty( $api->tested ) ) : ?>
+				<li><strong><?php _e('Compatible up to:') ?></strong> <?php echo $api->tested ?></li>
+			<?php endif; ?>		
+			<?php if ( !empty( $api->homepage ) ) : ?>
+				<li><a target="_blank" href="<?php echo $api->homepage ?>"><?php _e('Plugin Homepage &#187;') ?></a></li>
+			<?php endif; ?>
+		</ul>
+	</div>
+	<div id="section-holder" class="wrap">
+	<?php
+		if ( !empty( $api->tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) )
+			echo '<div class="updated"><p>' . __('<strong>Warning:</strong> This plugin has <strong>not been tested</strong> with your current version of WordPress.') . '</p></div>';
+
+		elseif ( !empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) )
+			echo '<div class="updated"><p>' . __('<strong>Warning:</strong> This plugin has <strong>not been marked as compatible</strong> with your version of WordPress.') . '</p></div>';
+
+		foreach ( (array) $api->sections as $section_name => $content ) {
+			if ( isset( $plugins_section_titles[ $section_name ] ) )
+				$title = $plugins_section_titles[ $section_name ];
+			else
+				$title = ucwords( str_replace( '_', ' ', $section_name ) );
+
+			$content = links_add_target($content, '_blank');
+			$san_section = esc_attr( $section_name );
+			$display = ( $section_name == $section ) ? 'block' : 'none';
+			?>
+			<div id="section-<?php echo $san_section; ?>" class="section" style="display: <?php echo $display; ?>;">
+				<h2 class="long-header"><?php echo $title; ?></h2>
+				<?php echo $content; ?>
+			</div>
+			<?php
+		}
+	echo "</div>\n";
+	iframe_footer();
+	exit;
+}
+add_action('install_plugins_pre_plugin-readme', 'plugin_readme_information');
Index: wp-admin/css/wp-admin-rtl.dev.css
===================================================================
--- wp-admin/css/wp-admin-rtl.dev.css	(revision 19728)
+++ wp-admin/css/wp-admin-rtl.dev.css	(working copy)
@@ -2148,26 +2148,31 @@
 	left: 0;
 }
 
-#plugin-information ul#sidemenu {
+#plugin-information ul#sidemenu,
+#plugin-readme ul#sidemenu {
 	left: auto;
 	right: 0;
 }
 
-#plugin-information h2 {
+#plugin-information h2,
+#plugin-readme h2 {
 	margin-right: 0;
 	margin-left: 200px;
 }
 
-#plugin-information .fyi {
+#plugin-information .fyi,
+#plugin-readme .fyi {
 	margin-left: 5px;
 	margin-right: 20px;
 }
 
-#plugin-information .fyi h2 {
+#plugin-information .fyi h2,
+#plugin-readme .fyi h2 {
 	margin-left: 0;
 }
 
-#plugin-information .fyi ul {
+#plugin-information .fyi ul,
+#plugin-readme .fyi ul {
 	padding: 10px 7px 10px 5px;
 }
 
@@ -2178,13 +2183,17 @@
 
 #plugin-information #section-screenshots ol,
 #plugin-information .updated,
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme .updated,
+#plugin-readme pre {
 	margin-right: 0;
 	margin-left: 215px;
 }
 
 #plugin-information .updated,
-#plugin-information .error {
+#plugin-information .error,
+#plugin-readme .updated,
+#plugin-readme .error {
 	clear: none;
 	direction: rtl;
 }
Index: wp-admin/css/wp-admin.dev.css
===================================================================
--- wp-admin/css/wp-admin.dev.css	(revision 19728)
+++ wp-admin/css/wp-admin.dev.css	(working copy)
@@ -6777,7 +6777,8 @@
 }
 
 /* Header on thickbox */
-#plugin-information-header {
+#plugin-information-header,
+#plugin-readme-header {
 	margin: 0;
 	padding: 0 5px;
 	font-weight: bold;
@@ -6786,7 +6787,8 @@
 	border-bottom-style: solid;
 	height: 2.5em;
 }
-#plugin-information ul#sidemenu {
+#plugin-information ul#sidemenu,
+#plugin-readme ul#sidemenu {
 	font-weight: normal;
 	margin: 0 5px;
 	position: absolute;
@@ -6814,28 +6816,33 @@
 	line-height: 2em;
 }
 
-#plugin-information h2 {
+#plugin-information h2,
+#plugin-readme h2 {
 	clear: none !important;
 	margin-right: 200px;
 }
 
-#plugin-information .fyi {
+#plugin-information .fyi,
+#plugin-readme .fyi {
 	margin: 0 10px 50px;
 	width: 210px;
 }
 
-#plugin-information .fyi h2 {
+#plugin-information .fyi h2,
+#plugin-readme .fyi h2 {
 	font-size: 0.9em;
 	margin-bottom: 0;
 	margin-right: 0;
 }
 
-#plugin-information .fyi h2.mainheader {
+#plugin-information .fyi h2.mainheader,
+#plugin-readme .fyi h2.mainheader {
 	padding: 5px;
 	-webkit-border-top-left-radius: 3px;
 	border-top-left-radius: 3px;
 }
 
+#plugin-readme .fyi ul,
 #plugin-information .fyi ul {
 	padding: 10px 5px 10px 7px;
 	margin: 0;
@@ -6844,16 +6851,20 @@
 	border-bottom-left-radius: 3px;
 }
 
-#plugin-information .fyi li {
+#plugin-information .fyi li,
+#plugin-readme .fyi li {
 	margin-right: 0;
 }
 
-#plugin-information #section-holder {
+#plugin-information #section-holder,
+#plugin-readme #section-holder {
 	padding: 10px;
 }
 
 #plugin-information .section ul,
-#plugin-information .section ol {
+#plugin-information .section ol,
+#plugin-readme .section ul,
+#plugin-readme .section ol {
 	margin-left: 16px;
 	list-style-type: square;
 	list-style-image: none;
@@ -6879,11 +6890,14 @@
 
 #plugin-information #section-screenshots ol,
 #plugin-information .updated,
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme .updated,
+#plugin-readme pre {
 	margin-right: 215px;
 }
 
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme pre {
 	padding: 7px;
 	overflow: auto;
 }
