WordPress.org

Making WordPress.org

Ticket #2968: 2968.4.diff

File 2968.4.diff, 20.8 KB (added by rmccue, 21 months ago)

Wrap REST API class include in class_exists

  • wordpress.org/public_html/wp-content/plugins/wporg-markdown/README.md

     
     1# WPORG Markdown Importer
     2
     3Imports Markdown from a remote site (like GitHub) into WordPress as pages.
     4
     5## Configuration
     6
     7Each importer needs to override the abstract methods:
     8
     9* `get_base()` - Base URL for imported pages. This will be stripped from the key before comparing.
     10* `get_manifest_url()` - URL pointing to the manifest.
     11* `get_post_type()` - Post type to import as.
     12
     13## Manifest Format
     14
     15The manifest should be a JSON object, with the keys set to the desired permalink (excluding the base path). Each item should also be a JSON object, containing the following keys:
     16
     17* `slug` - Post name to insert. (Must match the final path-part of the key.)
     18* `markdown_source` - URL for the Markdown file to parse into content.
     19* `parent` - Key for the parent to store under. (Must correspond to the non-final path-parts of the key.)
     20* `title` - Title to use when creating post. Used temporarily, will be updated from the Markdown file. If not specified, defaults to `slug` (but will be updated from Markdown source).
     21
     22**Note:** The Handbook index should have the slug `index`.
     23
     24Example:
     25
     26```json
     27{
     28        "foo": {
     29                "title": "Temporary Foo Title",
     30                "slug": "foo",
     31                "markdown_source": "https://raw.githubusercontent.com/WordPress/doc-repo/master/foo.md",
     32                "parent": null
     33        },
     34        "foo/bar": {
     35                "title": "Temporary Bar Title",
     36                "slug": "bar",
     37                "markdown_source": "https://raw.githubusercontent.com/WordPress/doc-repo/master/foo/bar.md",
     38                "parent": "foo"
     39        },
     40        "foo/bar/quux": {
     41                "title": "Temporary Quux Title",
     42                "slug": "quux",
     43                "markdown_source": "https://raw.githubusercontent.com/WordPress/doc-repo/master/foo/bar/quux.md",
     44                "parent": "foo/bar"
     45        }
     46}
     47```
  • wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-editor.php

     
     1<?php
     2
     3namespace WordPressdotorg\Markdown;
     4
     5use WP_Post;
     6
     7class Editor {
     8        public function __construct( Importer $importer ) {
     9                $this->importer = $importer;
     10        }
     11
     12        public function init() {
     13                add_filter( 'the_title', array( $this, 'filter_the_title_edit_link' ), 10, 2 );
     14                add_filter( 'get_edit_post_link', array( $this, 'redirect_edit_link_to_github' ), 10, 3 );
     15                add_filter( 'o2_filter_post_actions', array( $this, 'redirect_o2_edit_link_to_github' ), 11, 2 );
     16                add_action( 'wp_head', array( $this, 'render_edit_button_style' ) );
     17                add_action( 'edit_form_top', array( $this, 'render_editor_warning' ) );
     18        }
     19
     20        public function render_edit_button_style() {
     21                ?>
     22                <style>
     23                        a.github-edit {
     24                                margin-left: .5em;
     25                                font-size: .5em;
     26                                vertical-align: top;
     27                                display: inline-block;
     28                                border: 1px solid #eeeeee;
     29                                border-radius: 2px;
     30                                background: #eeeeee;
     31                                padding: .5em .6em .4em;
     32                                color: black;
     33                                margin-top: 0.1em;
     34                        }
     35                        a.github-edit > * {
     36                                opacity: 0.6;
     37                        }
     38                        a.github-edit:hover > * {
     39                                opacity: 1;
     40                                color: black;
     41                        }
     42                        a.github-edit img {
     43                                height: .8em;
     44                        }
     45                </style>
     46                <?php
     47        }
     48
     49        /**
     50         * Render a warning for editors accessing the edit page via the admin.
     51         *
     52         * @param WP_Post $post Post being edited.
     53         */
     54        public function render_editor_warning( WP_Post $post ) {
     55                if ( $post->post_type !== $this->importer->get_post_type() ) {
     56                        return;
     57                }
     58
     59                printf(
     60                        '<div class="notice notice-warning"><p>%s</p><p><a href="%s">%s</a></p></div>',
     61                        'This page is maintained on GitHub. Content, title, and slug edits here will be discarded on next sync.',
     62                        $this->get_markdown_edit_link( $post->ID ),
     63                        'Edit on GitHub'
     64                );
     65        }
     66
     67        /**
     68         * Append a "Edit on GitHub" link to Handbook document titles
     69         */
     70        public function filter_the_title_edit_link( $title, $id = null ) {
     71                // Only apply to the main title for the document
     72                if ( ! is_singular( $this->importer->get_post_type() )
     73                        || ! is_main_query()
     74                        || ! in_the_loop()
     75                        || $id !== get_queried_object_id() ) {
     76                        return $title;
     77                }
     78
     79                $markdown_source = $this->get_markdown_edit_link( get_the_ID() );
     80                if ( ! $markdown_source ) {
     81                        return $title;
     82                }
     83
     84                $src = plugins_url( 'assets/images/github-mark.svg', dirname( dirname( __DIR__ ) ) . '/wporg-cli/wporg-cli.php' );
     85
     86                return $title . ' <a class="github-edit" href="' . esc_url( $markdown_source ) . '"><img src="' . esc_url( $src ) . '"> <span>Edit</span></a>';
     87        }
     88
     89        /**
     90         * WP-CLI Handbook pages are maintained in the GitHub repo, so the edit
     91         * link should ridirect to there.
     92         */
     93        public function redirect_edit_link_to_github( $link, $post_id, $context ) {
     94                if ( is_admin() ) {
     95                        return $link;
     96                }
     97                $post = get_post( $post_id );
     98                if ( ! $post ) {
     99                        return $link;
     100                }
     101
     102                if ( $this->importer->get_post_type() !== $post->post_type ) {
     103                        return $link;
     104                }
     105
     106                $markdown_source = $this->get_markdown_edit_link( $post_id );
     107                if ( ! $markdown_source ) {
     108                        return $link;
     109                }
     110
     111                if ( 'display' === $context ) {
     112                        $markdown_source = esc_url( $markdown_source );
     113                }
     114
     115                return $markdown_source;
     116        }
     117
     118        /**
     119         * o2 does inline editing, so we also need to remove the class name that it looks for.
     120         *
     121         * o2 obeys the edit_post capability for displaying the edit link, so we also need to manually
     122         * add the edit link if it isn't there - it always redirects to GitHub, so it doesn't need to
     123         * obey the edit_post capability in this instance.
     124         */
     125        public static function redirect_o2_edit_link_to_github( $actions, $post_id ) {
     126                $post = get_post( $post_id );
     127                if ( ! $post ) {
     128                        return $actions;
     129                }
     130
     131                if ( $this->importer->get_post_type() !== $post->post_type ) {
     132                        return $actions;
     133                }
     134
     135                $markdown_source = $this->get_markdown_edit_link( $post_id );
     136                if ( ! $markdown_source ) {
     137                        return $actions;
     138                }
     139
     140                /*
     141                 * Define our own edit post action for o2.
     142                 *
     143                 * Notable differences from the original are:
     144                 * - the 'href' parameter always goes to the GitHub source.
     145                 * - the 'o2-edit' class is missing, so inline editing is disabled.
     146                 */
     147                $edit_action = array(
     148                        'action' => 'edit',
     149                        'href' => $markdown_source,
     150                        'classes' => array( 'edit-post-link' ),
     151                        'rel' => $post_id,
     152                        'initialState' => 'default'
     153                );
     154
     155                // Find and replace the existing edit action.
     156                $replaced = false;
     157                foreach( $actions as &$action ) {
     158                        if ( 'edit' === $action['action'] ) {
     159                                $action = $edit_action;
     160                                $replaced = true;
     161                                break;
     162                        }
     163                }
     164                unset( $action );
     165
     166                // If there was no edit action replaced, add it in manually.
     167                if ( ! $replaced ) {
     168                        $actions[30] = $edit_action;
     169                }
     170
     171                return $actions;
     172        }
     173
     174        protected function get_markdown_edit_link( $post_id ) {
     175                $markdown_source = $this->importer->get_markdown_source( $post_id );
     176                if ( is_wp_error( $markdown_source ) ) {
     177                        return '';
     178                }
     179                if ( 'github.com' !== parse_url( $markdown_source, PHP_URL_HOST )
     180                        || false !== stripos( $markdown_source, '/edit/master/' ) ) {
     181                        return $markdown_source;
     182                }
     183                $markdown_source = str_replace( '/blob/master/', '/edit/master/', $markdown_source );
     184                return $markdown_source;
     185        }
     186}
  • wordpress.org/public_html/wp-content/plugins/wporg-markdown/inc/class-importer.php

     
     1<?php
     2
     3namespace WordPressdotorg\Markdown;
     4
     5use WP_CLI;
     6use WP_Error;
     7use WP_Post;
     8use WP_Query;
     9use WPCom_GHF_Markdown_Parser;
     10
     11abstract class Importer {
     12        /**
     13         * Meta key to store source in.
     14         *
     15         * @var string
     16         */
     17        protected $meta_key = 'wporg_markdown_source';
     18
     19        /**
     20         * Meta key to store request ETag in.
     21         *
     22         * @var string
     23         */
     24        protected $etag_meta_key = 'wporg_markdown_etag';
     25
     26        /**
     27         * Posts per page to query for.
     28         *
     29         * This needs to be set at least as high as the number of pages being
     30         * imported, but should not be unbounded (-1).
     31         *
     32         * @var int
     33         */
     34        protected $posts_per_page = 350;
     35
     36        /**
     37         * Get base URL for all pages.
     38         *
     39         * This is used for generating the keys for the existing pages.
     40         *
     41         * @see static::get_existing_for_post()
     42         *
     43         * @return string Base URL to strip from page permalink.
     44         */
     45        abstract protected function get_base();
     46
     47        /**
     48         * Get manifest URL.
     49         *
     50         * This URL should point to a JSON file containing the manifest for the
     51         * site's content. (Typically raw.githubusercontent.com)
     52         *
     53         * @return string URL for the manifest file.
     54         */
     55        abstract protected function get_manifest_url();
     56
     57        /**
     58         * Get post type for the type being imported.
     59         *
     60         * @return string Post type slug to import as.
     61         */
     62        abstract public function get_post_type();
     63
     64        /**
     65         * Get existing data for a given post.
     66         *
     67         * @param WP_Post $post Post to get existing data for.
     68         * @return array 2-tuple of array key and data.
     69         */
     70        protected function get_existing_for_post( WP_Post $post ) {
     71                $key = rtrim( str_replace( $this->get_base(), '', get_permalink( $post->ID ) ), '/' );
     72                if ( empty( $key ) ) {
     73                        $key = 'index';
     74                }
     75
     76                $data = array(
     77                        'post_id' => $post->ID,
     78                );
     79                return array( $key, $data );
     80        }
     81
     82        /**
     83         * Import the manifest.
     84         *
     85         * Fetches the manifest, parses, and creates pages as needed.
     86         */
     87        public function import_manifest() {
     88                $response = wp_remote_get( $this->get_manifest_url() );
     89                if ( is_wp_error( $response ) ) {
     90                        if ( class_exists( 'WP_CLI' ) ) {
     91                                WP_CLI::error( $response->get_error_message() );
     92                        }
     93                        return $response;
     94                } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
     95                        if ( class_exists( 'WP_CLI' ) ) {
     96                                WP_CLI::error( 'Non-200 from Markdown source' );
     97                        }
     98                        return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
     99                }
     100                $manifest = json_decode( wp_remote_retrieve_body( $response ), true );
     101                if ( ! $manifest ) {
     102                        if ( class_exists( 'WP_CLI' ) ) {
     103                                WP_CLI::error( 'Invalid manifest' );
     104                        }
     105                        return new WP_Error( 'invalid-manifest', 'Manifest did not unfurl properly.' );;
     106                }
     107                // Fetch all handbook posts for comparison
     108                $q = new WP_Query( array(
     109                        'post_type'      => $this->get_post_type(),
     110                        'post_status'    => 'publish',
     111                        'posts_per_page' => $this->posts_per_page,
     112                ) );
     113                $existing = array();
     114                foreach ( $q->posts as $post ) {
     115                        list( $key, $data ) = $this->get_existing_for_post( $post );
     116                        $existing[ $key ] = $data;
     117                }
     118                $created = $updated = 0;
     119                foreach ( $manifest as $key => $doc ) {
     120                        // Already exists, update.
     121                        if ( ! empty( $existing[ $key ] ) ) {
     122                                $existing_id = $existing[ $key ]['post_id'];
     123                                if ( $this->update_post_from_manifest_doc( $existing_id, $doc ) ) {
     124                                        $updated++;
     125                                }
     126
     127                                continue;
     128                        }
     129                        if ( $this->process_manifest_doc( $doc, $existing, $manifest ) ) {
     130                                $created++;
     131                        }
     132                }
     133                if ( class_exists( 'WP_CLI' ) ) {
     134                        WP_CLI::success( "Successfully created {$created} and updated {$updated} handbook pages." );
     135                }
     136        }
     137
     138        /**
     139         * Process a document from the manifest.
     140         *
     141         * @param array $doc Document to process.
     142         * @param array $existing List of existing posts, will be added to.
     143         * @param array $manifest Manifest data.
     144         * @return boolean True if processing succeeded, false otherwise.
     145         */
     146        protected function process_manifest_doc( $doc, &$existing, $manifest ) {
     147                $post_parent = null;
     148                if ( ! empty( $doc['parent'] ) ) {
     149                        // Find the parent in the existing set
     150                        if ( empty( $existing[ $doc['parent'] ] ) ) {
     151                                if ( ! $this->process_manifest_doc( $manifest[ $doc['parent'] ], $existing, $manifest ) ) {
     152                                        return false;
     153                                }
     154                        }
     155                        if ( ! empty( $existing[ $doc['parent'] ] ) ) {
     156                                $parent = $existing[ $doc['parent'] ];
     157                                $post_parent = $parent['post_id'];
     158                        }
     159                }
     160                $post = $this->create_post_from_manifest_doc( $doc, $post_parent );
     161                if ( $post ) {
     162                        list( $key, $data ) = $this->get_existing_for_post( $post );
     163                        $existing[ $key ] = $data;
     164                        return true;
     165                }
     166                return false;
     167        }
     168
     169        /**
     170         * Create a new handbook page from the manifest document
     171         */
     172        protected function create_post_from_manifest_doc( $doc, $post_parent = null ) {
     173                if ( $doc['slug'] === 'index' ) {
     174                        $doc['slug'] = $this->get_post_type();
     175                }
     176                $post_data = array(
     177                        'post_type'   => $this->get_post_type(),
     178                        'post_status' => 'publish',
     179                        'post_parent' => $post_parent,
     180                        'post_title'  => wp_slash( $doc['slug'] ),
     181                        'post_name'   => sanitize_title_with_dashes( $doc['slug'] ),
     182                );
     183                if ( isset( $doc['title'] ) ) {
     184                        $doc['post_title'] = sanitize_text_field( wp_slash( $doc['title'] ) );
     185                }
     186                $post_id = wp_insert_post( $post_data );
     187                if ( ! $post_id ) {
     188                        return false;
     189                }
     190                if ( class_exists( 'WP_CLI' ) ) {
     191                        WP_CLI::log( "Created post {$post_id} for {$doc['slug']}." );
     192                }
     193                update_post_meta( $post_id, $this->meta_key, esc_url_raw( $doc['markdown_source'] ) );
     194                return get_post( $post_id );
     195        }
     196
     197        /**
     198         * Update an existing post from the manifest.
     199         *
     200         * @param int $post_id Existing post ID.
     201         * @param array $doc Document details from the manifest.
     202         * @return boolean True if updated, false otherwise.
     203         */
     204        protected function update_post_from_manifest_doc( $post_id, $doc ) {
     205                $did_update = update_post_meta( $post_id, $this->meta_key, esc_url_raw( $doc['markdown_source'] ) );
     206                return $did_update;
     207        }
     208
     209        /**
     210         * Update existing posts from Markdown source.
     211         *
     212         * Reparses the Markdown for every page.
     213         */
     214        public function import_all_markdown() {
     215                $q = new WP_Query( array(
     216                        'post_type'      => $this->get_post_type(),
     217                        'post_status'    => 'publish',
     218                        'fields'         => 'ids',
     219                        'posts_per_page' => $this->posts_per_page,
     220                ) );
     221                $ids = $q->posts;
     222                $success = 0;
     223                foreach( $ids as $id ) {
     224                        $ret = $this->update_post_from_markdown_source( $id );
     225                        if ( class_exists( 'WP_CLI' ) ) {
     226                                if ( is_wp_error( $ret ) ) {
     227                                        WP_CLI::warning( $ret->get_error_message() );
     228                                } elseif ( false === $ret ) {
     229                                        WP_CLI::log( "No updates for {$id}" );
     230                                        $success++;
     231                                } else {
     232                                        WP_CLI::log( "Updated {$id} from markdown source" );
     233                                        $success++;
     234                                }
     235                        }
     236                }
     237                if ( class_exists( 'WP_CLI' ) ) {
     238                        $total = count( $ids );
     239                        WP_CLI::success( "Successfully updated {$success} of {$total} pages." );
     240                }
     241        }
     242
     243        /**
     244         * Update a post from its Markdown source.
     245         *
     246         * @param int $post_id Post ID to update.
     247         * @return boolean|WP_Error True if updated, false if no update needed, error otherwise.
     248         */
     249        protected function update_post_from_markdown_source( $post_id ) {
     250                $markdown_source = $this->get_markdown_source( $post_id );
     251                if ( is_wp_error( $markdown_source ) ) {
     252                        return $markdown_source;
     253                }
     254                if ( ! function_exists( 'jetpack_require_lib' ) ) {
     255                        return new WP_Error( 'missing-jetpack-require-lib', 'jetpack_require_lib() is missing on system.' );
     256                }
     257
     258                // Transform GitHub repo HTML pages into their raw equivalents
     259                $markdown_source = preg_replace( '#https?://github\.com/([^/]+/[^/]+)/blob/(.+)#', 'https://raw.githubusercontent.com/$1/$2', $markdown_source );
     260                $markdown_source = add_query_arg( 'v', time(), $markdown_source );
     261
     262                // Grab the stored ETag, and use it to deduplicate.
     263                $args = array(
     264                        'headers' => array(),
     265                );
     266                $last_etag = get_post_meta( $post_id, $this->etag_meta_key, true );
     267                if ( ! empty( $last_etag ) ) {
     268                        $args['headers']['If-None-Match'] = $last_etag;
     269                }
     270
     271                $response = wp_remote_get( $markdown_source, $args );
     272                if ( is_wp_error( $response ) ) {
     273                        return $response;
     274                } elseif ( 304 === wp_remote_retrieve_response_code( $response ) ) {
     275                        // No update required!
     276                        return false;
     277                } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
     278                        return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
     279                }
     280
     281                $etag = wp_remote_retrieve_header( $response, 'etag' );
     282
     283                $markdown = wp_remote_retrieve_body( $response );
     284                // Strip YAML doc from the header
     285                $markdown = preg_replace( '#^---(.+)---#Us', '', $markdown );
     286
     287                $title = null;
     288                if ( preg_match( '/^#\s(.+)/', $markdown, $matches ) ) {
     289                        $title = $matches[1];
     290                        $markdown = preg_replace( '/^#\swp\s(.+)/', '', $markdown );
     291                }
     292                $markdown = trim( $markdown );
     293
     294                // Steal the first sentence as the excerpt
     295                $excerpt = '';
     296                if ( preg_match( '/^(.+)/', $markdown, $matches ) ) {
     297                        $excerpt = $matches[1];
     298                        $markdown = preg_replace( '/^(.+)/', '', $markdown );
     299                }
     300
     301                // Transform to HTML and save the post
     302                jetpack_require_lib( 'markdown' );
     303                $parser = new WPCom_GHF_Markdown_Parser();
     304                $parser->preserve_shortcodes = false;
     305                $html = $parser->transform( $markdown );
     306                $post_data = array(
     307                        'ID'           => $post_id,
     308                        'post_content' => wp_filter_post_kses( wp_slash( $html ) ),
     309                        'post_excerpt' => sanitize_text_field( wp_slash( $excerpt ) ),
     310                );
     311                if ( ! is_null( $title ) ) {
     312                        $post_data['post_title'] = sanitize_text_field( wp_slash( $title ) );
     313                }
     314                wp_update_post( $post_data );
     315
     316                // Set ETag for future updates.
     317                update_post_meta( $post_id, $this->etag_meta_key, wp_slash( $etag ) );
     318
     319                return true;
     320        }
     321
     322        /**
     323         * Retrieve the markdown source URL for a given post.
     324         */
     325        public function get_markdown_source( $post_id ) {
     326                $markdown_source = get_post_meta( $post_id, $this->meta_key, true );
     327                if ( ! $markdown_source ) {
     328                        return new WP_Error( 'missing-markdown-source', 'Markdown source is missing for post.' );
     329                }
     330
     331                return $markdown_source;
     332        }
     333}
  • wordpress.org/public_html/wp-content/plugins/wporg-markdown/plugin.php

     
     1<?php
     2/**
     3 * Plugin Name: WPORG Markdown Importer
     4 * Description: Automatic Markdown imports for handbooks and DevHub (CLI/API handbooks)
     5 * Author: Daniel Bachhuber and Ryan McCue
     6 */
     7
     8require __DIR__ . '/inc/class-editor.php';
     9require __DIR__ . '/inc/class-importer.php';
  • wordpress.org/public_html/wp-content/themes/pub/wporg-developer/functions.php

     
    6161require __DIR__ . '/inc/cli.php';
    6262
    6363/**
     64 * REST API handbook.
     65 */
     66if ( class_exists( '\\WordPressdotorg\\Markdown\\Importer' ) ) {
     67        require __DIR__ . '/inc/rest-api.php';
     68}
     69
     70/**
    6471 * Explanations for functions. hooks, classes, and methods.
    6572 */
    6673require( __DIR__ . '/inc/explanations.php' );
  • wordpress.org/public_html/wp-content/themes/pub/wporg-developer/inc/rest-api.php

     
     1<?php
     2
     3use WordPressdotorg\Markdown\Editor;
     4use WordPressdotorg\Markdown\Importer;
     5
     6class DevHub_REST_API extends Importer {
     7        /**
     8         * Singleton instance.
     9         *
     10         * @var static
     11         */
     12        protected static $instance;
     13
     14        /**
     15         * Get the singleton instance, or create if needed.
     16         *
     17         * @return static
     18         */
     19        public static function instance() {
     20                if ( empty( static::$instance ) ) {
     21                        static::$instance = new static();
     22                }
     23
     24                return static::$instance;
     25        }
     26
     27        protected function get_base() {
     28                return home_url( 'rest-api/' );
     29        }
     30
     31        protected function get_manifest_url() {
     32                return 'https://raw.githubusercontent.com/WP-API/docs/master/bin/manifest.json';
     33        }
     34
     35        public function get_post_type() {
     36                return 'rest-api-handbook';
     37        }
     38
     39        public function init() {
     40                add_action( 'init', array( $this, 'register_cron_jobs' ) );
     41                add_action( 'restapi_import_manifest', array( $this, 'import_manifest' ) );
     42                add_action( 'restapi_import_all_markdown', array( $this, 'import_all_markdown' ) );
     43
     44                $editor = new Editor( $this );
     45                $editor->init();
     46        }
     47
     48        public function register_cron_jobs() {
     49                if ( ! wp_next_scheduled( 'restapi_import_manifest' ) ) {
     50                        wp_schedule_event( time(), '15_minutes', 'restapi_import_manifest' );
     51                }
     52                if ( ! wp_next_scheduled( 'restapi_import_all_markdown' ) ) {
     53                        wp_schedule_event( time(), '15_minutes', 'restapi_import_all_markdown' );
     54                }
     55        }
     56}
     57
     58DevHub_REST_API::instance()->init();
     59