dolibarr 24.0.0-beta
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-2025 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024 William Mead <william.mead@manchenumerique.fr>
5 * Copyright (C) 2024-2025 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 $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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$prefix."facture as f WHERE f.fk_soc = s.rowid)",
122 'sql_template_delete' => "SELECT s.rowid FROM ".$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 ".$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 ".$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 ".$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 ".$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 ".$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 ".$prefix."socpeople as c INNER JOIN ".$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 ".$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 ".$prefix."facture as f WHERE f.fk_soc = s.rowid)",
158 'class' => 'Contact',
159 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
160 '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'),
161 'call_params' => array(
162 'delete' => array('user'), // $object->delete($user)
163 'update' => array('id', 'user') // $object->update($id, $user)
164 )
165 ),
166 'contact_prospect' => array(
167 'group' => 'Contact',
168 'label_key' => 'DATAPOLICY_CONTACT_PROSPECT',
169 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
170 'const_delete' => '',
171 'const_anonymize' => 'DATAPOLICY_CONTACT_PROSPECT_ANONYMIZE_DELAY',
172 'sql_template' => "SELECT c.rowid FROM ".$prefix."socpeople as c INNER JOIN ".$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 ".$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 ".$prefix."facture as f WHERE f.fk_soc = s.rowid)",
173 'class' => 'Contact',
174 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
175 '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'),
176 'call_params' => array(
177 'delete' => array('user'), // $object->delete($user)
178 'update' => array('id', 'user') // $object->update($id, $user)
179 )
180 ),
181 'contact_prospect_client' => array(
182 'group' => 'Contact',
183 'label_key' => 'DATAPOLICY_CONTACT_PROSPECT_CLIENT',
184 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
185 'const_delete' => '',
186 'const_anonymize' => 'DATAPOLICY_CONTACT_PROSPECT_CLIENT_ANONYMIZE_DELAY',
187 'sql_template' => "SELECT c.rowid FROM ".$prefix."socpeople as c INNER JOIN ".$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 ".$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 ".$prefix."facture as f WHERE f.fk_soc = s.rowid)",
188 'class' => 'Contact',
189 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
190 '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'),
191 'call_params' => array(
192 'delete' => array('user'), // $object->delete($user)
193 'update' => array('id', 'user') // $object->update($id, $user)
194 )
195 ),
196 'contact_niprosp_niclient' => array(
197 'group' => 'Contact',
198 'label_key' => 'DATAPOLICY_CONTACT_NIPROSPECT_NICLIENT',
199 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
200 'const_delete' => '',
201 'const_anonymize' => 'DATAPOLICY_CONTACT_NIPROSPECT_NICLIENT_ANONYMIZE_DELAY',
202 'sql_template' => "SELECT c.rowid FROM ".$prefix."socpeople as c INNER JOIN ".$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 ".$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 ".$prefix."facture as f WHERE f.fk_soc = s.rowid)",
203 'class' => 'Contact',
204 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
205 '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'),
206 'call_params' => array(
207 'delete' => array('user'), // $object->delete($user)
208 'update' => array('id', 'user') // $object->update($id, $user)
209 )
210 ),
211 'contact_fournisseur' => array(
212 'group' => 'Contact',
213 'label_key' => 'DATAPOLICY_CONTACT_FOURNISSEUR',
214 'picto' => img_picto('', 'contact', 'class="pictofixedwidth"'),
215 'const_delete' => '',
216 'const_anonymize' => 'DATAPOLICY_CONTACT_FOURNISSEUR_ANONYMIZE_DELAY',
217 // Same as tiers_fournisseur above: a contact attached to a pure supplier
218 // must be checked against llx_facture_fourn, not llx_facture (#38788).
219 'sql_template' => "SELECT c.rowid FROM ".$prefix."socpeople as c INNER JOIN ".$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 ".$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 ".$prefix."facture_fourn as f WHERE f.fk_soc = s.rowid)",
220 'class' => 'Contact',
221 'file' => DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php',
222 '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'),
223 'call_params' => array(
224 'delete' => array('user'), // $object->delete($user)
225 'update' => array('id', 'user') // $object->update($id, $user)
226 )
227 )
228 );
229 if (isModEnabled('member')) {
230 // --- Members --- Reference date: datefin (end of membership validity). tms (last modification) is intentionally NOT used here.
231 $sqltemplate = "SELECT a.rowid FROM ".$prefix."adherent as a WHERE a.entity = __ENTITY__ AND a.datefin < DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH)";
232 $sqltemplate .= " AND NOT EXISTS (SELECT ac.id FROM ".$prefix."actioncomm as ac WHERE ac.fk_element = a.rowid AND ac.elementtype = 'member' AND ac.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH))";
233 $sqltemplate .= " AND NOT EXISTS (SELECT s.rowid FROM ".$prefix."subscription as s WHERE s.fk_adherent = a.rowid AND s.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH))";
234
235 $arrayofpolicies['adherent'] = array(
236 'group' => 'Member',
237 'label_key' => 'DATAPOLICY_ADHERENT',
238 'picto' => img_picto('', 'member', 'class="pictofixedwidth"'),
239 'const_delete' => '',
240 'const_anonymize' => 'DATAPOLICY_ADHERENT_ANONYMIZE_DELAY',
241 'sql_template' => $sqltemplate,
242 'class' => 'Adherent',
243 'file' => DOL_DOCUMENT_ROOT . '/adherents/class/adherent.class.php',
244 'anonymize_fields' => array('lastname' => 'MAKEANONYMOUS', 'firstname' => 'MAKEANONYMOUS', 'societe' => '---', 'address' => '---', 'town' => '---', 'zip' => '---', 'phone' => '---', 'phone_perso' => '---', 'phone_mobile' => '---', 'email' => 'anonymous+__ID__@example.com', 'birth' => dol_mktime(0, 0, 0, 1, 1, 1900), 'photo' => '', 'url' => '---', 'fax' => '---', 'socialnetworks' => [], 'ip' => '0.0.0.0', 'note_private' => '---', 'note_public' => '---', 'statut' => 0), // Force status to "canceled / résilié"
245 'call_params' => array(
246 'delete' => array('user'), // $object->delete($user)
247 'update' => array('user') // $object->update($user)
248 )
249 );
250 }
251
252 if (isModEnabled('recruitment')) {
253 // --- Recruitment ---
254 $arrayofpolicies['recruitment_candidature'] = array(
255 'group' => 'Recruitment',
256 'label_key' => 'DATAPOLICY_RECRUITMENT_CANDIDATURE',
257 'picto' => img_picto('', 'recruitmentcandidature', 'class="pictofixedwidth"'),
258 'const_delete' => 'DATAPOLICY_RECRUITMENT_CANDIDATURE_DELETE_DELAY',
259 'const_anonymize' => '', // Anonymization not applicable
260 'sql_template_delete' => "SELECT c.rowid FROM ".$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 ".$prefix."actioncomm as ac WHERE ac.elementtype = 'recruitmentcandidature@recruitment' AND ac.fk_element = c.rowid AND ac.tms > DATE_SUB(__NOW__, INTERVAL __DELAY__ MONTH))",
261 'class' => 'RecruitmentCandidature',
262 'file' => DOL_DOCUMENT_ROOT . '/recruitment/class/recruitmentcandidature.class.php',
263 'anonymize_fields' => array(),
264 'call_params' => array(
265 'delete' => array('user'), // $object->delete($user)
266 'update' => array('user') // $object->update($user)
267 )
268 );
269 }
270
271 // TODO Allow an external module to add an entry into the array
272
273
274 return $arrayofpolicies;
275 }
276
283 public function cleanDataForDataPolicy(): int
284 {
285 global $conf, $user;
286
287 // Reset state properties for this specific execution run.
288 $this->nbupdated = 0;
289 $this->nbdeleted = 0;
290 $this->errorCount = 0;
291 $this->errorMessages = array();
292
293 // Tracks record IDs that have been processed in this run to prevent duplicate actions (e.g., anonymizing a just-deleted record).
294 $processedIds = array();
295 // Caches object instances to avoid redundant 'new Class()' calls, improving performance.
296 $objectInstances = array();
297
298 // Retrieve the master list of all data policies. This separates configuration from execution.
299 $dataPolicies = $this->getDataPolicies();
300
301 $this->db->begin();
302
303 // Iterate through each defined policy to apply its rules.
304 foreach ($dataPolicies as $policy) {
305 // Instantiate object only once per class type for efficiency.
306 if (! isset($objectInstances[$policy['class']])) {
307 require_once $policy['file'];
308 $classtoinit = $policy['class'];
309 $objectInstances[$policy['class']] = new $classtoinit($this->db);
310 }
311 $object = $objectInstances[$policy['class']];
312
313 // Process actions.
314 // ->errorCount, ->nbupdated and ->nbdelete will be set after that.
315
316 // The order of operations is critical: deletion is always processed before anonymization.
317 // This ensures that if a record meets criteria for both, it is deleted as the first and final action.
318 $this->_processPolicyAction($policy, 'delete', $object, $processedIds, $conf, $user);
319 // Now process anonymization
320 $this->_processPolicyAction($policy, 'anonymize', $object, $processedIds, $conf, $user);
321 }
322
323 // Finalize the transaction based on the outcome of all operations.
324 if (! $this->errorCount) {
325 $this->db->commit();
326 $this->output = $this->nbupdated . ' record(s) anonymized, ' . $this->nbdeleted . ' record(s) deleted.';
327 } else {
328 $this->db->rollback();
329 $this->error = implode("\n", $this->errorMessages);
330 }
331
332 return $this->errorCount ? 1 : 0;
333 }
334
347 private function _processPolicyAction($policy, $action, $object, &$processedIds, $conf, $user)
348 {
349 $constName = $policy['const_' . $action] ?? null;
350 $delay = $constName ? getDolGlobalInt($constName) : 0;
351
352 if ($delay <= 0) {
353 return;
354 }
355
356 // Prepare SQL query
357 $sqlPlaceholders = array(
358 '__ENTITY__' => (string) $conf->entity,
359 '__DELAY__' => (string) $delay,
360 '__NOW__' => "'" . $this->db->idate(dol_now()) . "'"
361 );
362 $sql = str_replace(array_keys($sqlPlaceholders), array_values($sqlPlaceholders), $policy['sql_template'.($action == 'delete' ? '_delete' : '')]);
363
364 $resql = $this->db->query($sql);
365
366 if (! $resql) {
367 $this->errorCount++;
368 $this->errorMessages[] = 'Error executing ' . $action . ' query for policy ' . $constName . ': ' . $this->db->lasterror();
369
370 return;
371 }
372
373 // Define the handler method for the action
374 $handlerMethod = '_handle' . ucfirst($action);
375
376 // Process the records found by the query
377 while ($obj = $this->db->fetch_object($resql)) {
378 if (in_array($obj->rowid, $processedIds) || ! method_exists($this, $handlerMethod)) {
379 continue;
380 }
382 $object = clone $object;
383 $object->fetch($obj->rowid);
384
385 // isObjectUsed() is a referential-integrity guard for deletion (cannot drop a
386 // thirdparty that still has quotes/orders/contracts pointing at it). Anonymization
387 // must still be allowed: personal data has to be erasable even when commercial
388 // documents from the retention period are kept (#38786).
389 if ($action === 'delete' && !empty($object->childtables) && method_exists($object, 'isObjectUsed') && $object->isObjectUsed() != 0) {
390 continue; // Not an error, just skipping.
391 }
392
393 // Dynamically call the appropriate handler (_handleDelete or _handleAnonymize)
394 $result = $this->$handlerMethod($object, $user, $policy);
395
396 // Record the outcome and add to processed list on success
397 $this->_recordActionResult($result, $object, $action);
398 $processedIds[] = $obj->rowid;
399 }
400 }
401
410 private function _handleDelete($object, $user, $policy): int
411 {
412 $callArgs = $this->_buildCallArguments($object, $user, $policy, 'delete');
413
414 return $object->delete(...$callArgs);
415 }
416
425 private function _handleAnonymize($object, $user, $policy): int
426 {
427 foreach ($policy['anonymize_fields'] as $field => $val) {
428 if ($val == 'MAKEANONYMOUS') {
429 // For each field with rule "MAKEANONYMOUS, set the new value, keeping the ID.
430 $object->$field = $field . '-anon-' . $object->id;
431 } else {
432 // For others, force the value, but only if not already empty.
433 if (!empty($object->$field)) {
434 $newval = str_replace('__ID__', $object->id ? (string) $object->id : '0', $val);
435 $object->$field = $newval;
436 }
437 }
438 }
439
440 $this->_anonymizeExtraFields($object);
441
442 $callArgs = $this->_buildCallArguments($object, $user, $policy, 'update');
443
444 $result = $object->update(...$callArgs);
445
446 if ($result > 0) {
447 $object->insertExtraFields();
448 }
449
450 return $result;
451 }
452
464 {
465 if (empty($object->table_element)) {
466 return;
467 }
468
469 // Retrieve extra fields definitions flagged as personal data for this object type
470 $sql = "SELECT ef.name, ef.type";
471 $sql .= " FROM " . $this->db->prefix() . "extrafields as ef";
472 $sql .= " WHERE ef.elementtype = '" . $this->db->escape($object->table_element) . "'";
473 $sql .= " AND ef.personal_data = 1";
474 $sql .= " AND ef.entity IN (0, " . (int) (isset($GLOBALS['conf']) ? $GLOBALS['conf']->entity : 1) . ")";
475
476 $resql = $this->db->query($sql);
477
478 if (!$resql) {
479 $this->errorCount++;
480 $this->errorMessages[] = 'Error fetching personal extra fields for ' . $object->table_element . ': ' . $this->db->lasterror();
481 return;
482 }
483
484 while ($efObj = $this->db->fetch_object($resql)) {
485 $fieldName = $efObj->name;
486 $fieldType = $efObj->type;
487
488 // Extra field values are stored in $object->array_options with key 'options_<name>'
489 $optionKey = 'options_' . $fieldName;
490
491 if (!isset($object->array_options[$optionKey])) {
492 continue;
493 }
494
495 if (empty($object->array_options[$optionKey])) {
496 continue;
497 }
498
499 if (in_array($fieldType, array('multiselect', 'checkbox'))) {
500 $object->array_options[$optionKey] = array(); // Default: treat as array
501 } elseif (in_array($fieldType, array('boolean', 'radio'))) {
502 $object->array_options[$optionKey] = '0'; // Default: treat as boolean
503 } elseif (in_array($fieldType, array('date', 'datetime', 'timestamp'))) {
504 $object->array_options[$optionKey] = dol_mktime(0, 0, 0, 1, 1, 1900); // Default: treat as timestamp
505 } elseif (in_array($fieldType, array('int', 'double', 'price', 'stars'))) {
506 $object->array_options[$optionKey] = 0; // Default: treat as numeric
507 } else {
508 $object->array_options[$optionKey] = '---'; // Default: treat as varchar/text
509 }
510 }
511 }
512
522 private function _buildCallArguments($object, $user, $policy, $method)
523 {
524 $availableArgs = array(
525 'id' => $object->id,
526 'user' => $user
527 );
528
529 $paramConfig = $policy['call_params'][$method] ?? [];
530
531 return array_map(
536 static function (string $paramName) use ($availableArgs) {
537 return $availableArgs[$paramName];
538 },
539 $paramConfig
540 );
541 }
542
551 private function _recordActionResult($result, $object, $action)
552 {
553 if ($result < 0) {
554 $this->errorCount++;
555 $this->errorMessages[] = 'Failed to ' . $action . ' record ID ' . $object->id . ' from class ' . get_class($object) . '. Error: ' . $object->errorsToString();
556 } else {
557 if ($action === 'delete') {
558 $this->nbdeleted++;
559 } elseif ($action === 'anonymize') {
560 // Only count as updated if the update method returns a positive result
561 $this->nbupdated++;
562 }
563 }
564 }
565}
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...
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)
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
isModEnabled($module)
Is Dolibarr module enabled.