diff --git a/backend/src/Controllers/PostsController.php b/backend/src/Controllers/PostsController.php index 2d90ff997..b5d2550fa 100644 --- a/backend/src/Controllers/PostsController.php +++ b/backend/src/Controllers/PostsController.php @@ -146,6 +146,8 @@ public function register_routes() { ], ] ); + + } /** @@ -240,8 +242,8 @@ public function posts_count( $request ) { $response = []; foreach ( $post_types as $slug => $label ) { - $counts = wp_count_posts( $slug ); - $counts->total = $counts->publish + $counts->draft + $counts->pending + $counts->private + $counts->future; + $counts = wp_count_posts( $slug ); + $counts->total = $counts->publish + $counts->draft + $counts->pending + $counts->private + $counts->future; $response[ $slug ] = $counts; } @@ -441,4 +443,5 @@ public function delete_post( $request ) { ] ); } + } diff --git a/backend/src/Controllers/PostsImportController.php b/backend/src/Controllers/PostsImportController.php new file mode 100644 index 000000000..abdd90506 --- /dev/null +++ b/backend/src/Controllers/PostsImportController.php @@ -0,0 +1,336 @@ +view = $view; + } + + /** + * Register routes. + */ + public function register_routes() { + $this->route( + '/posts/import', + [ + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'import_posts' ], + 'permission_callback' => function () { + return current_user_can( 'edit_published_posts' ); + }, + ], + ] + ); + } + + /*Import posts */ + + public function import_posts( $request ) { + $file = $request->get_file_params(); + + global $wpdb; + + $uri = wp_upload_dir(); + $target_dir = $uri; + $target_file = $target_dir['path'] . basename( $file['file']['name'] ); + $filename = basename( $file['file']['name'] ); + $file_type = pathinfo( $target_file, PATHINFO_EXTENSION ); + $default_max_upload_size = $this->return_bytes( ini_get( 'upload_max_filesize' ) ); + + // Allow certain file formats + if ( $file_type !== 'xml' ) { + + return rest_ensure_response( + [ + 'error' => true, + 'message' => __( 'Sorry, only XML files are allowed.' ), + ] + ); + + } else { + if ( is_numeric( $default_max_upload_size ) && $file['file']['size'] > $default_max_upload_size ) { + + return rest_ensure_response( + [ + 'error' => true, + 'message' => __( 'Sorry, your file is too large.' ), + ] + ); + + } + } + + if ( move_uploaded_file( $file['file']['tmp_name'], $target_file ) ) { + + $xml_data = $this->XMLtoArray( file_get_contents( $target_file ) ); + + $post_arr = []; + if ( $this->isAssoc( $xml_data['RSS']['CHANNEL']['ITEM'] ) ) { + $post_arr[] = $xml_data['RSS']['CHANNEL']['ITEM']; + } else { + $post_arr = $xml_data['RSS']['CHANNEL']['ITEM']; + } + + $sucess_imp_count = 0; + $fail_imp_count = 0; + + foreach ( $post_arr as $property ) { + + $current_user = wp_get_current_user(); + $post_data = [ + 'comment_status' => $property['WP:COMMENT_STATUS'], + 'ping_status' => $property['WP:PING_STATUS'], + 'post_author' => $current_user->ID, + 'post_content' => $property['DESCRIPTION'] ? $property['DESCRIPTION'] : '', + 'post_excerpt' => $property['EXCERPT:ENCODED'] ? $property['EXCERPT:ENCODED'] : '', + 'post_name' => $property['WP:POST_NAME'], + 'post_status' => $property['WP:STATUS'], + 'post_title' => $property['TITLE'], + 'post_type' => $property['WP:POST_TYPE'], + 'to_ping' => $property['WP:PING_STATUS'], + 'menu_order' => $property['WP:MENU_ORDER'], + ]; + + if ( ! function_exists( 'post_exists' ) ) { + require_once ABSPATH . 'wp-admin/includes/post.php'; + } + if ( 0 === post_exists( $property['TITLE'], '', '', $property['WP:POST_TYPE'] ) ) { + + /* Import Attachments */ + + if ( $property['WP:POST_TYPE'] === 'attachment' ) { + + $file = $property['WP:ATTACHMENT_URL']; + $filename = basename( $file ); + $read_file = file_get_contents( $file ); + + $upload_file = wp_upload_bits( $filename, null, $read_file ); + + if ( ! $upload_file['error'] ) { + + $wp_filetype = wp_check_filetype( $filename, null ); + $post_data['post_mime_type'] = $wp_filetype['type']; + $attachment_id = wp_insert_attachment( $post_data, $upload_file['file'] ); + if ( ! is_wp_error( $attachment_id ) ) { + require_once( ABSPATH . 'wp-admin' . '/includes/image.php' ); + $attachment_data = wp_generate_attachment_metadata( $attachment_id, $upload_file['file'] ); + wp_update_attachment_metadata( $attachment_id, $attachment_data ); + } + + $sucess_imp_count++; + + } + } else { + $new_post_id = wp_insert_post( $post_data ); + wp_set_post_terms( $new_post_id, null, 'category' ); + $post_meta = $property['WP:POSTMETA']; + $post_cat = $property['CATEGORY']; + + if ( is_array( $post_meta ) && count( $post_meta ) !== 0 ) { + foreach ( $post_meta as $meta_info ) { + + $meta_key = $meta_info['WP:META_KEY'] ? $meta_info['WP:META_KEY'] : ''; + $meta_value = $meta_info['WP:META_VALUE'] ? addslashes( $meta_info['WP:META_VALUE'] ) : ''; + if ( $meta_key !== '' && $meta_value !== '' ) { + + $wpdb->query( "INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value) values ({$new_post_id}, '{$meta_key}', '{$meta_value}')" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + } + } + } + + if ( is_array( $post_cat ) && count( $post_cat ) !== 0 ) { + + foreach ( $post_cat as $category ) { + + $term = term_exists( $category['content'], $category['DOMAIN'] ); + + wp_set_post_terms( $new_post_id, [ $term['term_taxonomy_id'] ], $category['DOMAIN'], true ); + + } + } + + $sucess_imp_count++; + } + } else { + $fail_imp_count++; + + } + } + + $post_fail_msg = __( 'Post Imported Failed!' ); + + if ( $sucess_imp_count > 1 ) { + $post_success_msg = $sucess_imp_count . ' ' . ucfirst( $property['WP:POST_TYPE'] ) . __( ' Imported Successfully!' ); + } elseif ( $sucess_imp_count === 1 ) { + $post_success_msg = ucfirst( $property['WP:POST_TYPE'] ) . __( ' Imported Successfully!' ); + } else { + $post_success_msg = ''; + } + + if ( $fail_imp_count > 1 ) { + $post_fail_msg = $fail_imp_count . ' ' . ucfirst( $property[' WP:POST_TYPE '] ) . __( ' Already exist or unable to Import!' ); + } elseif ( $fail_imp_count === 1 ) { + $post_fail_msg = ucfirst( $property['WP:POST_TYPE'] ) . __( ' Already exist or unable to Import!' ); + } else { + $post_fail_msg = ''; + } + + return rest_ensure_response( + [ + 'success' => true, + 'message' => $post_success_msg . ' ' . $post_fail_msg, + ] + ); + + } else { + + return rest_ensure_response( + [ + 'error' => true, + 'message' => __( 'Sorry, there was an error uploading your file.' ), + ] + ); + } + + } + + /* + This function read XML data and convert into array + */ + + public function XMLtoArray( $xml ) { + $xml_parser = xml_parser_create(); + xml_parse_into_struct( $xml_parser, $xml, $vals ); + xml_parser_free( $xml_parser ); + + $_tmp = ''; + foreach ( $vals as $xml_elem ) { + $x_tag = $xml_elem['tag']; + $x_level = $xml_elem['level']; + $x_type = $xml_elem['type']; + if ( $x_level !== 1 && $x_type === 'close' ) { + if ( isset( $multi_key[ $x_tag ][ $x_level ] ) ) { + $multi_key[ $x_tag ][ $x_level ] = 1; + } else { + $multi_key[ $x_tag ][ $x_level ] = 0; + } + } + if ( $x_level !== 1 && $x_type === 'complete' ) { + if ( $_tmp === $x_tag ) { + $multi_key[ $x_tag ][ $x_level ] = 1; + } + + $_tmp = $x_tag; + } + } + + foreach ( $vals as $xml_elem ) { + $x_tag = $xml_elem['tag']; + $x_level = $xml_elem['level']; + $x_type = $xml_elem['type']; + if ( $x_type === 'open' ) { + $level[ $x_level ] = $x_tag; + } + + $start_level = 1; + $php_stmt = '$xml_array'; + if ( $x_type === 'close' && $x_level !== 1 ) { + $multi_key[ $x_tag ][ $x_level ]++; + } + + while ( $start_level < $x_level ) { + $php_stmt .= '[$level[' . $start_level . ']]'; + if ( isset( $multi_key[ $level[ $start_level ] ][ $start_level ] ) && $multi_key[ $level[ $start_level ] ][ $start_level ] ) { + $php_stmt .= '[' . ( $multi_key[ $level[ $start_level ] ][ $start_level ] - 1 ) . ']'; + } + + $start_level++; + } + $add = ''; + if ( isset( $multi_key[ $x_tag ][ $x_level ] ) && $multi_key[ $x_tag ][ $x_level ] && ( $x_type === 'open' || $x_type === 'complete' ) ) { + if ( ! isset( $multi_key2[ $x_tag ][ $x_level ] ) ) { + $multi_key2[ $x_tag ][ $x_level ] = 0; + } else { + $multi_key2[ $x_tag ][ $x_level ]++; + } + + $add = '[' . $multi_key2[ $x_tag ][ $x_level ] . ']'; + } + if ( isset( $xml_elem['value'] ) && trim( $xml_elem['value'] ) !== '' && ! array_key_exists( 'attributes', $xml_elem ) ) { + if ( $x_type === 'open' ) { + $php_stmt_main = $php_stmt . '[$x_type]' . $add . '[\'content\'] = $xml_elem[\'value\'];'; + } else { + $php_stmt_main = $php_stmt . '[$x_tag]' . $add . ' = $xml_elem[\'value\'];'; + } + + eval( $php_stmt_main ); + + } + if ( array_key_exists( 'attributes', $xml_elem ) ) { + if ( isset( $xml_elem['value'] ) ) { + $php_stmt_main = $php_stmt . '[$x_tag]' . $add . '[\'content\'] = $xml_elem[\'value\'];'; + eval( $php_stmt_main ); + } + foreach ( $xml_elem['attributes'] as $key => $value ) { + $php_stmt_att = $php_stmt . '[$x_tag]' . $add . '[$key] = $value;'; + eval( $php_stmt_att ); + } + } + } + return $xml_array; + } + + /* + This function checks the array is associate or not. + */ + + public function isAssoc( array $arr ) { + if ( [] === $arr ) { + return false; + } else { + return array_keys( $arr ) !== range( 0, count( $arr ) - 1 ); + } + } + + /* + This function convert string formatted data size value into bytes. + */ + + function return_bytes( $val ) { + $val = trim( $val ); + + if ( is_numeric( $val ) ) { + return $val; + } + + $last = strtolower( $val[ strlen( $val ) - 1 ] ); + $val = substr( $val, 0, -1 ); // necessary since PHP 7.1; otherwise optional + + switch ( $last ) { + // The 'G' modifier is available since PHP 5.1.0 + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + + return $val; + } + + +} diff --git a/backend/src/Providers/RestServiceProvider.php b/backend/src/Providers/RestServiceProvider.php index 09c93e9db..8ed4a780a 100644 --- a/backend/src/Providers/RestServiceProvider.php +++ b/backend/src/Providers/RestServiceProvider.php @@ -13,6 +13,7 @@ use FL\Assistant\Controllers\NotificationsController; use FL\Assistant\Controllers\PostsController; use FL\Assistant\Controllers\PostsExportController; +use FL\Assistant\Controllers\PostsImportController; use FL\Assistant\Controllers\SearchController; use FL\Assistant\Controllers\TermsController; use FL\Assistant\Controllers\UpdatesController; @@ -39,6 +40,7 @@ class RestServiceProvider extends ServiceProviderAbstract { NotificationsController::class, PostsController::class, PostsExportController::class, + PostsImportController::class, TermsController::class, UpdatesController::class, UsersController::class, diff --git a/package-lock.json b/package-lock.json index 46fd28d9a..8b64a5d18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6268,12 +6268,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6288,17 +6290,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6415,7 +6420,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6427,6 +6433,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6441,6 +6448,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6448,12 +6456,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6472,6 +6482,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6552,7 +6563,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6564,6 +6576,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6685,6 +6698,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/apps/fl-import/app.js b/src/apps/fl-import/app.js new file mode 100644 index 000000000..2d012328a --- /dev/null +++ b/src/apps/fl-import/app.js @@ -0,0 +1,50 @@ +import React from 'react' +import { __ } from '@wordpress/i18n' +import { Page, Nav } from 'assistant/ui' +import { ImportDropUploader } from './import' + +export const ImportApp = ( { match, history } ) => ( + + + +) + + +const Main = ( ) => { + return ( + +
+ {__( + 'Import allows you to import posts from exported xml files.' + )} + +
+ + + + + + + +
+ ) +} + +ImportApp.Icon = ( { windowSize } ) => { + const size = 'mini' === windowSize ? 32 : 45 + return ( + + + + + + ) +} diff --git a/src/apps/fl-import/drop-listner.js b/src/apps/fl-import/drop-listner.js new file mode 100644 index 000000000..5f75d34af --- /dev/null +++ b/src/apps/fl-import/drop-listner.js @@ -0,0 +1,98 @@ +import React, { Fragment, useState } from 'react' +import classname from 'classnames' +import { __ } from '@wordpress/i18n' + +export const useFileDrop = ( handleDrop = () => { } ) => { + const [ isDragging, setIsDragging ] = useState( false ) + + const onDragEnter = e => { + e.preventDefault() + e.stopPropagation() + + if ( e.dataTransfer.items ) { + e.dataTransfer.effectAllowed = 'copy' + setIsDragging( true ) + } + return false + } + + const onDragLeave = e => { + e.preventDefault() + e.stopPropagation() + + if ( e.target === e.currentTarget ) { + setIsDragging( false ) + } + return false + } + + const onDragOver = e => { + + // Yea, it's necessary to prevent browser opening file + e.preventDefault() + e.stopPropagation() + } + + const onDrop = e => { + e.preventDefault() + e.stopPropagation() + + if ( e.dataTransfer.files && 0 < e.dataTransfer.files.length ) { + handleDrop( e.dataTransfer.files ) + } + + setIsDragging( false ) + } + + return { + bind: { + onDragEnter, + onDragLeave, + onDragOver, + onDrop, + }, + isDragging + } +} + +const DraggingView = () => { + return ( + +
{__( 'Drop files to begin Import.' )}
+ + ) +} + +export const FileDropListener = props => { + const { + children, + className, + onDrop = () => { }, + draggingView = + } = props + + const onFilesDropped = files => { + onDrop( files ) + } + const { bind } = useFileDrop( onFilesDropped ) + + const classes = classname( { + 'fl-asst-file-drop': true, + 'fl-asst-file-drop-is-dragging': true, + }, className ) + + const merged = { + ...props, + ...bind, + className: classes, + } + + return ( + +
+
{children}
+ {
{draggingView}
} +
+
+ ) +} diff --git a/src/apps/fl-import/import.js b/src/apps/fl-import/import.js new file mode 100644 index 000000000..cc780dec4 --- /dev/null +++ b/src/apps/fl-import/import.js @@ -0,0 +1,85 @@ +import React from 'react' +import { Icon } from 'assistant/ui' +import { registerStore, useStore, getStore, getDispatch } from 'assistant/data' +import { getWpRest } from 'assistant/utils/wordpress' +import './style.scss' +import { FileDropListener } from './drop-listner' +import { useHistory } from 'react-router-dom' + +registerStore('fl-import/uploader', { + state: { + current: 0, + items: [] + } +}) + +export const ImportDropUploader = ({ children }) => { + const { current, items } = useStore('fl-import/uploader') + const { setCurrent, setItems } = getDispatch('fl-import/uploader') + const wpRest = getWpRest() + let history = useHistory() + const onFilesDropped = files => { + const { current, items } = getStore('fl-import/uploader').getState() + + for (let i = 0; i < files.length; i++) { + items.push(files.item(i)) + } + + setItems([...items]) + + if (!current) { + uploadNextItem() + } + } + + const uploadNextItem = () => { + const { current, items } = getStore('fl-import/uploader').getState() + const file = items[current] + const data = new FormData() + + if (!file) { + setItems([]) + setCurrent(0) + return + } + + setCurrent(current + 1) + + data.append('file', file, file.name || file.type.replace('/', '.')) + + wpRest + .posts() + .import(data) + .then(response => { + onSuccess(response) + }) + .catch(error => { + onError(error) + }) + } + + const onSuccess = response => { + uploadNextItem() + if (response) { + alert(response.data.message) + if (typeof response.data.error == 'undefined') { + history.push('/fl-content') + } + } + } + + const onError = () => { + uploadNextItem() + alert('Error uploading import file.', { appearance: 'error' }) + } + + if (current) { + return ( +
+ Uploading {current} of {items.length} +
+ ) + } + + return {children} +} diff --git a/src/apps/fl-import/index.js b/src/apps/fl-import/index.js new file mode 100644 index 000000000..d7e6fd79b --- /dev/null +++ b/src/apps/fl-import/index.js @@ -0,0 +1,14 @@ +import { registerApp } from 'assistant' +import { __ } from '@wordpress/i18n' +import { ImportApp } from './app' + + +registerApp( 'fl-import', { + label: __( 'Import' ), + root: ImportApp, + icon: ImportApp.Icon, + accent: { + color: '#FA9200' + } +} ) + diff --git a/src/apps/fl-import/style.scss b/src/apps/fl-import/style.scss new file mode 100644 index 000000000..b269bc90c --- /dev/null +++ b/src/apps/fl-import/style.scss @@ -0,0 +1,60 @@ +.fl-asst-file-drop { + display: flex; + flex: 1 1 auto; + max-height: 100%; + min-height: 0; +} + +.fl-asst-file-drop-content-view { + display: flex; + flex: 1 1 auto; + max-height: 100%; + min-height: 0; + max-width: 100%; +} + +.fl-asst-file-drop-dragging-view { + display: flex; + position: absolute; + top:0; + left:0; + right:0; + bottom: 0; + background: none !important; + opacity: 0; + pointer-events: none; + max-height: 100%; +} + +.fl-asst-file-drop-is-dragging { + .fl-asst-file-drop-dragging-view { + opacity: 1; + pointer-events: none; + } + .fl-asst-file-drop-content-view { + pointer-events: none; + display: none + } +} + +.fl-asst-file-uploading { + opacity: 1; + + .fl-asst-file-uploading-text { + svg { + position: relative; + margin: 0 2px 0 0; + top: 2px; + } + } +} + +.fl-asst-file-drop-file-wrapper{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 100px 20px; + flex: 1 1 auto; + font-size: 1.2em; +} diff --git a/src/apps/index.js b/src/apps/index.js index 0b8eaa19c..d50e22f26 100644 --- a/src/apps/index.js +++ b/src/apps/index.js @@ -6,6 +6,7 @@ import './fl-comments' import './fl-updates' import './fl-labels' import './fl-cloud' +import './fl-import' import './examples' import './integrations' diff --git a/src/system/utils/wordpress/rest.js b/src/system/utils/wordpress/rest.js index 7bb4b6ea2..19496fb7f 100644 --- a/src/system/utils/wordpress/rest.js +++ b/src/system/utils/wordpress/rest.js @@ -187,6 +187,16 @@ const posts = () => { return http.post( `fl-assistant/v1/posts/${id}/clone`, config ) }, + /** + * Import posts + * @param data + * @param config + */ + import( file, config = {} ) { + config.cacheKey = 'posts' + return http.post( 'fl-assistant/v1/posts/import', file, config ) + }, + /** * Export a post * @param data