Merge pull request #22683 from mattwire/paymentstatushelpersall
[civicrm-core.git] / CRM / Import / Forms.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 use Civi\Api4\UserJob;
19
20 /**
21 * This class helps the forms within the import flow access submitted & parsed values.
22 */
23 class CRM_Import_Forms extends CRM_Core_Form {
24
25 /**
26 * User job id.
27 *
28 * This is the primary key of the civicrm_user_job table which is used to
29 * track the import.
30 *
31 * @var int
32 */
33 protected $userJobID;
34
35 /**
36 * @return int|null
37 */
38 public function getUserJobID(): ?int {
39 if (!$this->userJobID && $this->get('user_job_id')) {
40 $this->userJobID = $this->get('user_job_id');
41 }
42 return $this->userJobID;
43 }
44
45 /**
46 * Set user job ID.
47 *
48 * @param int $userJobID
49 */
50 public function setUserJobID(int $userJobID): void {
51 $this->userJobID = $userJobID;
52 // This set allows other forms in the flow ot use $this->get('user_job_id').
53 $this->set('user_job_id', $userJobID);
54 }
55
56 /**
57 * User job details.
58 *
59 * This is the relevant row from civicrm_user_job.
60 *
61 * @var array
62 */
63 protected $userJob;
64
65 /**
66 * Get User Job.
67 *
68 * API call to retrieve the userJob row.
69 *
70 * @return array
71 *
72 * @throws \API_Exception
73 */
74 protected function getUserJob(): array {
75 if (!$this->userJob) {
76 $this->userJob = UserJob::get()
77 ->addWhere('id', '=', $this->getUserJobID())
78 ->execute()
79 ->first();
80 }
81 return $this->userJob;
82 }
83
84 /**
85 * Get submitted values stored in the user job.
86 *
87 * @return array
88 * @throws \API_Exception
89 */
90 protected function getUserJobSubmittedValues(): array {
91 return $this->getUserJob()['metadata']['submitted_values'];
92 }
93
94 /**
95 * Fields that may be submitted on any form in the flow.
96 *
97 * @var string[]
98 */
99 protected $submittableFields = [
100 // Skip column header is actually a field that would be added from the
101 // datasource - but currently only in contact, it is always there for
102 // other imports, ditto uploadFile.
103 'skipColumnHeader' => 'DataSource',
104 'fieldSeparator' => 'DataSource',
105 'uploadFile' => 'DataSource',
106 'contactType' => 'DataSource',
107 'contactSubType' => 'DataSource',
108 'dateFormats' => 'DataSource',
109 'savedMapping' => 'DataSource',
110 'dataSource' => 'DataSource',
111 'dedupe_rule_id' => 'DataSource',
112 'onDuplicate' => 'DataSource',
113 'disableUSPS' => 'DataSource',
114 ];
115
116 /**
117 * Get the submitted value, accessing it from whatever form in the flow it is
118 * submitted on.
119 *
120 * @param string $fieldName
121 *
122 * @return mixed|null
123 * @throws \CRM_Core_Exception
124 */
125 public function getSubmittedValue(string $fieldName) {
126 if ($fieldName === 'dataSource') {
127 // Hard-coded handling for DataSource as it affects the contents of
128 // getSubmittableFields and can cause a loop.
129 return $this->controller->exportValue('DataSource', 'dataSource');
130 }
131 $mappedValues = $this->getSubmittableFields();
132 if (array_key_exists($fieldName, $mappedValues)) {
133 return $this->controller->exportValue($mappedValues[$fieldName], $fieldName);
134 }
135 return parent::getSubmittedValue($fieldName);
136
137 }
138
139 /**
140 * Get values submitted on any form in the multi-page import flow.
141 *
142 * @return array
143 */
144 public function getSubmittedValues(): array {
145 $values = [];
146 foreach (array_keys($this->getSubmittableFields()) as $key) {
147 $values[$key] = $this->getSubmittedValue($key);
148 }
149 return $values;
150 }
151
152 /**
153 * Get the available datasource.
154 *
155 * Permission dependent, this will look like
156 * [
157 * 'CRM_Import_DataSource_CSV' => 'Comma-Separated Values (CSV)',
158 * 'CRM_Import_DataSource_SQL' => 'SQL Query',
159 * ]
160 *
161 * The label is translated.
162 *
163 * @return array
164 */
165 protected function getDataSources(): array {
166 $dataSources = [];
167 foreach (['CRM_Import_DataSource_SQL', 'CRM_Import_DataSource_CSV'] as $dataSourceClass) {
168 $object = new $dataSourceClass();
169 if ($object->checkPermission()) {
170 $dataSources[$dataSourceClass] = $object->getInfo()['title'];
171 }
172 }
173 return $dataSources;
174 }
175
176 /**
177 * Get the name of the datasource class.
178 *
179 * This function prioritises retrieving from GET and POST over 'submitted'.
180 * The reason for this is the submitted array will hold the previous submissions
181 * data until after buildForm is called.
182 *
183 * This is problematic in the forward->back flow & option changing flow. As in....
184 *
185 * 1) Load DataSource form - initial default datasource is set to CSV and the
186 * form is via ajax (this calls DataSourceConfig to get the data).
187 * 2) User changes the source to SQL - the ajax updates the html but the
188 * form was built with the expectation that the csv-specific fields would be
189 * required.
190 * 3) When the user submits Quickform calls preProcess and buildForm and THEN
191 * retrieves the submitted values based on what has been added in buildForm.
192 * Only the submitted values for fields added in buildForm are available - but
193 * these have to be added BEFORE the submitted values are determined. Hence
194 * we look in the POST or GET to get the updated value.
195 *
196 * Note that an imminent refactor will involve storing the values in the
197 * civicrm_user_job table - this will hopefully help with a known (not new)
198 * issue whereby the previously submitted values (eg. skipColumnHeader has
199 * been checked or sql has been filled in) are not loaded via the ajax request.
200 *
201 * @return string|null
202 *
203 * @throws \CRM_Core_Exception
204 */
205 protected function getDataSourceClassName(): string {
206 $className = CRM_Utils_Request::retrieveValue(
207 'dataSource',
208 'String'
209 );
210 if (!$className) {
211 $className = $this->getSubmittedValue('dataSource');
212 }
213 if (!$className) {
214 $className = $this->getDefaultDataSource();
215 }
216 if ($this->getDataSources()[$className]) {
217 return $className;
218 }
219 throw new CRM_Core_Exception('Invalid data source');
220 }
221
222 /**
223 * Allow the datasource class to add fields.
224 *
225 * This is called as a snippet in DataSourceConfig and
226 * also from DataSource::buildForm to add the fields such
227 * that quick form picks them up.
228 *
229 * @throws \CRM_Core_Exception
230 */
231 protected function buildDataSourceFields(): void {
232 $dataSourceClass = $this->getDataSourceObject();
233 if ($dataSourceClass) {
234 $dataSourceClass->buildQuickForm($this);
235 }
236 }
237
238 /**
239 * Get the relevant datasource object.
240 *
241 * @return \CRM_Import_DataSource|null
242 *
243 * @throws \CRM_Core_Exception
244 */
245 protected function getDataSourceObject(): ?CRM_Import_DataSource {
246 $className = $this->getDataSourceClassName();
247 if ($className) {
248 /* @var CRM_Import_DataSource $dataSource */
249 return new $className($this->getUserJobID());
250 }
251 return NULL;
252 }
253
254 /**
255 * Allow the datasource class to add fields.
256 *
257 * This is called as a snippet in DataSourceConfig and
258 * also from DataSource::buildForm to add the fields such
259 * that quick form picks them up.
260 *
261 * @throws \CRM_Core_Exception
262 */
263 protected function getDataSourceFields(): array {
264 $className = $this->getDataSourceClassName();
265 if ($className) {
266 /* @var CRM_Import_DataSource $dataSourceClass */
267 $dataSourceClass = new $className();
268 return $dataSourceClass->getSubmittableFields();
269 }
270 return [];
271 }
272
273 /**
274 * Get the default datasource.
275 *
276 * @return string
277 */
278 protected function getDefaultDataSource(): string {
279 return 'CRM_Import_DataSource_CSV';
280 }
281
282 /**
283 * Get the fields that can be submitted in the Import form flow.
284 *
285 * These could be on any form in the flow & are accessed the same way from
286 * all forms.
287 *
288 * @return string[]
289 * @throws \CRM_Core_Exception
290 */
291 protected function getSubmittableFields(): array {
292 $dataSourceFields = array_fill_keys($this->getDataSourceFields(), 'DataSource');
293 return array_merge($this->submittableFields, $dataSourceFields);
294 }
295
296 /**
297 * Create a user job to track the import.
298 *
299 * @return int
300 *
301 * @throws \API_Exception
302 */
303 protected function createUserJob(): int {
304 $id = UserJob::create(FALSE)
305 ->setValues([
306 'created_id' => CRM_Core_Session::getLoggedInContactID(),
307 'type_id:name' => 'contact_import',
308 'status_id:name' => 'draft',
309 // This suggests the data could be cleaned up after this.
310 'expires_date' => '+ 1 week',
311 'metadata' => [
312 'submitted_values' => $this->getSubmittedValues(),
313 ],
314 ])
315 ->execute()
316 ->first()['id'];
317 $this->setUserJobID($id);
318 return $id;
319 }
320
321 /**
322 * @param string $key
323 * @param array $data
324 *
325 * @throws \API_Exception
326 * @throws \Civi\API\Exception\UnauthorizedException
327 */
328 protected function updateUserJobMetadata(string $key, array $data): void {
329 $metaData = array_merge(
330 $this->getUserJob()['metadata'],
331 [$key => $data]
332 );
333 UserJob::update(FALSE)
334 ->addWhere('id', '=', $this->getUserJobID())
335 ->setValues(['metadata' => $metaData])
336 ->execute();
337 $this->userJob['metadata'] = $metaData;
338 }
339
340 /**
341 * Get column headers for the datasource or empty array if none apply.
342 *
343 * This would be the first row of a csv or the fields in an sql query.
344 *
345 * If the csv does not have a header row it will be empty.
346 *
347 * @return array
348 *
349 * @throws \API_Exception
350 * @throws \CRM_Core_Exception
351 */
352 protected function getColumnHeaders(): array {
353 return $this->getDataSourceObject()->getColumnHeaders();
354 }
355
356 /**
357 * Get the number of importable columns in the data source.
358 *
359 * @return int
360 *
361 * @throws \API_Exception
362 * @throws \CRM_Core_Exception
363 */
364 protected function getNumberOfColumns(): int {
365 return $this->getDataSourceObject()->getNumberOfColumns();
366 }
367
368 /**
369 * Get x data rows from the datasource.
370 *
371 * At this stage we are fetching from what has been stored in the form
372 * during `postProcess` on the DataSource form.
373 *
374 * In the future we will use the dataSource object, likely
375 * supporting offset as well.
376 *
377 * @param int $limit
378 *
379 * @return array
380 *
381 * @throws \CRM_Core_Exception
382 * @throws \API_Exception
383 */
384 protected function getDataRows(int $limit): array {
385 return $this->getDataSourceObject()->getRows($limit);
386 }
387
388 }