diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..99bcd7e --- /dev/null +++ b/readme.txt @@ -0,0 +1,41 @@ +=== wp-flatbackups === +Contributors: cadeyrn +Donate link: +Tags: backup, YAML, flat files +Requires at least: 3.0 +Tested up to: 4.4 +Stable tag: 0.1 +License: GPLv3 +License URI: http://www.gnu.org/licenses/gpl-3.0.html +Required minimum PHP version: 5.3 + +Auto-export all published content on visit to flat, folder + files based structure. + +== Description == + +The plugin action is hooked into wp_footer, therefore executed on actual site visit. + +Content will be exported to wp-content/flat/{post_slug}/ folder ( one folder per post), all attachments copied (or hardlinked, if possible with the filesystem; this is automatic ). + +The content will be placed into in item.md file, in YAML + {post_content} format and the same format is applied to comments, named comment-{comment_id}.md in the same folder. + +This is not a classic backup, rather an export, to have your content in a readable format for the future. It can't be imported to WordPress (yet). + + +== Installation == + +1. Upload contents of `wp-flatbackups.zip` to the `/wp-content/plugins/` directory +2. Activate the plugin through the `Plugins` menu in WordPress + +== Changelog == + +Version numbering logic: + +* every A. indicates BIG changes. +* every .B version indicates new features. +* every ..C indicates bugfixes for A.B version. + += 0.1 = +*2015-07-16* + +* first stable release diff --git a/wp-flatbackups.php b/wp-flatbackups.php new file mode 100644 index 0000000..c613b71 --- /dev/null +++ b/wp-flatbackups.php @@ -0,0 +1,421 @@ + +Author URI: http://petermolnar.eu/ +License: GPLv3 +Required minimum PHP version: 5.3 +*/ + +/* Copyright 2015 Peter Molnar ( hello@petermolnar.eu ) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License, version 3, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +if (!class_exists('WP_FLATBACKUPS')): + +class WP_FLATBACKUPS { + + public function __construct () { + if (!function_exists('yaml_emit')) { + static::debug('`yaml_emit` function missing. Please install the YAML extension; otherwise this plugin will not work'); + } + + add_action( 'wp_footer', array( &$this, 'export_yaml')); + + } + + /** + * + */ + public static function export_yaml () { + + if (!function_exists('yaml_emit')) { + static::debug('`yaml_emit` function missing. Please install the YAML extension; otherwise this plugin will not work'); + return false; + } + + if (!is_singular()) + return false; + + $post = static::fix_post(); + + if ($post === false) + return false; + + $filename = $post->post_name; + + $flatroot = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'flat'; + $flatdir = $flatroot . DIRECTORY_SEPARATOR . $filename; + $flatfile = $flatdir . DIRECTORY_SEPARATOR . 'item.md'; + + $post_timestamp = get_the_modified_time( 'U', $post->ID ); + $file_timestamp = 0; + + if ( @file_exists($flatfile) ) { + $file_timestamp = @filemtime ( $flatfile ); + } + + $mkdir = array ( $flatroot, $flatdir ); + foreach ( $mkdir as $dir ) { + if ( !is_dir($dir)) { + if (!mkdir( $dir )) { + static::debug_log('Failed to create ' . $dir . ', exiting YAML creation'); + return false; + } + } + } + + touch($flatdir, $post_timestamp); + + // get all the attachments + $attachments = get_children( array ( + 'post_parent'=>$post->ID, + 'post_type'=>'attachment', + 'orderby'=>'menu_order', + 'order'=>'asc' + )); + + // 100 is there for sanity + // hardlink all the attachments; no need for copy + // unless you're on a filesystem that does not support hardlinks + if ( !empty($attachments) && count($attachments) < 100 ) { + $out['attachments'] = array(); + foreach ( $attachments as $aid => $attachment ) { + $attachment_path = get_attached_file( $aid ); + $attachment_file = basename( $attachment_path); + $target_file = $flatdir . DIRECTORY_SEPARATOR . $attachment_file; + static::debug ('should ' . $post->post_name . ' have this attachment?: ' . $attachment_file ); + if ( !is_file($target_file)) { + if (!link( $attachment_path, $target_file )) { + static::debug("could not hardlink '$attachment_path' to '$target_file'; trying to copy"); + if (!copy($attachment_path, $target_file )) { + static::debug("could not copy '$attachment_path' to '$target_file'; saving attachment failed!"); + } + } + } + } + } + + $comments = get_comments ( array( 'post_id' => $post->ID ) ); + if ( $comments ) { + foreach ($comments as $comment) { + $cf_timestamp = 0; + + $cfile = $flatdir . DIRECTORY_SEPARATOR . 'comment_' . $comment->comment_ID . '.md'; + + $c_timestamp = strtotime( $comment->comment_date ); + if ( @file_exists($cfile) ) { + $cf_timestamp = @filemtime ( $cfile ); + if ( $c_timestamp == $cf_timestamp ) { + continue; + } + } + + $c = array ( + 'id' => (int)$comment->comment_ID, + 'author' => $comment->comment_author, + 'author_email' => $comment->comment_author_email, + 'author_url' => $comment->comment_author_url, + 'date' => $comment->comment_date, + //'content' => $comment->comment_content, + 'useragent' => $comment->comment_agent, + 'type' => $comment->comment_type, + 'user_id' => (int)$comment->user_id, + ); + + if ( $avatar = get_comment_meta ($comment->comment_ID, "avatar", true)) + $c['avatar'] = $avatar; + + $social = static::preg_value($comment->comment_agent,'/Keyring_(.*?)_Reactions/' ); + + if ($social) { + $social = strtolower($social); + if ( $smeta = get_comment_meta ($comment->comment_ID, "keyring-${social}_reactions", true)) + $c['keyring_reactions_importer'] = json_encode($smeta); + } + + $cout = yaml_emit($c, YAML_UTF8_ENCODING ); + $cout .= "---\n" . $comment->comment_content; + + static::debug ('Exporting comment #' . $comment->comment_ID. ' to ' . $cfile ); + file_put_contents ($cfile, $cout); + touch ( $cfile, $c_timestamp ); + } + } + + if ( $file_timestamp == $post_timestamp ) { + return true; + } + + $out = static::yaml(); + + // write log + static::debug ('Exporting #' . $post->ID . ', ' . $post->post_name . ' to ' . $flatfile ); + file_put_contents ($flatfile, $out); + touch ( $flatfile, $post_timestamp ); + return true; + } + + + /** + * show post in YAML format (Grav friendly version) + * + */ + public static function yaml ( $postid = false ) { + + if (!function_exists('yaml_emit')) { + static::debug('`yaml_emit` function missing. Please install the YAML extension; otherwise this plugin will not work'); + return false; + } + + if (!$postid) + global $post; + else + $post = get_post($postid); + + $post = static::fix_post($post); + + if ( $post === false ) + return false; + + $postdata = static::raw_post_data($post); + + if (empty($postdata)) + return false; + + $excerpt = false; + if (isset($postdata['excerpt']) && empty($postdata['excerpt'])) { + $excerpt = $postdata['excerpt']; + unset($postdata['excerpt']); + } + + $content = $postdata['content']; + unset($postdata['content']); + + $out = yaml_emit($postdata, YAML_UTF8_ENCODING ); + if($excerpt) { + $out .= "\n" . $excerpt . "\n"; + } + + $out .= "---\n" . $content; + + return $out; + } + + /** + * raw data for various representations, like JSON or YAML + */ + public static function raw_post_data ( &$post = null ) { + $post = static::fix_post($post); + + if ($post === false) + return false; + + $content = $post->post_content; + + // fix all image attachments: resized -> original + $urlparts = parse_url(site_url()); + $domain = $urlparts ['host']; + $wp_upload_dir = wp_upload_dir(); + $uploadurl = str_replace( '/', "\\/", trim( str_replace( site_url(), '', $wp_upload_dir['url']), '/')); + + $pregstr = "/((https?:\/\/". $domain .")?\/". $uploadurl ."\/.*\/[0-9]{4}\/[0-9]{2}\/)(.*)-([0-9]{1,4})×([0-9]{1,4})\.([a-zA-Z]{2,4})/"; + + preg_match_all( $pregstr, $content, $resized_images ); + + if ( !empty ( $resized_images[0] )) { + foreach ( $resized_images[0] as $cntr => $imgstr ) { + $done_images[ $resized_images[2][$cntr] ] = 1; + $fname = $resized_images[2][$cntr] . '.' . $resized_images[5][$cntr]; + $width = $resized_images[3][$cntr]; + $height = $resized_images[4][$cntr]; + $r = $fname . '?resize=' . $width . ',' . $height; + $content = str_replace ( $imgstr, $r, $content ); + } + } + + $pregstr = "/(https?:\/\/". $domain .")?\/". $uploadurl ."\/.*\/[0-9]{4}\/[0-9]{2}\/(.*?)\.([a-zA-Z]{2,4})/"; + + preg_match_all( $pregstr, $content, $images ); + if ( !empty ( $images[0] )) { + + foreach ( $images[0] as $cntr=>$imgstr ) { + if ( !isset($done_images[ $images[1][$cntr] ]) ){ + if ( !strstr($images[1][$cntr], 'http')) + $fname = $images[2][$cntr] . '.' . $images[3][$cntr]; + else + $fname = $images[1][$cntr] . '.' . $images[2][$cntr]; + + $content = str_replace ( $imgstr, $fname, $content ); + } + } + } + + // get author name + $author_id = $post->post_author; + $author = get_the_author_meta ( 'display_name' , $author_id ); + + // exclude hidden meta and potential garbage + $exclude_meta = "/^_|^snap/"; + $exclude_meta = apply_filters ( __CLASS__ . '_exclude_meta', $exclude_meta); + + // get meta + $meta = get_post_meta($post->ID); + + foreach ($meta as $key => $value ) { + + if (preg_match($exclude_meta, $key)) { + if ($key == '_wp_old_slug') + $meta['old_slugs'] = $value; + + unset($meta[$key]); + continue; + } + + if (is_array($value) && count($value) == 1) { + $v = maybe_unserialize(array_pop($value)); + if (preg_match("/^snap.*/", $key)) { + $v = maybe_unserialize($v); + } + elseif ($key == 'syndication_urls') { + $v = explode("\n", trim($v)); + } + $meta[$key] = $v; + } + } + + // read all taxonomies + $taxonomies = get_object_taxonomies( $post ); + $post_taxonomies = array(); + + foreach ($taxonomies as $taxonomy ) { + $terms = wp_get_post_terms( $post->ID, $taxonomy ); + + if ( is_wp_error($terms)) { + static::debug($terms->get_error_message()); + continue; + } + + foreach ($terms as $term) { + $t_n = str_replace('post_', '', $term->taxonomy); + $post_taxonomies[ $t_n ][] = $term->name; + } + } + + // additional meta + $meta['id'] = $post->ID; + $meta['permalink'] = get_permalink( $post ); + $meta['shortlink'] = wp_get_shortlink( $post->ID ); + + // assemble the data + $out = array ( + 'title' => trim(get_the_title( $post->ID )), + 'modified_date' => get_the_modified_time('c', $post->ID), + 'date' => get_the_time('c', $post->ID), + 'slug' => $post->post_name, + 'taxonomy' => $post_taxonomies, + 'post_meta' => $meta, + 'author' => $author, + ); + + if($post->post_excerpt && !empty(trim($post->post_excerpt))) { + $out['excerpt'] = $post->post_excerpt; + } + + $out['content'] = $content; + + return $out; + } + + + /** + * do everything to get the Post object + */ + public static function fix_post ( &$post = null ) { + if ($post === null || !static::is_post($post)) + global $post; + + if (static::is_post($post)) + return $post; + + return false; + } + + /** + * test if an object is actually a post + */ + public static function is_post ( &$post ) { + if ( !empty($post) && is_object($post) && isset($post->ID) && !empty($post->ID) ) + return true; + + return false; + } + + /** + * + * debug messages; will only work if WP_DEBUG is on + * or if the level is LOG_ERR, but that will kill the process + * + * @param string $message + * @param int $level + */ + static function debug( $message, $level = LOG_NOTICE ) { + if ( @is_array( $message ) || @is_object ( $message ) ) + $message = json_encode($message); + + + switch ( $level ) { + case LOG_ERR : + wp_die( '

Error:

' . '

' . $message . '

' ); + exit; + default: + if ( !defined( 'WP_DEBUG' ) || WP_DEBUG != true ) + return; + break; + } + + error_log( __CLASS__ . " => " . $message ); + } + + /** + * debug log messages, if needed + * + public static function debug( $message) { + if (is_object($message) || is_array($message)) + $message = json_encode($message); + + if ( defined('WP_DEBUG') && WP_DEBUG == true ) + error_log ( __CLASS__ . ' => ' . $message); + } + */ + + /** + * + */ + public static function preg_value ( $string, $pattern, $index = 1 ) { + preg_match( $pattern, $string, $results ); + if ( isset ( $results[ $index ] ) && !empty ( $results [ $index ] ) ) + return $results [ $index ]; + else + return false; + } +} + +$WP_FLATBACKUPS = new WP_FLATBACKUPS(); + +endif; \ No newline at end of file