Making WordPress.org

Ticket #5247: hosting-handbook-importer.diff

File hosting-handbook-importer.diff, 18.3 KB (added by mikeschroder, 3 years ago)

First pass -- Hosting Handbook importer plugin.

  • new file wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/assets/images/github-mark.svg

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/assets/images/github-mark.svg wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/assets/images/github-mark.svg
    new file mode 100644
    index 000000000..99b0878c1
    - +  
     1<svg height="1024" width="1024" xmlns="http://www.w3.org/2000/svg">
     2  <path class="github-mark" d="M512 0C229.25 0 0 229.25 0 512c0 226.25 146.688 418.125 350.156 485.812 25.594 4.688 34.938-11.125 34.938-24.625 0-12.188-0.469-52.562-0.719-95.312C242 908.812 211.906 817.5 211.906 817.5c-23.312-59.125-56.844-74.875-56.844-74.875-46.531-31.75 3.53-31.125 3.53-31.125 51.406 3.562 78.47 52.75 78.47 52.75 45.688 78.25 119.875 55.625 149 42.5 4.654-33 17.904-55.625 32.5-68.375C304.906 725.438 185.344 681.5 185.344 485.312c0-55.938 19.969-101.562 52.656-137.406-5.219-13-22.844-65.094 5.062-135.562 0 0 42.938-13.75 140.812 52.5 40.812-11.406 84.594-17.031 128.125-17.219 43.5 0.188 87.312 5.875 128.188 17.281 97.688-66.312 140.688-52.5 140.688-52.5 28 70.531 10.375 122.562 5.125 135.5 32.812 35.844 52.625 81.469 52.625 137.406 0 196.688-119.75 240-233.812 252.688 18.438 15.875 34.75 47 34.75 94.75 0 68.438-0.688 123.625-0.688 140.5 0 13.625 9.312 29.562 35.25 24.562C877.438 930 1024 738.125 1024 512 1024 229.25 794.75 0 512 0z" />
     3</svg>
  • new file wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/inc/class-handbook.php

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/inc/class-handbook.php wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/inc/class-handbook.php
    new file mode 100644
    index 000000000..3884ff220
    - +  
     1<?php
     2
     3namespace WPOrg_Hosting_Handbook;
     4
     5class Handbook {
     6
     7        /**
     8         * Append a "Edit on GitHub" link to Handbook document titles
     9         */
     10        public static function filter_the_title_edit_link( $title, $id = null ) {
     11                // Only apply to the main title for the document
     12                if ( ! is_singular( 'handbook' )
     13                        || ! is_main_query()
     14                        || ! in_the_loop()
     15                        || is_embed()
     16                        || $id !== get_queried_object_id() ) {
     17                        return $title;
     18                }
     19
     20                $markdown_source = self::get_markdown_edit_link( get_the_ID() );
     21                if ( ! $markdown_source ) {
     22                        return $title;
     23                }
     24
     25                return $title . ' <a class="github-edit" href="' . esc_url( $markdown_source ) . '"><img src="' . esc_url( plugins_url( 'assets/images/github-mark.svg', dirname( __FILE__ ) ) ) . '"> <span>Edit</span></a>';
     26        }
     27
     28        /**
     29         * Hosting Handbook pages are maintained in the GitHub repo, so the edit
     30         * link should ridirect to there.
     31         */
     32        public static function redirect_edit_link_to_github( $link, $post_id, $context ) {
     33                if ( is_admin() ) {
     34                        return $link;
     35                }
     36                $post = get_post( $post_id );
     37                if ( ! $post ) {
     38                        return $link;
     39                }
     40
     41                if ( 'handbook' !== $post->post_type ) {
     42                        return $link;
     43                }
     44
     45                $markdown_source = self::get_markdown_edit_link( $post_id );
     46                if ( ! $markdown_source ) {
     47                        return $link;
     48                }
     49
     50                if ( 'display' === $context ) {
     51                        $markdown_source = esc_url( $markdown_source );
     52                }
     53
     54                return $markdown_source;
     55        }
     56
     57        /**
     58         * o2 does inline editing, so we also need to remove the class name that it looks for.
     59         *
     60         * o2 obeys the edit_post capability for displaying the edit link, so we also need to manually
     61         * add the edit link if it isn't there - it always redirects to GitHub, so it doesn't need to
     62         * obey the edit_post capability in this instance.
     63         */
     64        public static function redirect_o2_edit_link_to_github( $actions, $post_id ) {
     65                $post = get_post( $post_id );
     66                if ( ! $post ) {
     67                        return $actions;
     68                }
     69
     70                if ( 'handbook' !== $post->post_type ) {
     71                        return $actions;
     72                }
     73
     74                $markdown_source = self::get_markdown_edit_link( $post_id );
     75                if ( ! $markdown_source ) {
     76                        return $actions;
     77                }
     78
     79                /*
     80                 * Define our own edit post action for o2.
     81                 *
     82                 * Notable differences from the original are:
     83                 * - the 'href' parameter always goes to the GitHub source.
     84                 * - the 'o2-edit' class is missing, so inline editing is disabled.
     85                 */
     86                $edit_action = array(
     87                        'action' => 'edit',
     88                        'href' => $markdown_source,
     89                        'classes' => array( 'edit-post-link' ),
     90                        'rel' => $post_id,
     91                        'initialState' => 'default'
     92                );
     93
     94                // Find and replace the existing edit action.
     95                $replaced = false;
     96                foreach( $actions as &$action ) {
     97                        if ( 'edit' === $action['action'] ) {
     98                                $action = $edit_action;
     99                                $replaced = true;
     100                                break;
     101                        }
     102                }
     103                unset( $action );
     104
     105                // If there was no edit action replaced, add it in manually.
     106                if ( ! $replaced ) {
     107                        $actions[30] = $edit_action;
     108                }
     109
     110                return $actions;
     111        }
     112
     113        private static function get_markdown_edit_link( $post_id ) {
     114                $markdown_source = Markdown_Import::get_markdown_source( $post_id );
     115                if ( is_wp_error( $markdown_source ) ) {
     116                        return '';
     117                }
     118                if ( 'github.com' !== parse_url( $markdown_source, PHP_URL_HOST )
     119                        || false !== stripos( $markdown_source, '/edit/master/' ) ) {
     120                        return $markdown_source;
     121                }
     122                $markdown_source = str_replace( '/blob/master/', '/edit/master/', $markdown_source );
     123                return $markdown_source;
     124        }
     125}
  • new file wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/inc/class-markdown-import.php

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/inc/class-markdown-import.php wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/inc/class-markdown-import.php
    new file mode 100644
    index 000000000..59287b5c1
    - +  
     1<?php
     2
     3namespace WPOrg_Hosting_Handbook;
     4
     5use WP_Error;
     6use WP_Query;
     7
     8class Markdown_Import {
     9
     10        private static $handbook_manifest = 'https://raw.githubusercontent.com/wordpress/hosting-handbook/master/bin/handbook-manifest.json';
     11        private static $input_name = 'wporg-hosting-handbook-markdown-source';
     12        private static $meta_key = 'wporg-hosting-handbook-markdown-source';
     13        private static $nonce_name = 'wporg-hosting-handbook-markdown-source-nonce';
     14        private static $submit_name = 'wporg-hosting-handbook-markdown-import';
     15        private static $supported_post_types = array( 'handbook' );
     16        private static $posts_per_page = 100;
     17
     18        /**
     19         * Register our cron task if it doesn't already exist
     20         */
     21        public static function action_init() {
     22                if ( ! wp_next_scheduled( 'wporg_hosting_handbook_manifest_import' ) ) {
     23                        wp_schedule_event( time(), '15_minutes', 'wporg_hosting_handbook_manifest_import' );
     24                }
     25                if ( ! wp_next_scheduled( 'wporg_hosting_handbook_markdown_import' ) ) {
     26                        wp_schedule_event( time(), '15_minutes', 'wporg_hosting_handbook_markdown_import' );
     27                }
     28        }
     29
     30        public static function action_wporg_hosting_handbook_manifest_import() {
     31                $response = wp_remote_get( self::$handbook_manifest );
     32                if ( is_wp_error( $response ) ) {
     33                        return $response;
     34                } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
     35                        return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
     36                }
     37                $manifest = json_decode( wp_remote_retrieve_body( $response ), true );
     38                if ( ! $manifest ) {
     39                        return new WP_Error( 'invalid-manifest', 'Manifest did not unfurl properly.' );;
     40                }
     41                // Fetch all handbook posts for comparison
     42                $q = new WP_Query( array(
     43                        'post_type'      => self::$supported_post_types,
     44                        'post_status'    => 'publish',
     45                        'posts_per_page' => self::$posts_per_page,
     46                ) );
     47                $existing = $q->posts;
     48                $created = 0;
     49                foreach( $manifest as $doc ) {
     50                        // Already exists
     51                        if ( wp_filter_object_list( $existing, array( 'post_name' => $doc['slug'] ) ) ) {
     52                                continue;
     53                        }
     54                        $post_parent = null;
     55                        if ( ! empty( $doc['parent'] ) ) {
     56                                // Find the parent in the existing set
     57                                $parents = wp_filter_object_list( $existing, array( 'post_name' => $doc['parent'] ) );
     58                                if ( ! empty( $parents ) ) {
     59                                        $parent = array_shift( $parents );
     60                                } else {
     61                                        // Create the parent and add it to the stack
     62                                        if ( isset( $manifest[ $doc['parent'] ] ) ) {
     63                                                $parent_doc = $manifest[ $doc['parent'] ];
     64                                                $parent = self::create_post_from_manifest_doc( $parent_doc );
     65                                                if ( $parent ) {
     66                                                        $created++;
     67                                                        $existing[] = $parent;
     68                                                } else {
     69                                                        continue;
     70                                                }
     71                                        } else {
     72                                                continue;
     73                                        }
     74                                }
     75                                $post_parent = $parent->ID;
     76                        }
     77                        $post = self::create_post_from_manifest_doc( $doc, $post_parent );
     78                        if ( $post ) {
     79                                $created++;
     80                                $existing[] = $post;
     81                        }
     82                }
     83                if ( class_exists( 'WP_CLI' ) ) {
     84                        \WP_CLI::success( "Successfully created {$created} handbook pages." );
     85                }
     86        }
     87
     88        /**
     89         * Create a new handbook page from the manifest document
     90         */
     91        private static function create_post_from_manifest_doc( $doc, $post_parent = null ) {
     92                $post_data = array(
     93                        'post_type'   => 'handbook',
     94                        'post_status' => 'publish',
     95                        'post_parent' => $post_parent,
     96                        'post_title'  => sanitize_text_field( wp_slash( $doc['title'] ) ),
     97                        'post_name'   => sanitize_title_with_dashes( $doc['slug'] ),
     98                );
     99                $post_id = wp_insert_post( $post_data );
     100                if ( ! $post_id ) {
     101                        return false;
     102                }
     103                if ( class_exists( 'WP_CLI' ) ) {
     104                        \WP_CLI::log( "Created post {$post_id} for {$doc['title']}." );
     105                }
     106                update_post_meta( $post_id, self::$meta_key, esc_url_raw( $doc['markdown_source'] ) );
     107                return get_post( $post_id );
     108        }
     109
     110        public static function action_wporg_hosting_handbook_markdown_import() {
     111                $q = new WP_Query( array(
     112                        'post_type'      => self::$supported_post_types,
     113                        'post_status'    => 'publish',
     114                        'fields'         => 'ids',
     115                        'posts_per_page' => self::$posts_per_page,
     116                ) );
     117                $ids = $q->posts;
     118                $success = 0;
     119                foreach( $ids as $id ) {
     120                        $ret = self::update_post_from_markdown_source( $id );
     121                        if ( class_exists( 'WP_CLI' ) ) {
     122                                if ( is_wp_error( $ret ) ) {
     123                                        \WP_CLI::warning( $ret->get_error_message() );
     124                                } else {
     125                                        \WP_CLI::log( "Updated {$id} from markdown source" );
     126                                        $success++;
     127                                }
     128                        }
     129                }
     130                if ( class_exists( 'WP_CLI' ) ) {
     131                        $total = count( $ids );
     132                        \WP_CLI::success( "Successfully updated {$success} of {$total} handbook pages." );
     133                }
     134        }
     135
     136        /**
     137         * Handle a request to import from the markdown source
     138         */
     139        public static function action_load_post_php() {
     140                if ( ! isset( $_GET[ self::$submit_name ] )
     141                        || ! isset( $_GET[ self::$nonce_name ] )
     142                        || ! isset( $_GET['post'] ) ) {
     143                        return;
     144                }
     145                $post_id = (int) $_GET['post'];
     146                if ( ! current_user_can( 'edit_post', $post_id )
     147                        || ! wp_verify_nonce( $_GET[ self::$nonce_name ], self::$input_name )
     148                        || ! in_array( get_post_type( $post_id ), self::$supported_post_types, true ) ) {
     149                        return;
     150                }
     151
     152                $response = self::update_post_from_markdown_source( $post_id );
     153                if ( is_wp_error( $response ) ) {
     154                        wp_die( $response->get_error_message() );
     155                }
     156
     157                wp_safe_redirect( get_edit_post_link( $post_id, 'raw' ) );
     158                exit;
     159        }
     160
     161        /**
     162         * Add an input field for specifying Markdown source
     163         */
     164        public static function action_edit_form_after_title( $post ) {
     165                if ( ! in_array( $post->post_type, self::$supported_post_types, true ) ) {
     166                        return;
     167                }
     168                $markdown_source = get_post_meta( $post->ID, self::$meta_key, true );
     169                ?>
     170                <label>Markdown source: <input
     171                        type="text"
     172                        name="<?php echo esc_attr( self::$input_name ); ?>"
     173                        value="<?php echo esc_attr( $markdown_source ); ?>"
     174                        placeholder="Enter a URL representing a markdown file to import"
     175                        size="50" />
     176                </label> <?php
     177                        if ( $markdown_source ) :
     178                                $update_link = add_query_arg( array(
     179                                        self::$submit_name => 'import',
     180                                        self::$nonce_name  => wp_create_nonce( self::$input_name ),
     181                                ), get_edit_post_link( $post->ID, 'raw' ) );
     182                                ?>
     183                                <a class="button button-small button-primary" href="<?php echo esc_url( $update_link ); ?>">Import</a>
     184                        <?php endif; ?>
     185                <?php wp_nonce_field( self::$input_name, self::$nonce_name ); ?>
     186                <?php
     187        }
     188
     189        /**
     190         * Save the Markdown source input field
     191         */
     192        public static function action_save_post( $post_id ) {
     193
     194                if ( ! isset( $_POST[ self::$input_name ] )
     195                        || ! isset( $_POST[ self::$nonce_name ] )
     196                        || ! in_array( get_post_type( $post_id ), self::$supported_post_types, true ) ) {
     197                        return;
     198                }
     199
     200                if ( ! wp_verify_nonce( $_POST[ self::$nonce_name ], self::$input_name ) ) {
     201                        return;
     202                }
     203
     204                $markdown_source = '';
     205                if ( ! empty( $_POST[ self::$input_name ] ) ) {
     206                        $markdown_source = esc_url_raw( $_POST[ self::$input_name ] );
     207                }
     208                update_post_meta( $post_id, self::$meta_key, $markdown_source );
     209        }
     210
     211        /**
     212         * Filter cron schedules to add a 15 minute schedule
     213         */
     214        public static function filter_cron_schedules( $schedules ) {
     215                $schedules['15_minutes'] = array(
     216                        'interval' => 15 * MINUTE_IN_SECONDS,
     217                        'display'  => '15 minutes'
     218                );
     219                return $schedules;
     220        }
     221
     222        /**
     223         * Update a post from its Markdown source
     224         */
     225        private static function update_post_from_markdown_source( $post_id ) {
     226                $markdown_source = self::get_markdown_source( $post_id );
     227                if ( is_wp_error( $markdown_source ) ) {
     228                        return $markdown_source;
     229                }
     230                if ( ! function_exists( 'jetpack_require_lib' ) ) {
     231                        return new WP_Error( 'missing-jetpack-require-lib', 'jetpack_require_lib() is missing on system.' );
     232                }
     233
     234                // Transform GitHub repo HTML pages into their raw equivalents
     235                $markdown_source = preg_replace( '#https?://github\.com/([^/]+/[^/]+)/blob/(.+)#', 'https://raw.githubusercontent.com/$1/$2', $markdown_source );
     236                $markdown_source = add_query_arg( 'v', time(), $markdown_source );
     237                $response = wp_remote_get( $markdown_source );
     238                if ( is_wp_error( $response ) ) {
     239                        return $response;
     240                } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
     241                        return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
     242                }
     243
     244                $markdown = wp_remote_retrieve_body( $response );
     245                // Strip YAML doc from the header
     246                $markdown = preg_replace( '#^---(.+)---#Us', '', $markdown );
     247
     248                $title = null;
     249                if ( preg_match( '/^#\s(.+)/', $markdown, $matches ) ) {
     250                        $title = $matches[1];
     251                        $markdown = preg_replace( '/^#\s(.+)/', '', $markdown );
     252                }
     253
     254                // Transform to HTML and save the post
     255                jetpack_require_lib( 'markdown' );
     256                $parser = new \WPCom_GHF_Markdown_Parser;
     257                $html = $parser->transform( $markdown );
     258                $post_data = array(
     259                        'ID'           => $post_id,
     260                        'post_content' => wp_filter_post_kses( wp_slash( $html ) ),
     261                );
     262                if ( ! is_null( $title ) ) {
     263                        $post_data['post_title'] = sanitize_text_field( wp_slash( $title ) );
     264                }
     265                wp_update_post( $post_data );
     266                return true;
     267        }
     268
     269        /**
     270         * Retrieve the markdown source URL for a given post.
     271         */
     272        public static function get_markdown_source( $post_id ) {
     273                $markdown_source = get_post_meta( $post_id, self::$meta_key, true );
     274                if ( ! $markdown_source ) {
     275                        return new WP_Error( 'missing-markdown-source', 'Markdown source is missing for post.' );
     276                }
     277
     278                return $markdown_source;
     279        }
     280}
  • new file wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/wporg-hosting-handbook.php

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/wporg-hosting-handbook.php wordpress.org/public_html/wp-content/plugins/wporg-hosting-handbook/wporg-hosting-handbook.php
    new file mode 100644
    index 000000000..b7c0762ad
    - +  
     1<?php
     2/**
     3 * Plugin name: Hosting Team Handbook: WordPress.org Customizations
     4 * Description: Provides general customizations for the Hosting Team's presence on WordPress.org
     5 * Version:     0.1.0
     6 * Author:      WordPress.org
     7 * Author URI:  http://wordpress.org/
     8 * License:     GPLv2 or later
     9 */
     10
     11require_once dirname( __FILE__ ) . '/inc/class-markdown-import.php';
     12require_once dirname( __FILE__ ) . '/inc/class-handbook.php';
     13
     14/**
     15 * Registry of actions and filters
     16 */
     17add_action( 'init', array( 'WPOrg_Hosting_Handbook\Markdown_Import', 'action_init' ) );
     18add_action( 'wporg_hosting_handbook_manifest_import', array( 'WPOrg_Hosting_Handbook\Markdown_Import', 'action_wporg_hosting_handbook_manifest_import' ) );
     19add_action( 'wporg_hosting_handbook_markdown_import', array( 'WPOrg_Hosting_Handbook\Markdown_Import', 'action_wporg_hosting_handbook_markdown_import' ) );
     20add_action( 'load-post.php', array( 'WPOrg_Hosting_Handbook\Markdown_Import', 'action_load_post_php' ) );
     21add_action( 'edit_form_after_title', array( 'WPOrg_Hosting_Handbook\Markdown_Import', 'action_edit_form_after_title' ) );
     22add_action( 'save_post', array( 'WPOrg_Hosting_Handbook\Markdown_Import', 'action_save_post' ) );
     23add_filter( 'cron_schedules', array( 'WPOrg_Hosting_Handbook\Markdown_Import', 'filter_cron_schedules' ) );
     24add_filter( 'the_title', array( 'WPOrg_Hosting_Handbook\Handbook', 'filter_the_title_edit_link' ), 10, 2 );
     25add_filter( 'get_edit_post_link', array( 'WPOrg_Hosting_Handbook\Handbook', 'redirect_edit_link_to_github' ), 10, 3 );
     26add_filter( 'o2_filter_post_actions', array( 'WPOrg_Hosting_Handbook\Handbook', 'redirect_o2_edit_link_to_github' ), 11, 2 );
     27
     28add_action( 'wp_head', function(){
     29        ?>
     30        <style>
     31                pre code {
     32                        line-height: 16px;
     33                }
     34                a.github-edit {
     35                        margin-left: .5em;
     36                        font-size: .5em;
     37                        vertical-align: top;
     38                        display: inline-block;
     39                        border: 1px solid #eeeeee;
     40                        border-radius: 2px;
     41                        background: #eeeeee;
     42                        padding: .5em .6em .4em;
     43                        color: black;
     44                        margin-top: 0.1em;
     45                }
     46                a.github-edit > * {
     47                        opacity: 0.6;
     48                }
     49                a.github-edit:hover > * {
     50                        opacity: 1;
     51                        color: black;
     52                }
     53                a.github-edit img {
     54                        height: .8em;
     55                }
     56                .single-handbook div.table-of-contents {
     57                        margin: 0;
     58                        float: none;
     59                        padding: 0;
     60                        border: none;
     61                        box-shadow: none;
     62                        width: auto;
     63                }
     64                .single-handbook div.table-of-contents:after {
     65                        content: " ";
     66                        display: block;
     67                        clear: both;
     68                }
     69                .single-handbook .table-of-contents h2 {
     70                        display: none;
     71                }
     72                .single-handbook div.table-of-contents ul {
     73                        padding: 0;
     74                        margin-top: 0.4em;
     75                        margin-bottom: 1.1em;
     76                }
     77                .single-handbook div.table-of-contents > ul li {
     78                        display: inline-block;
     79                        padding: 0;
     80                        font-size: 12px;
     81                }
     82                .single-handbook div.table-of-contents > ul li a:after {
     83                        content: "|";
     84                        display: inline-block;
     85                        width: 20px;
     86                        text-align: center;
     87                        color: #eeeeee
     88                }
     89                .single-handbook div.table-of-contents > ul li:last-child a:after {
     90                        content: "";
     91                }
     92                .single-handbook div.table-of-contents ul ul {
     93                        display: none;
     94                }
     95                .single-handbook #secondary {
     96                        max-width: 240px;
     97                }
     98        </style>
     99        <?php
     100});