Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 419 lines (288 sloc) 9.661 kB
7c1c50d @benbalter initial commit
authored
1 <?php
2 /*
2a78361 @benbalter refinements
authored
3 Plugin Name: WordPress to Jekyll Exporter
4 Description: Exports WordPress posts, pages, and options as YAML files parsable by Jekyll
5 Version: 1.0
6 Author: Benjamin J. Balter
7 Author URI: http://ben.balter.com
8 License: GPLv3 or Later
7c1c50d @benbalter initial commit
authored
9
2a78361 @benbalter refinements
authored
10 Copyright 2012 Benjamin J. Balter (email : Ben@Balter.com)
7c1c50d @benbalter initial commit
authored
11
12 This program is free software; you can redistribute it and/or modify
2a78361 @benbalter refinements
authored
13 it under the terms of the GNU General Public License, version 2, as
7c1c50d @benbalter initial commit
authored
14 published by the Free Software Foundation.
15
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with this program; if not, write to the Free Software
23 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24 */
25
26 class Jekyll_Export {
27
2a78361 @benbalter refinements
authored
28 private $zip_folder = 'jekyll-export/'; //folder zip file extracts to
1dda9be @benbalter better recursion
authored
29
2a78361 @benbalter refinements
authored
30 public $rename_options = array( 'site', 'blog' ); //strings to strip from option keys on export
1dda9be @benbalter better recursion
authored
31
32 public $options = array( //array of wp_options value to convert to _config.yml
33 'name',
34 'description',
35 'url'
36 );
37
38 public $posts = array( //array of wp_posts fields to convert to YAML front matter
39 //will convert all post_meta and all taxonomies
40 'author',
41 'title',
42 'excerpt',
43 );
010bbec @benbalter better link conversion
authored
44
45 public $extra_html_include = false; //should un-markdownify-able HTML be included or skipped?
2a78361 @benbalter refinements
authored
46
47 /**
48 * Hook into WP Core
49 */
7c1c50d @benbalter initial commit
authored
50 function __construct() {
2a78361 @benbalter refinements
authored
51
52 add_action( 'admin_menu', array( &$this, 'register_menu' ) );
53 add_action( 'current_screen', array( &$this, 'callback' ) );
54
7c1c50d @benbalter initial commit
authored
55 }
2a78361 @benbalter refinements
authored
56
57 /**
58 * Listens for page callback, intercepts and runs export
59 */
60 function callback() {
61
15cec4a @benbalter fix for menu hook
authored
62 if ( get_current_screen()->id != 'export' )
63 return;
64
65 if ( !isset( $_GET['type'] ) || $_GET['type'] != 'jekyll' )
2a78361 @benbalter refinements
authored
66 return;
67
68 if ( !current_user_can( 'manage_options' ) )
69 return;
70
71 $this->export();
72 exit();
73
74 }
75
76
77 /**
78 * Add menu option to tools list
79 */
80 function register_menu() {
81
15cec4a @benbalter fix for menu hook
authored
82 add_management_page( __( 'Export to Jekyll', 'jekyll-export' ), __( 'Export to Jekyll', 'jekyll-export' ), 'manage_options', 'export.php?type=jekyll' );
2a78361 @benbalter refinements
authored
83
84 }
85
86
87 /**
88 * Get an array of all post and page IDs
89 * Note: We don't use core's get_posts as it doesn't scale as well on large sites
90 */
7c1c50d @benbalter initial commit
authored
91 function get_posts() {
2a78361 @benbalter refinements
authored
92
7c1c50d @benbalter initial commit
authored
93 global $wpdb;
010bbec @benbalter better link conversion
authored
94 return $wpdb->get_col( "SELECT ID FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN ('post', 'page' )" );
2a78361 @benbalter refinements
authored
95
7c1c50d @benbalter initial commit
authored
96 }
2a78361 @benbalter refinements
authored
97
98
99 /**
100 * Convert a posts meta data (both post_meta and the fields in wp_posts) to key value pairs for export
101 */
7c1c50d @benbalter initial commit
authored
102 function convert_meta( $post ) {
2a78361 @benbalter refinements
authored
103
104 //convert non-content columns in wp_posts table
105 foreach ( $post as $key => $value ) {
106
7c1c50d @benbalter initial commit
authored
107 if ( $key == 'post_content' )
108 continue;
4dcbe79 @benbalter fix for author and tag names
authored
109
110 //convert author from ID to display name
111 if ( $key == 'post_author' )
112 $value = get_userdata( $post->post_author )->display_name;
113
2a78361 @benbalter refinements
authored
114 //strip post_ from the key, as it will be page.foo in jekyll
7c1c50d @benbalter initial commit
authored
115 $key = str_replace( 'post_', '', $key );
1dda9be @benbalter better recursion
authored
116
117 if ( !in_array( $key, $this->posts ) )
118 continue;
119
7c1c50d @benbalter initial commit
authored
120 $output[ strtolower( $key) ] = $value;
2a78361 @benbalter refinements
authored
121
7c1c50d @benbalter initial commit
authored
122 }
4dcbe79 @benbalter fix for author and tag names
authored
123
124 //force post_type -> layout for ease of use on the Jekyll side
125 $output[ 'layout' ] = get_post_type( $post );
2a78361 @benbalter refinements
authored
126
127 //convert traditional post_meta values, hide hidden values
7c1c50d @benbalter initial commit
authored
128 foreach ( get_post_custom( $post ) as $key => $value ) {
2a78361 @benbalter refinements
authored
129
7c1c50d @benbalter initial commit
authored
130 if ( substr( $key, 0, 1 ) == '_' )
131 continue;
2a78361 @benbalter refinements
authored
132
7c1c50d @benbalter initial commit
authored
133 $output[ $key ] = $value;
2a78361 @benbalter refinements
authored
134
7c1c50d @benbalter initial commit
authored
135 }
2a78361 @benbalter refinements
authored
136
7c1c50d @benbalter initial commit
authored
137 return $output;
2a78361 @benbalter refinements
authored
138
7c1c50d @benbalter initial commit
authored
139 }
2a78361 @benbalter refinements
authored
140
141
142 /**
143 * Convert post taxonomies for export
144 */
7c1c50d @benbalter initial commit
authored
145 function convert_terms( $post ) {
146
147 $output = array();
2a78361 @benbalter refinements
authored
148 foreach ( get_taxonomies( array( 'object_type' => array( get_post_type( $post ) ) ) ) as $tax ) {
4dcbe79 @benbalter fix for author and tag names
authored
149
7c1c50d @benbalter initial commit
authored
150 $terms = wp_get_post_terms( $post, $tax );
4dcbe79 @benbalter fix for author and tag names
authored
151
152 //convert tax name for Jekyll
07be3b2 @benbalter fix for tag conversion
authored
153 if ( $tax == 'post_tag' )
4dcbe79 @benbalter fix for author and tag names
authored
154 $tax = 'tags';
155
7c1c50d @benbalter initial commit
authored
156 $output[ $tax ] = wp_list_pluck( $terms, 'name' );
4dcbe79 @benbalter fix for author and tag names
authored
157
7c1c50d @benbalter initial commit
authored
158 }
2a78361 @benbalter refinements
authored
159
7c1c50d @benbalter initial commit
authored
160 return $output;
2a78361 @benbalter refinements
authored
161
7c1c50d @benbalter initial commit
authored
162 }
163
2a78361 @benbalter refinements
authored
164
165 /**
166 * Loop through and convert all posts to MD files with YAML headers
167 */
168 function convert_posts() {
169
7c1c50d @benbalter initial commit
authored
170 foreach ( $this->get_posts() as $postID ) {
de3c605 @benbalter use md_extra
authored
171 $md = new Markdownify_Extra( null, false, $this->extra_html_include );
7c1c50d @benbalter initial commit
authored
172 $post = get_post( $postID );
2a78361 @benbalter refinements
authored
173 $meta = array_merge( $this->convert_meta( $post ), $this->convert_terms( $postID ) );
7c1c50d @benbalter initial commit
authored
174 $output = Spyc::YAMLDump($meta);
2a78361 @benbalter refinements
authored
175 $output .= "---\n";
de3c605 @benbalter use md_extra
authored
176 $output .= $md->parseString( apply_filters( 'the_content', $post->post_content ) );
7c1c50d @benbalter initial commit
authored
177 $this->write( $output, $post );
178 }
2a78361 @benbalter refinements
authored
179
180 }
181
182
183 /**
184 * Main function, bootstraps, converts, and cleans up
185 */
186 function export() {
187
188 if ( !class_exists( 'spyc' ) )
189 require_once dirname( __FILE__ ) . '/includes/spyc.php';
190
191 if ( !function_exists( 'Markdown' ) )
192 require_once dirname( __FILE__ ) . '/includes/markdownify/markdownify_extra.php';
010bbec @benbalter better link conversion
authored
193
2a78361 @benbalter refinements
authored
194 $this->dir = sys_get_temp_dir() . '/wp-jekyll-' . md5( time() ) . '/';
195 $this->zip = sys_get_temp_dir() . '/wp-jekyll.zip';
196 mkdir( $this->dir );
197 mkdir( $this->dir . '_posts/' );
198
199 $this->convert_options();
200 $this->convert_posts();
1dda9be @benbalter better recursion
authored
201 $this->convert_uploads();
010bbec @benbalter better link conversion
authored
202 $this->zip();
7c1c50d @benbalter initial commit
authored
203 $this->send();
2a78361 @benbalter refinements
authored
204 $this->cleanup();
205
7c1c50d @benbalter initial commit
authored
206 }
2a78361 @benbalter refinements
authored
207
208
209 /**
210 * Convert options table to _config.yml file
211 */
7c1c50d @benbalter initial commit
authored
212 function convert_options() {
2a78361 @benbalter refinements
authored
213
7c1c50d @benbalter initial commit
authored
214 $options = wp_load_alloptions();
215 foreach ( $options as $key => &$option ) {
216
217 if ( substr( $key, 0, 1 ) == '_' )
218 unset( $options[$key] );
2a78361 @benbalter refinements
authored
219
220 //strip site and blog from key names, since it will become site. when in Jekyll
221 foreach ( $this->rename_options as $rename ) {
222
223 $len = strlen( $rename );
224 if ( substr( $key, 0, $len ) != $rename )
225 continue;
226
227 $this->rename_key( $options, $key, substr( $key, $len ) );
228
229 }
230
7c1c50d @benbalter initial commit
authored
231 $option = maybe_unserialize( $option );
232
233 }
1dda9be @benbalter better recursion
authored
234
235 foreach ( $options as $key => $value ) {
236
237 if ( !in_array( $key, $this->options ) )
238 unset( $options[ $key ] );
239
240 }
2a78361 @benbalter refinements
authored
241
7c1c50d @benbalter initial commit
authored
242 $output = Spyc::YAMLDump( $options );
243
2a78361 @benbalter refinements
authored
244 //strip starting "---"
245 $output = substr( $output, 4 );
246
247 file_put_contents( $this->dir . '_config.yml', $output );
7c1c50d @benbalter initial commit
authored
248
249 }
2a78361 @benbalter refinements
authored
250
251
252 /**
253 * Write file to temp dir
254 */
7c1c50d @benbalter initial commit
authored
255 function write( $output, $post ) {
2a78361 @benbalter refinements
authored
256
1dda9be @benbalter better recursion
authored
257 if ( get_post_type( $post ) == 'page' ) {
258 mkdir( $this->dir . $post->post_name );
259 $filename = $post->post_name . '/index.md';
260 } else {
261 $filename = '_posts/' . date( 'Y-m-d', strtotime( $post->post_date ) ) . '-' . $post->post_name . '.md';
262 }
263
264 file_put_contents( $this->dir . $filename, $output );
2a78361 @benbalter refinements
authored
265
7c1c50d @benbalter initial commit
authored
266 }
2a78361 @benbalter refinements
authored
267
268
269 /**
270 * Zip temp dir
271 */
7c1c50d @benbalter initial commit
authored
272 function zip() {
2a78361 @benbalter refinements
authored
273
7c1c50d @benbalter initial commit
authored
274 //create zip
275 $zip = new ZipArchive();
276 $zip->open( $this->zip, ZIPARCHIVE::CREATE );
1dda9be @benbalter better recursion
authored
277 $this->_zip( $this->dir, $zip );
7c1c50d @benbalter initial commit
authored
278 $zip->close();
2a78361 @benbalter refinements
authored
279
7c1c50d @benbalter initial commit
authored
280 }
2a78361 @benbalter refinements
authored
281
282
283 /**
284 * Helper function to add a file to the zip
285 */
1dda9be @benbalter better recursion
authored
286 function _zip( $dir, &$zip ) {
287
7c1c50d @benbalter initial commit
authored
288 //loop through all files in directory
1dda9be @benbalter better recursion
authored
289 foreach ( glob( trailingslashit( $dir ) . '*' ) as $path ) {
290
291 if ( is_dir( $path ) ) {
292 $this->_zip( $path, $zip );
293 continue;
294 }
295
7c1c50d @benbalter initial commit
authored
296 //make path within zip relative to zip base, not server root
1dda9be @benbalter better recursion
authored
297 $local_path = '/' . str_replace( $this->dir, $this->zip_folder, $path );
7c1c50d @benbalter initial commit
authored
298
299 //add file
300 $zip->addFile( realpath( $path ), $local_path );
2a78361 @benbalter refinements
authored
301
7c1c50d @benbalter initial commit
authored
302 }
2a78361 @benbalter refinements
authored
303
7c1c50d @benbalter initial commit
authored
304 }
2a78361 @benbalter refinements
authored
305
306
307 /**
308 * Send headers and zip file to user
309 */
7c1c50d @benbalter initial commit
authored
310 function send() {
2a78361 @benbalter refinements
authored
311
7c1c50d @benbalter initial commit
authored
312 //send headers
313 header( 'Content-Type: application/zip' );
314 header( "Content-Disposition: attachment; filename=jekyll-export.zip" );
315 header( 'Content-Length: ' . filesize( $this->zip ) );
2a78361 @benbalter refinements
authored
316
7c1c50d @benbalter initial commit
authored
317 //read file
318 readfile( $this->zip );
2a78361 @benbalter refinements
authored
319
7c1c50d @benbalter initial commit
authored
320 }
2a78361 @benbalter refinements
authored
321
322
323 /**
324 * Clear temp files
325 */
326 function cleanup( ) {
327
1dda9be @benbalter better recursion
authored
328 $this->rmdir_recursive( $this->dir );
7c1c50d @benbalter initial commit
authored
329 unlink( $this->zip );
330
331 }
332
2a78361 @benbalter refinements
authored
333
334 /**
335 * Rename an assoc. array's key without changing the order
336 */
337 function rename_key( &$array, $from, $to ) {
338
339 $keys = array_keys( $array );
340 $index = array_search( $from, $keys );
341
342 if ( $index === false )
343 return;
344
345 $keys[ $index ] = $to;
346 $array = array_combine( $keys, $array );
347
348
349 }
1dda9be @benbalter better recursion
authored
350
351 function rmdir_recursive( $dir ) {
352
353 foreach( glob($dir . '/*' ) as $file ) {
354 if( is_dir( $file ) )
355 $this->rmdir_recursive( $file );
356 else
357 unlink( $file );
358 }
359
360 rmdir( $dir );
361
362 }
363
364 function convert_uploads() {
365
366 $upload_dir = wp_upload_dir();
367 $this->copy_recursive( $upload_dir['basedir'], $this->dir . str_replace( trailingslashit( get_home_url() ), '', $upload_dir['baseurl'] ) );
368
369 }
370
371 /**
372 * Copy a file, or recursively copy a folder and its contents
373 *
374 * @author Aidan Lister <aidan@php.net>
375 * @version 1.0.1
376 * @link http://aidanlister.com/2004/04/recursively-copying-directories-in-php/
377 * @param string $source Source path
378 * @param string $dest Destination path
379 * @return bool Returns TRUE on success, FALSE on failure
380 */
381 function copy_recursive($source, $dest) {
382
383 // Check for symlinks
384 if ( is_link( $source ) ) {
385 return symlink( readlink( $source ), $dest );
386 }
387
388 // Simple copy for a file
389 if ( is_file( $source ) ) {
390 return copy( $source, $dest );
391 }
392
393 // Make destination directory
394 if ( !is_dir($dest) ) {
395 mkdir($dest, null, true );
396 }
397
398 // Loop through the folder
399 $dir = dir($source);
400 while (false !== $entry = $dir->read()) {
401 // Skip pointers
402 if ($entry == '.' || $entry == '..') {
403 continue;
404 }
405
406 // Deep copy directories
407 $this->copy_recursive("$source/$entry", "$dest/$entry");
408 }
409
410 // Clean up
411 $dir->close();
412 return true;
413
414 }
415
7c1c50d @benbalter initial commit
authored
416 }
417
2a78361 @benbalter refinements
authored
418
419 $je = new Jekyll_Export();
Something went wrong with that request. Please try again.