=== DWD RSSAgg ===
Contributors: rssagg
Tags: rss, feed, import, atom, json, xml
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.2.37
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Import RSS 2.0, RSS 1.0 (RDF), Atom, other item-based XML, and JSON feeds into posts using admin-only campaigns, field mapping, images, filters, and WP-Cron schedules. Outbound HTTP only uses URLs you configure.

== Description ==

DWD RSSAgg helps site administrators import syndicated content into WordPress with predictable behavior and safety-minded defaults.

* Campaign wizard: discover a feed, map fields, configure featured images, preview, then save.
* Incremental imports: optional stop-on-first-duplicate so each run tends to add only new items (see campaign settings).
* Scheduling: manual, every N hours, or daily at a time (WP-Cron; use real server cron for reliability on low-traffic sites).
* REST API `rssagg/v1` for discover, campaigns (including delete), and runs (including dry run), restricted to administrators.
* Tools: database upgrade / repair, debug links, optional data purge on uninstall.

You are responsible for complying with source sites' terms, copyright, and robots rules when republishing content.

== Installation ==

1. Upload the `rssagg` folder to the `/wp-content/plugins/` directory, or install the ZIP via Plugins - Add New - Upload Plugin.
2. Activate the plugin through the Plugins screen in WordPress.
3. Open **DWD Plugins** in the admin menu, choose **DWD RSSAgg**, add a campaign from the wizard, then run an import or configure a schedule.

== Frequently Asked Questions ==

= Who can use DWD RSSAgg? =

Only users with the `manage_options` capability (typically Administrators) can access DWD RSSAgg screens, REST routes, and import actions.

= Does DWD RSSAgg collect visitor data? =

By default it does not collect front-end visitor data. It fetches remote URLs that you configure. See the privacy policy suggested text added under Settings - Privacy.

= Why did my scheduled import not run at the exact time? =

WordPress cron is triggered by site visits unless you set up a system cron that hits `wp-cron.php`. See DWD RSSAgg **Tools** for a short note and a link to the Plugin Handbook cron chapter.

== Screenshots ==

1. Screenshots are not bundled in this repository build; add assets here if you publish to WordPress.org.

== Changelog ==

= 1.2.37 =
* Campaigns: compact list with text-link actions, green/red on/off toggle, and **View feed** modal (live item cards).
* REST: `GET /rssagg/v1/campaigns/{id}/items` read-only feed viewer.

= 1.2.36 =
* Campaigns: clearer **Default featured image for this feed** picker (Media Library) with thumbnail preview on wizard and edit; campaigns list shows the default/fixed image when set.

= 1.2.35 =
* Multisite: network activation/deactivation, new-site bootstrap, per-site uninstall loop.

= 1.2.34 =
* Security: encrypt HTTP Basic passwords at rest; mask in REST/admin; reject unsigned heartbeat site_token.

= 1.2.33 =
* Security: manual redirect following with public-host checks; image sideload host validation.
* Security: signed-only remote wipe client.

= 1.2.31 =
* Featured and in-content images store in DWD Gallery first when that plugin is active; WordPress Media Library is the fallback.

= 1.2.28 =
* **Campaigns:** Turn a campaign On or Off from the campaigns list or edit screen. Off pauses scheduled (WP-Cron) imports; Run now / Dry run still work. Wizard can create a campaign already off. Schema v4 backfills `_rssagg_enabled` (default on) for existing campaigns so cron matching stays reliable.

= 1.2.26 =
* **Recorded version:** `rssagg_installed_version` is set on activation and **Run update / sync** only (hub manual-upload detection).

= 1.2.25 =
* Fix: **Update** / Tools actions and REST no longer require `manage_options` only (hub administrators with `manage_dwd_plugins_hub`).

= 1.2.24 =
* Fix: **DWD Plugins** fly-out shows RSSAgg again for hub administrators (`manage_dwd_plugins_hub`).

= 1.2.23 =
* Fix: admin access under **DWD Plugins** when the hub rewrites fly-out capabilities (restores access for users with `manage_options`).

= 1.2.22 =
* **Campaigns:** Setting **Append donation link on Facebook share (SocialAutoShare)** per campaign (default off). Works with DWD SocialAutoShare donation URL/label; RSSAgg-imported posts use `_rssagg_campaign_id` to decide per campaign.

= 1.2.21 =
* **Admin:** Section navigation (Dashboard, Campaigns, Add campaign, Import history, Tools) uses WordPress **nav-tab** styling for a consistent admin tab bar; improved tap targets and wrapping on narrow viewports.

= 1.2.20 =
* **Admin:** Under **DWD Plugins**, only **DWD RSSAgg** (dashboard) appears in the flyout. Campaigns, Add campaign, Import history, Tools, and campaign edit register as hidden screens; a section nav on each RSSAgg screen links between them. Parent menu highlight unchanged (`parent_file`).

= 1.2.19 =
* **Branding:** Plugin display name is now **DWD RSSAgg** (header, readme, primary admin labels, privacy policy entry name, outbound fetch User-Agent token `DWD-RSSAgg/`). Author line set to **DWD**.

= 1.2.18 =
* **Admin:** All screens register under the shared **DWD Plugins** menu (`dwd-plugins`): RSSAgg home, Campaigns, Add campaign, Import history, and Tools; edit campaign stays a hidden screen. Requires the **DWD Plugins Hub** plugin for the parent menu. Listed on the hub dashboard via `dwd_plugins_hub_additional_plugin_files`. Parent menu highlight uses `parent_file`.

= 1.2.17 =
* **Campaigns:** Delete action (Campaigns list and campaign edit) with confirmation — removes the campaign, its import run log rows, consumed-item duplicate barrier, and RSSAgg meta on posts that were linked to the campaign; does not delete those WordPress posts. **REST:** `DELETE rssagg/v1/campaigns/{id}` (same cleanup; rate limited).

= 1.2.16 =
* **Dashboard:** Statistics overview — imported post totals (all non-trash statuses and published only), posts created today (site calendar day) and in the last 7 days, campaign count, import run log totals (runs, items created/updated). Short-lived cache with refresh after each import run. Filters: `rssagg_dashboard_stats`, `rssagg_dashboard_stats_ttl`.

= 1.2.15 =
* **Post template:** `{author}` and `{authorlink}` now resolve from feed metadata (RSS/Atom/XML: `dc:creator`, Atom `author`, RSS `author`, iTunes author; SimplePie + `dc:creator` fallback; JSON Feed and common JSON shapes). `{authorlink}` falls back to a `mailto:` link when only an email is known, and shows plain text when there is a name but no URL.
* **Post template:** `{permalink}` renders a clickable HTML link (default label: hostname). Use `{permalink_url}` for the raw URL when you need plain text or your own markup.

= 1.2.14 =
* **Duplicate handling:** Each campaign remembers imported feed item keys on the campaign itself. If the WordPress post created from an item is deleted, the item is still treated as already imported (incremental runs stop at the barrier; non-incremental runs skip it). Keys are cleared when you use **Detach campaign**. Migration backfills from existing imported posts on upgrade. Filter: `rssagg_consumed_item_keys_max`.

= 1.2.13 =
* **Substack feeds:** Per-campaign option **Remove Substack image toolbar from post HTML** (wizard step 3 and campaign advanced settings). When enabled, strips Pencraft per-image controls (restack / full-size view) from imported body HTML and previews. Filter: `rssagg_strip_substack_pencraft`.

= 1.2.12 =
* **Featured image from feed:** Per-campaign **which image in slot order** (index into the ordered candidate list from the first feed item). Wizard step 3 and campaign edit show thumbnails with numbered choices; live preview and imports use the selection, with automatic fallback along the list if sideload fails.

= 1.2.11 =
* **HTML:** Strip literal `#image_title` placeholders from imported HTML (common in Substack-style feeds) and from generated excerpts.
* **Campaign edit:** Editable **Feed URL** field (saved with other advanced settings); live preview uses the field value.
* **Featured duplicate in body:** Match Substack CDN variants (`/image/fetch/` with different resize params) to the enclosure/featured URL by normalizing the underlying `substack-post-media…/public/images/…` key; consider `src`, `data-src`, and `srcset`. When a match is found, remove the surrounding **figure** when present so `<picture>`/caption chrome does not remain.

= 1.2.10 =
* **Wizard / campaign save:** Guard against repeated “Save” clicks (button disabled + in-flight flag, pending live-preview debounce cleared) so the REST API is not hammered; reduces duplicate draft campaigns and 429 / “too many requests” from hosts or security plugins.
* **REST:** Per-user burst limits on campaign create (10/minute) and update (40/minute per campaign) return **429** with a clear message instead of unbounded writes.

= 1.2.9 =
* **Import excerpt:** Mapped excerpt/summary is normalized to a short plain-text teaser (`wp_trim_words`, default word count from the `excerpt_length` filter). Feeds that put the full article in `<description>` no longer fill `post_excerpt` with the entire story. Filter: `rssagg_import_excerpt_word_limit`.

= 1.2.8 =
* **XML feeds:** Extract `content:encoded` / Atom `content` without double-escaping HTML. `DOMDocument::saveXML()` on CDATA/text nodes produced `&lt;tag&gt;…` in post bodies; text nodes now use literal data so markup imports as real HTML.

= 1.2.7 =
* **Discover / preview API:** Clear `message` fields when the download fails, the body looks like HTML instead of a feed, or no items parse; discover returns a `warning` when the response parses as a feed but yields zero entries. The wizard status lines show these strings.
* **Live preview:** Fallback title when the mapped title is empty so the visual preview still loads.
* **Wizard:** Back button on steps 2–5 to fix the URL, mapping, images, or review before save.

= 1.2.6 =
* **XML feeds:** Parse syndication XML from the same HTTP response as your campaign fetch (so HTTP Basic and custom headers still apply). Supports RSS 2.0 (`channel/item`), Atom (`feed/entry`), RSS 1.0 (`RDF/item`), and common `channel/item` layouts. Detection adds `rss1` and generic `xml`; unknown XML that yields items is reported as `xml`. WordPress `fetch_feed` remains as a fallback when the DOM parser returns no items.

= 1.2.5 =
* Featured image: **fallback Media Library image** when the feed item has no image (`_rssagg_featured_fallback_attachment_id`). Wizard (step Images) and campaign edit include pickers; edit screen also exposes featured source (feed vs fixed image). Imports, `{image}` in post templates, and previews use the fallback after feed resolution fails.

= 1.2.4 =
* Campaign edit (**Advanced settings**): **Campaign name** field — renames the campaign (WordPress post title) via REST; page heading and live preview title update after a successful save.

= 1.2.3 =
* Campaign edit (**Advanced settings**): **Featured image in body** — toggle the existing “remove featured image from post content when it matches” behavior (same meta as the wizard; default on). Live preview on the edit screen respects the checkbox before save.

= 1.2.2 =
* Campaign edit (**Advanced settings**): **Import schedule** — same options as the wizard (manual, every N hours, daily at hour:minute). Saves via REST; cron events are refreshed like other campaign updates.

= 1.2.1 =
* Per-campaign **Minutes between new posts** (wizard + Advanced settings, 0–720, default 0): when one import run creates more than one **new** WordPress post, the importer **waits** that many minutes after each new post before processing the next story (dry runs unchanged). Uses `sleep()` in the same request—raise PHP `max_execution_time` or reduce max stories per run if the host stops long imports. Filter: `rssagg_import_stagger_seconds`.

= 1.2.0 =
* Import: build the final post body (strip duplicate featured image, attribution block, inline image sideloading) in memory, then save with **at most one** follow-up `wp_update_post`. New posts targeting **Published** are saved as **draft** first, then published in that single follow-up so `transition_post_status` → `publish` runs **once** per post — avoids auto-share plugins (e.g. SocialAutoShare) seeing multiple publish-related saves for the same import.

= 1.1.9 =
* Campaign edit (**Advanced settings**): **Max stories per run** — same limit as in the wizard (`max_items`, 1–500). Each import considers up to that many feed items after ordering; “Stop on first duplicate” can end the run sooner.

= 1.1.8 =
* Campaigns: after **Run now** or **Dry run**, the result stays on screen in a dedicated summary panel (counts, end state, link to history when relevant) until you leave the page — no dismissible admin notice that can vanish immediately.

= 1.1.7 =
* Live preview: always uses the **first story** in the feed (after item order); removed the sample-item control. Preview **post title** heading always reflects the resolved title (including text derived from the post template before `{content}`), so template edits are visible at the top as well as in the body.

= 1.1.6 =
* Dashboard and Tools always show **RSSAgg version** and **database schema**; when a migration is pending, the same block notes the target schema version. Live post preview (campaign edit + wizard) forces preview links to open in a new tab so clicks do not leave wp-admin; wizard source-field table “link” column does the same.

= 1.1.5 =
* Campaign setting **Imported post date**: choose the item’s published date from the feed (default) or the date/time when the item is first imported. Re-imports keep the post date unchanged when using import time; feed-date mode can update the post date if the source timestamp changes.

= 1.1.4 =
* Dashboard / Tools: the maintenance action is labeled “Update”, shown only when a database upgrade (or future sync) is pending, with current vs target schema version and a confirmation that matches those numbers.

= 1.1.3 =
* Post template: when the template contains `{content}` and `{title}` appears in the portion before `{content}`, that segment (e.g. a `Missing: {title}` prefix) is applied to the WordPress **post title** on import, not only to the post body. Live preview uses the same derived title.

= 1.1.2 =
* Import: remote images in post content are sideloaded into the Media Library and `<img src>` is updated to the local attachment URL (cap per post, filter `rssagg_sideload_content_images_max`). Feed featured images were already sideloaded for the thumbnail; the same attachment is reused when that image still appears in the body. Attachments store `_rssagg_sideload_source` so the same remote URL is not downloaded again on later imports.

= 1.1.1 =
* Post template: allowed tags are buttons that insert placeholders at the caret; preview column uses a sticky aside with a scrollable frame on wide screens; preview requests no longer drop updates while a request is in flight; when the template includes `{title}`, the duplicate preview heading is hidden so edits around `{title}` show in the body preview.

= 1.1.0 =
* Live post preview: campaign edit and wizard use a sticky right column showing rendered title, excerpt, featured image, and HTML body (matching import logic). Preview updates on a short debounce as settings change. REST `POST /preview` returns `post_preview` and supports `item_order` (optional `preview_story_index` for API use; admin UI uses the first item).

= 1.0.7 =
* Campaign tags: pick default tags, optional tags from feed labels, optional auto tags from story text (frequency-ranked words with stopwords). Cap for new tag terms per post; filters `rssagg_tag_stopwords` and `rssagg_auto_tag_words_from_story`.

= 1.0.6 =
* Campaign categories meta box: hierarchical checklist, “Add auto categories” from feed labels, optional cap on new terms per post, Quick add link. Category assignment uses the category taxonomy for any post type that supports it.

= 1.0.5 =
* Post template panel styling and copy aligned with the campaign UI; help link to Tools. Wizard preview and campaign save send template fields; preview table can show a template column when custom template is enabled.

= 1.0.4 =
* WordPress Plugin Handbook alignment: WordPress.org-style readme sections; plugin header Domain Path and no placeholder URIs; extension hooks `rssagg_campaign_meta`, `rssagg_import_stories`, `rssagg_import_run_complete`.

= 1.0.3 =
* Fix: PHP parse error in import runner that blocked activation.
* Debug: query-arg debug mode and Plugins row link with diagnostics.

= 1.0.2 =
* Wizard preview with featured and body strip simulation; campaign list next-run hint; discover/importer parsing alignment.

= 1.0.1 =
* Per-campaign post author picker and validation.

= 1.0.0 =
* Initial feature set: wizard, importer, cron, REST, run log, Tools, privacy blurb.

== Upgrade Notice ==

= 1.1.0 =
Campaign editor and wizard show a live visual post preview beside your settings.

= 1.0.7 =
Adds tag assignment and automatic tag suggestions from feed labels and story content.

= 1.0.6 =
Campaign editor and wizard include the new category picker and optional feed-driven category matching.

= 1.0.5 =
Wizard and campaign editor now persist and preview the custom post template correctly.

= 1.0.4 =
Maintenance: documentation and header fields aligned with WordPress plugin guidelines; optional developer hooks documented in readme.
