| 1 |
<?php |
| 2 |
/** |
| 3 |
* Import screen. |
| 4 |
* |
| 5 |
* @package Easy_Invoice |
| 6 |
* @subpackage Controllers |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace EasyInvoice\Controllers; |
| 10 |
|
| 11 |
use EasyInvoice\Import\CsvImporter; |
| 12 |
use EasyInvoice\Import\SlicedImporter; |
| 13 |
use EasyInvoice\Import\SproutImporter; |
| 14 |
|
| 15 |
if ( ! defined( 'ABSPATH' ) ) { |
| 16 |
exit; |
| 17 |
} |
| 18 |
|
| 19 |
/** |
| 20 |
* One screen: what is on this site that could be imported, a dry-run count |
| 21 |
* for each source, and a button that does it. CSV uploads go through the |
| 22 |
* same screen with template files to download. |
| 23 |
*/ |
| 24 |
class ImportController { |
| 25 |
|
| 26 |
const PAGE_SLUG = 'easy-invoice-import'; |
| 27 |
const ACTION_RUN = 'easy_invoice_import_run'; |
| 28 |
const ACTION_TPL = 'easy_invoice_import_template'; |
| 29 |
|
| 30 |
public static function init(): void { |
| 31 |
add_action( 'admin_menu', [ __CLASS__, 'registerPage' ], 99 ); |
| 32 |
add_action( 'easy_invoice_admin_main_content', [ __CLASS__, 'maybeRenderPage' ], 11 ); |
| 33 |
add_action( 'admin_post_' . self::ACTION_RUN, [ __CLASS__, 'handleRun' ] ); |
| 34 |
add_action( 'admin_post_' . self::ACTION_TPL, [ __CLASS__, 'handleTemplate' ] ); |
| 35 |
add_filter( 'easy_invoice_self_rendering_pages', [ __CLASS__, 'claimPage' ] ); |
| 36 |
} |
| 37 |
|
| 38 |
public static function claimPage( $slugs ): array { |
| 39 |
$slugs = is_array( $slugs ) ? $slugs : []; |
| 40 |
$slugs[] = self::PAGE_SLUG; |
| 41 |
return $slugs; |
| 42 |
} |
| 43 |
|
| 44 |
public static function registerPage(): void { |
| 45 |
add_submenu_page( 'easy-invoice-hidden', __( 'Import', 'easy-invoice' ), __( 'Import', 'easy-invoice' ), 'manage_options', self::PAGE_SLUG, [ __CLASS__, 'renderShell' ] ); |
| 46 |
} |
| 47 |
|
| 48 |
public static function renderShell(): void { |
| 49 |
include EASY_INVOICE_PLUGIN_DIR . 'templates/main-template.php'; |
| 50 |
} |
| 51 |
|
| 52 |
/** |
| 53 |
* The importers that could run here, keyed by source. |
| 54 |
* |
| 55 |
* @return array<string,\EasyInvoice\Import\Importer> |
| 56 |
*/ |
| 57 |
public static function sources(): array { |
| 58 |
$sources = [ |
| 59 |
'sliced' => new SlicedImporter(), |
| 60 |
'sprout' => new SproutImporter(), |
| 61 |
]; |
| 62 |
/** |
| 63 |
* Filter the available import sources. |
| 64 |
* |
| 65 |
* @param array $sources Importer instances keyed by source name. |
| 66 |
*/ |
| 67 |
return (array) apply_filters( 'easy_invoice_import_sources', $sources ); |
| 68 |
} |
| 69 |
|
| 70 |
public static function maybeRenderPage( $page ): void { |
| 71 |
if ( self::PAGE_SLUG !== $page || ! current_user_can( 'manage_options' ) ) { |
| 72 |
return; |
| 73 |
} |
| 74 |
$sources = self::sources(); |
| 75 |
$previews = []; |
| 76 |
foreach ( $sources as $key => $importer ) { |
| 77 |
if ( $importer->available() ) { |
| 78 |
$previews[ $key ] = $importer->preview(); |
| 79 |
} |
| 80 |
} |
| 81 |
$result = get_transient( 'easy_invoice_import_result_' . get_current_user_id() ); |
| 82 |
delete_transient( 'easy_invoice_import_result_' . get_current_user_id() ); |
| 83 |
|
| 84 |
include EASY_INVOICE_PLUGIN_DIR . 'templates/admin/import-page.php'; |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Run an import. |
| 89 |
* |
| 90 |
* @return void |
| 91 |
*/ |
| 92 |
public static function handleRun(): void { |
| 93 |
check_admin_referer( self::ACTION_RUN ); |
| 94 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 95 |
wp_die( esc_html__( 'You do not have permission to import.', 'easy-invoice' ), '', [ 'response' => 403 ] ); |
| 96 |
} |
| 97 |
|
| 98 |
$source = isset( $_POST['source'] ) ? sanitize_key( $_POST['source'] ) : ''; |
| 99 |
|
| 100 |
// Imports of any size take a while; never let the request die halfway. |
| 101 |
if ( function_exists( 'set_time_limit' ) ) { |
| 102 |
@set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged |
| 103 |
} |
| 104 |
wp_raise_memory_limit( 'admin' ); |
| 105 |
|
| 106 |
if ( 'csv' === $source ) { |
| 107 |
$importer = new CsvImporter( self::uploaded( 'clients_csv' ), self::uploaded( 'invoices_csv' ) ); |
| 108 |
if ( ! $importer->preview()['clients'] && ! $importer->preview()['invoices'] ) { |
| 109 |
self::finish( [ 'error' => __( 'Choose at least one CSV file with a header row.', 'easy-invoice' ) ] ); |
| 110 |
} |
| 111 |
} else { |
| 112 |
$sources = self::sources(); |
| 113 |
if ( ! isset( $sources[ $source ] ) ) { |
| 114 |
self::finish( [ 'error' => __( 'Unknown import source.', 'easy-invoice' ) ] ); |
| 115 |
} |
| 116 |
$importer = $sources[ $source ]; |
| 117 |
} |
| 118 |
|
| 119 |
$report = $importer->run(); |
| 120 |
$report['label'] = $importer->label(); |
| 121 |
|
| 122 |
/** |
| 123 |
* Fires after an import has run. |
| 124 |
* |
| 125 |
* @param string $source Source key. |
| 126 |
* @param array $report Counts and notes. |
| 127 |
*/ |
| 128 |
do_action( 'easy_invoice_import_finished', $source, $report ); |
| 129 |
|
| 130 |
self::finish( $report ); |
| 131 |
} |
| 132 |
|
| 133 |
/** |
| 134 |
* Serve a CSV template. |
| 135 |
* |
| 136 |
* @return void |
| 137 |
*/ |
| 138 |
public static function handleTemplate(): void { |
| 139 |
check_admin_referer( self::ACTION_TPL ); |
| 140 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 141 |
wp_die( '', '', [ 'response' => 403 ] ); |
| 142 |
} |
| 143 |
$which = ( isset( $_GET['which'] ) && 'clients' === $_GET['which'] ) ? 'clients' : 'invoices'; |
| 144 |
nocache_headers(); |
| 145 |
header( 'Content-Type: text/csv; charset=utf-8' ); |
| 146 |
header( 'Content-Disposition: attachment; filename="easy-invoice-' . $which . '-template.csv"' ); |
| 147 |
echo CsvImporter::template( $which ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download. |
| 148 |
exit; |
| 149 |
} |
| 150 |
|
| 151 |
/** |
| 152 |
* Path of an uploaded CSV, moved into the uploads dir; '' when none. |
| 153 |
* |
| 154 |
* @param string $field Form field. |
| 155 |
* @return string |
| 156 |
*/ |
| 157 |
private static function uploaded( string $field ): string { |
| 158 |
if ( empty( $_FILES[ $field ]['tmp_name'] ) || UPLOAD_ERR_OK !== (int) ( $_FILES[ $field ]['error'] ?? 1 ) ) { |
| 159 |
return ''; |
| 160 |
} |
| 161 |
if ( ! function_exists( 'wp_handle_upload' ) ) { |
| 162 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 163 |
} |
| 164 |
$moved = wp_handle_upload( $_FILES[ $field ], [ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- validated by wp_handle_upload. |
| 165 |
'test_form' => false, |
| 166 |
'mimes' => [ 'csv' => 'text/csv', 'txt' => 'text/plain' ], |
| 167 |
] ); |
| 168 |
if ( ! is_array( $moved ) || ! empty( $moved['error'] ) || empty( $moved['file'] ) ) { |
| 169 |
return ''; |
| 170 |
} |
| 171 |
// Delete after the request: the file has no reason to stay in the media folder. |
| 172 |
register_shutdown_function( static function () use ( $moved ) { |
| 173 |
wp_delete_file( $moved['file'] ); |
| 174 |
} ); |
| 175 |
return (string) $moved['file']; |
| 176 |
} |
| 177 |
|
| 178 |
/** |
| 179 |
* Stash the result and go back to the screen. |
| 180 |
* |
| 181 |
* @param array $report Report or ['error' => msg]. |
| 182 |
* @return void |
| 183 |
*/ |
| 184 |
private static function finish( array $report ): void { |
| 185 |
set_transient( 'easy_invoice_import_result_' . get_current_user_id(), $report, 5 * MINUTE_IN_SECONDS ); |
| 186 |
wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ); |
| 187 |
exit; |
| 188 |
} |
| 189 |
} |
| 190 |
|