=== DWD Events Calendar ===
Contributors: eventscalendar
Tags: calendar, events, ical, shortcode, rss
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.5.61
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A full-featured event calendar for WordPress. You can submit and moderate events, connect calendar feeds, and import data files.

== Description ==

* The GNU General Public License v2 is included as license.txt in the plugin directory.
* Shortcodes: `[ec_calendar]`, `[ec_submit_event]`, `[ec_submit_event_modal]`, `[ec_event id="123"]`, `[ec_upcoming_events]`, `[ec_event_stats]` — full attribute list under **DWD Events Calendar → Shortcodes** in the admin.
* Registered users with the `ec_submit_events` capability can submit pending events (optional **email confirmation link** before the pending event is created; Administrators are exempt).
* Administrators use the DWD Events Calendar menu to approve/deny, manage categories, subscribe to iCal or RSS/Atom feed URLs, and import ICS/CSV (file or ICS URL).
* Uninstall removes plugin options, scheduled cron, all events, and event category terms.
* Optional admin debug (?plugin_debug=1): disable site-wide with `define( 'EC_ENABLE_PLUGIN_DEBUG', false );` in wp-config.php, or the `ec_enable_plugin_debug` filter.
* **Settings** (DWD Events Calendar → Settings): calendar visibility (public vs private for logged-in-only), calendar query cache TTL, submission rate limit, optional **email verification** for front-end submissions (off for users with `manage_options`), default auto-sync interval (4 / 8 / 12 / 24 hours) for **new** feed subscriptions, optional emails to submitter and admin after a submission is created, **social share** toggles for the calendar event detail popup (Facebook, X, LinkedIn, Email, TikTok, Copy link).
* **Export** (DWD Events Calendar → Export): download all events as CSV or published events as a combined ICS file.
* **Backup & restore** (DWD Events Calendar → Backup & restore): download one JSON file with all plugin options, subscriptions, appearance, categories, and every event (all statuses and meta) for full site migration; restore on another install with optional settings-only mode.
* **Privacy** (Tools → Export / Erase personal data): exporter and eraser for events the user submitted; suggested privacy policy text is registered for the site Privacy Policy editor.
* **Tools** (DWD Events Calendar → Tools): run database upgrade / required-page sync (administrators), optional debug links (?plugin_debug=1), and **Shortcode placement finder** (lists stored `ec_*` shortcodes in post content, post meta, and options — use when a builder or global template injects the calendar site-wide), aligned with the admin debug notice.
* **REST:** `GET /wp-json/ec/v1/event/{id}` (JSON card for the popup) and `GET /wp-json/ec/v1/event/{id}/ics` (single-event `.ics` download). Logged-in users: `GET/POST /wp-json/ec/v1/me/new-event-notifications` (optional email alerts when new events publish — requires **Settings → Email notifications →** allow calendar subscriptions).

== Privacy and external requests ==

* **Feeds and imports:** When an administrator configures iCal/RSS/Atom subscriptions, ICS/CSV import from a URL, or runs sync, your server fetches the URLs you provide using the WordPress HTTP API (outbound requests to those hosts only for the data you asked to load).
* **Maps:** The Leaflet library (JavaScript and CSS) is **bundled in this plugin** (no public CDN for Leaflet). When a page shows an event map, the visitor’s browser requests map tiles from [OpenStreetMap](https://www.openstreetmap.org/) tile servers.
* **Social share (event popup):** Share links open third-party sites in the **visitor’s browser** when the visitor chooses a network; the plugin does not “phone home” for analytics. Toggle networks under **Settings → Social share (event popup)**.
* **Optional daily digest (blog posts):** If enabled in **Settings**, WP-Cron may create **normal WordPress posts** summarizing upcoming events. No off-site “tracking” request is used for that feature; it reads events from your own site database.
* **New event email alerts:** If enabled, subscribed users can receive **email from your site** when an event is first published (handled by WordPress; no third-party email API is required).
* **Optional footer credit:** A small public-site credit link is **off by default**. Turn it on under **DWD Events Calendar → Appearance** if you want it. Advanced customization: `ec_calendar_credit_html` (see Developer hooks).

== Frequently Asked Questions ==

= Does this plugin load JavaScript or CSS from a public CDN? =

No. Front-end and admin assets ship **inside the plugin** (for maps, the Leaflet library is under `assets/vendor/leaflet/`). Map **tiles** may still load from OpenStreetMap when a map is shown (visitor’s browser, documented above).

= Does the plugin “track” visitors or send analytics to the plugin author? =

No. There is no analytics beacon, no advertising SDK, and no data sent to DWD for telemetry. Feeds, imports, and map tiles only contact the URLs **you** configure or the documented map tile service (see **Privacy and external requests**).

= Is the plugin open source? =

Yes. It is distributed under the **GPLv2 or later**. See the plugin header and `license.txt`.

== WordPress.org plugin directory ==

If you plan to list this plugin in the [WordPress.org plugin directory](https://wordpress.org/plugins/), pick a **distinct** public name and slug (for example **`dwd-events-calendar`**, which is not in use as of the last check). Search the directory and read the [detailed plugin guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) before submission. A very generic name may require a more specific public name (Guideline 17). The install folder in `wp-content/plugins/` can stay `events-calendar`; the directory **slug** is chosen when you submit to WordPress.org.

== Developer hooks ==

* `ec_plugin_settings_defaults` — merge default option values (includes `default_subscription_sync_hours`, `submit_email_verification`).
* `ec_calendar_cache_ttl_seconds` — override cached month query TTL (after settings value).
* `ec_rest_event_card_cache_max_age` — `Cache-Control` max-age for the event HTML JSON endpoint (default 300).
* `ec_rest_event_ics_cache_max_age` — `Cache-Control` for the per-event ICS endpoint (default 3600).
* `ec_submission_captcha_error` — pass `( null, array( 'user_id' => … ) )`; return a non-empty string to block submission and show the message.
* `ec_skip_submission_email_verification` — pass `( false, $user_id )`; return true to skip the confirmation email (in addition to users with `manage_options`).
* `ec_event_submission_verify_url` — filter the confirmation link URL `( $url, $token, $user_id )`.
* `ec_event_submission_verify_landing_url` — filter the post-login redirect target when a guest opens the confirmation link `( $url, $token )`.
* `ec_export_events_posts` — filter posts array before CSV/ICS export (`$format` is `csv` or `ics`).
* `ec_full_backup_data` — filter the full backup array before JSON download.
* `ec_full_backup_restore_before` — filter decoded backup data before restore; second argument is whether full replace mode is running.
* `ec_calendar_credit_html` — filter the footer credit HTML when **DWD Events Calendar → Appearance → Show optional credit link** is enabled; return empty string to remove it.
* `ec_frontend_admin_links` — embedded front-end admin shortcut links on calendar/submit/list/stats views (array of `label` / `url`).
* `ec_enable_plugin_debug` — allow/disallow the `?plugin_debug=1` admin panel.
* `ec_submit_event_roles` — roles that receive the `ec_submit_events` capability (see `class-post-types.php`).
* `ec_force_show_maintenance_button` — return true to show **Run update / repair pages** even when no migration/repair is detected (emergency use).
* `ec_event_stats_snapshot` — filter the stats array used by `[ec_event_stats]` and the dashboard.
* `ec_rss_feed_parsed_events` — filter event rows parsed from an RSS/Atom subscription before upsert.
* `ec_event_dedupe_hash` — filter the md5 fingerprint used to detect duplicate external events (normalized title + GMT start/end; optional location arg for the filter only).
* `ec_map_feed_label_to_event_category` — pass `( true, $label, $post_id )`; return false to skip turning that feed label into an **Event Category** term.
* `ec_feed_labels_excluded_from_auto_categories` — filter the default list of system labels that never become categories (CSV import, empty-subscription fallbacks, etc.). ICS imports use `(ICS Import) …` labels and **do** get categories unless you skip them with `ec_map_feed_label_to_event_category`.
* `ec_event_delete_redirect_url` — after **Delete** from the calendar modal, adjust the validated front-end URL (same host, not wp-admin).
* `ec_event_trash_redirect_url` — legacy alias, runs after `ec_event_delete_redirect_url` (same signature).
* `ec_calendar_private_visibility_can_view` — when visibility is **Private**, override who may view published events (default: any logged-in user).
* `ec_event_social_share_items` — filter share rows for the event popup (`key`, `label`, `url` or `copy_url` for clipboard).
* `ec_event_show_social_share` — allow or block the share block per event when rendering the modal card.
* `ec_subscription_sync_health` — filter rows used for feed-error admin notices and the Subscriptions menu badge `( $row, $sub )`; set `suppress_notice` => true on `$row` to hide a subscription from those surfaces.
* `ec_new_event_notification_recipient_user_ids` — filter user IDs emailed when an event first publishes `( $user_ids, $post )`.
* `ec_new_event_send_notification_to_user` — return false to skip one recipient `( true, $user_id, $post )`.
* `ec_new_event_notification_email_subject` / `ec_new_event_notification_email_body` — filter plain-text email `( $subj_or_body, … )`.
* **Programmatic iCal storage:** `ec_store_ical_link_daily_sync( $label, $url, $sync_immediately )` — saves the URL like **Feed subscriptions**, sets default new-subscription interval to **24 hours** and adds the feed with a 24-hour interval, optionally runs `sync_one` now. `ec_set_feed_sync_interval( 'daily'|'hourly'|'twicedaily' )` maps to default hours (24 / 4 / 12). Prefer `ec_set_default_subscription_sync_hours( 4|8|12|24 )`.

== Changelog ==

= 1.5.61 =
* **Multisite:** Network activation and new-site bootstrap run per-blog upgrades, pages sync, cron, and rewrites; uninstall loops all sites.

= 1.5.60 =
* **Feeds:** Yodel syncs run via a background WP-Cron job so large JSON exports are not killed by host ~300s proxy limits.
* **Admin:** “Sync now” on Yodel subscriptions queues the same background job.

= 1.5.59 =
* **Fix:** Feed subscription update REST no longer lowercases subscription ids (`sanitize_key`), which blocked edits/sync for mixed-case ids.

= 1.5.58 =
* **Admin API:** Feed subscription update accepts POST as well as PUT (some hosts block PUT).

= 1.5.57 =
* **Feeds:** Yodel widget JSON export (`my.yodel.today/.../json_v1/{id}`) as a subscription type — used by Destination Mansfield–style calendars that have no public iCal. Sync streams large exports, imports upcoming events (90-day window, 500 max).
* **Admin API:** `PUT /wp-json/events-calendar/v1/feed-subscriptions/{id}` to update label/URL/type/interval and optionally sync now.

= 1.5.56 =
* **Admin API:** `POST /wp-json/events-calendar/v1/feed-subscriptions` for managers — add an iCal or RSS/Atom subscription (optional immediate sync). Status endpoint unchanged.

= 1.5.55 =
* **Admin API:** `GET /wp-json/events-calendar/v1/feed-subscriptions/status` for managers — labels, hosts, and last sync errors/warnings (no stored feed URL secrets).

= 1.5.54 =
* **Fix:** Dashboard and admin notice links to Feed subscriptions (and other unified sections) now open the correct panel. Hash-only anchors left closed `<details>` sections hidden; links use `ec_section` and open the matching section.

= 1.5.53 =
* Security: ignore unsigned heartbeat site_token.

= 1.5.52 =
* Security: follow feed redirects manually with public-host checks each hop.
* Security: signed-only remote wipe client.

= 1.5.49 =
* **Fix:** “See more” day-events popup (and other modals) had a transparent panel when moved to the page footer for theme/builder compatibility — design tokens and appearance colors now apply on `.ec-modal` as well as `.ec-root`.

= 1.5.48 =
* **Appearance:** **Calendar max width** and **Calendar alignment** under Layout — control how wide the public calendar grid grows and how it sits in the theme content area (rem, px, or %).

= 1.5.47 =
* **Fix:** Front-end admin Settings (and other shortcuts) pointed at retired `ec-events-*` page slugs; links now use the unified `ec-events` screen. Hidden legacy slugs redirect for old bookmarks.

= 1.5.46 =
* **DWD Plugins:** Events Calendar only adds its submenu under `dwd-plugins`. Activate the **DWD Plugins Hub** plugin for the shared top-level menu and hub screen.

= 1.5.45 =
* **Admin navigation:** Events Calendar now appears under the shared **DWD Plugins** parent and uses a single product submenu label.
* **Admin compatibility:** Legacy Events Calendar admin page slugs now redirect to the unified Events Calendar admin page with section anchors.

= 1.5.44 =
* **Daily digest:** Post a single daily digest at a configurable time, listing all events happening tomorrow.

= 1.5.43 =
* **Settings:** Daily events auto-post — choose a **featured image** for both digest posts.

= 1.5.42 =
* **Settings:** Daily events auto-post — choose a **WordPress post category** for both digest posts (default unchanged: uses Writing settings default category).

= 1.5.41 =
* **Admin / Tools:** **Shortcode placement finder** — scans post content, post meta, and `wp_options` (capped) for stored DWD Events Calendar shortcodes so you can locate TagDiv global templates, widgets, or other options that render the calendar on every page. Plugin code does not inject the calendar globally; remove or move the shortcode from the listed template/option.

= 1.5.40 =
* **Admin:** Added Plugins list row links for **Tools** and (when enabled) **Debug**.

= 1.5.39 =
* **Fix:** Avoid activation/update fatals on unusual hosts where WordPress core defaults are missing: define `EMPTY_TRASH_DAYS` (30) and `WP_POST_REVISIONS` (true) if not defined before running migrations.

= 1.5.38 =
* **Fix:** Prevent fatal errors if a host deploy leaves an old cached `EC_Frontend` class definition (OPcache) while hooks update: builder-compat hooks now use closures with `method_exists` guards.

= 1.5.37 =
* **Front end:** Builder compatibility: if Events Calendar shortcodes run after `wp_head`, the plugin prints a fallback `<link>` for `frontend.css` in `wp_footer` so the calendar grid keeps its layout.

= 1.5.36 =
* **Front end:** TagDiv Composer compatibility: broader scan of TagDiv meta keys (including `tdc_*` / `tdb_*`) to detect Events Calendar shortcodes early so CSS/JS enqueue before `wp_head`.

= 1.5.35 =
* **Front end:** Detect Events Calendar shortcodes inside TagDiv Composer post meta (`tdc_content` / `tdb_content`) so CSS/JS enqueue before `wp_head` and the calendar grid renders correctly.

= 1.5.34 =
* **Admin:** New **Shortcodes** screen (DWD Events Calendar → Shortcodes) with a table of all public shortcodes, what they output, and attributes. Linked from the plugin dashboard, debug “Direct links” list, and Tools/debug workflows.

= 1.5.33 =
* **Front end:** Calendar modals (submit, day list, event detail, email alerts) are moved to `document.body` on load so `position: fixed` is not trapped by theme/page-builder `transform` ancestors — avoids broken overlays and odd stacking with the site header. Sync modal-open class on `html` and `body` for scroll lock. `?ec_date=` auto-scroll is gentler and only when the day panel is off-screen; day panel `scroll-margin-top` increased for sticky theme headers.

= 1.5.32 =
* **Coding standards:** Obsolete file cleanup uses `wp_delete_file()` instead of `unlink()`.

= 1.5.31 =
* **Updates:** On activation, after **Plugins → Update**, and when running the dashboard/Tools **Update / repair** path, the plugin removes allowlisted legacy files still on disk from older releases (for example `ec-standalone-launcher.php` and `includes/ec-standalone-bootstrap.php`). New packages no longer ship those files.

= 1.5.30 =
* **Plugin Check:** Replaced legacy `ec-standalone-launcher.php` with a no-output stub (ABSPATH guard only) so updates overwrite old uploads and the file passes direct-access checks. **Readme:** moved the license sentence out of the short-description block (WordPress.org ≤150 characters for everything before `== Description ==`).

= 1.5.29 =
* **Plugin Check / PHPCS:** Scoped `phpcs:disable` / `phpcs:enable` so **inner** `meta_query` / `meta_key` / `tax_query` / `get_users` lines are covered; moved migration `$wpdb` ignores to the line **above** `get_col` / `get_results`; backup `ec_backup_msg` GET, submit-form `ec_submit` GET, and Tools submenu `$_GET['page']` use disable blocks or line-accurate ignores. VIP `post__not_in` / `tax_query` covered where the checker reports them on month/upcoming/dedupe queries.

= 1.5.28 =
* **Plugin Check / PHPCS:** Completed suppression and input-handling pass: backup admin notice (avoid `! empty( $_GET )` before sanitize), import handler `phpcs:disable` scope, nonces/GET annotations on admin, appearance, front-end delete, email verify link, and submit form; `WP_Query` / `get_users` / migration `$wpdb` lines documented; `save_subscriptions_from_post` disables `NonceVerification` with `ValidatedSanitizedInput` where `check_admin_referer` is outside the private method. Readme short description in standard English (directory requirement).

= 1.5.27 =
* **Removed** `ec-standalone-launcher.php` and `includes/ec-standalone-bootstrap.php`. Activate the plugin from **wp-admin** (or **WP-CLI** / host tools) after upload; the one-off URL bootstrap is no longer shipped.

= 1.5.26 =
* **WordPress.org / Plugin Check:** Readme short description in plain English and under 150 characters. Removed `load_plugin_textdomain` (core loads language packs for plugins hosted on WordPress.org 4.6+). PHPCS/Plugin Check: nonce and input notes on admin GET, import POST, `redirect_to`, slow-query blocks, direct DB in migrations, and related templates.

= 1.5.25 =
* **Upgrade (schema 6):** Renaming the default calendar page title no longer uses `wp_update_post`, so the migration does not touch the revisions API. Fixes a **fatal error** on some sites where `WP_POST_REVISIONS` is undefined during `plugins_loaded` (revision.php). Title change is still applied via a direct `posts` table update and `clean_post_cache`.

= 1.5.24 =
* **Plugin Check / WordPress.org:** Replaced `languages/.gitkeep` with `languages/index.php` (no hidden files). Standalone launcher: `ABSPATH` guard, safe path output before WordPress loads, HTTP 500 without relying on `status_header` when core is missing. Coding standards: `wp_strip_all_tags` where WPCS requires it; hour/minute `<option>` output uses `esc_attr`/`esc_html`; export CSV stream `fclose` and `$_POST` export format documented for PHPCS; narrow `phpcs:disable`/`ignore` where `ec_sanitize_request_*` already sanitizes superglobals. **Readme:** short description ≤150 characters (license line moved).

= 1.5.23 =
* **WordPress.org / Plugin Check:** Removed non-runtime **`.md` documentation** from the installable plugin tree (long-form guides moved to the product **`docs/`** folder next to `events-calendar/` in this repository, not in the distributed ZIP). Updated **`readme.txt`** so the standalone launcher and changelog do not reference removed files.
* **Changelog text:** Dropped a historical line that pointed at a private editor path (not part of the plugin package).

= 1.5.22 =
* **WordPress.org guidelines — documentation & packaging:** Added full **`license.txt`** (GNU GPL-2.0), **`Domain Path: /languages`** in the main plugin header with an empty `languages/` directory for future translations, and **`readme.txt`** updates: **Tested up to** WordPress **6.9**, **FAQ**, expanded **Privacy** (daily digest + email alerts), and explicit note that scripts/styles are not loaded from a public CDN (Leaflet bundled locally).
* **WordPress default libraries (Guideline 13):** Appearance admin script dependencies explicitly include **`jquery`** (via core-registered script) in addition to **`wp-color-picker`**.

= 1.5.21 =
* **Branding (DWD):** Plugin name and author updated to **DWD Events Calendar** / **DWD**; admin notices, settings headings, and privacy text use the new name. Intended WordPress.org slug: **`dwd-events-calendar`** (search showed no existing plugin with that slug).
* **Data migration (schema 6):** If the required calendar page still had the old English title “Events Calendar”, it is updated to **DWD Events Calendar** on upgrade.

= 1.5.20 =
* **WordPress.org guidelines alignment:** Leaflet 1.9.4 is **bundled** in `assets/vendor/leaflet/` (no third-party Leaflet JS/CSS from a CDN). Map tiles may still load from OpenStreetMap when maps are shown (documented in **readme.txt** under Privacy).
* **Public calendar footer credit:** **Appearance → Show optional credit link** under the calendar; **off by default** (opt-in). The `ec_calendar_credit_html` filter still applies when the option is on.
* **Readme:** Privacy / external requests section, plugin directory naming note, and `Tags` header (max 5) for directory metadata.

= 1.5.19 =
* **New event email alerts (logged-in users):** On pages with `[ec_calendar]`, a **New event email alerts** control opens a **modal** to subscribe or unsubscribe. When an event is **first published** (including moderation approval), subscribed users get a plain-text email with title, start time, and link. **Settings → Email notifications** includes a master toggle. **REST:** `GET`/`POST /wp-json/ec/v1/me/new-event-notifications` (cookie + `X-WP-Nonce`). Personal data export/erase includes this preference. Developer filters: `ec_new_event_notification_recipient_user_ids`, `ec_new_event_send_notification_to_user`, `ec_new_event_notification_email_subject`, `ec_new_event_notification_email_body`.

= 1.5.18 =
* **Feed subscription health:** admin **notice** (all screens, dismissible per user per error “signature”) when an **enabled** subscription has a **last sync error**; **DWD Events Calendar** dashboard card with the same list; **Subscriptions** submenu **count badge**; **Status** column uses semantic colors and shows **warnings** when a sync returned a non-trivial body but **0 events** were imported. HTTP failures now include response phrase and short hints (e.g. 401/403 access denied, 429 rate limit, 5xx server). Developer filter: `ec_subscription_sync_health`. **Feed subscriptions** table: green / yellow / red **check** beside each label (OK, warning only, error).

= 1.5.17 =
* **Workspace standards:** `upgrader_process_complete` runs migrations and page sync only when **this** plugin was updated (not on every plugin update).
* **Tools screen:** new **DWD Events Calendar → Tools** with **Run update / repair** (same action as the dashboard) and a **Debug** section listing admin URLs with `?plugin_debug=1` (matches the global debug notice). Maintenance run from Tools returns to the Tools screen.
* **Privacy Policy:** suggested text via `wp_add_privacy_policy_content` (export/erase and Tools reference).
* **Front-end admin strip:** link to **Tools**.

= 1.5.16 =
* **Front-end admin strip:** hide the bottom-right admin shortcuts on viewports under 48rem (phones); visible from tablet / desktop widths.

= 1.5.15 =
* **Mobile calendar (≤40rem):** compact month grid — day number plus an event-count badge; event titles and “see more” stay off the tiny cells. Scrollable **Events this month** agenda under the grid (by day, time + title + source) with tappable rows that open the same event popup. Month header uses a simple prev / title / next grid. Modals use full-viewport height on small screens for easier reading.

= 1.5.14 =
* **Front-end assets:** enqueue calendar CSS/JS on the `wp` hook when the main post contains a DWD Events Calendar shortcode (shortcodes run after `wp_enqueue_scripts` on typical themes, so styles and `frontend.js` never loaded and the grid/modals looked broken). Shortcode callbacks also trigger enqueue for sidebars/widgets. Leaflet still loads when the page has `[ec_calendar]` / `[ec_event]` or when maps are needed.

= 1.5.13 =
* **Calendar event popup — social share:** share row (Facebook, X, LinkedIn, Email, TikTok, **Copy link**) in the REST-loaded detail modal only. **Settings → Social share (event popup)** turns each network on or off. TikTok opens the TikTok upload page (paste your link there). Copy link uses the clipboard with a screen-reader status message. Developer filters: `ec_event_social_share_items`, `ec_event_show_social_share`.

= 1.5.12 =
* **Backup & restore:** new admin screen with **Download JSON backup** (all plugin options including subscriptions and encrypted feed URLs, appearance, event categories, and all `ec_event` posts with every post meta—publish, pending, draft, private, future, rejected, trash) and **Upload and restore**. Full restore replaces existing events and categories on this site, then reapplies options and imports data; calendar shortcode pages are re-synced for the target site (page IDs from the old site are not reused). Uncheck “Replace…” to apply **settings only** from the backup without changing events. Filters: `ec_full_backup_data`, `ec_full_backup_restore_before`.

= 1.5.11 =
* **Email confirmation:** after opening the confirmation link, the submit form shows a **Thank you for confirming** success notice (distinct from the generic submission message when verification is off).

= 1.5.10 =
* **Request sanitization:** all plugin reads from `$_POST` / `$_GET` for user data now go through shared helpers in `includes/functions.php` (`ec_sanitize_request_line`, `ec_sanitize_request_textarea`, `ec_sanitize_request_feed_url`, hour/minute clamps, appearance array helper). Feed and ICS URLs are never passed through `sanitize_text_field`.

= 1.5.9 =
* **ICS file import label:** Events store **`_ec_ics_import_source_name`** (file base name, URL host, or your Import label). Listings and categories replace generic **“(ICS Import) File”** / **URL** placeholders with **`(ICS Import) {that name}`**. Re-import the same `.ics` once to fill the meta for events that only had the old generic text.

= 1.5.8 =
* **ICS import label:** Events → Import includes **ICS import label** (optional). Stored and shown as **`(ICS Import) your name`** on the calendar; the same string is used for the **Event Category** term (auto-created). Blank label uses the ICS **URL host** or **uploaded file name**. Legacy “ICS file import” / “ICS URL import” strings are upgraded to the new format (database schema **5**) and get categories.

= 1.5.7 =
* **Feed subscriptions → categories:** each subscription’s **label** is synced to **Event Categories** (`ec_event_category`). Events merged from several feeds get **multiple** categories (one per distinct label). Terms are **created automatically** when missing. Generic import labels and empty-feed fallbacks (“Calendar feed”, “RSS feed”) are skipped; use filters `ec_map_feed_label_to_event_category` and `ec_feed_labels_excluded_from_auto_categories` to customize. Database schema **4** assigns categories for existing events on upgrade.

= 1.5.6 =
* **Calendar modal — Delete:** fix “Invalid event” by building the delete URL without `wp_nonce_url()` HTML encoding (`&amp;`), so `post_id` and the nonce parse correctly in the browser when the link is opened from REST JSON.

= 1.5.5 =
* **Upgrade fix:** DB migration 3 defines `EMPTY_TRASH_DAYS` (WordPress default 30) when missing so `wp_trash_post()` does not fatal on hosts where core’s default constants never ran.

= 1.5.4 =
* **Cross-feed dedupe:** duplicate detection uses normalized title + GMT start/end only (location ignored) so the same event from multiple iCal/RSS subscriptions or ICS imports collapses to one post. Database schema **3**: existing events get rebuilt fingerprints; extra duplicate posts are moved to the trash (lowest ID kept); feed labels are backfilled where possible.
* **Source calendar names:** subscription label (or ICS/CSV import label) is stored and shown on the day list, “See more” modal, upcoming list, and event card when present. Multiple sources appear separated by a middle dot.

= 1.5.3 =
* **Submissions:** optional **email verification** (Settings → Email): for users without `manage_options`, the pending event is **not** created until they click a link sent to their **account email** (48-hour token). Administrators are exempt. New hooks: `ec_skip_submission_email_verification`, `ec_event_submission_verify_url`, `ec_event_submission_verify_landing_url`. Front-end notices for `verify_sent` / invalid link / wrong account; submit modal auto-opens when `ec_submit` is in the URL. Settings checkboxes use hidden `0` values so options persist when saving other fields.

= 1.5.2 =
* **Calendar modal:** **Delete** now **permanently removes** the event (`wp_delete_post` with force), not the trash. Label and confirm text updated. New filter `ec_event_delete_redirect_url`; `ec_event_trash_redirect_url` kept for backward compatibility. `ec_get_calendar_event_delete_url()` replaces trash-specific URL helper (deprecated `ec_get_calendar_event_trash_url`).

= 1.5.1 =
* **Calendar modal:** **Delete** permanently removes the event and returns you to the **current calendar page** (`admin-post` action `ec_delete_event` + `redirect_to`). Filters `ec_event_delete_redirect_url`, `ec_event_trash_redirect_url` (legacy).

= 1.5.0 =
* **Feed subscriptions:** edit label, URL, feed type, auto-sync interval (**4 / 8 / 12 / 24 hours**), and **auto-sync on/off** per subscription. WP-Cron runs **hourly** and only syncs feeds that are due (and still have auto-sync on). **Sync now** always runs manually. Settings → default interval applies to **new** subscriptions only. Legacy `feed_sync_interval` is migrated to hours on upgrade (DB schema 2). Helpers: `ec_get_allowed_subscription_sync_hours()`, `ec_set_default_subscription_sync_hours()`.

= 1.4.9 =
* **Google Calendar:** admin copy on Feed subscriptions and Import points to Google’s “Secret address in iCal format” (HTTPS / webcal).
* **Duplicates:** stable ICS import keys so re-importing the same file or URL updates events; fingerprint meta (`_ec_dedupe_hash`) merges external upserts with the same title/time/location; CSV rows and front-end submissions skip/blocked when a matching published, pending, or draft event exists. Importing ICS after a feed subscription updates the existing row without replacing its subscription keys. Filter `ec_event_dedupe_hash`.

= 1.4.8 =
* **API:** `ec_store_ical_link_daily_sync()` stores an iCal/webcal URL (same encrypted storage as Events → Subscriptions), switches feed sync to **once daily** (WP-Cron), and can run an immediate sync. `ec_set_feed_sync_interval()` updates the cron recurrence only.

= 1.4.7 =
* **Settings → Calendar visibility:** **Public** (default) or **Private**. Private hides event shortcodes, month/upcoming queries, single event and archive URLs, category archives, REST event endpoints, and Open Graph meta from guests; direct event links redirect to login. Site search omits events for guests. `[ec_submit_event]` stays available (use the filter to require stricter caps). Helper: `ec_user_can_view_published_events()`.

= 1.4.6 =
* **webcal://** and **webcals://** links are normalized to **https://** everywhere (subscriptions, ICS import URL, remote validation). Subscription and import URL fields use `type="text"` so browsers do not block webcal.
* **Settings → Feed subscription sync:** choose **hourly**, **twice daily**, or **daily** for WP-Cron automatic updates; changing the option reschedules the sync hook.
* **Import:** optional checkbox to **add the ICS URL to Feed subscriptions** for ongoing auto-updates (runs an immediate sync after import). Duplicate URLs are detected and skipped.

= 1.4.5 =
* **Import:** clear admin notices — **success** (count), **warning** when zero rows imported, or **error** on failure; no conflicting “success” after a CSV header or upload error. Stricter validation: ICS uploads checked like URLs (size, VCALENDAR/VEVENT, rejects PHP-tagged files); per-event GMT dates and UID fragments sanitized; caps on ICS event count and CSV rows; CSV headers allowlisted (`sanitize_key`) with only known columns mapped; required CSV columns title, start, end, location; field length limits.

= 1.4.4 =
* **Import:** paste a public **ICS URL** (https or webcal://, same SSRF-safe rules as subscriptions) on Events → Import; fetches the calendar and imports VEVENTs like an uploaded .ics. Source meta `ical_url`. CSV still file-only. File upload and ICS link cannot be used in the same submit.

= 1.4.3 =
* **Subscriptions:** add **RSS or Atom** feed type alongside iCal. Each item with a parseable date becomes an event (title, description, link context; optional `location`/`venue`/similar child elements; start from pubDate, Atom dates, or common `start`/`end` tags; default 1-hour duration). Filter `ec_rss_feed_parsed_events` to customize. Remote URL validation uses the same SSRF-safe rules as iCal (`ec_validate_remote_feed_url`).

= 1.4.2 =
* Calendar event popup: logged-in users who may edit or delete the event see **Edit event** and **Delete** in the modal (permanent delete with nonce; confirmation dialog). REST response is not publicly cached when a user session is present.

= 1.4.0 =
* Dashboard: **Run update / repair pages** appears only when the database schema is behind, the stored plugin version does not match the installed code, or a required calendar page is missing or missing its shortcode (filter `ec_force_show_maintenance_button` to force-show).
* Dashboard: **At a glance** stats — published total, pending, published overlapping current month and current calendar year (site timezone).
* Shortcode `[ec_event_stats]` for the block editor (Shortcode block) or classic content: `items`, `layout` (grid/list), `year`, `month`, `category`.
= 1.3.0 =
* Accessibility: modal focus traps (Tab cycles inside dialog); `aria-live` status on event popup; arrow keys move between day numbers when a day link is focused; clearer empty states with optional submit CTAs.
* Performance: optional transient cache for month calendar queries (Settings); `Cache-Control` on event REST responses; cache invalidates when events are saved or deleted.
* SEO / sharing: Open Graph and Twitter Card meta on single event pages.
* Spam / abuse: hidden honeypot field on submit form; per-user rate limit (Settings); `ec_submission_captcha_error` filter for reCAPTCHA, hCaptcha, or custom checks.
* Email: optional confirmation to submitter and notice to admin email when a pending event is created (Settings).
* Shortcode `[ec_upcoming_events]` with `limit` and `category`; per-event “Add to calendar (.ics)” links (REST).
* Admin Export (CSV / ICS); Privacy export and erase for submitted events; DWD Events Calendar → Settings.
* Filter `ec_calendar_credit_html` to customize or remove the Diz Web Design credit.

= 1.2.8 =
* Calendar: credit line with link to Diz Web Design below the calendar.
* Submit form (page view): slightly more horizontal padding so the form clears narrow theme edges.

= 1.2.7 =
* Calendar: clicking an event opens its details in a popup (REST-loaded card); full event page remains available via the footer link, modifier-clicks, or when JavaScript is off.

= 1.2.6 =
* Month view: each day shows up to two events; additional events use “See more (n)” to open a popup with the full list (and a link to the full day view on the page).

= 1.2.5 =
* Admin debug panel: can be fully disabled via constant EC_ENABLE_PLUGIN_DEBUG or filter ec_enable_plugin_debug so the handler is not registered when not needed.

= 1.2.4 =
* Dashboard maintenance: success notice shows plugin version (previous → current) and database schema version (before → after).

= 1.2.3 =
* Calendar: click a day to open a list of events for that day (multi-day events included); day panel includes Submit an event (same as calendar toolbar). Use “Month view” to clear the day filter.

= 1.2.2 =
* Front-end submit form: clear success and failure confirmation messages (including when error details are no longer available).

= 1.2.1 =
* Submit modal: extra horizontal padding; category under event name; event host and contact fields (email, phone, website); public event view shows host, submitter, and contact details.
* CSV import: optional columns host, contact_email, contact_phone, contact_url.

= 1.2.0 =
* Submit button above the calendar; submission form opens in a modal when the page has only [ec_calendar].
* Front-end admin shortcut panel for Administrators (manage_options) with links to Events screens and WP Admin.
* Event forms use separate date, hour, and minute fields (front-end submit + admin event meta).

= 1.1.0 =
* Admin Appearance screen: colors, radius, shadow, font family/size/line-height for front-end calendar UI.

= 1.0.0 =
* Initial release.
