| | 1 | <?php |
| | 2 | |
| | 3 | class DevHub_CLI { |
| | 4 | |
| | 5 | private static $commands_manifest = 'https://raw.githubusercontent.com/wp-cli/handbook/master/bin/commands-manifest.json'; |
| | 6 | private static $meta_key = 'wporg_cli_markdown_source'; |
| | 7 | private static $supported_post_types = array( 'command' ); |
| | 8 | private static $posts_per_page = 350; |
| | 9 | |
| | 10 | public function __construct() { |
| | 11 | $this->init(); |
| | 12 | } |
| | 13 | |
| | 14 | public function init() { |
| | 15 | add_action( 'init', array( $this, 'action_init_register_cron_jobs' ) ); |
| | 16 | add_action( 'init', array( $this, 'action_init_register_post_types' ) ); |
| | 17 | add_action( 'devhub_cli_manifest_import', array( $this, 'action_devhub_cli_manifest_import' ) ); |
| | 18 | add_action( 'devhub_cli_markdown_import', array( $this, 'action_devhub_cli_markdown_import' ) ); |
| | 19 | } |
| | 20 | |
| | 21 | public static function action_init_register_cron_jobs() { |
| | 22 | if ( ! wp_next_scheduled( 'devhub_cli_manifest_import' ) ) { |
| | 23 | wp_schedule_event( time(), 'twicedaily', 'devhub_cli_manifest_import' ); |
| | 24 | } |
| | 25 | if ( ! wp_next_scheduled( 'devhub_cli_markdown_import' ) ) { |
| | 26 | wp_schedule_event( time(), 'twicedaily', 'devhub_cli_markdown_import' ); |
| | 27 | } |
| | 28 | } |
| | 29 | |
| | 30 | public static function action_init_register_post_types() { |
| | 31 | $supports = array( |
| | 32 | 'comments', |
| | 33 | 'custom-fields', |
| | 34 | 'editor', |
| | 35 | 'excerpt', |
| | 36 | 'revisions', |
| | 37 | 'title', |
| | 38 | ); |
| | 39 | register_post_type( 'command', array( |
| | 40 | 'has_archive' => 'cli/commands', |
| | 41 | 'label' => __( 'Commands', 'wporg' ), |
| | 42 | 'labels' => array( |
| | 43 | 'name' => __( 'Commands', 'wporg' ), |
| | 44 | 'singular_name' => __( 'Command', 'wporg' ), |
| | 45 | 'all_items' => __( 'Commands', 'wporg' ), |
| | 46 | 'new_item' => __( 'New Command', 'wporg' ), |
| | 47 | 'add_new' => __( 'Add New', 'wporg' ), |
| | 48 | 'add_new_item' => __( 'Add New Command', 'wporg' ), |
| | 49 | 'edit_item' => __( 'Edit Command', 'wporg' ), |
| | 50 | 'view_item' => __( 'View Command', 'wporg' ), |
| | 51 | 'search_items' => __( 'Search Commands', 'wporg' ), |
| | 52 | 'not_found' => __( 'No Commands found', 'wporg' ), |
| | 53 | 'not_found_in_trash' => __( 'No Commands found in trash', 'wporg' ), |
| | 54 | 'parent_item_colon' => __( 'Parent Command', 'wporg' ), |
| | 55 | 'menu_name' => __( 'Commands', 'wporg' ), |
| | 56 | ), |
| | 57 | 'public' => true, |
| | 58 | 'hierarchical'=> true, |
| | 59 | 'rewrite' => array( |
| | 60 | 'feeds' => false, |
| | 61 | 'slug' => 'cli/commands', |
| | 62 | 'with_front' => false, |
| | 63 | ), |
| | 64 | 'supports' => $supports, |
| | 65 | ) ); |
| | 66 | } |
| | 67 | |
| | 68 | public static function action_devhub_cli_manifest_import() { |
| | 69 | $response = wp_remote_get( self::$commands_manifest ); |
| | 70 | if ( is_wp_error( $response ) ) { |
| | 71 | return $response; |
| | 72 | } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { |
| | 73 | return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' ); |
| | 74 | } |
| | 75 | $manifest = json_decode( wp_remote_retrieve_body( $response ), true ); |
| | 76 | if ( ! $manifest ) { |
| | 77 | return new WP_Error( 'invalid-manifest', 'Manifest did not unfurl properly.' );; |
| | 78 | } |
| | 79 | // Fetch all handbook posts for comparison |
| | 80 | $q = new WP_Query( array( |
| | 81 | 'post_type' => self::$supported_post_types, |
| | 82 | 'post_status' => 'publish', |
| | 83 | 'posts_per_page' => self::$posts_per_page, |
| | 84 | ) ); |
| | 85 | $existing = array(); |
| | 86 | foreach( $q->posts as $post ) { |
| | 87 | $cmd_path = rtrim( str_replace( home_url( 'cli/commands/' ), '', get_permalink( $post->ID ) ), '/' ); |
| | 88 | $existing[ $cmd_path ] = array( |
| | 89 | 'post_id' => $post->ID, |
| | 90 | 'cmd_path' => $cmd_path, |
| | 91 | ); |
| | 92 | } |
| | 93 | $created = 0; |
| | 94 | foreach( $manifest as $doc ) { |
| | 95 | // Already exists |
| | 96 | if ( wp_filter_object_list( $existing, array( 'cmd_path' => $doc['cmd_path'] ) ) ) { |
| | 97 | continue; |
| | 98 | } |
| | 99 | if ( self::process_manifest_doc( $doc, $existing, $manifest ) ) { |
| | 100 | $created++; |
| | 101 | } |
| | 102 | } |
| | 103 | if ( class_exists( 'WP_CLI' ) ) { |
| | 104 | \WP_CLI::success( "Successfully created {$created} handbook pages." ); |
| | 105 | } |
| | 106 | } |
| | 107 | |
| | 108 | private static function process_manifest_doc( $doc, &$existing, $manifest ) { |
| | 109 | $post_parent = null; |
| | 110 | if ( ! empty( $doc['parent'] ) ) { |
| | 111 | // Find the parent in the existing set |
| | 112 | $parents = wp_filter_object_list( $existing, array( 'cmd_path' => $doc['parent'] ) ); |
| | 113 | if ( empty( $parents ) ) { |
| | 114 | if ( ! self::process_manifest_doc( $manifest[ $doc['parent'] ], $existing, $manifest ) ) { |
| | 115 | return; |
| | 116 | } |
| | 117 | $parents = wp_filter_object_list( $existing, array( 'cmd_path' => $doc['parent'] ) ); |
| | 118 | } |
| | 119 | if ( ! empty( $parents ) ) { |
| | 120 | $parent = array_shift( $parents ); |
| | 121 | $post_parent = $parent['post_id']; |
| | 122 | } |
| | 123 | } |
| | 124 | $post = self::create_post_from_manifest_doc( $doc, $post_parent ); |
| | 125 | if ( $post ) { |
| | 126 | $cmd_path = rtrim( str_replace( home_url( 'cli/commands/' ), '', get_permalink( $post->ID ) ), '/' ); |
| | 127 | $existing[ $cmd_path ] = array( |
| | 128 | 'post_id' => $post->ID, |
| | 129 | 'cmd_path' => $cmd_path, |
| | 130 | ); |
| | 131 | return true; |
| | 132 | } |
| | 133 | return false; |
| | 134 | } |
| | 135 | |
| | 136 | public static function action_devhub_cli_markdown_import() { |
| | 137 | $q = new WP_Query( array( |
| | 138 | 'post_type' => self::$supported_post_types, |
| | 139 | 'post_status' => 'publish', |
| | 140 | 'fields' => 'ids', |
| | 141 | 'posts_per_page' => self::$posts_per_page, |
| | 142 | ) ); |
| | 143 | $ids = $q->posts; |
| | 144 | $success = 0; |
| | 145 | foreach( $ids as $id ) { |
| | 146 | $ret = self::update_post_from_markdown_source( $id ); |
| | 147 | if ( class_exists( 'WP_CLI' ) ) { |
| | 148 | if ( is_wp_error( $ret ) ) { |
| | 149 | \WP_CLI::warning( $ret->get_error_message() ); |
| | 150 | } else { |
| | 151 | \WP_CLI::log( "Updated {$id} from markdown source" ); |
| | 152 | $success++; |
| | 153 | } |
| | 154 | } |
| | 155 | } |
| | 156 | if ( class_exists( 'WP_CLI' ) ) { |
| | 157 | $total = count( $ids ); |
| | 158 | \WP_CLI::success( "Successfully updated {$success} of {$total} CLI command pages." ); |
| | 159 | } |
| | 160 | } |
| | 161 | |
| | 162 | /** |
| | 163 | * Create a new handbook page from the manifest document |
| | 164 | */ |
| | 165 | private static function create_post_from_manifest_doc( $doc, $post_parent = null ) { |
| | 166 | $post_data = array( |
| | 167 | 'post_type' => 'command', |
| | 168 | 'post_status' => 'publish', |
| | 169 | 'post_parent' => $post_parent, |
| | 170 | 'post_title' => sanitize_text_field( wp_slash( $doc['title'] ) ), |
| | 171 | 'post_name' => sanitize_title_with_dashes( $doc['slug'] ), |
| | 172 | ); |
| | 173 | $post_id = wp_insert_post( $post_data ); |
| | 174 | if ( ! $post_id ) { |
| | 175 | return false; |
| | 176 | } |
| | 177 | if ( class_exists( 'WP_CLI' ) ) { |
| | 178 | \WP_CLI::log( "Created post {$post_id} for {$doc['title']}." ); |
| | 179 | } |
| | 180 | update_post_meta( $post_id, self::$meta_key, esc_url_raw( $doc['markdown_source'] ) ); |
| | 181 | return get_post( $post_id ); |
| | 182 | } |
| | 183 | |
| | 184 | /** |
| | 185 | * Update a post from its Markdown source |
| | 186 | */ |
| | 187 | private static function update_post_from_markdown_source( $post_id ) { |
| | 188 | $markdown_source = self::get_markdown_source( $post_id ); |
| | 189 | if ( is_wp_error( $markdown_source ) ) { |
| | 190 | return $markdown_source; |
| | 191 | } |
| | 192 | if ( ! function_exists( 'jetpack_require_lib' ) ) { |
| | 193 | return new WP_Error( 'missing-jetpack-require-lib', 'jetpack_require_lib() is missing on system.' ); |
| | 194 | } |
| | 195 | |
| | 196 | // Transform GitHub repo HTML pages into their raw equivalents |
| | 197 | $markdown_source = preg_replace( '#https?://github\.com/([^/]+/[^/]+)/blob/(.+)#', 'https://raw.githubusercontent.com/$1/$2', $markdown_source ); |
| | 198 | $markdown_source = add_query_arg( 'v', time(), $markdown_source ); |
| | 199 | $response = wp_remote_get( $markdown_source ); |
| | 200 | if ( is_wp_error( $response ) ) { |
| | 201 | return $response; |
| | 202 | } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { |
| | 203 | return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' ); |
| | 204 | } |
| | 205 | |
| | 206 | $markdown = wp_remote_retrieve_body( $response ); |
| | 207 | // Strip YAML doc from the header |
| | 208 | $markdown = preg_replace( '#^---(.+)---#Us', '', $markdown ); |
| | 209 | |
| | 210 | $title = null; |
| | 211 | if ( preg_match( '/^#\s(.+)/', $markdown, $matches ) ) { |
| | 212 | $title = $matches[1]; |
| | 213 | $markdown = preg_replace( '/^#\swp\s(.+)/', '', $markdown ); |
| | 214 | } |
| | 215 | |
| | 216 | // Transform to HTML and save the post |
| | 217 | jetpack_require_lib( 'markdown' ); |
| | 218 | $parser = new \WPCom_GHF_Markdown_Parser; |
| | 219 | $html = $parser->transform( $markdown ); |
| | 220 | $post_data = array( |
| | 221 | 'ID' => $post_id, |
| | 222 | 'post_content' => wp_filter_post_kses( wp_slash( $html ) ), |
| | 223 | ); |
| | 224 | if ( ! is_null( $title ) ) { |
| | 225 | $post_data['post_title'] = sanitize_text_field( wp_slash( $title ) ); |
| | 226 | } |
| | 227 | wp_update_post( $post_data ); |
| | 228 | return true; |
| | 229 | } |
| | 230 | |
| | 231 | /** |
| | 232 | * Retrieve the markdown source URL for a given post. |
| | 233 | */ |
| | 234 | public static function get_markdown_source( $post_id ) { |
| | 235 | $markdown_source = get_post_meta( $post_id, self::$meta_key, true ); |
| | 236 | if ( ! $markdown_source ) { |
| | 237 | return new WP_Error( 'missing-markdown-source', 'Markdown source is missing for post.' ); |
| | 238 | } |
| | 239 | |
| | 240 | return $markdown_source; |
| | 241 | } |
| | 242 | |
| | 243 | } |
| | 244 | |
| | 245 | $devhub_cli = new DevHub_CLI(); |