=== CSV Tables ===
Contributors: 404ops
Tags: table, csv, excel, spreadsheet, block
Requires at least: 5.9
Tested up to: 7.1
Requires PHP: 8.0
Stable tag: 1.3.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Turn a CSV or Excel file into a fast, sortable table you can embed anywhere.

== Description ==

CSV Tables imports a CSV or Excel file and turns it into a real,
sortable, searchable table you can drop into any post or page. Every feature
listed below is free, on every table: no licence key, no upgrade prompt inside
the plugin, and no artificial ceiling on how many rows or tables you create.

There is a separate paid add-on -- see "Free vs Pro" below -- but it adds
things this plugin never had rather than switching anything off. Nothing that
worked in 1.0.0 needs a key in 1.1.0.

There is a practical ceiling, though, and it is worth knowing before you import
something enormous. An embedded table is rendered into the page as complete
HTML -- every row of it, in the response -- and the visitor's browser does the
sorting, filtering, searching and paging from there. That is exactly what makes
it fast and cache-friendly, with no request back to your server per
interaction. It also means page weight grows with row count. A typical
six-column table of short values costs roughly 210 bytes of HTML per row, so
1,000 rows is about 200 KB of HTML and 10,000 rows is about 2 MB, before
compression.

In practice: up to a few thousand rows per table is comfortable anywhere. Tens
of thousands makes a genuinely heavy page, and you should measure it on your
own hosting before shipping it. Hundreds of thousands of rows is not something
you should embed in a page at all.

= What you get =

* **Import a CSV file** by upload, with automatic delimiter detection (comma, semicolon, or tab) and encoding handling. The file is read as a stream and never loaded into memory whole, so memory use stays flat no matter how long the file is. Import does run as a single web request with no chunking and no resume, so the size you can actually get through is bounded by your host's PHP time limit -- if a big import times out, split the file and import it in parts.
* **Import an Excel (.xlsx) workbook.** The first row is detected as a header automatically, or you can set it yourself.
* **Link a table to a CSV on the web and re-pull it whenever you like.** Paste a public `http://` or `https://` link instead of uploading a file, then hit Sync now on the tables list any time you want fresh data -- as often as you like, free. Your site re-fetches the file and swaps the rows in one step, so visitors never see a half-updated table. If a fetch fails, the table keeps serving the data it already had, and the reason is written down where you can read it, alongside a history of recent runs. For a Google Sheet, use File -> Share -> Publish to web and choose CSV; the ordinary sharing link returns a web page rather than data. Refreshing on a schedule, without you being there, is what the Pro add-on adds -- see "Free vs Pro".
* **Edit the table by hand** afterward, in a spreadsheet-style editor: add or delete rows, add columns, and edit any cell inline, without re-importing.
* **Reorder columns** by dragging a row on the Columns tab, or with the arrow buttons beside it — the arrows work from the keyboard, so it is not a mouse-only feature.
* **Manage every table from one screen**: delete it, duplicate it, or export it back out to CSV or Excel.
* **Embed a table** with the Data Table block, or with the `[data_table id="123"]` shortcode.
* **Import your existing TablePress tables**, one at a time or all at once. It's read-only on TablePress -- nothing in your TablePress data is ever changed or deleted, so you can run both plugins side by side, check the results, and only deactivate TablePress once you're happy. Running the import again later re-syncs the same table instead of creating a duplicate, and your posts and pages keep rendering their existing `[table id="…"]` shortcodes and TablePress blocks automatically once TablePress is switched off. See the FAQ below for exactly what does and doesn't carry across.
* **Visitors can sort, filter, search, and page** through your table with no page reload, in any of six visual style presets and three responsive layouts for small screens (horizontal scroll, collapse to stacked rows, or hide lower-priority columns). Each filterable column gets its own control -- a text box, a range, or a dropdown, depending on how that column is set up -- and it works together with search, sorting, and pagination rather than instead of them. A range filter matches what the column actually shows: type a plain number for a Number column, a dollar-and-cents amount for Currency, or pick a day or a time for Date and Time.
* **Visitors can export what they're looking at**, if you turn the buttons on: download as CSV, copy to clipboard, or print.
* **Every export is formula-safe.** A cell that starts with `=`, `+`, or `@` is neutralized on the way out of both the visitor-facing export buttons and the admin CSV/Excel export, so a spreadsheet cell can't be turned into a formula that runs when someone opens the file.
* **See which tables people actually use**, on a Reports screen that counts views and interactions per table over the last 30 days. It is off until you switch it on, and the screen tells you what turning it on costs before you do — the counter sends one small request back to your own site per view, which means a page your cache would have served for free runs WordPress instead. Nothing leaves your server either way.
* **A REST API**, for a script, another plugin, or a headless front end that wants your table data as JSON instead of scraping the rendered HTML: `GET /wp-json/ops404-data-tables/v1/tables`, `/tables/{id}`, and `/tables/{id}/rows` (paginated, `X-WP-Total`/`X-WP-TotalPages` headers included). Every route needs the same permission as every screen in this plugin's own admin menu — there is no public, unauthenticated read, so an external client authenticates the way WordPress's REST API already expects (a logged-in session, or an Application Password).
* **No tracking of your visitors, at all.** This plugin sets no cookies, stores no identifiers, and sends nothing to us or to any third party. Rendering a table makes no outbound request of its own, so a cached page costs exactly nothing. The one exception is deliberate and yours: if you point a table at a CSV link and give it a schedule, your site fetches that link -- the address you typed, nobody else's -- on the schedule you chose. Leave every table on "Only when I click Sync now" and nothing is ever fetched.

Everything above is included, free, on every table, and none of it is gated
behind a licence key.

== Free vs Pro ==

CSV Tables Pro is a separate add-on plugin. It is not required, it is
not bundled in this download, and installing it changes nothing about the
features above -- they stay free, and they keep working if a licence lapses.

The add-on adds three things:

* **Scheduled syncing.** A table linked to a CSV URL can refresh itself every hour or once a day instead of waiting for you to click Sync now. The fetching itself is free and always will be -- getting at your own data is not something we would charge for -- so what you are paying for is not having to be there. WordPress runs scheduled jobs when someone visits your site, so a sync can land a little after its hour on a quiet site. If a licence lapses, scheduled tables stop refreshing and your schedule setting is kept, not deleted: renewing picks up exactly where it left off, and Sync now keeps working throughout.
* **Highlight rules.** Colour a cell, or a whole row, when a column meets a condition -- over a number, before a date, containing a word, empty. Comparisons use the column's own type, so a Currency rule of `1000` means one thousand dollars rather than the text "1000".
* **Row detail.** A column you hide on the Columns tab currently just disappears from the page. With the add-on it moves into an expandable panel under each row instead, so a wide table stays readable on a phone without losing any data.

The last two are per table, and both are configured on a "Highlighting" tab
that only exists while a licence is active; the schedule lives on each table's
own Source tab. If a licence lapses, tables render exactly
as they do here and your saved rules are kept, not deleted.

For current Pro product and licence details, see
https://404ops.com/plugins/404ops-data-tables

== Installation ==

1. Upload the plugin files to `/wp-content/plugins/csv-tables`, or install through the WordPress plugins screen directly.
2. Activate the plugin through the "Plugins" screen in WordPress.
3. In the WordPress sidebar, go to **404ops → Data Tables**. That screen lists your tables and carries its own navigation row at the top -- click **Add New** there (or the "Add New" button beside the page title), upload a CSV or Excel file, and you'll land straight in that table's editor.
4. Embed it with the Data Table block, or paste `[data_table id="123"]` (using your table's own id) into any post or page.
5. Already using TablePress? Go to **404ops → Data Tables → Import from TablePress** to bring your existing tables in.

== External services ==

This plugin makes no outbound request of its own as part of rendering,
importing, editing, or exporting a table. There is no licensing check, no
update-nag telemetry, and no analytics beacon -- the optional, off-by-default
view and interaction tracking described above (Reports screen) writes only
to your own database and never leaves your server.

The one exception is a source you choose yourself: if you link a table to a
CSV file on the web (the Source tab), this plugin fetches that exact URL --
never a service of our choosing -- when you click "Sync now", or
automatically on the schedule you set if the Pro add-on is installed. Nothing
is ever fetched unless a table has a URL source and you either click Sync now
or set a schedule; leave a table on "Only when I click Sync now" and nothing
is ever requested for it.

That request carries no data about your site, your visitors, or your
content beyond what any ordinary HTTP GET reveals: your site's home URL is
sent in the User-Agent header so whoever runs the destination server can see
what is polling them. The fetch goes only to the address you typed, is
subject to WordPress's own `wp_safe_remote_get()` protections against
reaching your own internal network, and is capped at 20 MB with a 20 second
timeout.

Because the destination is whatever URL you provide -- your own server, a
spreadsheet host, or any other publisher -- there is no single third party
whose terms of service or privacy policy governs this feature; the ones that
apply are whichever service actually hosts the file you link. If you use a
Google Sheet published to the web, for example, that is
https://policies.google.com/terms and https://policies.google.com/privacy.

== Frequently Asked Questions ==

= Does this plugin make external requests? =

Only one, and only if you ask for it. If you link a table to a CSV file on
the web instead of uploading one, your site fetches that exact URL when you
click Sync now, or on a schedule if you have the Pro add-on -- see "External
services" above for exactly what that sends and when. Uploading a file makes
no request at all: it is parsed locally and then deleted, and every table
lives in this plugin's own database tables. Nothing is ever sent back to us.

= Where is my data stored? =

In this plugin's own custom database tables -- table definitions, column
settings, and row data all live there, not in WordPress posts or postmeta.
The file you upload is deleted from your server immediately after it's
imported; only the parsed data remains.

= Is there a limit on rows or tables? =

None that this plugin imposes: nothing is capped by tier or licence, and you can
create as many tables as you like. A single embedded table does have a practical
size limit, because the whole table is rendered into the page -- see the page
weight arithmetic in the description above. A few thousand rows per table is
comfortable; tens of thousands makes a heavy page worth measuring on your own
hosting; hundreds of thousands is not workable as an embedded table.

= Can I import an Excel file, not just CSV? =

Yes, `.xlsx` workbooks are supported directly, no conversion needed. (Excel
import needs your server's PHP `zip` and `xml` extensions, which almost every
host has enabled; if yours doesn't, CSV import still works and the Add New
screen will tell you so.)

= Is my exported file safe to open in another spreadsheet program? =

Yes. Every export -- the admin's CSV/Excel export and the visitor-facing
export buttons -- neutralizes any cell that starts with `=`, `+`, or `@`
before it's written out, so a cell from an untrusted import can't turn into a
spreadsheet formula that runs on open.

= Does an embedded table slow down my cached pages? =

Not in server load. Once the page is rendered, the table needs nothing further
from your server: sorting, filtering, searching and paging all happen in the
visitor's browser. There's no analytics call, no beacon, and no background
request of any kind on a page view, so a page served from your cache stays a
page served from your cache.

It does affect page weight, though: every row is part of the HTML your cache
stores and sends, so a big table makes a big cached page. See the description
above for what that costs per row.

= How do I import my existing TablePress tables? =

Go to **404ops → Data Tables → Import from TablePress**. It lists every
table TablePress knows about (whether TablePress is still active or has
already been deactivated, as long as its data is still in your database),
with its row and column counts and a plain-English note for anything about
that specific table that won't carry across. Import one table at a time, or
use "Import all". Nothing about TablePress is ever changed or deleted --
every read goes through core WordPress functions only, and there's an
automated test that snapshots TablePress's own storage before an import and
asserts it is byte-for-byte identical afterward. Running an import again
later re-syncs that table's rows from TablePress rather than creating a
second copy, and any display settings you've customized here since the
first import (a different style preset, a hidden column) are left alone on
a re-sync.

= If I import from TablePress, do my old `[table]` shortcodes and blocks break? =

No, once TablePress has been deactivated. While TablePress is still active,
its own shortcode and block keep working exactly as before -- this plugin
never contests them. The moment TablePress is deactivated, this plugin picks
up the same `[table id="…"]` shortcode, the `[table-info]` shortcode, and
the TablePress block, and resolves them through the import you already ran,
so existing posts and pages keep rendering without any content being
rewritten. An id that hasn't been imported yet renders nothing for a visitor
and a short explanation for anyone who can edit posts.

= What doesn't carry over when I import from TablePress? =

Everything in a TablePress table's rows and columns is imported, including
rows TablePress had marked hidden -- there's no per-row visibility switch
here, so a hidden row becomes an ordinary, visible row on import. A column
TablePress had marked hidden carries its hidden state across instead, since
this plugin does have a per-column visibility setting: it stays hidden here
too, on the first import only -- a later re-sync never re-hides a column
you've since chosen to show. Any cells that used TablePress's merged-cell
(colspan/rowspan)
markup, which import as blank since merged cells aren't supported here. A
few table-level TablePress options have no equivalent and are listed,
table by table, right on the import screen -- among them: multiple header
rows (only the first becomes column labels), footer rows (they import as
ordinary trailing rows), a table's printed name/description, custom CSS
classes, and TablePress's "Custom Commands" field, which is arbitrary
JavaScript and is deliberately never imported or run. Cells containing raw
HTML (bold text, links typed directly into a TablePress cell) display as
plain, escaped text here rather than being rendered, because cell content is
always escaped by design. The import screen tells you, table by table,
which of these actually apply to what you're about to import -- nothing is
approximated silently.

= What happens to my data if I delete the plugin? =

Nothing is deleted. Deactivating and removing the plugin leaves your tables
and their data in your database untouched, so you don't lose anything by
uninstalling and later reinstalling.

= How does the front-end table stay fast without JavaScript frameworks? =

The table is rendered as plain, complete HTML first, with every row present, so
it is fully readable and indexable exactly as-is. A small (~35&nbsp;KB) script
then enhances it in place, and sorting, filtering, searching and paging are
that enhancement -- a column header is a button the script wires up, not a
link. So if the script fails to load, or JavaScript is off, you still have the
complete and correct table, in its original order, rather than a sortable one.
Nothing is hidden behind the script: what you can read without it is the whole
table, not a first page of it.

= Does this plugin have a REST API? =

Yes, read-only: `GET /wp-json/ops404-data-tables/v1/tables` lists your
tables, `/tables/{id}` returns one table's name and column definitions, and
`/tables/{id}/rows` returns its data, paginated. Every route requires the
same permission as this plugin's own admin screens (`manage_options` by
default) -- there is no public, unauthenticated read, even for a table you
have already embedded on a public page. Authenticate the way WordPress's
REST API already supports: a logged-in browser session, or an Application
Password for an external script. Creating or editing a table through the
API is not supported yet; use the table editor or the manual-grid screen for
that.

== Screenshots ==

1. Every table you have built, with its source, row count and shortcode.
2. The table editor: name each column, set its type, and choose what visitors can do.
3. Import a CSV or XLSX file, or link a CSV source. Manual refresh of a linked CSV with Sync now is free; hourly or daily automatic refresh requires the separate CSV Tables Pro add-on.

== Changelog ==

= 1.3.1 =
* Updates the Pro licensing client to select updates for this plugin when several 404ops plugins share a site.
* Prevents overlapping scheduled and manual refreshes from mixing rows in a table.

= 1.3.0 =
* New: a read-only REST API. `GET /wp-json/ops404-data-tables/v1/tables`, `/tables/{id}`, and `/tables/{id}/rows` (paginated) return your tables and their data as JSON, for a script, another plugin, or a headless front end. Every route requires the same permission as this plugin's own admin screens -- see the FAQ.
* wp.org compliance pass: added an "External services" section disclosing exactly what the CSV-link sync feature sends and when (nothing else in this plugin makes an outbound request); corrected a FAQ answer that had drifted out of sync with that same feature; raised the minimum WordPress version to 5.9.
* No behaviour changed for any existing table, shortcode, block, or schedule.

= 1.2.0 =
* New: link a table to a CSV on the web instead of uploading a file, on the table's new Source tab. Sync now re-fetches it and swaps the rows in one step, so visitors never see a half-updated table. Refreshing on a schedule -- every hour or once a day -- is a Pro add-on feature; the linking and the manual refresh are free.
* If a fetch fails, the table keeps serving the data it already had, and the reason is written down where you can read it: the Source tab shows the last result and a history of recent runs.
* "Sync now" on the tables list runs the same job by hand, through exactly the code the schedule uses.
* A table imported from a file can be pointed at a link later without losing its id, its shortcode, its columns or any of its settings.
* Scheduled runs (Pro) use WordPress's own scheduler, which fires when someone visits your site -- so a sync can land a little after its hour on a quiet site. Every table stays on "Only when I click Sync now" until you change it, and nothing is ever fetched from anywhere you did not type in yourself.
* Fixed: adding a column during a sync could, if the database refused to advance the table's column numbering, have filed the new column under a key an existing column already owned -- replacing that column while every row kept the old values under it. It now stops and says so instead.

= 1.1.0 =
* New: an optional paid add-on, CSV Tables Pro, adds highlight rules and expandable row detail. See "Free vs Pro" above.
* Nothing was moved out of the free tier to make room for it. Every feature that worked in 1.0.0 still works, unlicensed, unchanged.
* Added the `ops404_data_tables_booted` action, which is how the add-on boots. Free installs fire it and nothing listens.
* The License screen now says which tier a site is on and gives a purchased add-on somewhere to put its activation form.

= 1.0.0 =
* Initial release: CSV and Excel import, a spreadsheet-style table editor,
  block and shortcode embedding, front-end sorting/filtering/search/pagination,
  visitor CSV export and copy, six style presets, and three responsive layouts.
* Import from TablePress: read-only, re-runnable, one table or all of them,
  with existing `[table]` shortcodes and blocks kept working automatically
  once TablePress is deactivated.

== Upgrade Notice ==

= 1.3.0 =
Adds a read-only REST API (see the FAQ) and a wp.org compliance pass: an "External services" section, a corrected FAQ answer, and a WordPress 5.9 minimum (up from 5.8). No existing table, shortcode, block, or schedule changes behaviour.

= 1.2.0 =
Tables can now be linked to a CSV on the web and refreshed with one click, free; refreshing them on a schedule is a Pro add-on feature. Nothing changes for existing tables: every one stays on manual until you say otherwise, and your site fetches nothing until you do.

= 1.1.0 =
A safe upgrade: nothing was removed and nothing became paid. 1.1.0 adds a boot
action and a License screen so an optional paid add-on has somewhere to attach.
Every feature you have today keeps working with no licence key.

= 1.0.0 =
Initial release.
