-
Notifications
You must be signed in to change notification settings - Fork 85
/
class-markdown-import.php
364 lines (331 loc) · 12.1 KB
/
class-markdown-import.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
<?php
/**
* Markdown Import
*
* This functionality has been disabled as of 2020-08-12. All of the lesson plans have been imported
* to learn.wordpress.org and can be updated via the WP admin interface. Leaving this here for now
* in case we need to re-activate for some reason.
*/
namespace WPOrg_Learn;
use WP_Error, WP_Query;
// These actions/filters should not be added while markdown import is disabled.
//add_action( 'init', array( 'WPOrg_Learn\Markdown_Import', 'action_init' ) );
//add_action( 'wporg_learn_manifest_import', array( 'WPOrg_Learn\Markdown_Import', 'action_wporg_learn_manifest_import' ) );
//add_action( 'wporg_learn_markdown_import', array( 'WPOrg_Learn\Markdown_Import', 'action_wporg_learn_markdown_import' ) );
//add_action( 'load-post.php', array( 'WPOrg_Learn\Markdown_Import', 'action_load_post_php' ) );
//add_action( 'edit_form_after_title', array( 'WPOrg_Learn\Markdown_Import', 'action_edit_form_after_title' ) );
//add_action( 'save_post', array( 'WPOrg_Learn\Markdown_Import', 'action_save_post' ) );
//add_filter( 'cron_schedules', array( 'WPOrg_Learn\Markdown_Import', 'filter_cron_schedules' ) );
// This filter is still necessary because the lesson plans that were originally imported from GitHub still require
// that image assets be loaded from the same repositories.
add_filter( 'the_content', array( 'WPOrg_Learn\Markdown_Import', 'replace_image_links' ) );
/**
* Class Markdown_Import
*
* @package WPOrg_Learn
*/
class Markdown_Import {
private static $lesson_plan_manifest = 'https://wptrainingteam.github.io/manifest.json';
private static $input_name = 'wporg-learn-markdown-source';
private static $meta_key = 'wporg_learn_markdown_source';
private static $nonce_name = 'wporg-learn-markdown-source-nonce';
private static $submit_name = 'wporg-learn-markdown-import';
private static $supported_post_type = 'lesson-plan';
private static $posts_per_page = 100;
/**
* Register our cron task if it doesn't already exist
*/
public static function action_init() {
if ( ! wp_next_scheduled( 'wporg_learn_manifest_import' ) ) {
wp_schedule_event( time(), '15_minutes', 'wporg_learn_manifest_import' );
}
if ( ! wp_next_scheduled( 'wporg_learn_markdown_import' ) ) {
wp_schedule_event( time(), '15_minutes', 'wporg_learn_markdown_import' );
}
}
/**
* Actions taken on `wporg_learn_manifest_import` event.
*/
public static function action_wporg_learn_manifest_import() {
$response = wp_remote_get( self::$lesson_plan_manifest );
if ( is_wp_error( $response ) ) {
return $response;
} elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
}
$manifest = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! $manifest ) {
return new WP_Error( 'invalid-manifest', 'Manifest did not unfurl properly.' );
}
// Fetch all lesson plan posts for comparison
$q = new WP_Query( array(
'post_type' => self::$supported_post_type,
'post_status' => 'publish',
'posts_per_page' => self::$posts_per_page,
) );
$existing = $q->posts;
$created = 0;
foreach ( $manifest as $doc ) {
// Already exists
if ( wp_filter_object_list( $existing, array( 'post_name' => $doc['slug'] ) ) ) {
if ( class_exists( 'WP_CLI' ) ) {
\WP_CLI::log( "Found {$doc['slug']} already exits." );
}
continue;
}
$post_parent = null;
if ( ! empty( $doc['parent'] ) ) {
// Find the parent in the existing set
$parents = wp_filter_object_list( $existing, array( 'post_name' => $doc['parent'] ) );
if ( ! empty( $parents ) ) {
$parent = array_shift( $parents );
} else {
// Create the parent and add it to the stack
if ( isset( $manifest[ $doc['parent'] ] ) ) {
$parent_doc = $manifest[ $doc['parent'] ];
$parent = self::create_post_from_manifest_doc( $parent_doc );
if ( $parent ) {
$created++;
$existing[] = $parent;
} else {
continue;
}
} else {
continue;
}
}
$post_parent = $parent->ID;
}
$post = self::create_post_from_manifest_doc( $doc, $post_parent );
if ( $post ) {
$created++;
$existing[] = $post;
}
}
if ( class_exists( 'WP_CLI' ) ) {
\WP_CLI::success( "Successfully created {$created} lesson plan pages." );
}
}
/**
* Create a new lesson plan page from the manifest document
*/
private static function create_post_from_manifest_doc( $doc, $post_parent = null ) {
$post_data = array(
'post_type' => self::$supported_post_type,
'post_status' => 'publish',
'post_parent' => $post_parent,
'post_title' => sanitize_text_field( wp_slash( $doc['title'] ) ),
'post_name' => sanitize_title_with_dashes( $doc['slug'] ),
);
$post_id = wp_insert_post( $post_data );
if ( ! $post_id ) {
return false;
}
if ( class_exists( 'WP_CLI' ) ) {
\WP_CLI::log( "Created post {$post_id} for {$doc['title']}." );
}
update_post_meta( $post_id, self::$meta_key, esc_url_raw( $doc['markdown_source'] ) );
return get_post( $post_id );
}
/**
* Actions taken on `wporg_learn_markdown_import` event.
*/
public static function action_wporg_learn_markdown_import() {
$q = new WP_Query( array(
'post_type' => self::$supported_post_type,
'post_status' => 'publish',
'fields' => 'ids',
'posts_per_page' => self::$posts_per_page,
) );
$ids = $q->posts;
$success = 0;
foreach ( $ids as $id ) {
$ret = self::update_post_from_markdown_source( $id );
if ( class_exists( 'WP_CLI' ) ) {
if ( is_wp_error( $ret ) ) {
\WP_CLI::warning( $ret->get_error_message() );
} else {
\WP_CLI::log( "Updated {$id} from markdown source" );
$success++;
}
}
}
if ( class_exists( 'WP_CLI' ) ) {
$total = count( $ids );
\WP_CLI::success( "Successfully updated {$success} of {$total} lesson plan pages." );
}
}
/**
* Handle a request to import from the markdown source
*/
public static function action_load_post_php() {
if ( ! isset( $_GET[ self::$submit_name ] )
|| ! isset( $_GET[ self::$nonce_name ] )
|| ! isset( $_GET['post'] ) ) {
return;
}
$post_id = (int) $_GET['post'];
if ( ! current_user_can( 'edit_post', $post_id )
|| ! wp_verify_nonce( $_GET[ self::$nonce_name ], self::$input_name )
|| get_post_type( $post_id ) !== self::$supported_post_type ) {
return;
}
$response = self::update_post_from_markdown_source( $post_id );
if ( is_wp_error( $response ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_die( $response->get_error_message() );
}
wp_safe_redirect( get_edit_post_link( $post_id, 'raw' ) );
exit;
}
/**
* Add an input field for specifying Markdown source
*/
public static function action_edit_form_after_title( $post ) {
if ( $post->post_type !== self::$supported_post_type ) {
return;
}
$markdown_source = get_post_meta( $post->ID, self::$meta_key, true );
?>
<label>Markdown source: <input
type="text"
name="<?php echo esc_attr( self::$input_name ); ?>"
value="<?php echo esc_attr( $markdown_source ); ?>"
placeholder="Enter a URL representing a markdown file to import"
size="50" />
</label>
<?php
if ( $markdown_source ) :
$update_link = add_query_arg( array(
self::$submit_name => 'import',
self::$nonce_name => wp_create_nonce( self::$input_name ),
), get_edit_post_link( $post->ID, 'raw' ) );
?>
<a class="button button-small button-primary" href="<?php echo esc_url( $update_link ); ?>">Import</a>
<?php endif; ?>
<?php wp_nonce_field( self::$input_name, self::$nonce_name ); ?>
<?php
}
/**
* Save the Markdown source input field
*/
public static function action_save_post( $post_id ) {
if ( ! isset( $_POST[ self::$input_name ] )
|| ! isset( $_POST[ self::$nonce_name ] )
|| get_post_type( $post_id ) !== self::$supported_post_type ) {
return;
}
if ( ! wp_verify_nonce( $_POST[ self::$nonce_name ], self::$input_name ) ) {
return;
}
$markdown_source = '';
if ( ! empty( $_POST[ self::$input_name ] ) ) {
$markdown_source = esc_url_raw( $_POST[ self::$input_name ] );
}
update_post_meta( $post_id, self::$meta_key, $markdown_source );
}
/**
* Filter cron schedules to add a 15 minute schedule
*/
public static function filter_cron_schedules( $schedules ) {
$schedules['15_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => '15 minutes',
);
return $schedules;
}
/**
* Update a post from its Markdown source
*/
private static function update_post_from_markdown_source( $post_id ) {
$markdown_source = self::get_markdown_source( $post_id );
if ( is_wp_error( $markdown_source ) ) {
return $markdown_source;
}
if ( ! function_exists( 'jetpack_require_lib' ) ) {
return new WP_Error( 'missing-jetpack-require-lib', 'jetpack_require_lib() is missing on system.' );
}
// Transform GitHub repo HTML pages into their raw equivalents
//$markdown_source = preg_replace( '#https?://github\.com/([^/]+/[^/]+)/blob/(.+)#', 'https://raw.githubusercontent.com/$1/$2', $markdown_source );
$markdown_source = add_query_arg( 'v', time(), $markdown_source );
$response = wp_remote_get( $markdown_source );
if ( is_wp_error( $response ) ) {
return $response;
} elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'invalid-http-code', 'Markdown source returned non-200 http code.' );
}
$markdown = wp_remote_retrieve_body( $response );
// Strip YAML doc from the header
$markdown = preg_replace( '#^---(.+)---#Us', '', $markdown );
$title = null;
if ( preg_match( '/^#\s(.+)/', $markdown, $matches ) ) {
$title = $matches[1];
$markdown = preg_replace( '/^#\s(.+)/', '', $markdown );
}
// Transform to HTML and save the post
jetpack_require_lib( 'markdown' );
$parser = new \WPCom_GHF_Markdown_Parser();
$html = $parser->transform( $markdown );
$html = self::replace_markdown_checkboxes( $html );
$post_data = array(
'ID' => $post_id,
'post_content' => wp_filter_post_kses( wp_slash( $html ) ),
);
if ( ! is_null( $title ) ) {
$post_data['post_title'] = sanitize_text_field( wp_slash( $title ) );
}
wp_update_post( $post_data );
return true;
}
/**
* Retrieve the markdown source URL for a given post.
*/
public static function get_markdown_source( $post_id ) {
$markdown_source = get_post_meta( $post_id, self::$meta_key, true );
if ( ! $markdown_source ) {
return new WP_Error( 'missing-markdown-source', 'Markdown source is missing for post.' );
}
return $markdown_source;
}
/**
* Replace markdown checkboxes in the post-processed HTML.
*
* @param string $html The HTML after translation from markup.
*
* @return string The HTML after potentially replacing markdown checkboxes with HTML ones.
*/
public static function replace_markdown_checkboxes( $html ) {
$empty_check_markup = '<input type="checkbox" id="" disabled="" class="task-list-item-checkbox">';
$full_check_markup = '<input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked="">';
// We need to allow inputs with all of our attributes for wp_filter_post_kses().
global $allowedposttags;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$allowedposttags['input'] = array(
'type' => array(),
'disabled' => array(),
'checked' => array(),
'class' => array(),
'id' => array(),
);
$html = preg_replace( '/\[ \]/', $empty_check_markup, $html );
$html = preg_replace( '/\[x\]/', $full_check_markup, $html );
return $html;
}
/**
* Source images from the GitHub repo.
*
* @param string $content
*
* @return string|string[]
*/
public static function replace_image_links( $content ) {
$post_id = get_the_ID();
$markdown_source = self::get_markdown_source( $post_id );
if ( is_wp_error( $markdown_source ) ) {
return $content;
}
$markdown_source = str_replace( '/README.md', '', $markdown_source );
$content = str_replace( '<img src="/images/', '<img src="' . $markdown_source . '/images/', $content );
return $content;
}
}