v1.0.1 — security hardening, TEC integration fixes, and documentation
Security: - Validate CSV upload MIME type server-side via finfo - Deliver import notices via per-user transient (prevents GET-param spoofing) - Sanitise translatable success string with wp_kses to block HTML injection - Switch sanitize_url to esc_url_raw; wp_kses_post to sanitize_textarea_field for plain-text bio Bug fixes: - Guard preg_replace null return in normalise_name() to prevent TypeError on PHP 8 - Replace generic save_post hook with save_post_tec_speaker / save_post_tribe_events so saves no longer need a manual revision check and cannot interact with TEC's own save_post handler at priority 15 TEC integration: - Check for tribe-select2 / tribe-select2-css handles first (TEC ships SelectWoo, not vanilla Select2); CDN was previously always loaded unnecessarily - Type-specific save hooks make event/speaker save paths explicit and independent Improvements: - Add register_activation_hook to flush rewrite rules on activation - Wrap instantiation in plugins_loaded so TEC is guaranteed loaded first - Show admin notice and skip TEC-specific hooks when TEC is inactive - Cap event picker query at PICKER_LIMIT = 200 (was unbounded -1) - Register front-end CSS via wp_add_inline_style on wp_enqueue_scripts - absint() on speaker IDs in option value attributes Documentation: - Write full README.md (was blank) - Add CHANGELOG.md with detailed 1.0.0 and 1.0.1 entries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ef4229cce
commit
fef0766e67
3 changed files with 276 additions and 90 deletions
46
CHANGELOG.md
Normal file
46
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.0.1] — 2026-05-17
|
||||
|
||||
### Security
|
||||
|
||||
- **CSV MIME validation** — the importer now checks the uploaded file's MIME type server-side via `finfo` before opening it. The browser-side `accept=".csv"` attribute is not a security control and was the only prior gate.
|
||||
- **Admin notice spoofing** — import result banners (success/error) are now delivered via a per-user transient rather than raw GET parameters. A crafted URL can no longer display false success or error messages to an admin.
|
||||
- **HTML in translatable string** — the success notice now passes through `wp_kses()` restricted to `<strong>`, preventing a tampered translation file from injecting arbitrary markup.
|
||||
- **`esc_url_raw` over `sanitize_url`** — switched to the canonical WordPress function for storing URLs (functionally equivalent; `sanitize_url` is an undocumented alias).
|
||||
- **`sanitize_textarea_field` for CSV bio** — the importer previously used `wp_kses_post()`, which permitted HTML in a field described to users as plain-text. Now consistently plain-text.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **`normalise_name()` null return** — `preg_replace()` returns `null` on failure (e.g. backtracking limit, invalid UTF-8). Both calls now fall back to the input string via `?? $name`, preventing a `TypeError` on PHP 8.
|
||||
- **Revision saves** — the generic `save_post` hook was replaced with type-specific `save_post_tec_speaker` and `save_post_tribe_events` hooks. `save_post_tribe_events` does not fire for revisions (which have post type `'revision'`), so the separate `wp_is_post_revision()` guard is no longer needed.
|
||||
|
||||
### Integration
|
||||
|
||||
- **Correct SelectWoo handles** — The Events Calendar ships SelectWoo (a Select2 fork) under the handles `tribe-select2` (JS) and `tribe-select2-css` (CSS), not `select2`. The admin script loader now checks for `tribe-select2` first, then `select2`, then falls back to a CDN copy. Previously, TEC's registration was never detected and the CDN was always loaded unnecessarily.
|
||||
- **Type-specific save hooks** — replaced the single generic `save_post` hook (priority 10) with `save_post_tec_speaker` and `save_post_tribe_events`. TEC hooks its own meta save to `save_post` at priority 15; using type-specific hooks eliminates any ordering dependency and makes intent explicit.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Activation hook** — added `register_activation_hook` to register the CPT and flush rewrite rules on activation, fixing the "Speaker archive returns 404 until Permalinks are re-saved" issue on first install.
|
||||
- **`plugins_loaded` wrapper** — the plugin is now instantiated inside `add_action('plugins_loaded', …)` instead of at file-include time, ensuring all plugins (including TEC) are loaded before the constructor runs.
|
||||
- **TEC dependency notice** — if The Events Calendar is not active, an admin notice is shown. TEC-specific hooks (event display, event meta box) are skipped; the Speaker CPT, admin menu, and shortcode remain functional.
|
||||
- **Admin picker cap** — the event speaker picker query is capped at 200 results (`PICKER_LIMIT` constant) instead of an unbounded `posts_per_page => -1`, preventing memory issues on large installs.
|
||||
- **`absint()` on option values** — speaker IDs written into `<option value>` attributes are now explicitly cast with `absint()`.
|
||||
- **Styles via `wp_add_inline_style`** — front-end CSS is now registered through WordPress's style system (`wp_register_style` + `wp_add_inline_style` on `wp_enqueue_scripts`) rather than echoed as a raw `<style>` tag mid-content. Caching, minification, and CSP plugins can now manage the styles correctly.
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] — 2026-05-16
|
||||
|
||||
Initial release.
|
||||
|
||||
- `tec_speaker` custom post type with title, editor, excerpt, featured image, website URL, and email.
|
||||
- Searchable multi-select speaker picker on event edit screens (Select2/SelectWoo).
|
||||
- Automatic speaker display beneath event meta on single event pages.
|
||||
- `[tec_speakers]` shortcode with `limit` and `search` attributes.
|
||||
- CSV bulk importer with duplicate-name detection and honorific normalisation.
|
||||
98
README.md
98
README.md
|
|
@ -1,2 +1,98 @@
|
|||
# speakers-for-the-events-calendar
|
||||
# Speakers for The Events Calendar
|
||||
|
||||
A WordPress plugin that adds a Speaker custom post type to [The Events Calendar](https://theeventscalendar.com/), letting you create speaker profiles, attach them to events, display them on event pages, and bulk-import them from CSV.
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Version |
|
||||
|---|---|
|
||||
| WordPress | 6.0+ |
|
||||
| PHP | 8.0+ |
|
||||
| The Events Calendar | Any current release |
|
||||
|
||||
## Features
|
||||
|
||||
- **Speaker CPT** — a dedicated `tec_speaker` post type with title, bio (editor), excerpt, featured image, website URL, and email fields.
|
||||
- **Event speaker picker** — a searchable multi-select on every event edit screen, powered by SelectWoo (TEC's bundled library) with an automatic CDN fallback.
|
||||
- **Front-end display** — speakers are rendered in a responsive card grid beneath the event meta block on single event pages.
|
||||
- **`[tec_speakers]` shortcode** — display a filterable speaker archive anywhere on the site.
|
||||
- **CSV bulk importer** — import speakers from a spreadsheet; duplicate names are skipped automatically.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Upload the `speakers-for-the-events-calendar` folder to `/wp-content/plugins/`.
|
||||
2. Activate through **Plugins → Installed Plugins**.
|
||||
3. The Events Calendar must be installed and active; the plugin shows an admin notice if it is not.
|
||||
|
||||
## Usage
|
||||
|
||||
### Adding speakers
|
||||
|
||||
Go to **Speakers → Add New**. Fill in the title (speaker name), the bio in the editor, and optionally a website URL and email in the **Speaker Details** sidebar panel. Set a featured image for the speaker photo.
|
||||
|
||||
### Attaching speakers to an event
|
||||
|
||||
Open any event in the editor. A **Speakers** meta box appears in the main column. Type to search and click names to select; click again to deselect.
|
||||
|
||||
### Displaying speakers
|
||||
|
||||
Speakers are automatically displayed beneath the event meta block on single event pages — no configuration needed.
|
||||
|
||||
To display a speaker directory on any page or post, use the shortcode:
|
||||
|
||||
```
|
||||
[tec_speakers]
|
||||
[tec_speakers limit="20"]
|
||||
[tec_speakers search="Jane"]
|
||||
```
|
||||
|
||||
| Attribute | Default | Description |
|
||||
|---|---|---|
|
||||
| `limit` | `50` | Maximum number of speakers to display |
|
||||
| `search` | _(empty)_ | Filter speakers by name or content |
|
||||
|
||||
### CSV import
|
||||
|
||||
Go to **Speakers → Import CSV**. The CSV must have a header row with these columns (order matters):
|
||||
|
||||
```
|
||||
name, bio, email, website, image
|
||||
```
|
||||
|
||||
| Column | Required | Notes |
|
||||
|---|---|---|
|
||||
| `name` | Yes | Speaker's full name |
|
||||
| `bio` | No | Plain-text biography |
|
||||
| `email` | No | Stored as private meta, not displayed publicly |
|
||||
| `website` | No | Full URL including `https://` |
|
||||
| `image` | No | Ignored — add photos via Featured Image after import |
|
||||
|
||||
Speakers whose name (ignoring common honorifics and extra whitespace) matches an existing speaker are skipped. No existing records are overwritten.
|
||||
|
||||
## Hooks
|
||||
|
||||
### Actions
|
||||
|
||||
| Hook | Description |
|
||||
|---|---|
|
||||
| `tribe_events_single_event_after_the_meta` | Where speakers are injected on single event pages |
|
||||
|
||||
### Filters
|
||||
|
||||
None currently. Open an issue if you need to customise output or query arguments.
|
||||
|
||||
## Data
|
||||
|
||||
| Meta key | Post type | Description |
|
||||
|---|---|---|
|
||||
| `_tec_event_speakers` | `tribe_events` | Array of `tec_speaker` post IDs attached to the event |
|
||||
| `_tec_speaker_website` | `tec_speaker` | Speaker website URL |
|
||||
| `_tec_speaker_email` | `tec_speaker` | Speaker contact email (private) |
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## License
|
||||
|
||||
GPLv2 or later. See [https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html).
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: Speakers for The Events Calendar
|
||||
* Plugin URI: https://git.ankh-morpork.discworld.network/laurence/speakers-for-the-events-calendar
|
||||
* Description: Add speaker profiles to The Events Calendar events. Registers a Speaker custom post type, provides a searchable speaker picker on event edit screens, displays speakers on event pages, and includes a CSV bulk importer.
|
||||
* Version: 1.0.0
|
||||
* Version: 1.0.1
|
||||
* Author: Laurence Horrocks-Barlow
|
||||
* Author URI: https://qsplace.co.uk
|
||||
* Text Domain: speakers-for-tec
|
||||
|
|
@ -16,27 +16,39 @@
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
register_activation_hook( __FILE__, function () {
|
||||
$plugin = new Speakers_For_TEC();
|
||||
$plugin->register_cpt();
|
||||
flush_rewrite_rules();
|
||||
} );
|
||||
|
||||
final class Speakers_For_TEC {
|
||||
|
||||
const CPT = 'tec_speaker';
|
||||
const META_KEY = '_tec_event_speakers'; // stored on tribe_events posts
|
||||
const CPT = 'tec_speaker';
|
||||
const META_KEY = '_tec_event_speakers';
|
||||
const PICKER_LIMIT = 200;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Boot
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'init', [ $this, 'register_cpt' ] );
|
||||
add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] );
|
||||
add_action( 'save_post', [ $this, 'save_meta' ], 10, 2 );
|
||||
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
|
||||
add_action( 'init', [ $this, 'register_cpt' ] );
|
||||
add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] );
|
||||
add_action( 'save_post_' . self::CPT, [ $this, 'save_speaker_meta' ], 10, 2 );
|
||||
add_action( 'save_post_tribe_events', [ $this, 'save_event_meta' ], 10, 2 );
|
||||
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
|
||||
add_action( 'admin_post_sftec_import', [ $this, 'handle_csv_import' ] );
|
||||
add_action( 'admin_notices', [ $this, 'admin_notices' ] );
|
||||
add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ] );
|
||||
add_shortcode( 'tec_speakers', [ $this, 'speakers_shortcode' ] );
|
||||
|
||||
// TEC single-event display — hook fires after the event meta block
|
||||
add_action( 'tribe_events_single_event_after_the_meta', [ $this, 'display_event_speakers' ] );
|
||||
if ( class_exists( 'Tribe__Events__Main' ) ) {
|
||||
add_action( 'tribe_events_single_event_after_the_meta', [ $this, 'display_event_speakers' ] );
|
||||
} else {
|
||||
add_action( 'admin_notices', [ $this, 'notice_tec_missing' ] );
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -46,17 +58,17 @@ final class Speakers_For_TEC {
|
|||
public function register_cpt(): void {
|
||||
register_post_type( self::CPT, [
|
||||
'labels' => [
|
||||
'name' => __( 'Speakers', 'speakers-for-tec' ),
|
||||
'singular_name' => __( 'Speaker', 'speakers-for-tec' ),
|
||||
'add_new_item' => __( 'Add New Speaker', 'speakers-for-tec' ),
|
||||
'edit_item' => __( 'Edit Speaker', 'speakers-for-tec' ),
|
||||
'new_item' => __( 'New Speaker', 'speakers-for-tec' ),
|
||||
'view_item' => __( 'View Speaker', 'speakers-for-tec' ),
|
||||
'search_items' => __( 'Search Speakers', 'speakers-for-tec' ),
|
||||
'not_found' => __( 'No speakers found', 'speakers-for-tec' ),
|
||||
'not_found_in_trash' => __( 'No speakers in trash','speakers-for-tec' ),
|
||||
'all_items' => __( 'All Speakers', 'speakers-for-tec' ),
|
||||
'menu_name' => __( 'Speakers', 'speakers-for-tec' ),
|
||||
'name' => __( 'Speakers', 'speakers-for-tec' ),
|
||||
'singular_name' => __( 'Speaker', 'speakers-for-tec' ),
|
||||
'add_new_item' => __( 'Add New Speaker', 'speakers-for-tec' ),
|
||||
'edit_item' => __( 'Edit Speaker', 'speakers-for-tec' ),
|
||||
'new_item' => __( 'New Speaker', 'speakers-for-tec' ),
|
||||
'view_item' => __( 'View Speaker', 'speakers-for-tec' ),
|
||||
'search_items' => __( 'Search Speakers', 'speakers-for-tec' ),
|
||||
'not_found' => __( 'No speakers found', 'speakers-for-tec' ),
|
||||
'not_found_in_trash' => __( 'No speakers in trash', 'speakers-for-tec' ),
|
||||
'all_items' => __( 'All Speakers', 'speakers-for-tec' ),
|
||||
'menu_name' => __( 'Speakers', 'speakers-for-tec' ),
|
||||
],
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
|
|
@ -74,12 +86,21 @@ final class Speakers_For_TEC {
|
|||
] );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dependency notice
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function notice_tec_missing(): void {
|
||||
echo '<div class="notice notice-error"><p>'
|
||||
. esc_html__( 'Speakers for The Events Calendar requires The Events Calendar plugin to be installed and active.', 'speakers-for-tec' )
|
||||
. '</p></div>';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Meta boxes
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function add_meta_boxes(): void {
|
||||
// Extra fields on speaker edit screen
|
||||
add_meta_box(
|
||||
'sftec_speaker_details',
|
||||
__( 'Speaker Details', 'speakers-for-tec' ),
|
||||
|
|
@ -89,7 +110,6 @@ final class Speakers_For_TEC {
|
|||
'default'
|
||||
);
|
||||
|
||||
// Speaker picker on TEC event edit screen
|
||||
if ( post_type_exists( 'tribe_events' ) ) {
|
||||
add_meta_box(
|
||||
'sftec_event_speakers',
|
||||
|
|
@ -126,7 +146,7 @@ final class Speakers_For_TEC {
|
|||
|
||||
$speakers = get_posts( [
|
||||
'post_type' => self::CPT,
|
||||
'posts_per_page' => -1,
|
||||
'posts_per_page' => self::PICKER_LIMIT,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'post_status' => 'publish',
|
||||
|
|
@ -146,7 +166,7 @@ final class Speakers_For_TEC {
|
|||
foreach ( $speakers as $speaker ) {
|
||||
printf(
|
||||
'<option value="%d"%s>%s</option>',
|
||||
$speaker->ID,
|
||||
absint( $speaker->ID ),
|
||||
in_array( $speaker->ID, $selected, true ) ? ' selected' : '',
|
||||
esc_html( $speaker->post_title )
|
||||
);
|
||||
|
|
@ -161,32 +181,34 @@ final class Speakers_For_TEC {
|
|||
// Saving meta
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
public function save_speaker_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Speaker detail fields
|
||||
if (
|
||||
isset( $_POST['_sftec_speaker_nonce'] )
|
||||
&& wp_verify_nonce( $_POST['_sftec_speaker_nonce'], 'sftec_speaker_details_save' )
|
||||
&& $post->post_type === self::CPT
|
||||
&& current_user_can( 'edit_post', $post_id )
|
||||
! isset( $_POST['_sftec_speaker_nonce'] )
|
||||
|| ! wp_verify_nonce( $_POST['_sftec_speaker_nonce'], 'sftec_speaker_details_save' )
|
||||
|| ! current_user_can( 'edit_post', $post_id )
|
||||
) {
|
||||
update_post_meta( $post_id, '_tec_speaker_website', sanitize_url( $_POST['sftec_speaker_website'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_tec_speaker_email', sanitize_email( $_POST['sftec_speaker_email'] ?? '' ) );
|
||||
return;
|
||||
}
|
||||
update_post_meta( $post_id, '_tec_speaker_website', esc_url_raw( $_POST['sftec_speaker_website'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_tec_speaker_email', sanitize_email( $_POST['sftec_speaker_email'] ?? '' ) );
|
||||
}
|
||||
|
||||
// Event → speaker associations
|
||||
if (
|
||||
isset( $_POST['_sftec_event_speakers_nonce'] )
|
||||
&& wp_verify_nonce( $_POST['_sftec_event_speakers_nonce'], 'sftec_event_speakers_save' )
|
||||
&& $post->post_type === 'tribe_events'
|
||||
&& current_user_can( 'edit_post', $post_id )
|
||||
) {
|
||||
$ids = array_map( 'intval', (array) ( $_POST['sftec_event_speakers'] ?? [] ) );
|
||||
update_post_meta( $post_id, self::META_KEY, array_filter( $ids ) );
|
||||
public function save_event_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
! isset( $_POST['_sftec_event_speakers_nonce'] )
|
||||
|| ! wp_verify_nonce( $_POST['_sftec_event_speakers_nonce'], 'sftec_event_speakers_save' )
|
||||
|| ! current_user_can( 'edit_post', $post_id )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$ids = array_map( 'intval', (array) ( $_POST['sftec_event_speakers'] ?? [] ) );
|
||||
update_post_meta( $post_id, self::META_KEY, array_filter( $ids ) );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -213,8 +235,6 @@ final class Speakers_For_TEC {
|
|||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_styles();
|
||||
|
||||
$heading = count( $speakers ) > 1
|
||||
? __( 'Speakers', 'speakers-for-tec' )
|
||||
: __( 'Speaker', 'speakers-for-tec' );
|
||||
|
|
@ -253,8 +273,6 @@ final class Speakers_For_TEC {
|
|||
return '<p>' . esc_html__( 'No speakers found.', 'speakers-for-tec' ) . '</p>';
|
||||
}
|
||||
|
||||
$this->enqueue_styles();
|
||||
|
||||
ob_start();
|
||||
echo '<div class="sftec-speakers-archive">';
|
||||
foreach ( $speakers as $speaker ) {
|
||||
|
|
@ -301,17 +319,17 @@ final class Speakers_For_TEC {
|
|||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Styles (enqueued once per page load)
|
||||
// Styles — registered through WordPress, not echoed inline
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function enqueue_styles(): void {
|
||||
static $done = false;
|
||||
if ( $done ) {
|
||||
return;
|
||||
}
|
||||
$done = true;
|
||||
public function enqueue_styles(): void {
|
||||
wp_register_style( 'sftec-speakers', false, [], '1.0.0' );
|
||||
wp_enqueue_style( 'sftec-speakers' );
|
||||
wp_add_inline_style( 'sftec-speakers', $this->get_css() );
|
||||
}
|
||||
|
||||
echo '<style id="sftec-speakers-css">
|
||||
private function get_css(): string {
|
||||
return '
|
||||
.sftec-event-speakers { margin: 2em 0; border-top: 1px solid #e0e0e0; padding-top: 1.5em; }
|
||||
.sftec-speakers-heading { font-size: 1.15em; font-weight: 600; margin: 0 0 1em; }
|
||||
.sftec-speakers-list,
|
||||
|
|
@ -324,7 +342,7 @@ final class Speakers_For_TEC {
|
|||
.sftec-speaker-name a:hover { text-decoration: underline; }
|
||||
.sftec-speaker-bio { margin: 0 0 0.4em; font-size: 0.875em; color: #555; line-height: 1.4; }
|
||||
.sftec-speaker-link { font-size: 0.825em; }
|
||||
</style>';
|
||||
';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -337,8 +355,13 @@ final class Speakers_For_TEC {
|
|||
return;
|
||||
}
|
||||
|
||||
// Use WP-bundled Select2 if available (TEC ships it), otherwise CDN fallback
|
||||
if ( wp_script_is( 'select2', 'registered' ) ) {
|
||||
// TEC ships SelectWoo as 'tribe-select2'; fall back to a plain 'select2'
|
||||
// registration from another plugin, then finally our own CDN copy.
|
||||
if ( wp_script_is( 'tribe-select2', 'registered' ) ) {
|
||||
wp_enqueue_script( 'tribe-select2' );
|
||||
wp_enqueue_style( 'tribe-select2-css' );
|
||||
$handle = 'tribe-select2';
|
||||
} elseif ( wp_script_is( 'select2', 'registered' ) ) {
|
||||
wp_enqueue_script( 'select2' );
|
||||
wp_enqueue_style( 'select2' );
|
||||
$handle = 'select2';
|
||||
|
|
@ -438,14 +461,28 @@ final class Speakers_For_TEC {
|
|||
wp_die( 'Unauthorised' );
|
||||
}
|
||||
|
||||
$transient_key = 'sftec_import_result_' . get_current_user_id();
|
||||
$redirect_url = admin_url( 'edit.php?post_type=' . self::CPT . '&page=sftec-import-speakers' );
|
||||
|
||||
if ( empty( $_FILES['speakers_csv']['tmp_name'] ) ) {
|
||||
wp_redirect( $this->import_redirect( [ 'sftec_err' => 'no_file' ] ) );
|
||||
set_transient( $transient_key, [ 'err' => 'no_file' ], 60 );
|
||||
wp_redirect( $redirect_url );
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate MIME type server-side — browser accept=".csv" is not a security control
|
||||
$finfo = new \finfo( FILEINFO_MIME_TYPE );
|
||||
$mime = $finfo->file( $_FILES['speakers_csv']['tmp_name'] );
|
||||
if ( ! in_array( $mime, [ 'text/plain', 'text/csv', 'application/csv', 'application/vnd.ms-excel' ], true ) ) {
|
||||
set_transient( $transient_key, [ 'err' => 'invalid_type' ], 60 );
|
||||
wp_redirect( $redirect_url );
|
||||
exit;
|
||||
}
|
||||
|
||||
$handle = fopen( $_FILES['speakers_csv']['tmp_name'], 'r' );
|
||||
if ( ! $handle ) {
|
||||
wp_redirect( $this->import_redirect( [ 'sftec_err' => 'read_fail' ] ) );
|
||||
set_transient( $transient_key, [ 'err' => 'read_fail' ], 60 );
|
||||
wp_redirect( $redirect_url );
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -488,11 +525,12 @@ final class Speakers_For_TEC {
|
|||
continue;
|
||||
}
|
||||
|
||||
$bio = sanitize_textarea_field( $bio );
|
||||
$post_id = wp_insert_post( [
|
||||
'post_type' => self::CPT,
|
||||
'post_title' => sanitize_text_field( $name ),
|
||||
'post_content' => wp_kses_post( $bio ),
|
||||
'post_excerpt' => wp_trim_words( wp_strip_all_tags( $bio ), 30 ),
|
||||
'post_content' => $bio,
|
||||
'post_excerpt' => wp_trim_words( $bio, 30 ),
|
||||
'post_status' => 'publish',
|
||||
] );
|
||||
|
||||
|
|
@ -502,10 +540,10 @@ final class Speakers_For_TEC {
|
|||
}
|
||||
|
||||
if ( $email ) {
|
||||
update_post_meta( $post_id, '_tec_speaker_email', sanitize_email( trim( $email ) ) );
|
||||
update_post_meta( $post_id, '_tec_speaker_email', sanitize_email( trim( $email ) ) );
|
||||
}
|
||||
if ( $website ) {
|
||||
update_post_meta( $post_id, '_tec_speaker_website', sanitize_url( trim( $website ) ) );
|
||||
update_post_meta( $post_id, '_tec_speaker_website', esc_url_raw( trim( $website ) ) );
|
||||
}
|
||||
|
||||
$existing_names[ $key ] = true;
|
||||
|
|
@ -514,25 +552,20 @@ final class Speakers_For_TEC {
|
|||
|
||||
fclose( $handle );
|
||||
|
||||
wp_redirect( $this->import_redirect( [
|
||||
'sftec_created' => $created,
|
||||
'sftec_skipped' => $skipped,
|
||||
'sftec_errors' => $errors,
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
set_transient( $transient_key, [
|
||||
'created' => $created,
|
||||
'skipped' => $skipped,
|
||||
'errors' => $errors,
|
||||
], 60 );
|
||||
|
||||
private function import_redirect( array $args ): string {
|
||||
return add_query_arg(
|
||||
array_merge( [ 'post_type' => self::CPT, 'page' => 'sftec-import-speakers' ], $args ),
|
||||
admin_url( 'edit.php' )
|
||||
);
|
||||
wp_redirect( $redirect_url );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function normalise_name( string $name ): string {
|
||||
$name = strtolower( trim( $name ) );
|
||||
$name = preg_replace( '/^(dr\.?|prof\.?|mr\.?|mrs\.?|ms\.?|miss\.?)\s+/u', '', $name );
|
||||
return preg_replace( '/\s+/', ' ', $name );
|
||||
$name = preg_replace( '/^(dr\.?|prof\.?|mr\.?|mrs\.?|ms\.?|miss\.?)\s+/u', '', $name ) ?? $name;
|
||||
return preg_replace( '/\s+/', ' ', $name ) ?? $name;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -540,16 +573,22 @@ final class Speakers_For_TEC {
|
|||
// -------------------------------------------------------------------------
|
||||
|
||||
public function admin_notices(): void {
|
||||
if ( ! isset( $_GET['sftec_created'] ) && ! isset( $_GET['sftec_err'] ) ) {
|
||||
$transient_key = 'sftec_import_result_' . get_current_user_id();
|
||||
$result = get_transient( $transient_key );
|
||||
|
||||
if ( ! is_array( $result ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_GET['sftec_err'] ) ) {
|
||||
delete_transient( $transient_key );
|
||||
|
||||
if ( isset( $result['err'] ) ) {
|
||||
$msgs = [
|
||||
'no_file' => __( 'No file was uploaded.', 'speakers-for-tec' ),
|
||||
'read_fail' => __( 'Could not read the uploaded file.', 'speakers-for-tec' ),
|
||||
'no_file' => __( 'No file was uploaded.', 'speakers-for-tec' ),
|
||||
'read_fail' => __( 'Could not read the uploaded file.', 'speakers-for-tec' ),
|
||||
'invalid_type' => __( 'The uploaded file is not a valid CSV.', 'speakers-for-tec' ),
|
||||
];
|
||||
$msg = $msgs[ $_GET['sftec_err'] ] ?? __( 'An unknown error occurred.', 'speakers-for-tec' );
|
||||
$msg = $msgs[ $result['err'] ] ?? __( 'An unknown error occurred.', 'speakers-for-tec' );
|
||||
printf(
|
||||
'<div class="notice notice-error is-dismissible"><p>%s %s</p></div>',
|
||||
esc_html__( 'Import failed:', 'speakers-for-tec' ),
|
||||
|
|
@ -559,14 +598,19 @@ final class Speakers_For_TEC {
|
|||
}
|
||||
|
||||
printf(
|
||||
'<div class="notice notice-success is-dismissible"><p>'
|
||||
. __( 'Import complete — <strong>%d created</strong>, %d skipped (already exist), %d errors.', 'speakers-for-tec' )
|
||||
. '</p></div>',
|
||||
(int) $_GET['sftec_created'],
|
||||
(int) $_GET['sftec_skipped'],
|
||||
(int) $_GET['sftec_errors']
|
||||
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
|
||||
wp_kses(
|
||||
sprintf(
|
||||
/* translators: 1: number of speakers created, 2: number skipped, 3: number of errors */
|
||||
__( 'Import complete — <strong>%1$d created</strong>, %2$d skipped (already exist), %3$d errors.', 'speakers-for-tec' ),
|
||||
(int) $result['created'],
|
||||
(int) $result['skipped'],
|
||||
(int) $result['errors']
|
||||
),
|
||||
[ 'strong' => [] ]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
new Speakers_For_TEC();
|
||||
add_action( 'plugins_loaded', fn() => new Speakers_For_TEC() );
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue