dolibarr 25.0.0-alpha
datapolicycron.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2018 Nicolas ZABOURI <info@inovea-conseil.com>
3 * Copyright (C) 2018-2026 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024 William Mead <william.mead@manchenumerique.fr>
5 * Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
6 * Copyright (C) 2025 Quentin VIAL--GOUTEYRON <quentin.vial-gouteyron@atm-consulting.fr>
7 * Copyright (C) 2025 Waël Almoman <info@almoman.com>
8 *
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
33{
35 public $db;
37 public $error;
39 public $output;
41 private $nbupdated = 0;
43 private $nbdeleted = 0;
45 private $errorCount = 0;
47 private $errorMessages = array();
48
53 public function __construct(DoliDB $db)
54 {
55 $this->db = $db;
56 }
57
64 public function getDataPolicies()
65 {
66 $sanitized_prefix = $this->db->prefix();
67
68 $arrayofpolicies = array(
69 // --- Third Parties ---
70 'tiers_client' => array(
71 'group' => 'ThirdParty',
72 'label_key' => 'DATAPOLICY_TIERS_CLIENT',
73 'picto' => img_picto('', 'company', 'class="pictofixedwidth"'),
74 'const_delete' => '',
75 'const_anonymize' => 'DATAPOLICY_TIERS_CLIENT_ANONYMIZE_DELAY',
76 'sql_template' => "SELECT s.rowid FROM ".$sanitized_prefix."societe as s WHERE s.entity = __ENTITY__ AND s.client = ".Societe::CUSTOMER." AND s.fournisseur = 0 AND s.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND NOT EXISTS (SELECT a.id FROM ".$sanitized_prefix."actioncomm as a WHERE a.fk_soc = s.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM ".$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
77 'class' => 'Societe',
78 'file' => DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php',
79 'anonymize_fields' => array('name' => 'MAKEANONYMOUS', 'name_alias' => 'MAKEANONYMOUS', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone' => '---', 'email' => 'anonymous+__ID__@example.com', 'url' => '---', 'fax' => '---', 'siret' => '---', 'siren' => '---', 'ape' => '---', 'idprof4' => '---', 'idprof5' => '---', 'idprof6' => '---', 'tva_intra' => '---', 'capital' => 0, 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
80 'call_params' => array(
81 'delete' => array('id', 'user'), // $object->delete($id, $user)
82 'update' => array('id', 'user') // $object->update($id, $user)
83 )
84 ),
85 'tiers_prospect' => array(
86 'group' => 'ThirdParty',
87 'label_key' => 'DATAPOLICY_TIERS_PROSPECT',
88 'picto' => img_picto('', 'company', 'class="pictofixedwidth"'),
89 'const_delete' => '',
90 'const_anonymize' => 'DATAPOLICY_TIERS_PROSPECT_ANONYMIZE_DELAY',
91 'sql_template' => "SELECT s.rowid FROM ".$sanitized_prefix."societe as s WHERE s.entity = __ENTITY__ AND s.client = ".Societe::PROSPECT." AND s.fournisseur = 0 AND s.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND NOT EXISTS (SELECT a.id FROM ".$sanitized_prefix."actioncomm as a WHERE a.fk_soc = s.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM ".$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
92 'class' => 'Societe',
93 'file' => DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php',
94 'anonymize_fields' => array('name' => 'MAKEANONYMOUS', 'name_alias' => 'MAKEANONYMOUS', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone' => '---', 'email' => 'anonymous+__ID__@example.com', 'url' => '---', 'fax' => '---', 'siret' => '---', 'siren' => '---', 'ape' => '---', 'idprof4' => '---', 'idprof5' => '---', 'idprof6' => '---', 'tva_intra' => '---', 'capital' => 0, 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
95 'call_params' => array(
96 'delete' => array('id', 'user'), // $object->delete($id, $user)
97 'update' => array('id', 'user') // $object->update($id, $user)
98 )
99 ),
100 'tiers_prospect_client' => array(
101 'group' => 'ThirdParty',
102 'label_key' => 'DATAPOLICY_TIERS_PROSPECT_CLIENT',
103 'picto' => img_picto('', 'company', 'class="pictofixedwidth"'),
104 'const_delete' => '',
105 'const_anonymize' => 'DATAPOLICY_TIERS_PROSPECT_CLIENT_ANONYMIZE_DELAY',
106 'sql_template' => "SELECT s.rowid FROM ".$sanitized_prefix."societe as s WHERE s.entity = __ENTITY__ AND s.client = ".Societe::CUSTOMER_AND_PROSPECT." AND s.fournisseur = 0 AND s.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND NOT EXISTS (SELECT a.id FROM ".$sanitized_prefix."actioncomm as a WHERE a.fk_soc = s.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM ".$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
107 'class' => 'Societe',
108 'file' => DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php',
109 'anonymize_fields' => array('name' => 'MAKEANONYMOUS', 'name_alias' => 'MAKEANONYMOUS', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone' => '---', 'email' => 'anonymous+__ID__@example.com', 'url' => '---', 'fax' => '---', 'siret' => '---', 'siren' => '---', 'ape' => '---', 'idprof4' => '---', 'idprof5' => '---', 'idprof6' => '---', 'tva_intra' => '---', 'capital' => 0, 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
110 'call_params' => array(
111 'delete' => array('id', 'user'), // $object->delete($id, $user)
112 'update' => array('id', 'user') // $object->update($id, $user)
113 )
114 ),
115 'tiers_niprosp_niclient' => array(
116 'group' => 'ThirdParty',
117 'label_key' => 'DATAPOLICY_TIERS_NIPROSPECT_NICLIENT',
118 'picto' => img_picto('', 'company', 'class="pictofixedwidth"'),
119 'const_delete' => 'DATAPOLICY_TIERS_NIPROSPECT_NICLIENT_DELETE_DELAY',
120 'const_anonymize' => 'DATAPOLICY_TIERS_NIPROSPECT_NICLIENT_ANONYMIZE_DELAY',
121 'sql_template' => "SELECT s.rowid FROM ".$sanitized_prefix."societe as s WHERE s.entity = __ENTITY__ AND s.client = ".Societe::NO_CUSTOMER." AND s.fournisseur = 0 AND s.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND NOT EXISTS (SELECT a.id FROM ".$sanitized_prefix."actioncomm as a WHERE a.fk_soc = s.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM ".$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
122 'sql_template_delete' => "SELECT s.rowid FROM ".$sanitized_prefix."societe as s WHERE s.entity = __ENTITY__ AND s.client = ".Societe::NO_CUSTOMER." AND s.fournisseur = 0 AND s.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND NOT EXISTS (SELECT a.id FROM ".$sanitized_prefix."actioncomm as a WHERE a.fk_soc = s.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM ".$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
123 'class' => 'Societe',
124 'file' => DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php',
125 'anonymize_fields' => array('name' => 'MAKEANONYMOUS', 'name_alias' => 'MAKEANONYMOUS', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone' => '---', 'email' => 'anonymous+__ID__@example.com', 'url' => '---', 'fax' => '---', 'siret' => '---', 'siren' => '---', 'ape' => '---', 'idprof4' => '---', 'idprof5' => '---', 'idprof6' => '---', 'tva_intra' => '---', 'capital' => 0, 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
126 'call_params' => array(
127 'delete' => array('id', 'user'), // $object->delete($id, $user)
128 'update' => array('id', 'user') // $object->update($id, $user)
129 )
130 ),
131 'tiers_fournisseur' => array(
132 'group' => 'ThirdParty',
133 'label_key' => 'DATAPOLICY_TIERS_FOURNISSEUR',
134 'picto' => img_picto('', 'supplier', 'class="pictofixedwidth"'),
135 'const_delete' => '',
136 'const_anonymize' => 'DATAPOLICY_TIERS_FOURNISSEUR_ANONYMIZE_DELAY',
137 // Suppliers must be checked against llx_facture_fourn (purchase invoices),
138 // not llx_facture (sales invoices). A pure supplier with no sales would
139 // otherwise always pass the filter and get anonymized regardless of
140 // purchase history (#38788).
141 'sql_template' => "SELECT s.rowid FROM ".$sanitized_prefix."societe as s WHERE s.entity = __ENTITY__ AND s.fournisseur = 1 AND s.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND NOT EXISTS (SELECT a.id FROM ".$sanitized_prefix."actioncomm as a WHERE a.fk_soc = s.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM ".$sanitized_prefix."facture_fourn as f WHERE f.fk_soc = s.rowid)",
142 'class' => 'Societe',
143 'file' => DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php',
144 'anonymize_fields' => array('name' => 'MAKEANONYMOUS', 'name_alias' => 'MAKEANONYMOUS', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone' => '---', 'email' => 'anonymous+__ID__@example.com', 'url' => '---', 'fax' => '---', 'siret' => '---', 'siren' => '---', 'ape' => '---', 'idprof4' => '---', 'idprof5' => '---', 'idprof6' => '---', 'tva_intra' => '---', 'capital' => 0, 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
145 'call_params' => array(
146 'delete' => array('id', 'user'), // $object->delete($id, $user)
147 'update' => array('id', 'user') // $object->update($id, $user)
148 )
149 ),
150 // --- Contacts ---
151 'contact_client' => array(
152 'group' => 'Contact',
153 'label_key' => 'DATAPOLICY_CONTACT_CLIENT',
154 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
155 'const_delete' => '',
156 'const_anonymize' => 'DATAPOLICY_CONTACT_CLIENT_ANONYMIZE_DELAY',
157 'sql_template' => "SELECT c.rowid FROM "
158 .$sanitized_prefix."socpeople as c INNER JOIN "
159 .$sanitized_prefix."societe as s ON s.rowid = c.fk_soc WHERE c.entity = __ENTITY__ AND c.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND s.client = ".Societe::CUSTOMER." AND s.fournisseur = 0 AND NOT EXISTS (SELECT a.id FROM "
160 .$sanitized_prefix."actioncomm as a WHERE a.fk_contact = c.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM "
161 .$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
162 'class' => 'Contact',
163 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
164 'anonymize_fields' => array('lastname' => 'MAKEANONYMOUS', 'firstname' => 'MAKEANONYMOUS', 'poste' => '---', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone_pro' => '---', 'phone_perso' => '---', 'phone_mobile' => '---', 'email' => 'anonymous+__ID__@example.com', 'photo' => '', 'url' => '---', 'fax' => '---', 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
165 'call_params' => array(
166 'delete' => array('user'), // $object->delete($user)
167 'update' => array('id', 'user') // $object->update($id, $user)
168 )
169 ),
170 'contact_prospect' => array(
171 'group' => 'Contact',
172 'label_key' => 'DATAPOLICY_CONTACT_PROSPECT',
173 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
174 'const_delete' => '',
175 'const_anonymize' => 'DATAPOLICY_CONTACT_PROSPECT_ANONYMIZE_DELAY',
176 'sql_template' => "SELECT c.rowid FROM "
177 .$sanitized_prefix."socpeople as c INNER JOIN "
178 .$sanitized_prefix."societe as s ON s.rowid = c.fk_soc WHERE c.entity = __ENTITY__ AND c.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND s.client = ".Societe::PROSPECT." AND s.fournisseur = 0 AND NOT EXISTS (SELECT a.id FROM "
179 .$sanitized_prefix."actioncomm as a WHERE a.fk_contact = c.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM "
180 .$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
181 'class' => 'Contact',
182 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
183 'anonymize_fields' => array('lastname' => 'MAKEANONYMOUS', 'firstname' => 'MAKEANONYMOUS', 'poste' => '---', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone_pro' => '---', 'phone_perso' => '---', 'phone_mobile' => '---', 'email' => 'anonymous+__ID__@example.com', 'photo' => '', 'url' => '---', 'fax' => '---', 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
184 'call_params' => array(
185 'delete' => array('user'), // $object->delete($user)
186 'update' => array('id', 'user') // $object->update($id, $user)
187 )
188 ),
189 'contact_prospect_client' => array(
190 'group' => 'Contact',
191 'label_key' => 'DATAPOLICY_CONTACT_PROSPECT_CLIENT',
192 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
193 'const_delete' => '',
194 'const_anonymize' => 'DATAPOLICY_CONTACT_PROSPECT_CLIENT_ANONYMIZE_DELAY',
195 'sql_template' => "SELECT c.rowid FROM "
196 .$sanitized_prefix."socpeople as c INNER JOIN "
197 .$sanitized_prefix."societe as s ON s.rowid = c.fk_soc WHERE c.entity = __ENTITY__ AND c.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND s.client = ".Societe::CUSTOMER_AND_PROSPECT." AND s.fournisseur = 0 AND NOT EXISTS (SELECT a.id FROM "
198 .$sanitized_prefix."actioncomm as a WHERE a.fk_contact = c.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM "
199 .$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
200 'class' => 'Contact',
201 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
202 'anonymize_fields' => array('lastname' => 'MAKEANONYMOUS', 'firstname' => 'MAKEANONYMOUS', 'poste' => '---', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone_pro' => '---', 'phone_perso' => '---', 'phone_mobile' => '---', 'email' => 'anonymous+__ID__@example.com', 'photo' => '', 'url' => '---', 'fax' => '---', 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
203 'call_params' => array(
204 'delete' => array('user'), // $object->delete($user)
205 'update' => array('id', 'user') // $object->update($id, $user)
206 )
207 ),
208 'contact_niprosp_niclient' => array(
209 'group' => 'Contact',
210 'label_key' => 'DATAPOLICY_CONTACT_NIPROSPECT_NICLIENT',
211 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
212 'const_delete' => '',
213 'const_anonymize' => 'DATAPOLICY_CONTACT_NIPROSPECT_NICLIENT_ANONYMIZE_DELAY',
214 'sql_template' => "SELECT c.rowid FROM "
215 .$sanitized_prefix."socpeople as c INNER JOIN "
216 .$sanitized_prefix."societe as s ON s.rowid = c.fk_soc WHERE c.entity = __ENTITY__ AND c.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND s.client = ".Societe::NO_CUSTOMER." AND s.fournisseur = 0 AND NOT EXISTS (SELECT a.id FROM "
217 .$sanitized_prefix."actioncomm as a WHERE a.fk_contact = c.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM "
218 .$sanitized_prefix."facture as f WHERE f.fk_soc = s.rowid)",
219 'class' => 'Contact',
220 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
221 'anonymize_fields' => array('lastname' => 'MAKEANONYMOUS', 'firstname' => 'MAKEANONYMOUS', 'poste' => '---', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone_pro' => '---', 'phone_perso' => '---', 'phone_mobile' => '---', 'email' => 'anonymous+__ID__@example.com', 'photo' => '', 'url' => '---', 'fax' => '---', 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
222 'call_params' => array(
223 'delete' => array('user'), // $object->delete($user)
224 'update' => array('id', 'user') // $object->update($id, $user)
225 )
226 ),
227 'contact_fournisseur' => array(
228 'group' => 'Contact',
229 'label_key' => 'DATAPOLICY_CONTACT_FOURNISSEUR',
230 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
231 'const_delete' => '',
232 'const_anonymize' => 'DATAPOLICY_CONTACT_FOURNISSEUR_ANONYMIZE_DELAY',
233 // Same as tiers_fournisseur above: a contact attached to a pure supplier
234 // must be checked against llx_facture_fourn, not llx_facture (#38788).
235 'sql_template' => "SELECT c.rowid FROM "
236 .$sanitized_prefix."socpeople as c INNER JOIN "
237 .$sanitized_prefix."societe as s ON s.rowid = c.fk_soc WHERE c.entity = __ENTITY__ AND c.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND s.fournisseur = 1 AND NOT EXISTS (SELECT a.id FROM "
238 .$sanitized_prefix."actioncomm as a WHERE a.fk_contact = c.rowid AND a.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)) AND NOT EXISTS (SELECT f.rowid FROM "
239 .$sanitized_prefix."facture_fourn as f WHERE f.fk_soc = s.rowid)",
240 'class' => 'Contact',
241 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
242 'anonymize_fields' => array('lastname' => 'MAKEANONYMOUS', 'firstname' => 'MAKEANONYMOUS', 'poste' => '---', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone_pro' => '---', 'phone_perso' => '---', 'phone_mobile' => '---', 'email' => 'anonymous+__ID__@example.com', 'photo' => '', 'url' => '---', 'fax' => '---', 'socialnetworks' => [], 'geolat' => 0, 'geolong' => 0, 'ip' => '0.0.0.0'),
243 'call_params' => array(
244 'delete' => array('user'), // $object->delete($user)
245 'update' => array('id', 'user') // $object->update($id, $user)
246 )
247 )
248 );
249 if (isModEnabled('member')) {
250 // --- Members --- Reference date: datefin (end of membership validity). tms (last modification) is intentionally NOT used here.
251 $sqltemplate = "SELECT a.rowid FROM ".$sanitized_prefix."adherent as a WHERE a.entity = __ENTITY__ AND a.datefin < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)";
252 $sqltemplate .= " AND NOT EXISTS (SELECT ac.id FROM ".$sanitized_prefix."actioncomm as ac WHERE ac.fk_element = a.rowid AND ac.elementtype = 'member' AND ac.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH))";
253 $sqltemplate .= " AND NOT EXISTS (SELECT s.rowid FROM ".$sanitized_prefix."subscription as s WHERE s.fk_adherent = a.rowid AND s.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH))";
254
255 $arrayofpolicies['adherent'] = array(
256 'group' => 'Member',
257 'label_key' => 'DATAPOLICY_ADHERENT',
258 'picto' => img_picto('', 'member', 'class="pictofixedwidth"'),
259 'const_delete' => '',
260 'const_anonymize' => 'DATAPOLICY_ADHERENT_ANONYMIZE_DELAY',
261 'sql_template' => $sqltemplate,
262 'class' => 'Adherent',
263 'file' => DOL_DOCUMENT_ROOT . '/adherents/class/adherent.class.php',
264 'anonymize_fields' => array(
265 'lastname' => 'MAKEANONYMOUS', 'firstname' => 'MAKEANONYMOUS', 'societe' => '---', 'address' => '---', 'town' => '---', 'zip' => '---',
266 'phone' => '---', 'phone_perso' => '---', 'phone_mobile' => '---', 'email' => 'anonymous+__ID__@example.com', 'birth' => dol_mktime(0, 0, 0, 1, 1, 1900),
267 'photo' => '', 'url' => '---', 'fax' => '---', 'socialnetworks' => [], 'ip' => '0.0.0.0', 'note_private' => '---', 'note_public' => '---', 'statut' => 0
268 ), // Force status to "canceled"
269 'call_params' => array(
270 'delete' => array('user'), // $object->delete($user)
271 'update' => array('user') // $object->update($user)
272 )
273 );
274 }
275
276 if (isModEnabled('recruitment')) {
277 // --- Recruitment ---
278 $arrayofpolicies['recruitment_candidature'] = array(
279 'group' => 'Recruitment',
280 'label_key' => 'DATAPOLICY_RECRUITMENT_CANDIDATURE',
281 'picto' => img_picto('', 'recruitmentcandidature', 'class="pictofixedwidth"'),
282 'const_delete' => 'DATAPOLICY_RECRUITMENT_CANDIDATURE_DELETE_DELAY',
283 'const_anonymize' => '', // Anonymization not applicable
284 'sql_template_delete' => "SELECT c.rowid FROM ".$sanitized_prefix."recruitment_recruitmentcandidature as c WHERE c.entity = __ENTITY__ AND c.tms < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH) AND NOT EXISTS (SELECT ac.id FROM ".$sanitized_prefix."actioncomm as ac WHERE ac.elementtype = 'recruitmentcandidature@recruitment' AND ac.fk_element = c.rowid AND ac.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH))",
285 'class' => 'RecruitmentCandidature',
286 'file' => DOL_DOCUMENT_ROOT . '/recruitment/class/recruitmentcandidature.class.php',
287 'anonymize_fields' => array(),
288 'call_params' => array(
289 'delete' => array('user'), // $object->delete($user)
290 'update' => array('user') // $object->update($user)
291 )
292 );
293 }
294
295 // TODO Allow an external module to add an entry into the array
296
297
298 return $arrayofpolicies;
299 }
300
307 public function cleanDataForDataPolicy(): int
308 {
309 global $conf, $user;
310
311 // Reset state properties for this specific execution run.
312 $this->nbupdated = 0;
313 $this->nbdeleted = 0;
314 $this->errorCount = 0;
315 $this->errorMessages = array();
316
317 // Tracks record IDs that have been processed in this run to prevent duplicate actions (e.g., anonymizing a just-deleted record).
318 $processedIds = array();
319 // Caches object instances to avoid redundant 'new Class()' calls, improving performance.
320 $objectInstances = array();
321
322 // Retrieve the master list of all data policies. This separates configuration from execution.
323 $dataPolicies = $this->getDataPolicies();
324
325 $this->db->begin();
326
327 // Iterate through each defined policy to apply its rules.
328 foreach ($dataPolicies as $policy) {
329 // Instantiate object only once per class type for efficiency.
330 if (! isset($objectInstances[$policy['class']])) {
331 require_once $policy['file'];
332 $classtoinit = $policy['class'];
333 $objectInstances[$policy['class']] = new $classtoinit($this->db);
334 }
335 $object = $objectInstances[$policy['class']];
336
337 // Process actions.
338 // ->errorCount, ->nbupdated and ->nbdelete will be set after that.
339
340 // The order of operations is critical: deletion is always processed before anonymization.
341 // This ensures that if a record meets criteria for both, it is deleted as the first and final action.
342 $this->_processPolicyAction($policy, 'delete', $object, $processedIds, $conf, $user);
343 // Now process anonymization
344 $this->_processPolicyAction($policy, 'anonymize', $object, $processedIds, $conf, $user);
345 }
346
347 // Finalize the transaction based on the outcome of all operations.
348 if (! $this->errorCount) {
349 $this->db->commit();
350 $this->output = $this->nbupdated . ' record(s) anonymized, ' . $this->nbdeleted . ' record(s) deleted.';
351 } else {
352 $this->db->rollback();
353 $this->error = implode("\n", $this->errorMessages);
354 }
355
356 return $this->errorCount ? 1 : 0;
357 }
358
371 private function _processPolicyAction($policy, $action, $object, &$processedIds, $conf, $user)
372 {
373 $constName = $policy['const_' . $action] ?? null;
374 $delay = $constName ? getDolGlobalInt($constName) : 0;
375
376 if ($delay <= 0) {
377 return;
378 }
379
380 // Prepare SQL query
381 $sqlPlaceholders = array(
382 '__ENTITY__' => (string) ((int) $conf->entity),
383 '__DELAY__' => (string) ((int) $delay),
384 '__NOW__' => "'" . $this->db->idate(dol_now()) . "'"
385 );
386 $sql = str_replace(array_keys($sqlPlaceholders), array_values($sqlPlaceholders), $policy['sql_template'.($action == 'delete' ? '_delete' : '')]); // @phan-suppress-current-line SqlInjection
387
388 $resql = $this->db->query($sql);
389
390 if (! $resql) {
391 $this->errorCount++;
392 $this->errorMessages[] = 'Error executing ' . $action . ' query for policy ' . $constName . ': ' . $this->db->lasterror();
393
394 return;
395 }
396
397 // Define the handler method for the action
398 $handlerMethod = '_handle' . ucfirst($action);
399
400 // Process the records found by the query
401 while ($obj = $this->db->fetch_object($resql)) {
402 if (in_array($obj->rowid, $processedIds) || ! method_exists($this, $handlerMethod)) {
403 continue;
404 }
406 $object = clone $object;
407 $object->fetch($obj->rowid);
408
409 // isObjectUsed() is a referential-integrity guard for deletion (cannot drop a
410 // thirdparty that still has quotes/orders/contracts pointing at it). Anonymization
411 // must still be allowed: personal data has to be erasable even when commercial
412 // documents from the retention period are kept (#38786).
413 if ($action === 'delete' && !empty($object->childtables) && method_exists($object, 'isObjectUsed') && $object->isObjectUsed() != 0) {
414 continue; // Not an error, just skipping.
415 }
416
417 // Dynamically call the appropriate handler (_handleDelete or _handleAnonymize)
418 $result = $this->$handlerMethod($object, $user, $policy);
419
420 // Record the outcome and add to processed list on success
421 $this->_recordActionResult($result, $object, $action);
422 $processedIds[] = $obj->rowid;
423 }
424 }
425
434 private function _handleDelete($object, $user, $policy): int
435 {
436 $callArgs = $this->_buildCallArguments($object, $user, $policy, 'delete');
437
438 return $object->delete(...$callArgs);
439 }
440
449 private function _handleAnonymize($object, $user, $policy): int
450 {
451 foreach ($policy['anonymize_fields'] as $field => $val) {
452 if ($val == 'MAKEANONYMOUS') {
453 // For each field with rule "MAKEANONYMOUS, set the new value, keeping the ID.
454 $object->$field = $field . '-anon-' . $object->id;
455 } else {
456 // For others, force the value, but only if not already empty.
457 if (!empty($object->$field)) {
458 $newval = str_replace('__ID__', $object->id ? (string) $object->id : '0', $val);
459 $object->$field = $newval;
460 }
461 }
462 }
463
464 $this->_anonymizeExtraFields($object);
465
466 $callArgs = $this->_buildCallArguments($object, $user, $policy, 'update');
467
468 $result = $object->update(...$callArgs);
469
470 if ($result > 0) {
471 $object->insertExtraFields();
472 }
473
474 return $result;
475 }
476
488 {
489 if (empty($object->table_element)) {
490 return;
491 }
492
493 // Retrieve extra fields definitions flagged as personal data for this object type
494 $sql = "SELECT ef.name, ef.type";
495 $sql .= " FROM " . $this->db->prefix() . "extrafields as ef";
496 $sql .= " WHERE ef.elementtype = '" . $this->db->escape($object->table_element) . "'";
497 $sql .= " AND ef.personal_data = 1";
498 $sql .= " AND ef.entity IN (0, " . (int) (isset($GLOBALS['conf']) ? $GLOBALS['conf']->entity : 1) . ")";
499
500 $resql = $this->db->query($sql);
501
502 if (!$resql) {
503 $this->errorCount++;
504 $this->errorMessages[] = 'Error fetching personal extra fields for ' . $object->table_element . ': ' . $this->db->lasterror();
505 return;
506 }
507
508 while ($efObj = $this->db->fetch_object($resql)) {
509 $fieldName = $efObj->name;
510 $fieldType = $efObj->type;
511
512 // Extra field values are stored in $object->array_options with key 'options_<name>'
513 $optionKey = 'options_' . $fieldName;
514
515 if (!isset($object->array_options[$optionKey])) {
516 continue;
517 }
518
519 if (empty($object->array_options[$optionKey])) {
520 continue;
521 }
522
523 if (in_array($fieldType, array('multiselect', 'checkbox'))) {
524 $object->array_options[$optionKey] = array(); // Default: treat as array
525 } elseif (in_array($fieldType, array('boolean', 'radio'))) {
526 $object->array_options[$optionKey] = '0'; // Default: treat as boolean
527 } elseif (in_array($fieldType, array('date', 'datetime', 'timestamp'))) {
528 $object->array_options[$optionKey] = dol_mktime(0, 0, 0, 1, 1, 1900); // Default: treat as timestamp
529 } elseif (in_array($fieldType, array('int', 'double', 'price', 'stars'))) {
530 $object->array_options[$optionKey] = 0; // Default: treat as numeric
531 } else {
532 $object->array_options[$optionKey] = '---'; // Default: treat as varchar/text
533 }
534 }
535 }
536
546 private function _buildCallArguments($object, $user, $policy, $method)
547 {
548 $availableArgs = array(
549 'id' => $object->id,
550 'user' => $user
551 );
552
553 $paramConfig = $policy['call_params'][$method] ?? [];
554
555 return array_map(
560 static function (string $paramName) use ($availableArgs) {
561 return $availableArgs[$paramName];
562 },
563 $paramConfig
564 );
565 }
566
575 private function _recordActionResult($result, $object, $action)
576 {
577 if ($result < 0) {
578 $this->errorCount++;
579 $this->errorMessages[] = 'Failed to ' . $action . ' record ID ' . $object->id . ' from class ' . get_class($object) . '. Error: ' . $object->errorsToString();
580 } else {
581 if ($action === 'delete') {
582 $this->nbdeleted++;
583 } elseif ($action === 'anonymize') {
584 // Only count as updated if the update method returns a positive result
585 $this->nbupdated++;
586 }
587 }
588 }
589}
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
Class DataPolicyCron.
getDataPolicies()
Defines and returns the centralized data policy configuration.
_recordActionResult($result, $object, $action)
Records the result of an action, updating counters and error messages.
cleanDataForDataPolicy()
Main cron task execution method.
_anonymizeExtraFields($object)
Anonymizes extra fields flagged as personal data (personal_data = 1) for a given object.
_buildCallArguments($object, $user, $policy, $method)
Builds the dynamic argument list for method calls based on policy configuration.
_handleAnonymize($object, $user, $policy)
Handles the specific logic for anonymizing an object.
_handleDelete($object, $user, $policy)
Handles the specific logic for deleting an object.
__construct(DoliDB $db)
Constructor.
Class to manage Dolibarr database access.
const PROSPECT
Third party type is a prospect.
const CUSTOMER_AND_PROSPECT
Third party type is a customer and a prospect.
const CUSTOMER
Third party type is a customer.
const NO_CUSTOMER
Third party type is no customer.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dol_now($mode='gmt')
Return date for now.
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
isModEnabled($module)
Is Dolibarr module enabled.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)