WordPress.org

Make WordPress Core

Ticket #17902: 17902.3.patch

File 17902.3.patch, 30.6 KB (added by kurtpayne, 8 years ago)

Updated for 3.5

  • wp-admin/css/colors-classic.dev.css

     
    619619}
    620620
    621621div#media-upload-header,
    622 div#plugin-information-header {
     622div#plugin-information-header,
     623div#plugin-readme-header {
    623624        background-color: #f9f9f9;
    624625        border-bottom-color: #dfdfdf;
    625626}
     
    19261927}
    19271928
    19281929/* Install Plugins */
    1929 #plugin-information .fyi ul {
     1930#plugin-information .fyi ul,
     1931#plugin-readme .fyi ul {
    19301932        background-color: #eaf3fa;
    19311933}
    19321934
    1933 #plugin-information .fyi h2.mainheader {
     1935#plugin-information .fyi h2.mainheader,
     1936#plugin-readme .fyi h2.mainheader {
    19341937        background-color: #cee1ef;
    19351938}
    19361939
    19371940#plugin-information pre,
    1938 #plugin-information code {
     1941#plugin-information code,
     1942#plugin-readme pre,
     1943#plugin-readme code {
    19391944        background-color: #ededff;
    19401945}
    19411946
    1942 #plugin-information pre {
     1947#plugin-information pre,
     1948#plugin-readme pre {
    19431949        border: 1px solid #ccc;
    19441950}
    19451951
  • wp-admin/css/colors-fresh.dev.css

     
    610610}
    611611
    612612div#media-upload-header,
    613 div#plugin-information-header {
     613div#plugin-information-header,
     614div#plugin-readme-header {
    614615        background-color: #f9f9f9;
    615616        border-bottom-color: #dfdfdf;
    616617}
     
    15421543}
    15431544
    15441545/* Install Plugins */
    1545 #plugin-information .fyi ul {
     1546#plugin-information .fyi ul,
     1547#plugin-readme .fyi ul {
    15461548        background-color: #eaf3fa;
    15471549}
    15481550
    1549 #plugin-information .fyi h2.mainheader {
     1551#plugin-information .fyi h2.mainheader,
     1552#plugin-readme .fyi h2.mainheader {
    15501553        background-color: #cee1ef;
    15511554}
    15521555
    15531556#plugin-information pre,
    1554 #plugin-information code {
     1557#plugin-information code,
     1558#plugin-readme pre,
     1559#plugin-readme code {
    15551560        background-color: #ededff;
    15561561}
    15571562
    1558 #plugin-information pre {
     1563#plugin-information pre,
     1564#plugin-readme pre {
    15591565        border: 1px solid #ccc;
    15601566}
    15611567
  • wp-admin/css/wp-admin.dev.css

     
    71647164}
    71657165
    71667166/* Header on thickbox */
    7167 #plugin-information-header {
     7167#plugin-information-header,
     7168#plugin-readme-header {
    71687169        margin: 0;
    71697170        padding: 0 5px;
    71707171        font-weight: bold;
     
    71737174        border-bottom-style: solid;
    71747175        height: 2.5em;
    71757176}
    7176 #plugin-information ul#sidemenu {
     7177#plugin-information ul#sidemenu,
     7178#plugin-readme ul#sidemenu {
    71777179        font-weight: normal;
    71787180        margin: 0 5px;
    71797181        position: absolute;
     
    72017203        line-height: 2em;
    72027204}
    72037205
    7204 #plugin-information h2 {
     7206#plugin-information h2,
     7207#plugin-readme h2 {
    72057208        clear: none !important;
    72067209        margin-right: 200px;
    72077210}
    72087211
    7209 #plugin-information .fyi {
     7212#plugin-information .fyi,
     7213#plugin-readme .fyi {
    72107214        margin: 0 10px 50px;
    72117215        width: 210px;
    72127216}
    72137217
    7214 #plugin-information .fyi h2 {
     7218#plugin-information .fyi h2,
     7219#plugin-readme .fyi h2 {
    72157220        font-size: 0.9em;
    72167221        margin-bottom: 0;
    72177222        margin-right: 0;
    72187223}
    72197224
    7220 #plugin-information .fyi h2.mainheader {
     7225#plugin-information .fyi h2.mainheader,
     7226#plugin-readme .fyi h2.mainheader {
    72217227        padding: 5px;
    72227228        -webkit-border-top-left-radius: 3px;
    72237229        border-top-left-radius: 3px;
    72247230}
    72257231
     7232#plugin-readme .fyi ul,
    72267233#plugin-information .fyi ul {
    72277234        padding: 10px 5px 10px 7px;
    72287235        margin: 0;
     
    72317238        border-bottom-left-radius: 3px;
    72327239}
    72337240
    7234 #plugin-information .fyi li {
     7241#plugin-information .fyi li,
     7242#plugin-readme .fyi li {
    72357243        margin-right: 0;
    72367244}
    72377245
    7238 #plugin-information #section-holder {
     7246#plugin-information #section-holder,
     7247#plugin-readme #section-holder {
    72397248        padding: 10px;
    72407249}
    72417250
    72427251#plugin-information .section ul,
    7243 #plugin-information .section ol {
     7252#plugin-information .section ol,
     7253#plugin-readme .section ul,
     7254#plugin-readme .section ol {
    72447255        margin-left: 16px;
    72457256        list-style-type: square;
    72467257        list-style-image: none;
     
    72667277
    72677278#plugin-information #section-screenshots ol,
    72687279#plugin-information .updated,
    7269 #plugin-information pre {
     7280#plugin-information pre,
     7281#plugin-readme .updated,
     7282#plugin-readme pre {
    72707283        margin-right: 215px;
    72717284}
    72727285
    7273 #plugin-information pre {
     7286#plugin-information pre,
     7287#plugin-readme pre {
    72747288        padding: 7px;
    72757289        overflow: auto;
    72767290}
  • wp-admin/css/wp-admin-rtl.dev.css

     
    22392239        float: right;
    22402240}
    22412241
    2242 #plugin-information ul#sidemenu {
     2242#plugin-information ul#sidemenu,
     2243#plugin-readme ul#sidemenu {
    22432244        left: auto;
    22442245        right: 0;
    22452246}
    22462247
    2247 #plugin-information h2 {
     2248#plugin-information h2,
     2249#plugin-readme h2 {
    22482250        margin-right: 0;
    22492251        margin-left: 200px;
    22502252}
    22512253
    2252 #plugin-information .fyi {
     2254#plugin-information .fyi,
     2255#plugin-readme .fyi {
    22532256        margin-left: 5px;
    22542257        margin-right: 20px;
    22552258}
    22562259
    2257 #plugin-information .fyi h2 {
     2260#plugin-information .fyi h2,
     2261#plugin-readme .fyi h2 {
    22582262        margin-left: 0;
    22592263}
    22602264
    2261 #plugin-information .fyi ul {
     2265#plugin-information .fyi ul,
     2266#plugin-readme .fyi ul {
    22622267        padding: 10px 7px 10px 5px;
    22632268}
    22642269
     
    22692274
    22702275#plugin-information #section-screenshots ol,
    22712276#plugin-information .updated,
    2272 #plugin-information pre {
     2277#plugin-information pre,
     2278#plugin-readme .updated,
     2279#plugin-readme pre {
    22732280        margin-right: 0;
    22742281        margin-left: 215px;
    22752282}
    22762283
    22772284#plugin-information .updated,
    2278 #plugin-information .error {
     2285#plugin-information .error,
     2286#plugin-readme .updated,
     2287#plugin-readme .error {
    22792288        clear: none;
    22802289        direction: rtl;
    22812290}
  • wp-admin/plugin-install.php

     
    66 * @subpackage Administration
    77 */
    88// TODO route this pages via a specific iframe handler instead of the do_action below
    9 if ( !defined( 'IFRAME_REQUEST' ) && isset( $_GET['tab'] ) && ( 'plugin-information' == $_GET['tab'] ) )
     9if ( !defined( 'IFRAME_REQUEST' ) && isset( $_GET['tab'] ) && ( in_array( $_GET['tab'], array( 'plugin-information', 'plugin-readme' ) ) ) )
    1010        define( 'IFRAME_REQUEST', true );
    1111
    1212/** WordPress Administration Bootstrap */
     
    2828$parent_file = 'plugins.php';
    2929
    3030wp_enqueue_script( 'plugin-install' );
    31 if ( 'plugin-information' != $tab )
     31if ( !in_array( $tab, array('plugin-information', 'plugin-readme') ) )
    3232        add_thickbox();
    3333
    3434$body_id = $tab;
  • wp-admin/includes/class-wp-plugins-list-table.php

     
    420420                                                        $author = '<a href="' . $plugin_data['AuthorURI'] . '" title="' . esc_attr__( 'Visit author homepage' ) . '">' . $plugin_data['Author'] . '</a>';
    421421                                                $plugin_meta[] = sprintf( __( 'By %s' ), $author );
    422422                                        }
     423                                        $slug = basename( $plugin_file, '.php' );
     424                                        $plugin_meta[] = '<a href="' . self_admin_url( 'plugin-install.php?tab=plugin-readme&amp;readme=true&amp;plugin=' . $slug .
     425                                                                '&amp;TB_iframe=true&amp;width=600&amp;height=550' ) . '" class="thickbox" title="' .
     426                                                                esc_attr( sprintf( __( 'More information about %s' ), "{$plugin_data['Name']} {$plugin_data['Version']}" ) ) . '">' . __( 'Details' ) . '</a>';
    423427                                        if ( ! empty( $plugin_data['PluginURI'] ) )
    424428                                                $plugin_meta[] = '<a href="' . $plugin_data['PluginURI'] . '" title="' . esc_attr__( 'Visit plugin site' ) . '">' . __( 'Visit plugin site' ) . '</a>';
    425429
  • wp-admin/includes/plugin-install.php

     
    380380        exit;
    381381}
    382382add_action('install_plugins_pre_plugin-information', 'install_plugin_information');
     383
     384/**
     385 * Get information from a plugin locally
     386 * @param string $slug
     387 * @since 3.4.0
     388 */
     389function local_plugin_api( $slug ) {
     390
     391        // Try to find the plugin file from the slug
     392        $plugin_data = array();
     393        if ( file_exists( ABSPATH . PLUGINDIR . "/$slug.php" ) ) {
     394                $plugin_data = get_plugin_data( ABSPATH . PLUGINDIR . "/$slug.php" );
     395        } elseif ( file_exists( ABSPATH . PLUGINDIR . "/$slug/$slug.php" ) ) {
     396                $plugin_data = get_plugin_data( ABSPATH . PLUGINDIR . "/$slug/$slug.php" );
     397        }
     398       
     399        // Try to load data from readme.txt
     400        $readme_data = false;
     401        if ( file_exists( ABSPATH . PLUGINDIR . "/$slug/readme.txt" ) ) {
     402
     403                // Get markddown library, turn off plugin functionality
     404                if ( !defined( 'MARKDOWN_WP_POSTS' ) )
     405                        define( 'MARKDOWN_WP_POSTS', false );
     406                if ( !defined( 'MARKDOWN_WP_COMMENTS' ) )
     407                        define( 'MARKDOWN_WP_COMMENTS', false );
     408                if ( !class_exists( 'Markdown_Parser' ) )
     409                        include_once( ABSPATH . 'wp-admin/includes/markdown.php' );
     410               
     411                include_once( ABSPATH . 'wp-admin/includes/class-wp-plugin-readme-parser.php' );
     412                $readme_parser = new wp_plugin_readme_parser();
     413                $readme_data = $readme_parser->parse_readme_file( ABSPATH . PLUGINDIR . "/$slug/readme.txt" );
     414        }
     415       
     416        // If there's no readme (e.g. hello.php) create a fake structure
     417        if ( empty( $readme_data ) ) {
     418                $readme_data = array(
     419                        'contributors'    => '',
     420                        'requires_at_least' => '',
     421                        'tested_up_to'    => '',
     422                        'tags'                    => array(),
     423                        'sections'                => array(
     424                                'description'   => !empty( $plugin_data['Description'] ) ? $plugin_data['Description'] : ''
     425                        )
     426                );
     427        }
     428
     429        // Convert to an API-style response
     430        $api = array(
     431                'name'             => !empty( $plugin_data['Name'] ) ? $plugin_data['Name'] : $slug,
     432                'slug'             => $slug,
     433                'version'               => !empty( $plugin_data['Version'] ) ? $plugin_data['Version'] : '',
     434                'author'                 => !empty( $plugin_data['AuthorName'] ) ? $plugin_data['AuthorName'] : '',
     435                'author_profile' => null,
     436                'contributors'   => $readme_data['contributors'],
     437                'requires'         => $readme_data['requires_at_least'],
     438                'tested'                 => $readme_data['tested_up_to'],
     439                'compatibility'  => null,
     440                'rating'                 => null,
     441                'num_ratings'   => null,
     442                'downloaded'     => null,
     443                'last_updated'   => null,
     444                'added'           => null,
     445                'homepage'         => !empty( $plugin_data['PluginURI'] ) ? $plugin_data['PluginURI'] : '',
     446                'sections'         => $readme_data['sections'],
     447                'download_link'  => null,
     448                'tags'             => $readme_data['tags']
     449        );
     450
     451        // Done
     452        return (object) $api;
     453}
     454
     455/**
     456 * Display local plugin information in dialog box form.
     457 * Pull from the readme.txt file and the plugin header
     458 * @since 3.4.0
     459 */
     460function plugin_readme_information() {
     461        global $tab;
     462
     463        $api = local_plugin_api( stripslashes( $_REQUEST['plugin'] ) );
     464
     465        if ( is_wp_error($api) )
     466                wp_die($api);
     467
     468        $plugins_allowedtags = array(
     469                'a'     => array(
     470                        'href'   => array(),
     471                        'title'  => array(),
     472                        'target' => array()
     473                ),
     474                'abbr' => array(
     475                        'title' => array()
     476                ),
     477                'acronym' => array(
     478                        'title' => array()
     479                ),
     480                'code'   => array(),
     481                'pre'   => array(),
     482                'em'     => array(),
     483                'strong' => array(),
     484                'div'   => array(),
     485                'p'       => array(),
     486                'ul'     => array(),
     487                'ol'     => array(),
     488                'li'     => array(),
     489                'h1'     => array(),
     490                'h2'     => array(),
     491                'h3'     => array(),
     492                'h4'     => array(),
     493                'h5'     => array(),
     494                'h6'     => array(),
     495                'img'   => array(
     496                        'src'   => array(),
     497                        'class' => array(),
     498                        'alt'   => array()
     499                )
     500        );
     501
     502        $plugins_section_titles = array(
     503                'description'  => _x('Description',  'Plugin installer section title'),
     504                'installation' => _x('Installation', 'Plugin installer section title'),
     505                'faq'             => _x('FAQ',            'Plugin installer section title'),
     506                'changelog'     => _x('Changelog',      'Plugin installer section title'),
     507                'other_notes'  => _x('Other Notes',  'Plugin installer section title')
     508        );
     509
     510        // No screenshots at this time
     511        if ( !empty( $api->sections['screenshots'] ) )
     512                unset( $api->sections['screenshots'] );
     513       
     514        // Sanitize HTML
     515        foreach ( (array)$api->sections as $section_name => $content )
     516                $api->sections[$section_name] = wp_kses( $content, $plugins_allowedtags );
     517        foreach ( array( 'version', 'author', 'requires', 'tested', 'homepage', 'downloaded', 'slug' ) as $key ) {
     518                if ( isset( $api->$key ) )
     519                        $api->$key = wp_kses( $api->$key, $plugins_allowedtags );
     520        }
     521
     522        // Default to the Description tab, Do not translate, API returns English.
     523        $section = isset( $_REQUEST['section'] ) ? stripslashes( $_REQUEST['section'] ) : 'description';
     524        if ( empty($section) || ! isset($api->sections[ $section ]) )
     525                $section = array_shift( $section_titles = array_keys((array)$api->sections) );
     526
     527        iframe_header( __( 'Plugin Details' ) );
     528        ?>
     529        <div id="<?php echo $tab; ?>-header">
     530                <ul id="sidemenu">
     531                        <?php foreach ( (array) $api->sections as $section_name => $content ) : ?>
     532                                <?php                           
     533                                        if ( isset( $plugins_section_titles[ $section_name ] ) )
     534                                                $title = $plugins_section_titles[ $section_name ];
     535                                        else
     536                                                $title = ucwords( str_replace( '_', ' ', $section_name ) );
     537
     538                                        $class = ( $section_name == $section ) ? ' class="current"' : '';
     539                                        $href = add_query_arg( array('tab' => $tab, 'section' => $section_name) );
     540                                        $href = esc_url($href);
     541                                        $san_section = esc_attr( $section_name );
     542                                ?>
     543                                <li><a name="<?php echo $san_section; ?>" href="<?php echo $href; ?>" <?php echo $class; ?>><?php echo $title; ?></a></li>
     544                        <?php endforeach ; ?>
     545                </ul>
     546        </div>
     547        <div class="alignright fyi">
     548                <h2 class="mainheader"><?php /* translators: For Your Information */ _e('FYI') ?></h2>
     549                <ul>
     550                        <?php if ( !empty( $api->version ) ) : ?>
     551                                <li><strong><?php _e('Version:') ?></strong> <?php echo $api->version ?></li>
     552                        <?php endif; ?>
     553                        <?php if ( !empty( $api->author ) ) : ?>
     554                                <li><strong><?php _e('Author:') ?></strong> <?php echo $api->author ?></li>
     555                        <?php endif; ?>
     556                        <?php if ( !empty( $api->requires ) ) : ?>
     557                                <li><strong><?php _e('Requires WordPress Version:') ?></strong> <?php printf(__('%s or higher'), $api->requires) ?></li>
     558                        <?php endif; ?>
     559                        <?php if ( !empty( $api->tested ) ) : ?>
     560                                <li><strong><?php _e('Compatible up to:') ?></strong> <?php echo $api->tested ?></li>
     561                        <?php endif; ?>         
     562                        <?php if ( !empty( $api->homepage ) ) : ?>
     563                                <li><a target="_blank" href="<?php echo $api->homepage ?>"><?php _e('Plugin Homepage &#187;') ?></a></li>
     564                        <?php endif; ?>
     565                </ul>
     566        </div>
     567        <div id="section-holder" class="wrap">
     568        <?php
     569                if ( !empty( $api->tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) )
     570                        echo '<div class="updated"><p>' . __('<strong>Warning:</strong> This plugin has <strong>not been tested</strong> with your current version of WordPress.') . '</p></div>';
     571
     572                elseif ( !empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) )
     573                        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>';
     574
     575                foreach ( (array) $api->sections as $section_name => $content ) {
     576                        if ( isset( $plugins_section_titles[ $section_name ] ) )
     577                                $title = $plugins_section_titles[ $section_name ];
     578                        else
     579                                $title = ucwords( str_replace( '_', ' ', $section_name ) );
     580
     581                        $content = links_add_target($content, '_blank');
     582                        $san_section = esc_attr( $section_name );
     583                        $display = ( $section_name == $section ) ? 'block' : 'none';
     584                        ?>
     585                        <div id="section-<?php echo $san_section; ?>" class="section" style="display: <?php echo $display; ?>;">
     586                                <h2 class="long-header"><?php echo $title; ?></h2>
     587                                <?php echo $content; ?>
     588                        </div>
     589                        <?php
     590                }
     591        echo "</div>\n";
     592        iframe_footer();
     593        exit;
     594}
     595add_action('install_plugins_pre_plugin-readme', 'plugin_readme_information');
  • wp-admin/includes/class-wp-plugin-install-list-table.php

     
    3434                $tabs['popular']  = _x( 'Popular','Plugin Installer' );
    3535                $tabs['new']      = _x( 'Newest','Plugin Installer' );
    3636
    37                 $nonmenu_tabs = array( 'plugin-information' ); //Valid actions to perform which do not have a Menu item.
     37                $nonmenu_tabs = array( 'plugin-information', 'plugin-readme' ); //Valid actions to perform which do not have a Menu item.
    3838
    3939                $tabs = apply_filters( 'install_plugins_tabs', $tabs );
    4040                $nonmenu_tabs = apply_filters( 'install_plugins_nonmenu_tabs', $nonmenu_tabs );
  • wp-admin/includes/class-wp-plugin-readme-parser.php

     
     1<?php
     2
     3/**
     4 * Parse a plugin's readme.txt file
     5 * Based on http://code.svn.wordpress.org/plugin-readme-parser/parse-readme.php
     6 * @link http://wordpress.org/extend/plugins/about/readme.txt
     7 * @link http://wordpress.org/extend/plugins/about/validator/
     8 * @pacakge WordPress
     9 * @version 1.0
     10 */
     11class wp_plugin_readme_parser {
     12
     13                /**
     14                 * Readme.txt file contents
     15                 * This string will change with each pass in the process
     16                 * @var string
     17                 */
     18                protected $_readme_contents = '';
     19               
     20                /**
     21                 * Special section names
     22                 * @var array
     23                 */
     24                protected $_special_sections = array(
     25                                'description',
     26                                'installation',
     27                                'frequently_asked_questions',
     28                                'screenshots',
     29                                'changelog',
     30                                'change_log',
     31                                'upgrade_notice'
     32                );
     33
     34                /**
     35                 * Minimum version of WordPress
     36                 * @var string
     37                 */
     38                protected $_requires_at_least = '';
     39
     40                /**
     41                 * Maximum compatible version of WordPress
     42                 * @var string
     43                 */
     44                protected $_tested_up_to = '';
     45
     46                /**
     47                 * Stable tag for the plugin
     48                 * @var string
     49                 */
     50                protected $_stable_tag = '';
     51
     52                /**
     53                 * Keywords for the plugin directory search
     54                 * @var array
     55                 */
     56                protected $_tags = array();
     57
     58                /**
     59                 * Authors who wrote the plugin
     60                 * @var array
     61                 */
     62                protected $_contributors = array();
     63
     64                /**
     65                 * Where to donate
     66                 * @var string
     67                 */
     68                protected $_donate_link = '';
     69
     70                /**
     71                 * License type
     72                 * @var string
     73                 */
     74                protected $_license = '';
     75
     76                /**
     77                 * Recognized keys for records
     78                 * @var array
     79                 */
     80                protected $_keys = array(
     81                                'requires_at_least' => '/Requires at least:[ \t]*(.+)/i',
     82                                'tested_up_to'    => '/Tested up to:[ \t]*(.+)/i',
     83                                'stable_tag'            => '/Stable tag:[ \t]*(.+)/i',
     84                                'tags'                    => '/Tags:[ \t]*(.+)/i',
     85                                'contributors'    => '/Contributors:[ \t]*(.+)/i',
     86                                'donate_link'      => '/Donate link:[ \t]*(.+)/i',
     87                                'license'                  => '/License:[ \t]*(.+)/i'
     88                );
     89               
     90                /**
     91                 * Is the description a copy of the short description?
     92                 * @var bool
     93                 */
     94                protected $_is_excerpt = false;
     95
     96                /**
     97                 * Was the short description truncated to 150 characters?
     98                 * @var bool
     99                 */
     100                protected $_is_truncated = false;
     101
     102                /**
     103                 * Short description of the plugin
     104                 * @var string
     105                 */
     106                protected $_short_description = '';
     107
     108                /**
     109                 * List of the plugin's screenshots
     110                 * @var array
     111                 */
     112                protected $_screenshots = array();
     113               
     114                /**
     115                 * Content outside of the standard sections
     116                 * @var string
     117                 */
     118                protected $_remaining_content = '';
     119               
     120                /**
     121                 * Upgrade notices for users
     122                 * Key = version, value = message.  Example:
     123                 * 1.0 => Please upgrade
     124                 * 1.1 => Dire security bug
     125                 * 1.2 => Minor UI issue
     126                 * @var array
     127                 */
     128                protected $_upgrade_notice = array();
     129               
     130                /**
     131                 * Allowed tags in sections
     132                 * @var array
     133                 */
     134                private $_allowed_tags = array(
     135                                'a'               => array(
     136                                                'href'     => array(),
     137                                                'title'   => array(),
     138                                                'rel'           => array()
     139                                ),
     140                                'blockquote' => array( 'cite' => array() ),
     141                                'br'             => array(),
     142                                'cite'     => array(),
     143                                'p'               => array(),
     144                                'code'     => array(),
     145                                'pre'           => array(),
     146                                'em'             => array(),
     147                                'strong'         => array(),
     148                                'ul'             => array(),
     149                                'ol'             => array(),
     150                                'li'             => array(),
     151                                'h3'             => array(),
     152                                'h4'             => array()
     153                );
     154
     155                /**
     156                 * Parse a plugin's readme.txt file.  Expects a path to the file.
     157                 * @param string $file
     158                 * @return string
     159                 */
     160                public function parse_readme_file ( $file ) {
     161                                if ( !file_exists( $file ) ) {
     162                                                throw new Exception('File not found');
     163                                }
     164                                $this->_readme_contents = file_get_contents( $file );
     165                                return $this->_parse_readme();
     166                }
     167
     168                /**
     169                 * Parse a plugin's readme.txt.  Expect's the file contents as string.
     170                 * @param type $file_contents
     171                 * @return type
     172                 */
     173                public function parse_readme_contents( $file_contents ) {
     174                                $this->_readme_contents = $file_contents;
     175                                return $this->_parse_readme();
     176                }
     177               
     178                /**
     179                 * Extract sections
     180                 * @return array
     181                 */
     182                private function _exract_sections( ) {
     183                                $ret = array();
     184
     185                                $sections = preg_split( '/^[\s]*==[\s]*(.+?)[\s]*==/m', $this->_readme_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY );
     186
     187                                // Check if the first element is a short description
     188                                $this->_short_description = '';
     189                                $this->_is_truncated      = false;
     190                                $this->_is_excerpt              = true;
     191                                if ( '==' != substr($sections[0][0], 0, 2) ) {
     192                                                $short_description = array_shift( $sections );
     193                                               
     194                                                // If the user doesn't include a full description, this will be used, not truncated, and converted to markdown.  If they
     195                                                // do include a full description, this field will be overwritten
     196                                                $ret['description'] = $this->_markdown( $short_description );
     197
     198                                                $short_description = $this->_sanitize_text( $short_description );
     199                                                if ( strlen( $short_description ) > 150 )
     200                                                                $this->_is_truncated = true;
     201                                                $this->_short_description = substr( $short_description, 0, 150 );
     202                                }
     203
     204                                // Sanitize titles / sections
     205                                for ( $i = 0 ; $i < count( $sections ) ; $i += 2 ) {
     206                                                $title  = $this->_sanitize_text( $sections[$i] );
     207                                                $contents = preg_replace( '/^[\s]*=[\s]+(.+?)[\s]+=/m', '<h4>$1</h4>', $sections[$i+1] );
     208                                                $contents = $this->_markdown( $contents );
     209                                                $ret[$title] = $contents;                                         
     210                                                if ( $this->_is_excerpt && 'description' == strtolower( strip_tags( $title ) ) )
     211                                                                $this->_is_excerpt = false;
     212                                }
     213
     214                                // Remove the sections from the readme file
     215                                $this->_readme_contents = trim( str_replace( $sections, '', $this->_readme_contents ) );
     216                               
     217                                // Done
     218                                return $this->_process_sections( $ret );
     219                }
     220
     221                /**
     222                 * Post-process sections
     223                 * Any special business logic (e.g. change_log -> changelog) is done here
     224                 * @param array $sections
     225                 * @return array
     226                 */
     227                private function _process_sections( $sections ) {
     228                               
     229                                // Rename sections to lower-case-underscore notation relegate non-special
     230                                // content to the "remaining content" section
     231                                $_sections = array();
     232                                $this->_remaining_content = '';
     233                                foreach ( (array) $sections as $k => $v ) {
     234                                                $name = strtolower( preg_replace( '/[^a-zA-Z_]/', '_', $k ) );
     235                                                $_sections[$name] = $v;
     236
     237                                                // Is this "remaining content" ?
     238                                                if ( !in_array( $name, $this->_special_sections ) ) {
     239                                                                $title_id = esc_attr( $k );
     240                                                                $title_id = str_replace( ' ', '-', $title_id );
     241                                                                $this->_remaining_content .= sprintf("\n<h3 id=\"%s\">%s</h3>\n%s", $name, $k, $v);
     242                                                }
     243                                }
     244                                $sections = $_sections;
     245
     246                                // Upgrade notice section
     247                                $this->_upgrade_notice = array();
     248                                if ( array_key_exists( 'upgrade_notice', $sections ) ) {
     249                                                $upgrade_notice = array();
     250                                                $notices = preg_split( '/<h4>(.*?)<\/h4>/', $sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
     251                                                if ( empty( $notices ) && count( $notices ) > 1 ) {
     252                                                                for ( $i = 0 ; $i < count( $notices ) ; $i += 2 )
     253                                                                                $upgrade_notice[$notices[$i]] = substr( $this->_sanitize_text( @$notices[$i+1] ), 0, 300 );
     254                                                } elseif ( is_array( $notices ) && 1 == count( $notices ) ) {
     255                                                                $upgrade_notice[ $this->_stable_tag ] = $notices[0];
     256                                                }
     257                                                $this->_upgrade_notice = $upgrade_notice;
     258                                                unset( $sections['upgrade_notice'] );
     259                                }
     260
     261                                // Screenshots
     262                                $this->_screenshots = array();
     263                                if ( array_key_exists( 'screenshots', $sections ) ) {
     264                                                preg_match_all( '/<li>(.*?)<\/li>/s', $sections['screenshots'], $screenshots, PREG_SET_ORDER);
     265                                                if ( $screenshots ) {
     266                                                                foreach ( (array) $screenshots as $screenshot )
     267                                                                                $this->_screenshots[] = $screenshot[1];
     268                                                }
     269                                }
     270
     271                                // Fix change_log -> changelog
     272                                if ( array_key_exists( 'change_log', $sections ) )
     273                                                $sections['changelog'] = $sections['change_log'];
     274
     275                                return $sections;
     276                }
     277
     278                /**
     279                 * Post-process records
     280                 * Any special business logic (e.g. splitting tags, contributors) is done here
     281                 * @param array $records
     282                 * @return array
     283                 */
     284                private function _process_records( $records ) {
     285                               
     286                                // Escape donate link URL
     287                                if ( !empty( $records['donate_link'] ) )
     288                                                $records['donate_link'] = esc_url( $records['donate_link'] );
     289                               
     290                                // Split up tags
     291                                $tags = preg_split('/,[\s]*?/', trim( $records['tags'] ) );
     292                                foreach ( (array) $tags as $k => $v ) {
     293                                                $v = $this->_sanitize_text( $v );
     294                                                if ( !empty( $v ) )
     295                                                                $tags[$k] = $this->_sanitize_text( $v );
     296                                                else
     297                                                                unset( $tags[$k] );
     298                                }
     299                                $records['tags'] = $tags;
     300
     301                                // Split up contributors
     302                                $contributors = preg_split( '/,[\s]*/', trim( $records['contributors'] ) );
     303                                foreach ( (array) $contributors as $k => $v ) {
     304                                                $v = $this->_sanitize_text( $v );
     305                                                if ( !empty( $v ) )
     306                                                                $contributors[$k] = sanitize_user( $v );
     307                                                else
     308                                                                unset( $contributors[$k] );
     309                                }
     310                                $records['contributors'] = $contributors;
     311
     312                                // Assign records to object properties
     313                                foreach ( (array) $records as $k => $v )
     314                                                if (in_array( $k , array_keys( $this->_keys ) ) )
     315                                                                $this->{"_$k"} = $v;
     316
     317                                // Done
     318                                return $records;
     319                }
     320
     321                /**
     322                 * Extract key-value pairs
     323                 * @return array
     324                 */
     325                private function _extract_records( ) {
     326                                $ret = array();
     327                                foreach ( (array) $this->_keys as $k => $v ) {
     328                                                if ( preg_match( $v, $this->_readme_contents, $matches ) ) {
     329                                                                $ret[$k] = $this->_sanitize_text( $matches[1] );
     330                                                                $this->_readme_contents = trim( str_replace( $matches[0], '', $this->_readme_contents ) );
     331                                                }
     332                                }
     333                                return $this->_process_records( $ret );
     334                }
     335               
     336                /**
     337                 * Parse a readme file into an associative array
     338                 * @return string
     339                 */
     340                protected function _parse_readme() {
     341
     342                                // Normalize white-space
     343                                $file_contents = str_replace( array( "\r\n", "\r" ), "\n", $this->_readme_contents );
     344                                $this->_readme_contents = trim( $this->_readme_contents );
     345
     346                                // Remove UTF-8 byte-order-mark
     347                                if ( 0 === strpos( $this->_readme_contents, "\xEF\xBB\xBF" ) )
     348                                                $this->_readme_contents = substr( $this->_readme_contents, 3 );
     349
     350                                // === Plugin Name ===
     351                                // Must be the very first thing.                               
     352                                if ( !preg_match( '/^===(.*)===/', $this->_readme_contents, $_name ) )
     353                                                return array(); // require a name
     354                                $name = trim( $_name[1], '=' );
     355                                $name = $this->_sanitize_text( $name );
     356                                $this->_readme_contents = str_replace( $_name[0], '', $this->_readme_contents );
     357                               
     358                                // Extract the "key: value" records to the local scope
     359                                extract( $this->_extract_records() );
     360                               
     361                                // Extract the sections (e.g. == HEADING == .... text ...)
     362                                $sections = $this->_exract_sections();
     363                                extract( $sections );
     364                               
     365                                // Compile the final array
     366                                return array(
     367                                                'name'                    => isset( $name )                       ? $name                         : '',
     368                                                'tags'                    => isset( $tags )                       ? $tags                         : array(),
     369                                                'requires_at_least' => isset( $requires_at_least ) ? $requires_at_least : '',
     370                                                'tested_up_to'    => isset( $tested_up_to )       ? $tested_up_to         : '',
     371                                                'stable_tag'            => isset( $stable_tag )         ? $stable_tag           : 'trunk',
     372                                                'contributors'    => isset( $contributors )       ? $contributors         : array(),
     373                                                'donate_link'      => isset( $donate_link )        ? $donate_link          : '',
     374                                                'short_description' => $this->_short_description,
     375                                                'screenshots'      => $this->_screenshots,
     376                                                'is_excerpt'            => $this->_is_excerpt,
     377                                                'is_truncated'    => $this->_is_truncated,
     378                                                'sections'                => $sections,
     379                                                'remaining_content' => $this->_remaining_content,
     380                                                'upgrade_notice'        => $this->_upgrade_notice
     381                                );
     382                }
     383
     384                /**
     385                 * Make the text safe for use in a browser
     386                 * @param string $text
     387                 * @return string
     388                 */
     389                private function _sanitize_text( $text ) {
     390                                $text = strip_tags( $text );
     391                                $text = esc_html( $text );
     392                                $text = trim( $text );
     393                                return $text;
     394                }
     395
     396                /**
     397                 * Format text.  Use markdown, with some extra code -> backtick formatting.
     398                 * @param type $text
     399                 * @return type
     400                 */
     401                private function _markdown( $text ) {
     402                                $text = trim( $text );
     403                                $text = $this->_convert_code( $text );
     404                                $text = Markdown( $text );                       
     405                                $text = balanceTags( $text );
     406                                $text = wp_kses( $text, $this->_allowed_tags );
     407                                $text = trim( $text );
     408                                return $text;
     409                }
     410
     411                /**
     412                 * First take any user formatted code blocks and turn them into backticks so that
     413                 * markdown will preserve things like underscores in code blocks
     414                 * @param type $text
     415                 * @param type $markdown
     416                 * @return type
     417                 */
     418                private function _convert_code( $text ) {
     419                                $text = preg_replace_callback( '/(<pre><code>|<code>)(.*?)(<\/code>\/pre>|<\/code>)/s', array( $this, '_decodeit' ), $text );
     420                                $text = str_replace( array("\r\n", "\r"), "\n", $text );
     421                                // Markdown seems to be choking on block level stuff too.  Let's just encode it and be done with it.
     422                                $text = preg_replace_callback( '/(^|\n)`(.*?)`/s', array( $this, '_encodeit' ), $text );
     423                                return $text;
     424                }
     425               
     426                /**
     427                 * Encode Markdown to HTML
     428                 * @param array $matches Output from preg_replace_callback
     429                 * @return string
     430                 */
     431                private function _encodeit( $matches ) {
     432                                $text = trim( $matches[2] );
     433                                $text = htmlspecialchars( $text, ENT_QUOTES );
     434                                $text = str_replace( array( "\r\n", "\r" ), "\n", $text );
     435                                $text = preg_replace( "|\n\n\n+|", "\n\n", $text );
     436                                $text = str_replace(
     437                                                array( '&amp;lt;', '&amp;&gt; ' ),
     438                                                array( '&lt;'   , '&gt;'           ),
     439                                                $text
     440                                );
     441                                $text = "<code>$text</code>";
     442                                if ( "`" != $matches[1] )
     443                                                $text = "<pre>$text</pre>";
     444                                return $text;
     445                }
     446
     447                /**
     448                 * De-code HTML, turn it back into markdown
     449                 * @param array $matches Output from preg_replace_callback
     450                 * @return string
     451                 */
     452                private function _decodeit( $matches ) {
     453                                $text = $matches[2];
     454                                $text = html_entity_decode( $text );
     455                                $text = str_replace(
     456                                                array( '<br />', '&#38;', '&#39;'),
     457                                                array( ''         , '&' , "'"   ),
     458                                                $text
     459                                );                       
     460                                if ( '<pre><code>' == $matches[1] )
     461                                                $text = "\n$text\n";
     462                                return "`$text`";
     463                }
     464}