What this holds about a person¶
4 tables, 2 columns about a person, 10 things held outside a column, 0 kinds of data subject, 1 destination. Nothing here is kept indefinitely.
What it holds¶
seo_redirects¶
Nothing on this table keys a row to anybody.
What makes a row go away: swept — The miss log is capped. Every logged row past the merchant's own cap is dropped on the next miss, least-seen first, in batches — so the table is a working vocabulary of the store's broken URLs rather than an archive that only grows. A row a merchant turned into a redirect is not swept, because it has stopped being a record of a visit and started being a rule they wrote.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
path |
about |
What somebody asked this store for and did not get, which is the whole point of the extension. The query string is stripped (Detector::path()), deliberately: one row per store and path is what makes a repeat visit a counted hit rather than a second row, so what is kept is a vocabulary of broken URLs and not a visit log. It is HTML-entity-decoded on the way in because core's own Request encodes everything in $_GET, and a path carrying a < would otherwise be written down as something nobody asked for. |
retain |
referrer |
about |
Where the most recent visitor to hit this broken path came from — the one column in this extension that can carry a person. It is kept whole, query string included, and that asymmetry with the path above is deliberate on the path's side: a broken link is only diagnosable from the entire URL that carried it, while a path is only countable with its query taken off. The consequence is stated rather than discovered: a marketing or a mail link carrying an address or a tracking id in its query string can land here, clamped to 255 characters. There is no IP address, no customer id, no session id and no timestamped trail beside it, so it is a sample and never a record of one person's visits. | blank |
referrer is kept differently from the rest of the table: overwritten — No value persists. The row is unique on the store and the path, and every subsequent miss on that path replaces this column with its own referrer wherever that one is not empty — so what the table holds is the most recent referring URL per broken path and never a second one. Declaring it never would be accurate about the column and misleading about the data: nothing accumulates here.
What it holds that is not in a column¶
A file, a cookie and a key in the session are exactly where an erasure written as row deletion reaches nothing, and no schema can be diffed to find one. Each is listed here with what keys it to a person, why it is there, and what removes it.
A file on disk. kyvero.log in the store's own DIR_LOGS, the one diagnostic file every extension in this repository shares, capped at 1 MiB and trimmed oldest-first.
Nothing keys this to a person — every message is put through the diary's redaction rule before a byte is written, and what this extension writes to it is a page count and a route.
Why it is here: A generation run that skipped a thousand products needs one place to look, and a support conversation without it is guesswork on both sides.
What an erasure does: retain.
A file on disk. kyvero.log in the store's own DIR_LOGS, the one diagnostic file every extension in this repository shares, capped at 1 MiB and trimmed oldest-first.
Nothing keys this to a person — every message is put through the diary's redaction rule before a byte is written, and what this extension writes to it is a route and an override count.
Why it is here: A page whose canonical tag is not what the merchant expects needs one place to look, and a support conversation without it is guesswork on both sides.
What an erasure does: retain.
A file on disk. Not a file on disk at all: php://temp, the in-memory buffer the CSV export is assembled in before it is read back into the response. It is opened, written a row at a time and closed inside one request, and PHP spills it to a temporary file only if it outgrows its memory budget. What goes into it is what the export screen shows — routes, page keys, canonicals and directives.
Nothing keys this to a person — the buffer holds page metadata and is keyed to the export the administrator asked for, which does not outlive the request that asked for it.
Why it is here: A merchant reviewing overrides across a large catalogue does it in a spreadsheet, and an export written straight into the response would have to hold the whole file in memory as a string.
What an erasure does: retain.
A file on disk. kyvero.log in the store's own DIR_LOGS, the one diagnostic file every extension in this repository shares, capped at 1 MiB and trimmed oldest-first.
Nothing keys this to a person — every message is put through the diary's redaction rule before a byte is written, and what this extension writes to it is a product count and a template name.
Why it is here: A generation run that stopped halfway needs one place to look, and a support conversation without it is guesswork on both sides.
What an erasure does: retain.
A file on disk. Not a file on disk at all: php://temp, the in-memory buffer the CSV export is assembled in before it is read back into the response. It is opened, written a row at a time and closed inside one request, and PHP spills it to a temporary file only if it outgrows its memory budget. What goes into it is what the export screen shows — product ids, image paths and alt text.
Nothing keys this to a person — the buffer holds catalog rows and is keyed to the export the administrator asked for, which does not outlive the request that asked for it.
Why it is here: A merchant editing a few thousand lines of alt text does it in a spreadsheet, and an export written straight into the response would have to hold the whole file in memory as a string.
What an erasure does: retain.
A file on disk. kyvero.log in the store's own DIR_LOGS, the one diagnostic file every extension in this repository shares, capped at 1 MiB and trimmed oldest-first.
Nothing keys this to a person — every message is put through the diary's redaction rule before a byte is written, which is what makes the file survivable where OpenCart puts its own error log.
Why it is here: A merchant whose redirects stopped firing needs one place to look, and a support conversation without it is guesswork on both sides.
What an erasure does: retain.
A file on disk. Not a file at all: php://temp, the in-memory buffer both CSV exports, the redirects file and the Broken URLs list, are written into and read straight back out of, inside the one request that serves the download. Nothing is opened on disk and no path exists anywhere.
Nothing keys this to a person — it holds the same rows the screen above it is already showing, which are a path, a count and a sample referrer, and nothing that names anybody.
Why it is here: A merchant fixing a hundred broken URLs does it in a spreadsheet, and the alternative to an export is retyping the screen.
What an erasure does: retain.
A file on disk. kyvero.log in the store's own DIR_LOGS, the one diagnostic file every extension in this repository shares, capped at 1 MiB and trimmed oldest-first.
Nothing keys this to a person — every message is put through the diary's redaction rule before a byte is written, and what this extension writes to it is a URL count and a batch number.
Why it is here: A sitemap that stopped being regenerated needs one place to look, and a support conversation without it is guesswork on both sides.
What an erasure does: retain.
A file on disk. robots.txt and the sitemap*.xml set, under DIR_OPENCART and so inside the document root — deliberately, because a crawler fetches them over HTTP and a sitemap it cannot fetch is not a sitemap. The names are fixed by Names (sitemap.xml, the per-store-and-language parts, and the .part a run builds before it renames it over the live one) with no part of any name taken from a request. What is in them is catalog URLs: addresses of pages the store's own navigation already links to, with the dates they last changed. Nothing about a person is in one, and there is no version of a sitemap that could hold anything about one — a sitemap is a list of public addresses.
Nothing keys this to a person — a sitemap is keyed to a store and a language, and a robots.txt to the web root. There is no person anywhere near either.
Why it is here: A search engine finds the pages of a large catalogue by being handed a list of them, and it has to be able to fetch that list from the web root. A run builds one a batch at a time and renames the finished file over the live one, so a crawler never reads a half-written sitemap; the fourth site is what an uninstall does to a robots.txt this extension created, so a file nobody else can regenerate is not left behind.
What an erasure does: retain.
A file on disk. Not a file at all: the standard error of the command line door — seo.php, the script a person or a crontab line runs from a terminal, and the controller behind it. Six one-line refusals — it could not find the store's config.php, the configuration is not an OpenCart 4 one, core's vendored packages are in neither place a release keeps them, an exception got out, the command line controller would not load, or the module is switched off so nothing was regenerated.
Nothing keys this to a person — all six are fixed sentences about a store that could not be booted or a module that is switched off, written before this extension has read a row of anything.
Why it is here: A script that exits silently because it could not find the store is one a person debugs by guessing, and these are the guesses it saves them. On OpenCart 4.1.0.4 this script is the only scheduler the store has, so a crontab line failing without saying why is a sitemap that quietly stops being current.
What an erasure does: retain.
What it deliberately does not hold¶
Each of these is a column that could have been stored and was not, with the reason it was not. They are decisions rather than omissions.
The remove-everything screen does not empty seo_markup_override, and there is a Schema::drop() in this extension that nothing calls. Both are deliberate and neither is an oversight.
What that table holds is catalog data a merchant authored — the canonical URL they decided a page should declare, and the four crawler directives they set on it. Emptying it is undo my SEO, which is a different act from remove everything held about a person: a different warning, a different blast radius, and a different person deciding. Wiring it to this button would put a merchant's own work behind a label that is false about it, and the failure mode is somebody pressing a data-protection button and silently changing what search engines are told about every page in their store. So the screen here opens on an empty plan, which is the true answer — there is nothing about a person to remove — and the act that would empty the table stays unbuilt until somebody wants a screen with the right words on it.
The remove-everything screen does not empty seo_alt_text, and there is a Schema::drop() in this extension that nothing calls. Both are deliberate and neither is an oversight.
What that table holds is catalog data a merchant authored — a sentence describing a photograph, written or approved by them, per image, per language. Emptying it is remove the work I did, which is a different act from remove everything held about a person: a different warning, a different blast radius, and a different person deciding. Wiring it to this button would put a merchant's own writing behind a label that is false about it, and the failure mode is somebody pressing a data-protection button and losing a year of copywriting. So the screen here opens on an empty plan, which is the true answer — there is nothing about a person to remove — and the act that would empty the table stays unbuilt until somebody wants a screen with the right words on it.
An IP address, a customer id, a session id or a user agent beside the miss — anything that would say who hit the broken path.
It is the single decision that keeps this table out of the erasure business. What a merchant needs is this URL is broken and people are still following it, which is a path and a count; who those people were adds nothing to fixing it and would turn a vocabulary of broken URLs into a record of where individuals went. The absence is also why the table above declares nobody: nothing here can be keyed to anybody, and the answer to a request is the remove-everything screen rather than a lookup.
One row per visit, with a timestamp, instead of one row per path with a hit counter.
A trail is what would make this a log of people rather than of URLs. The unique key on the store and the path is what prevents it, and the counter carries everything the merchant reads — how bad the miss is — without keeping when each one happened to whom.
Every referrer a path was reached from, rather than the most recent non-empty one.
One sample answers the question a merchant asks — where is this broken link, so I can get it fixed — and a list would answer a different one nobody asked. It is also the difference between a column that can carry an address once and a column that accumulates them.
The query string on the path that missed.
Taken off before anything is written. Two visits to one broken page with different tracking parameters are one broken page, and keeping the query would both split the counter and store whatever a campaign or a mail link had put in it.
Where it goes¶
The merchant's own staff, in their own browser: the CSV of the Broken URLs, every path still on the list with the most recent referrer beside it. It goes no further than whoever the merchant gave access on extension/seo/seo/redirects to — the same people who can already read the list on the screen.
Chosen by: merchant. What reaches it: seo_redirects.path, seo_redirects.referrer.
Every destination above is one you configured — your own mail transport, your own API caller presenting your own credential. Nothing goes anywhere this extension chose: a destination we picked that anything personal reached would fail the build, not by default, not behind a setting and not with a warning.
What the standard asks, and what this extension answers¶
Nothing here is kept for good: every table this extension creates answers to something that removes a row from it.
Each row below is keyed by the sub-paragraph of the General Data Protection Regulation
it comes from, so that you can read the source and disagree with us. What each state
means is on the data-protection boundary,
once, rather than reworded here. declared is not a pass: it says what the thing is,
not that the thing is fine.
| Article | What this extension supplies toward it | This extension |
|---|---|---|
5(1)(c) |
Every column this extension can put in a store is written down with the one sentence saying why it is there, and a column that is not fails the build — so what a store keeps is what somebody decided to keep rather than what accumulated. Beside it, in the same file and the extension's own voice, is what it deliberately does not keep, and why. | checked, 30 — every column this extension's schema can put in a store |
30(1)(c) |
What this extension holds about a person is published column by column — what the column is, whether it names somebody or points at them, and why it exists — so the record a merchant has to keep can be copied off a page rather than reconstructed out of the database. | declared |
7(1) |
Where anything this extension does rests on somebody having agreed to it, the declaration names the wording they agreed to and where the proof of it is recorded — and where nothing rests on consent it says so, because we did not need any is an answer and a blank is not. | declared |
7(3) |
A declared consent carries the path by which it is withdrawn, or the build fails — because withdrawing has to be no harder than giving, and a consent whose withdrawal path nobody wrote down is one a merchant discovers they cannot honour on the day somebody asks. | checked, 0 — every consent declared anywhere in it |
15(1) |
Every table holding anything about a person answers who that person is and how a request reaches them — a column on the table itself, the path through a table this extension does not own, or the plain statement that nothing on it keys a row to anybody — so that a request either has somewhere to arrive or is told outright that there is nowhere, rather than a screen having to guess which rows are whose. | checked, 1 — every table holding anything about a person |
16(1) |
A column holding a frozen copy of something the store holds elsewhere names the column it was copied from, so that correcting the original is an instruction a merchant can follow rather than a shrug about why the two disagree. | declared |
21(3) |
The row that records somebody saying stop names the subject it is keyed on and the verdict an erasure gives it, so that a live instruction to stop mailing survives the request that was meant to enforce it rather than being deleted by it. | declared |
25(2) |
A column that is only collected when a merchant turns something on names the setting that decides it, and the shipped value is read off the configuration declaration rather than restated here — so what a store collects out of the box is a fact on a page instead of something read out of a controller. | declared |
5(1)(e) |
Every table this extension can put in a store answers what makes a row holding a person go away — one of six verdicts, and a table that answers 'never' says why in the same breath or fails the build — so a store keeping something for ever is keeping it on purpose. | checked, 1 — every table an answer is owed for |
15(1)(d) |
The period each table is kept for is published beside what it holds, as the mechanism and — where there is one — the number or the setting it is read from, so that a merchant answering somebody who asks how long their data will be stored is copying an answer rather than composing one. | declared |
17(3) |
Everything an erasure deliberately keeps is published with the reason it was kept, and a kept column with no reason beside it fails the build — because the exemption a merchant relies on is one they have to be able to state, and the screen states it to them at the moment they press the button. | declared |
17(1) |
A merchant erases one named person from a screen, and what happens is what the declaration said would happen: the rows and the files an erasure takes are gone, what it keeps is still there, the person standing beside them is untouched, and pressing it a second time is safe. Asserted by running it on a real store — the only place in this standard where erasure behaviour is ever observed, and the only place a file is. | Asserted on a real store by make smoke, and deliberately not here — this page is generated by make check, which is green with no store at all, so a verdict rendered from it would rest on nothing. |
30(1)(f) |
The envisaged time limits for erasure are published per table rather than per extension, so the line a merchant copies into their own record says which data it is about instead of averaging seven answers into one. | declared |
15(1)(c) |
Every destination this extension's holdings leave it for is published by name, with which of those holdings reach it — so answering somebody who asks who their data was disclosed to is reading a page rather than reading the source. | declared |
20(2) |
This extension transmits nothing directly to another controller, and says so out loud rather than leaving it unmentioned: the destinations that exist are the published list, an export is a file the merchant receives and hands on themselves, and there is no path by which we send one controller's data to another on their behalf. | declared |
30(1)(d) |
Every destination is written down with who chose it, and a destination this extension chose itself that anything personal reaches fails the build — not by default, not behind a setting, not with a warning, because a store owner cannot consent on behalf of the people in the file. | checked, 1 — every destination anything leaves it for |
15(3) |
The screen that says what is held about one named person also hands that statement over: one button produces a file carrying every holding it just listed — what is held, why it is there and what removes it — so answering somebody who asked is sending what the screen showed rather than retyping it into an email. What the file does not carry is the values themselves, which stay in the store; 20(1) below says what that costs. Every extension ships that screen and that button byte for byte, so the file is the same file whichever extension a merchant happened to open. |
checked, 2 — every holding the file carries |
20(1) |
That file is JSON — structured, commonly used and machine-readable — rather than a screen printed to paper, so whoever asked for it can read it with something other than their eyes. What it carries is the inventory and not the contents: every holding this extension has about that person, folded out of the declaration without a table being opened, which is why two people's files differ in the address at the top and nowhere else. Portability is the right to receive the data itself, and this is not that — the values are in the merchant's own database, and what this supplies is a machine-readable statement of where each one is and what removes it. The row is declared for that reason and not machine: what the rule behind it decides is that every extension ships one export byte for byte, which says what the file is and not that the file is what this article asks for. |
declared |
30(1)(g) |
The technical and organisational measures are the security baseline's subject, published on its own page with its own rows and its own scanners, and this page links to it rather than restating any of it. What is asserted here is the seam between them: every file this extension writes is declared in both places and cross-checked both ways, so the two descriptions cannot drift apart. | declared |
KYV-D1 |
Everything this extension holds about a person can be removed on purpose, from a screen, without uninstalling it — and the screen shows what will go before it goes. Uninstalling keeps it all, because an OpenCart upgrade is an uninstall followed by an install and an extension that dropped its tables on the way out would destroy a store's data on every routine update. A holding every one of whose columns is kept for ever, with nothing anywhere able to remove any of it, fails the build. | checked, 1 — every holding the remove-everything screen reaches |
What is left over¶
No column SEO Suite Pro holds is declared a frozen copy of anything the store keeps elsewhere, so this page names no second place a correction has to be made first. What each column is, and where its contents came from, is in its own row above.
Removing all of it is one button, on SEO Suite Pro's own settings screen at Admin > Extensions > Extensions > Modules > SEO Suite Pro. It prints what will go — every table, every file, every key, with a count beside each line and the reason beside the ones it keeps — and nothing goes until you press the second button. It cannot be undone: no soft delete, no recycle bin, no undo, because a recycle bin for personal data is personal data that is still there. Uninstalling does not do it, deliberately: an OpenCart upgrade is an uninstall followed by an install, so an extension that dropped its tables on the way out would destroy your data every time you updated it. Your settings, your configuration and whatever the extension remembers about itself are not touched.
What the law asks of you — you are the controller, and installing an extension is not a compliance process — is on the data-protection boundary, which is also where the words above are defined and where it says why there is no badge. What OpenCart's own erasure feature does underneath all of this, and the things about it worth knowing first, is on what core's own GDPR feature does.