What OpenCart's own GDPR feature does, and what it does not¶
OpenCart ships a data-protection feature of its own, and this page is about where it ends. It is here because our extensions tell you what they will erase, and that sentence is only worth anything if it is clear what the platform underneath already handles.
What core does¶
Out of the box, and at no extra cost, a stock OpenCart 4 store gives a customer a real route to their own data:
- a request form on the storefront, where somebody can ask for an export of their data or for their account to be removed;
- an email verification step, so a request only becomes real once somebody at that address has followed a link;
- an approve / deny queue in the admin, under Customers → GDPR, so a merchant decides rather than a script;
- a cron job that carries out approved removals;
- and an export, mailed to the customer, covering their account, their addresses and their order history.
That is more than a good many shopping platforms of comparable size ship at all, and it is part of the software rather than something to buy. Everything below is a description of how it behaves, read against the two releases named in each finding's disclosure. It is not a complaint, and none of it is a reason to avoid the feature. It is what a merchant needs to know to use it without being surprised.
The one thing to know first, which is not a defect¶
Neither release touches
oc_order. A completed removal leaves the order rows (name, email, telephone, both addresses) exactly where they were.
On 4.0.2.0 the erasure is a flat list of DELETE statements
(catalog/model/account/customer.php:46-59); on 4.1.0.3 it was rewritten to
cascade through activity, addresses, affiliate, approval, reward, transaction,
wishlist, histories, IPs and authorisations
(catalog/model/account/customer.php:147-188) — a cascade that stops at its second
step, as a finding below explains. Either way, orders survive.
This is stated first because it is the single fact that most often surprises a merchant, and because it is arguably the right behaviour rather than a bug: an order is a financial record you have other obligations about, and a system that silently shredded your accounts the moment somebody asked would be a worse system. What it means in practice is that a completed core removal is not the end of the work. Deciding what to do about the order rows, and about anything any extension holds, is yours, and it is the reason our own erasure surface exists.
Things to know about the flow¶
Each of these is written as what you will observe, with what to do about it. The file and line each was read at sits behind the disclosure under it, along with the cron loop that four of them happen inside. A merchant does not need any of that, and somebody checking our homework should not have to take our word for it.
Where the reported upstream line says not yet reported, that is what it means: we have written it down here and have not yet filed it with the OpenCart project. That is a gap of ours, not of theirs.
The delay the screen advertises does not happen¶
The admin screen says that account-deletion requests are processed after a set number of days, so that fraud checks, chargebacks and refunds can run first. The query that finds requests which have waited long enough adds those days to today rather than subtracting them from the request, so every approved removal is already eligible on the next run.
What you will see. An approved removal is carried out the next time the cron runs, whatever the delay is set to. Nothing waits.
What to do about it. Treat approval as the moment of deletion rather than the start of a countdown, and run whatever the delay was there to allow — fraud check, chargeback window, refund — before you press Approve. Leaving the request sitting in the queue is the only delay there is.
Reported upstream: Not yet reported.
Where this was read, release by release
| Release | Read at |
|---|---|
4.0.2.0 |
catalog/model/account/gdpr.php:31 |
4.0.2.1 |
not read |
4.0.2.2 |
not read |
4.0.2.3 |
not read |
4.1.0.0 |
not read |
4.1.0.1 |
not read |
4.1.0.2 |
not read |
4.1.0.3 |
catalog/model/account/gdpr.php:121 |
4.1.0.4 |
not affected — the query is unchanged (catalog/model/account/gdpr.php:121) and nothing runs it: cron.php fatals building the template library (cron.php:205, system/library/template/twig.php:36) nineteen lines above its cron/cron dispatch, because the vendor autoloader it required was commented out and the file it named removed (cron.php:20). No approved removal is carried out at all, early or late |
A guest is told their data was deleted when none of it was¶
The cron marks the request complete first and looks for a customer account second. The completion mail hangs off the marking, and checks only that the request asked for removal and that it is now complete — never that an account was found, let alone deleted.
What you will see. Somebody who ordered without registering asks for their data to be removed and receives a mail saying it has been. Nothing was removed, because there was no account to remove: their order rows are exactly where they were.
What to do about it. Before approving a removal, check whether the address belongs to a registered customer. Where it does not, the mail core sends is wrong and the work is still yours — deal with the order rows yourself, or deny the request and answer the person directly rather than letting the cron answer for you.
Reported upstream: Not yet reported.
Where this was read, release by release
| Release | Read at |
|---|---|
4.0.2.0 |
catalog/controller/cron/gdpr.php:8-18, catalog/controller/mail/gdpr.php:87 |
4.0.2.1 |
not read |
4.0.2.2 |
not read |
4.0.2.3 |
not read |
4.1.0.0 |
not read |
4.1.0.1 |
not read |
4.1.0.2 |
not read |
4.1.0.3 |
catalog/controller/cron/gdpr.php:27-37, catalog/controller/mail/gdpr.php:117 |
4.1.0.4 |
not affected — the loop and the mail are unchanged (catalog/controller/cron/gdpr.php:27-37, catalog/controller/mail/gdpr.php:117) and neither runs: cron.php fatals building the template library (cron.php:205, system/library/template/twig.php:36) nineteen lines above its cron/cron dispatch, because the vendor autoloader it required was commented out and the file it named removed (cron.php:20). Nobody is told anything, and nothing is deleted either |
The loop in full, from 4.1.0.3's catalog/controller/cron/gdpr.php. The
request is marked complete on line 30; whether there is an account to
delete is asked on line 34.
27 $results = $this->model_account_gdpr->getExpires();
28
29 foreach ($results as $result) {
30 $this->model_account_gdpr->editStatus($result['gdpr_id'], 3);
31
32 $customer_info = $this->model_account_customer->getCustomerByEmail($result['email']);
33
34 if ($customer_info) {
35 $this->model_account_customer->deleteCustomer($customer_info['customer_id']);
36 }
37 }
On 4.0.2.0 that same mail is sent again every day¶
The cron hands the request's numeric id to a method whose parameter is the request's code. The update matches no row, the request never leaves the queue the cron reads, and the next run picks it up again. There is no terminating condition.
What you will see. On 4.0.2.0, somebody who asked for removal receives the same "your data has been deleted" mail once a day, indefinitely — and on a store where they had no account, none of it was ever true.
What to do about it. Deny or delete the request in the admin list once you have dealt with it. Both write the status the cron is reading, and either one ends the loop; nothing the cron itself does ever will.
Reported upstream: Not yet reported.
Where this was read, release by release
| Release | Read at |
|---|---|
4.0.2.0 |
catalog/model/account/gdpr.php:8 |
4.0.2.1 |
not read |
4.0.2.2 |
not read |
4.0.2.3 |
not read |
4.1.0.0 |
not read |
4.1.0.1 |
not read |
4.1.0.2 |
not read |
4.1.0.3 |
not affected — the status write lands, and leaves the row stuck instead (catalog/model/account/gdpr.php:48-49) |
4.1.0.4 |
not affected — the status write lands here too (catalog/model/account/gdpr.php:48-49), and the cron that would repeat the mail never runs: cron.php fatals building the template library (cron.php:205, system/library/template/twig.php:36) nineteen lines above its cron/cron dispatch, because the vendor autoloader it required was commented out and the file it named removed (cron.php:20) |
On 4.1.0.3 a completed removal keeps showing as pending¶
The status column is written through a boolean cast, so writing the code for complete stores the code for pending. The row drops out of the cron's queue, which is why it is not the daily mail above, and settles on the wrong label.
What you will see. On 4.1.0.3 the deletion happens, the mail goes out, and the request sits in the admin list marked Pending for ever. Anything you count or filter on that list counts it as outstanding.
What to do about it. Delete the request row once the mail has gone out: the work is done and the row is the only thing still saying otherwise. If you keep a record of which requests you answered and when, keep it somewhere other than that list.
Reported upstream: Not yet reported.
Where this was read, release by release
| Release | Read at |
|---|---|
4.0.2.0 |
not affected — the status write misses its row entirely, and repeats the mail instead (catalog/model/account/gdpr.php:8) |
4.0.2.1 |
not read |
4.0.2.2 |
not read |
4.0.2.3 |
not read |
4.1.0.0 |
not read |
4.1.0.1 |
not read |
4.1.0.2 |
not read |
4.1.0.3 |
catalog/model/account/gdpr.php:48-49 |
4.1.0.4 |
not affected — the parameter became int and the write still casts to bool, so the defect is intact (catalog/model/account/gdpr.php:48-49), but the only caller that passes 3 is the cron: cron.php fatals building the template library (cron.php:205, system/library/template/twig.php:36) nineteen lines above its cron/cron dispatch, because the vendor autoloader it required was commented out and the file it named removed (cron.php:20). The row stops at the status the admin screen wrote, and the removal it was waiting on never happens |
On 4.1 a removal deletes the account and abandons the rest¶
The removal deletes the customer's account first and then works through everything attached to it, one step at a time. The second step calls a method the address model does not have, and the run stops there with an error. Everything after that step is never reached: addresses, affiliate details, approvals, reward points, transactions, the wishlist, the account history, the IP log and saved logins all stay in the database, attached to an account that no longer exists. The request had already left the queue before the account was deleted, so nothing ever goes back to finish the job.
What you will see. The account is gone, the request no longer waits in the queue, the completion mail has gone out, and nothing on the GDPR screen says anything went wrong. The only record is one line in the error log naming Proxy::deleteAddresses, and only if error logging is on. The reward points are still in the reward table, attached to nobody, and a reward report can no longer show them. One run of the cron gets through one removal at most: any other approved removal waits for the next run, which stops at that one in the same way.
What to do about it. Before you approve a removal, delete the customer yourself under Customers → Customers. The admin screen deletes the account and everything attached to it, and gets all the way through. When the cron then runs, it finds no account to delete and has nothing to stop on. Where the cron has already done a removal, what it left behind is still in the database under the old customer ID, and removing it is up to you.
Reported upstream: Not yet reported.
Where this was read, release by release
| Release | Read at |
|---|---|
4.0.2.0 |
not affected — the removal is a flat list of DELETE statements with nothing in between that can fail (catalog/model/account/customer.php:46-59) |
4.0.2.1 |
not affected — the same flat list of DELETE statements (catalog/model/account/customer.php:46-59) |
4.0.2.2 |
not affected — the same flat list of DELETE statements (catalog/model/account/customer.php:46-58) |
4.0.2.3 |
not affected — the same flat list of DELETE statements (catalog/model/account/customer.php:90-102) |
4.1.0.0 |
catalog/controller/cron/gdpr.php:30-35, catalog/model/account/customer.php:175, catalog/model/account/customer.php:185, catalog/model/account/customer.php:200 |
4.1.0.1 |
catalog/controller/cron/gdpr.php:30-35, catalog/model/account/customer.php:147, catalog/model/account/customer.php:157, catalog/model/account/customer.php:172 |
4.1.0.2 |
catalog/controller/cron/gdpr.php:30-35, catalog/model/account/customer.php:147, catalog/model/account/customer.php:157, catalog/model/account/customer.php:172 |
4.1.0.3 |
catalog/controller/cron/gdpr.php:30-35, catalog/model/account/customer.php:148, catalog/model/account/customer.php:158, catalog/model/account/customer.php:173 |
4.1.0.4 |
not affected — every method the removal calls exists (catalog/model/account/customer.php:149-190), and the cron that calls it never runs anyway: cron.php fatals building the template library (cron.php:205, system/library/template/twig.php:36) nineteen lines above its cron/cron dispatch, because the vendor autoloader it required was commented out and the file it named removed (cron.php:20) |
An export leaves when you approve it, not when the cron runs¶
The delay on the screen reads as though it governs both kinds of request. It does not: only a removal ever reaches the state the cron looks for. Approving an export marks it complete on the spot, and the export is built and mailed by the handler attached to that approval.
What you will see. Press Approve on an export request and the file goes out immediately. There is no window in which to change your mind, and the number of days on the screen has no bearing on it.
What to do about it. Satisfy yourself about the request before approving it rather than after. The verification core does proves that somebody at that address followed a link, not that they are the customer — and an export carries order history, both addresses and a telephone number, so where an address alone is not enough to convince you, ask before you press the button.
Reported upstream: Not yet reported.
Where this was read, release by release
| Release | Read at |
|---|---|
4.0.2.0 |
admin/controller/customer/gdpr.php:233-234, admin/controller/mail/gdpr.php:14-15 |
4.0.2.1 |
not read |
4.0.2.2 |
not read |
4.0.2.3 |
not read |
4.1.0.0 |
not read |
4.1.0.1 |
not read |
4.1.0.2 |
not read |
4.1.0.3 |
admin/controller/customer/gdpr.php:258-259, admin/controller/mail/gdpr.php:30-31 |
4.1.0.4 |
admin/controller/customer/gdpr.php:258-259, admin/controller/mail/gdpr.php:30-31 |
The mail that says "your request was approved" cannot be sent¶
The handler that would send it asks whether the request's action is approve. A request's action is only ever export or remove — the storefront form accepts nothing else, and the column is six characters wide. The branch has no reachable path.
What you will see. A customer whose removal you approve hears nothing at that moment. The next thing they receive is the completion mail, whenever the cron next runs — which, because of the delay above, is the same day.
What to do about it. If you want to acknowledge an approval, send that message yourself. Nothing in the flow will send it, on either release, and no setting turns it on.
Reported upstream: Not yet reported.
Where this was read, release by release
| Release | Read at |
|---|---|
4.0.2.0 |
admin/controller/mail/gdpr.php:19, catalog/controller/information/gdpr.php:99-104 |
4.0.2.1 |
not read |
4.0.2.2 |
not read |
4.0.2.3 |
not read |
4.1.0.0 |
not read |
4.1.0.1 |
not read |
4.1.0.2 |
not read |
4.1.0.3 |
admin/controller/mail/gdpr.php:35, catalog/controller/information/gdpr.php:111-116 |
4.1.0.4 |
admin/controller/mail/gdpr.php:35, catalog/controller/information/gdpr.php:111-116 |
Releases this was read on¶
The disclosures above name a release for every version of OpenCart any of our extensions claims a tested pass on, and say for each one either where the behaviour was read or, plainly, not read. Nobody has read core's GDPR flow on most of the point releases, and the table says so rather than implying a coverage that does not exist.
That is enforced rather than remembered: adding a release to an extension's tested list fails our build until every finding on this page has something to say about it, even if the honest thing to say is that nobody looked.