/[drupal]/contributions/modules/taxonomy_csv/taxonomy_csv.api.inc
ViewVC logotype

Contents of /contributions/modules/taxonomy_csv/taxonomy_csv.api.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 2.4 - (show annotations) (download) (as text)
Tue Oct 20 20:21:44 2009 UTC (5 weeks, 1 day ago) by danielkm
Branch: MAIN
CVS Tags: HEAD
Changes since 2.3: +1322 -524 lines
File MIME type: text/x-php
By Daniel Berthereau: Added export of terms and vocabularies.
1 <?php
2 // $Id: taxonomy_csv.api.inc,v 2.3 2009/10/05 09:43:12 danielkm Exp $
3
4 /**
5 * @file
6 * This API allows to use taxonomy_csv import/export functions from other module.
7 *
8 * Functions sets:
9 * 1a. Prepare and import a vocabulary : taxonomy_csv_vocabulary_import
10 * 1b. Prepare and export a vocabulary : taxonomy_csv_vocabulary_export
11 * 2a. Prepare and import a line : taxonomy_csv_line_import
12 * 2b. Prepare and export a line from a term: taxonomy_csv_line_export
13 * 3. Prepare and import a term : taxonomy_csv_term_import
14 * 4. Errors helpers
15 * 5. Infos and log messages
16 *
17 *
18 * Structure of import api:
19 * - 1. Batch prepare import of file or text
20 * - 2. Process import line by line (see below)
21 * - 3. Evaluate vocabulary and finish process
22 *
23 * Process import structure (line by line import from a batch set):
24 * - 1. Validate line
25 * 1. Clean input line
26 * 2. Check line items
27 * - 2. Prepare to process items matching import type (eventual loop)
28 * - 3. Process import
29 * 1. Find previous or existing term (see below)
30 * 2. Update or create term
31 * 3. Cache term
32 *
33 * Find a term before update or create it:
34 * - 1. In previous imported
35 * 1. In parent if structure
36 * 2. In whole cache in all cases
37 * 3. In extended cache if related (internal use only)
38 * - 2. In existing terms of the vocabulary (except ignore all)
39 * 1. In parent if structure
40 * 2. In whole vocabulary in all cases
41 * 3. In all vocabularies if related (internal use only)
42 *
43 *
44 * Structure of export api:
45 * - 1. Batch prepare of vocabulary
46 * - 2. Export depending on format
47 *
48 *
49 * To add a new csv scheme, need:
50 * - a define without space,
51 * - items in _taxonomy_csv_info_lists,
52 * - items in taxonomy_csv.js and taxonomy_csv.css,
53 * - a description in forms (taxonomy_csv.module),
54 * - an advanced help.
55 * - a case in _taxonomy_csv_check_items(),
56 * - a case in taxonomy_csv_import_line_items(),
57 * - eventually specific options.
58 * - a case in taxonomy_csv_export_line_items() if possible,
59 */
60
61 /**
62 * Available import/export schemas.
63 */
64 define('TAXONOMY_CSV_FORMAT_ALONE_TERMS', 'alone_terms');
65 define('TAXONOMY_CSV_FORMAT_FIELDS_LINKS', 'fields_links');
66 define('TAXONOMY_CSV_FORMAT_FLAT', 'flat');
67 define('TAXONOMY_CSV_FORMAT_TREE_STRUCTURE', 'tree_structure');
68 define('TAXONOMY_CSV_FORMAT_POLYHIERARCHY', 'polyhierarchy');
69 define('TAXONOMY_CSV_FORMAT_PARENTS', 'parents');
70 define('TAXONOMY_CSV_FORMAT_CHILDREN', 'children');
71 define('TAXONOMY_CSV_FORMAT_FIELDS', 'fields');
72 define('TAXONOMY_CSV_FORMAT_RELATIONS', 'relations');
73 define('TAXONOMY_CSV_FORMAT_DESCRIPTIONS', 'descriptions');
74 define('TAXONOMY_CSV_FORMAT_WEIGHTS', 'weights');
75 define('TAXONOMY_CSV_FORMAT_SYNONYMS', 'synonyms');
76 define('TAXONOMY_CSV_FORMAT_TAXONOMY_MANAGER', 'taxonomy_manager');
77
78 /**
79 * Available import options.
80 */
81 define('TAXONOMY_CSV_EXISTING_UPDATE', 'update'); // When no difference between merge and replace.
82 define('TAXONOMY_CSV_EXISTING_UPDATE_MERGE', 'update_merge');
83 define('TAXONOMY_CSV_EXISTING_UPDATE_REPLACE', 'update_replace');
84 define('TAXONOMY_CSV_EXISTING_IGNORE', 'ignore'); // When no difference between create and all. Equal to duplicate.
85 define('TAXONOMY_CSV_EXISTING_IGNORE_CREATE', 'ignore_create');
86 define('TAXONOMY_CSV_EXISTING_IGNORE_ALL', 'ignore_all'); // Ignore even existing terms in additional columns.
87 define('TAXONOMY_CSV_EXISTING_IGNORE_PREVIOUS', 'ignore_previous'); // Internal only.
88
89 /**
90 * List of watchdog options.
91 */
92 define('TAXONOMY_CSV_WATCHDOG_ERROR', 3); // Stop import process.
93 define('TAXONOMY_CSV_WATCHDOG_WARNING', 4); // Stop line process and go to next one.
94 define('TAXONOMY_CSV_WATCHDOG_NOTICE', 5); // Continue current line process.
95 define('TAXONOMY_CSV_WATCHDOG_INFO', 6); // Successfully processed.
96 define('TAXONOMY_CSV_WATCHDOG_DEBUG', 7); // Internal use only.
97 define('TAXONOMY_CSV_WATCHDOG_NONE', 9); // Internal use only.
98
99 /**
100 * Information about import process. Use too default Drupal constants:
101 * SAVED_NEW = 1
102 * SAVED_UPDATED = 2
103 * Possibly use of:
104 * SAVED_DELETED = 3
105 */
106 define('TAXONOMY_CSV_ERROR', 0);
107 define('TAXONOMY_CSV_NEW_UPDATED', 4);
108 define('TAXONOMY_CSV_UNCHANGED', 5);
109
110 /**
111 * @defgroup taxonomy_csv_vocabulary Import/export a vocabulary
112 * @{
113 * Functions allowing import of a vocabulary into a new or an existing one and
114 * export of a vocabulary to a file.
115 */
116
117 /**
118 * Prepare the import of a vocabulary.
119 * If not used in a form, don't forget to use batch_process().
120 *
121 * @param $options
122 * Array of options:
123 * source_choice : 'path', 'url' or 'text'
124 * path : path to local csv file
125 * url : url to distant or local csv file
126 * file : object file if file is already uploaded
127 * text : csv text to import
128 * import_format : see _taxonomy_csv_info_lists('list_import_format')
129 * delimiter : one character csv delimiter (default: ",")
130 * enclosure : zero or one character csv enclosure (default: none, i.e. '"')
131 * vocabulary_target: 'autocreate' (default), 'duplicate' or 'existing'
132 * vocabulary_id : vocabulary id to duplicate or to import into
133 * existing_items : see _taxonomy_csv_info_lists('list_import_option')
134 * relations_create_subrelations: boolean. Specific to related terms import
135 * relations_all_vocabularies : boolean. Id
136 * internal_cache : boolean. tweak to use (default) or not internal cache
137 * hierarchy_check: boolean. tweak to check (default) or not hierarchy of vocabulary
138 * hierarchy_level: if hierarchy_check is false, set hierarchy level (0, 1 or 2)
139 * line_checks : boolean. tweak to check (default) or not format of lines
140 * utf8_check : boolean. tweak to check (default) or not utf8 format
141 * result_stats : boolean. Display (default) or not stats
142 * result_terms : boolean. Display (default) or not list of imported terms
143 * result_level : Log level: 'none', 'warnings' (default), 'notices' or 'infos'
144 * result_type : Display of log: 'by_message' (default) or 'by_line'
145 * Needed options are source_choice, path or url or text, import_format and existing_items.
146 * @return
147 * Array of errors or nothing (batch process to execute).
148 */
149 function taxonomy_csv_vocabulary_import($options) {
150 // Check and eventually update options.
151 $result = _taxonomy_csv_vocabulary_import_check_options($options);
152 if (count($result)) {
153 return $result;
154 }
155
156 // Remove useless option, because text is saved.
157 if ($options['source_choice'] == 'text') {
158 $options['text'] = '';
159 }
160
161 // Calculates number of lines to be imported.
162 // Automatically detects line endings.
163 ini_set('auto_detect_line_endings', '1');
164 $handle = fopen($options['file']->filepath, 'r');
165 $lines_count = 0;
166 while (fgetcsv($handle, 32768, $options['delimiter'], $options['enclosure'])) {
167 $lines_count++;
168 }
169 fclose($handle);
170 $options['total_lines'] = $lines_count;
171 if (!$options['total_lines']) {
172 return array('source_choice' => t('No term to import. Import finished.'));
173 }
174
175 // Prepare vocabularies.
176 // If source is fields_links, check and eventual creation of vocabularies are made each line.
177 if ($options['import_format'] != TAXONOMY_CSV_FORMAT_FIELDS_LINKS) {
178 // User choose to autocreate or duplicate a vocabulary.
179 switch ($options['vocabulary_target']) {
180 case 'autocreate':
181 $new_vocabulary = taxonomy_csv_vocabulary_create(($options['source_choice'] != 'text') ? $options['file']->filename : '');
182 $options['vocabulary_id'] = $new_vocabulary['vid'];
183 break;
184
185 case 'duplicate':
186 $new_vocabulary = taxonomy_csv_vocabulary_duplicate($options['vocabulary_id']);
187 $options['vocabulary_id'] = $new_vocabulary['vid'];
188 break;
189 }
190 $options['vocabulary'] = taxonomy_vocabulary_load($options['vocabulary_id']);
191 }
192
193 // Prepare import batch.
194 $batch = array(
195 'title' => ($options['source_choice'] != 'text') ?
196 t('Importing terms from CSV file "%filename"...', array('%filename' => $options['file']->filename)) :
197 t('Importing terms from text...'),
198 'init_message' => t('Starting uploading of datas...'),
199 'progress_message' => '',
200 'error_message' => t('An error occurred during the import.'),
201 'finished' => '_taxonomy_csv_vocabulary_import_finished',
202 'file' => dirname(drupal_get_filename('module', 'taxonomy_csv')) .'/taxonomy_csv.api.inc',
203 'progressive' => TRUE,
204 'operations' => array(
205 0 => array('_taxonomy_csv_vocabulary_import_process', array($options)),
206 ),
207 );
208
209 batch_set($batch);
210 }
211
212 /**
213 * Validate options of imported vocabulary or line.
214 *
215 * @param $options
216 * Array of options.
217 * @return
218 * Array of messages errors if any.
219 * By reference options are cleaned and completed.
220 */
221 function _taxonomy_csv_vocabulary_import_check_options(&$options) {
222 $messages = array();
223
224 // Set default value for unset options.
225 foreach (array(
226 'delimiter' => ',',
227 'enclosure' => '',
228 'vocabulary_target' => 'autocreate',
229 'internal_cache' => TRUE,
230 'hierarchy_check' => TRUE,
231 'line_checks' => TRUE,
232 'utf8_check' => TRUE,
233 'result_stats' => 'result_stats',
234 'result_terms' => 'result_terms',
235 'result_level' => 'notices',
236 'result_type' => 'by_message',
237 // Default options of specific imports.
238 'relations_create_subrelations' => FALSE,
239 'relations_all_vocabularies' => FALSE,
240 ) as $key => $value) {
241 if (!isset($options[$key])) {
242 $options[$key] = $value;
243 }
244 }
245
246 // Check if there is write access and prepare file.
247 switch ($options['source_choice']) {
248 case 'path':
249 case 'url':
250 // Upload if not already uploaded.
251 if (!isset($options['file'])) {
252 // Upload from a path.
253 if ($options['source_choice'] == 'path') {
254 $options['file'] = file_save_upload($options['path']);
255 }
256 // Upload from an url.
257 else {
258 $filename = file_save_data(
259 file_get_contents($options['url']),
260 basename($options['url']),
261 'FILE_EXISTS_RENAME');
262 $options['file'] = new stdClass();
263 $options['file']->filename = basename($filename);
264 $options['file']->filepath = $filename;
265 $options['file']->filesize = filesize($filename);
266 }
267 }
268
269 if (!$options['file']) {
270 $messages['path'] = t("You choose to import a taxonomy by a file, but you don't set its name or its size is greater than the server's limit of !max_size.", array('!max_size' => format_size(parse_size(ini_get('upload_max_filesize')))));
271 }
272 elseif (!$options['file']->filesize) {
273 $messages['path'] = t('Size of your file is null.');
274 }
275 break;
276
277 case 'text':
278 if (empty($options['text'])) {
279 $messages['text'] = t('You choose to import a taxonomy by a text, but the text is empty.');
280 }
281 elseif (!isset($options['file'])) {
282 // Prepare import by text: save text as a temp file to simplify process and avoid memory congestion.
283 $filename = file_save_data(
284 $options['text'],
285 'taxo_csv',
286 'FILE_EXISTS_RENAME');
287 $options['file'] = new stdClass();
288 $options['file']->filename = basename($filename);
289 $options['file']->filepath = $filename;
290 $options['file']->filesize = filesize($filename);
291
292 if (!$filename) {
293 $messages['text'] = t('Import by text needs access to temp directory. Import failed.');
294 }
295 }
296 break;
297
298 default:
299 $messages['source_choice'] = t('Source choice should be "path", "url" or "text".');
300 }
301
302 // Delimiter and enclosure greater than one character are useless with fgetcsv.
303 if (drupal_strlen($options['delimiter']) != 1) {
304 $messages['delimiter'] = t('Delimiter should be a one character string.');
305 }
306 if (drupal_strlen($options['enclosure']) == 0) {
307 // With fgetcsv, $enclosure == '' bugs, so use default quote enclosure if none.
308 $options['enclosure'] = '"';
309 }
310 elseif (drupal_strlen($options['enclosure']) > 1) {
311 $messages['enclosure'] = t('Enclosure lenght cannot be greater than one character.');
312 }
313 if ($options['delimiter'] == $options['enclosure']) {
314 $messages['delimiter'] = t('Delimiter and enclosure cannot be same character.');
315 }
316
317 if (!in_array($options['vocabulary_target'], array(
318 'autocreate',
319 'duplicate',
320 'existing',
321 ))) {
322 $messages['vocabulary_target'] = t('Destination target should be "autocreate", "duplicate" or "existing".');
323 }
324
325 if ($options['vocabulary_target'] == 'duplicate'
326 || $options['vocabulary_target'] == 'existing') {
327 $list_vocabularies = taxonomy_get_vocabularies();
328 if (!isset($list_vocabularies[$options['vocabulary_id']])) {
329 $messages['vocabulary_id'] = t("You choose to use an existing vocabulary, but you haven't choose it.");
330 }
331 }
332
333 $list_import_format = _taxonomy_csv_info_lists('list_import_format');
334 $list_import_option = _taxonomy_csv_info_lists('list_import_option');
335 $list_import_format_allowed_import_option = _taxonomy_csv_info_lists('list_import_format_allowed_import_option');
336 if (!array_key_exists($options['import_format'], $list_import_format)) {
337 $messages['import_format'] = t('Source content "!import_format" is not managed.', array(
338 '!import_format' => $list_import_format[$options['import_format']]
339 ));
340 }
341 elseif (!in_array($options['existing_items'], $list_import_format_allowed_import_option[$options['import_format']])) {
342 $messages['existing_items'] = t('Import option "%existing_items" cannot be used with source content "!import_format".', array(
343 '%existing_items' => $list_import_option[$options['existing_items']],
344 '!import_format' => $list_import_format[$options['import_format']],
345 ));
346 }
347
348 if (!$options['existing_items']) {
349 $messages['existing_items'] = t('Please set what will become existing terms.');
350 }
351 elseif (!array_key_exists($options['existing_items'], $list_import_option)) {
352 $messages['existing_items'] = t('Import option "!existing_items" is not managed.', array(
353 '!existing_items' => $list_import_option[$options['existing_items']]
354 ));
355 }
356
357 if ($options['hierarchy_check']
358 && ($options['hierarchy_level'] < 0 || $options['hierarchy_level'] > 2)) {
359 $messages['hierarchy_level'] = t('You need to set hierarchy level if hierarchy check of vocabulary is disabled.');
360 }
361
362 // Advertise if Pathauto module is activated. Issue http://drupal.org/node/540916.
363 if (module_exists('pathauto')) {
364 $messages['pathauto'] = t("<strong>Warning</strong>: Pathauto module is activated.<br />
365 This module slows down taxonomy import process and only few terms can be imported. It's advised to disabled it manually in !modules_section. Settings aren't lost when you disable it - and not uninstall it -.<br />
366 After import process, you can reactivate Pathauto and eventually bulk generate aliases for newly imported terms.", array(
367 '!modules_section' => l(t('modules section'), 'admin/build/modules'),
368 ));
369 }
370
371 return $messages;
372 }
373
374 /**
375 * Batch process of vocabulary import.
376 *
377 * @param $options
378 * Array of batch options.
379 * @param &$context
380 * Batch context to keep results and messages.
381 * @return
382 * NULL because use of &$context.
383 */
384 function _taxonomy_csv_vocabulary_import_process($options, &$context) {
385 $first = FALSE;
386
387 // First callback.
388 if (empty($context['sandbox'])) {
389 // Remember options as batch_set can't use form_storage.
390 // It allows too that first line in result is numbered 1 and not 0.
391 $context['results'][0] = $options;
392
393 $first = TRUE;
394
395 // Automatically detect line endings.
396 ini_set('auto_detect_line_endings', '1');
397
398 // Initialize some variables.
399 $context['results'][0]['current_line'] = 0;
400 $context['results'][0]['worst_line'] = 0;
401 $context['results'][0]['worst_message'] = 999;
402 $context['results'][0]['handle'] = fopen($options['file']->filepath, 'r');
403 $context['sandbox']['handle_pointer'] = 0;
404 $context['sandbox']['max'] = $options['total_lines'];
405 $context['sandbox']['previous_items'] = array(
406 'name' => array(),
407 'tid' => array(),
408 );
409
410 // Enable or disable cache.
411 $result = _taxonomy_csv_term_cache($options['internal_cache'], 'use_cache');
412 }
413 elseif (!is_resource($context['results'][0]['handle'])) {
414 // Recall file and set pointer in case of memory or time out.
415 $context['results'][0]['handle'] = fopen($options['file']->filepath, 'r');
416 fseek($context['results'][0]['handle'], $context['sandbox']['handle_pointer']);
417
418 // Reload internal static cache if it has been removed in case of long batch.
419 if ($options['internal_cache']
420 && isset($context['results'][0]['imported_terms'])
421 && (_taxonomy_csv_term_cache('', 'count') == 0)) {
422 $result = _taxonomy_csv_term_cache($context['results'][0]['imported_terms'], 'set_cache');
423 }
424 }
425
426 // Load and process one line.
427 $worst_line = &$context['results'][0]['worst_line'];
428 $worst_message = &$context['results'][0]['worst_message'];
429 $handle = &$context['results'][0]['handle'];
430 $line_number = &$context['results'][0]['current_line'];
431 $previous_items = &$context['sandbox']['previous_items'];
432 $line = fgetcsv($handle, 32768, $options['delimiter'], $options['enclosure']);
433 if ($line) {
434 $line_number++;
435
436 // Remember pointer in case of memory or time out.
437 $context['sandbox']['handle_pointer'] = ftell($handle);
438
439 // Skip eventual UTF-8 byte order mark.
440 if ($first) {
441 if (strncmp($line[0], "\xEF\xBB\xBF", 3) === 0) {
442 $line[0] = substr($line[0], 3);
443 $first = FALSE;
444 }
445 }
446
447 // Process import of current line.
448 $result = taxonomy_csv_line_import($line, $options, $previous_items);
449
450 // Remember processed line.
451 $previous_items['name'] = $result['name'];
452 $previous_items['tid'] = $result['tid'];
453
454 // Remember worst message of imported lines.
455 $worst_message_new = _taxonomy_csv_message_get_worst_message($result['msg']);
456 if ($worst_message_new < $worst_message) {
457 $worst_message = $worst_message_new;
458 $worst_line = $line_number;
459 };
460
461 if ($options['internal_cache']) {
462 // Remember only wanted logs.
463 switch ($options['result_level']) {
464 case 'none':
465 break;
466 case 'warnings':
467 $list_messages = array();
468 foreach ($result['msg'] as $value) {
469 foreach ($value as $msg) {
470 if (_taxonomy_csv_message_get_level($msg) <= TAXONOMY_CSV_WATCHDOG_WARNING) {
471 $list_messages[$msg] = $msg;
472 }
473 }
474 }
475 if (count($list_messages)) {
476 sort($list_messages);
477 $context['results'][$line_number] = $list_messages;
478 }
479 break;
480 case 'notices':
481 $list_messages = array();
482 foreach ($result['msg'] as $value) {
483 foreach ($value as $msg) {
484 if (_taxonomy_csv_message_get_level($msg) <= TAXONOMY_CSV_WATCHDOG_NOTICE) {
485 $list_messages[$msg] = $msg;
486 }
487 }
488 }
489 sort($list_messages);
490 $context['results'][$line_number] = $list_messages;
491 break;
492 case 'infos':
493 $list_messages = array();
494 foreach ($result['msg'] as $value) {
495 foreach ($value as $msg) {
496 $list_messages[$msg] = $msg;
497 }
498 }
499 sort($list_messages);
500 $context['results'][$line_number] = $list_messages;
501 break;
502 case 'full':
503 $context['results'][$line_number] = $result;
504 break;
505 }
506
507 // Save static cache to keep informations about process in case of batch timeout.
508 $context['results'][0]['imported_terms'] =& _taxonomy_csv_term_cache('', 'dump');
509 }
510
511 // Inform about progress.
512 $context['message'] = t('Line !line_number of !total_lines processed: !line', array(
513 '!line_number' => $line_number,
514 '!total_lines' => $options['total_lines'],
515 // $previous_items['name'] can be used, except with taxonomy_manager format.
516 '!line' => implode(', ', $line),
517 ));
518
519 // Check worst message of imported lines and update progress.
520 if (_taxonomy_csv_message_get_level($worst_message) > TAXONOMY_CSV_WATCHDOG_ERROR) {
521 $context['finished'] = $line_number / $context['sandbox']['max'];
522 }
523 else {
524 $context['finished'] = 1;
525 if ($options['internal_cache']) {
526 $result = _taxonomy_csv_term_cache('', 'clear');
527 }
528 }
529 }
530 // Keep last previous items with fields_links in order to check vocabulary.
531 if ($options['import_format'] == TAXONOMY_CSV_FORMAT_FIELDS_LINKS) {
532 $context['results'][0]['last_items'] = $previous_items;
533 }
534 }
535
536 /**
537 * Callback for finished batch import and display result informations.
538 */
539 function _taxonomy_csv_vocabulary_import_finished($success, $results, $operations) {
540 // $results[0] is used to save options and some infos (imported terms), as batch process can't use $form_state.
541 $options = &$results[0];
542 unset($results[0]);
543
544 // Close imported file.
545 if ($options['handle']) {
546 fclose($options['handle']);
547 }
548
549 // Clear internal cache.
550 if ($options['internal_cache']) {
551 $result = _taxonomy_csv_term_cache('', 'clear');
552 }
553
554 // Display general info.
555 drupal_set_message(_taxonomy_csv_info_chosen_options($options), 'status');
556
557 // Check and info on used or created vocabularies.
558 $vocabularies = ($options['import_format'] == TAXONOMY_CSV_FORMAT_FIELDS_LINKS) ?
559 $options['last_items']['tid']['vocabulary'] :
560 array($options['vocabulary_id']);
561 $message = t('!count used or created.', array('!count' => format_plural(count($vocabularies), 'A vocabulary has been', '@count vocabularies have been')));
562 drupal_set_message($message, 'status');
563 foreach ($vocabularies as $vocabulary_id) {
564 // Check and update hierarchy of vocabularies.
565 $vocabulary = taxonomy_vocabulary_load($vocabulary_id);
566 $new_hierarchy[$vocabulary_id] = ($options['hierarchy_check']) ?
567 taxonomy_csv_vocabulary_check_hierarchy($vocabulary_id) :
568 taxonomy_csv_vocabulary_check_hierarchy($vocabulary_id, $options['hierarchy_level']);
569
570 // Display general info about vocabulary.
571 $message = ($options['import_format'] == TAXONOMY_CSV_FORMAT_FIELDS_LINKS) ?
572 t('Vocabulary "%vocabulary_name" has been used or created.', array('%vocabulary_name' => $vocabulary->name)) :
573 _taxonomy_csv_info_vocabulary_destination($vocabulary, $options['vocabulary_target']);
574 $message .= '<br />';
575 $message .= _taxonomy_csv_info_vocabulary_result($vocabulary, $options['vocabulary_target'], $new_hierarchy[$vocabulary_id]) .'<br />';
576 if (!$options['hierarchy_check']) {
577 $message .= t('Hierarchy level has been manually set.') .'<br />';
578 }
579 drupal_set_message($message, 'status');
580 }
581
582 // Prepare batch result message.
583 if (!$success) {
584 $message = t('Importation failed');
585 $message .= '<br />'. t('This issue is related to import process or to size import and probably not to content.');
586 if ($options['current_line'] == $options['total_lines']) {
587 $message .= '<br />'. t('Import process was successful, but the evaluation of hierarchy level has failed, because it is highly memory consumming. You need to change its value with default Drupal functions or set it in Tweaks section of Taxonomy csv.') .' '. t('This problem can be caused too by a high log level. You can reduce it in advanced options.');
588 }
589 else {
590 $message .= '<br />'. t('Import process was successful until the line !line_count of a total of !total_lines. You can first check your file on this line and check file uploading.', array(
591 '!line_count' => $options['current_line'],
592 '!total_lines' => $options['total_lines'],
593 ));
594 }
595 $message .= '<br />'. t('You can reduce log level or disable internal cache to avoid this error.');
596 $message .= '<br />'. t('You can reinstall module from a fresh release or submit an issue on <a href="!link">Taxonomy CSV import/export module</a>.', array(
597 '!link' => url('http://drupal.org/project/issues/taxonomy_csv/'),
598 ));
599
600 drupal_set_message($message, 'error');
601 }
602 else {
603 // Short summary information.
604 switch (_taxonomy_csv_message_get_level($options['worst_message'])) {
605 case TAXONOMY_CSV_WATCHDOG_ERROR:
606 drupal_set_message(t('Errors have been reported during import process. Process failed at line !line_number with error: !message', array(
607 '!line_number' => $options['worst_line'],
608 '!message' => _taxonomy_csv_info_result_text($options['worst_message']),
609 )), 'error');
610 break;
611 case TAXONOMY_CSV_WATCHDOG_WARNING:
612 drupal_set_message(t('Warnings have been reported during import process (bad formatted lines). First line skipped is line !line_number with message: !message', array(
613 '!line_number' => $options['worst_line'],
614 '!message' => _taxonomy_csv_info_result_text($options['worst_message']),
615 )), 'error');
616 break;
617 case TAXONOMY_CSV_WATCHDOG_NOTICE:
618 drupal_set_message(t('Notices have been reported during import process (bad formatted or empty lines). Lines processed. First notice on line !line_number with message: !message', array(
619 '!line_number' => $options['worst_line'],
620 '!message' => _taxonomy_csv_info_result_text($options['worst_message']),
621 )), 'warning');
622 break;
623 case TAXONOMY_CSV_WATCHDOG_INFO:
624 default:
625 drupal_set_message(t('No error, warnings or notices have been reported during import process.'), 'status');
626 break;
627 }
628
629 if (!$options['line_checks']) {
630 drupal_set_message(t('Line checks have been disabled. Some warnings and notices may have not been reported.'), 'warning');
631 }
632
633 if (!$options['internal_cache']) {
634 drupal_set_message(t('No more information about process because internal cache is disabled.'), 'notice');
635 }
636 else {
637 // Display stats and eventually lists about imported terms.
638 if ($options['result_stats'] || $options['result_terms']) {
639 if (isset($options['imported_terms'])) {
640 if ($options['result_terms']) {
641 drupal_set_message(_taxonomy_csv_info_terms_stats($options['imported_terms'], 'full'), 'status');
642 }
643 elseif ($options['result_stats']) {
644 drupal_set_message(_taxonomy_csv_info_terms_stats($options['imported_terms'], 'stats'), 'status');
645 }
646 }
647 else {
648 drupal_set_message(t('No term was imported.'), 'warning');
649 }
650 }
651
652 // Display detailled result of import.
653 if ($options['result_level'] != 'none') {
654 if (count($results)) {
655 drupal_set_message(t('Available informations about lines import.'));
656 call_user_func("_taxonomy_csv_info_result_{$options['result_type']}", $results, $options['result_level']);
657 }
658 else {
659 drupal_set_message(t('No more information reported about lines import.'));
660 }
661 }
662 }
663 }
664 }
665
666 /**
667 * Prepare the export of a vocabulary.
668 * If not used in a form, don't forget to use batch_process().
669 *
670 * @param $options
671 * Array of options:
672 * export_format: see _taxonomy_csv_info_lists('list_export_format')
673 * vocabulary_id: vocabulary id to export (default: 0, which means all)
674 * delimiter : one character csv delimiter (default: ",")
675 * enclosure : zero or one character csv enclosure (default: "")
676 * line_ending : end of line character: 'Unix' (default), 'Mac' or 'Microsoft DOS'
677 * order : order of exported terms: 'name' (default), 'tid' or 'weight'
678 * fields_links_terms_ids : specific to fields_links: 'name_if_needed' (default), 'name', 'tid'
679 * fields_links_vocabularies_ids: specific to fields_links: 'none' (default), 'name', 'vid'
680 * Only export_format is needed.
681 * @return
682 * Array of errors or nothing (batch process to execute).
683 */
684 function taxonomy_csv_vocabulary_export($options) {
685 // Check and eventually update options.
686 $result = _taxonomy_csv_vocabulary_export_check_options($options);
687 if (count($result)) {
688 return $result;
689 }
690
691 // Csv variables.
692 $options['separator'] = $options['enclosure'] . $options['delimiter'] . $options['enclosure'];
693 $line_ending = array(
694 'Unix' => "\n",
695 'Mac' => "\r",
696 'Microsoft DOS' => "\r\n",
697 );
698 $options['end_of_line'] = $line_ending[$options['line_ending']];
699
700 // Prepare export batch.
701 $batch = array(
702 'title' => t('Exporting terms to CSV file...'),
703 'init_message' => t('Starting downloading of datas...'),
704 'progress_message' => '',
705 'error_message' => t('An error occurred during the export.'),
706 'finished' => '_taxonomy_csv_vocabulary_export_finished',
707 'file' => dirname(drupal_get_filename('module', 'taxonomy_csv')) .'/taxonomy_csv.api.inc',
708 'progressive' => TRUE,
709 'operations' => array(
710 0 => array('_taxonomy_csv_vocabulary_export_process', array($options))
711 ),
712 );
713
714 batch_set($batch);
715 }
716
717 /**
718 * Validate options of exported vocabulary or line.
719 *
720 * @param $options
721 * Array of options.
722 * @return
723 * Array of messages errors if any.
724 * By reference options are cleaned and completed.
725 */
726 function _taxonomy_csv_vocabulary_export_check_options(&$options) {
727 $messages = array();
728
729 // Set default value for unset options.
730 foreach (array(
731 'vocabulary_id' => 0,
732 'delimiter' => ',',
733 'enclosure' => '',
734 'line_ending' => 'Unix',
735 'order' => 'name',
736 // Default options of specific imports.
737 'fields_links_terms_ids' => 'name_if_needed',
738 'fields_links_vocabularies_ids' => 'none',
739 ) as $key => $value) {
740 if (!isset($options[$key])) {
741 $options[$key] = $value;
742 }
743 }
744
745 // Check if there is write access and prepare file (first time only).
746 if (!isset($options['file'])) {
747 // Set filename.
748 if ($options['vocabulary_id']) {
749 $vocabulary = taxonomy_vocabulary_load($options['vocabulary_id']);
750 $vocabulary_name = $vocabulary->name;
751 }
752 else {
753 $vocabulary_name = t('Taxonomy');
754 }
755 // Create file.
756 $filename = file_save_data(
757 '',
758 "$vocabulary_name.csv",
759 'FILE_EXISTS_RENAME');
760 if (!$filename) {
761 $messages['file'] = t('Export needs access to temp directory. Export failed.');
762 }
763 else {
764 $options['file'] = new stdClass();
765 $options['file']->filename = basename($filename);
766 $options['file']->filepath = $filename;
767 $options['file']->filesize = filesize($filename);
768 }
769 }
770
771 $list_export_format = _taxonomy_csv_info_lists('list_export_format');
772 if (!array_key_exists($options['export_format'], $list_export_format)) {
773 $messages['export_format'] = t('Export format "!export_format" is not managed.', array(
774 '!export_format' => $list_export_format[$options['export_format']],
775 ));
776 }
777
778 $list_vocabularies = taxonomy_get_vocabularies();
779 if (!$list_vocabularies) {
780 $messages['vocabulary_id'] = t('No vocabulary to export.');
781 }
782 elseif ($options['vocabulary_id']) {
783 if (!isset($list_vocabularies[$options['vocabulary_id']])) {
784 $messages['vocabulary_id'] = t("You choose to export a vocabulary, but it doesn't exist.");
785 }
786 }
787
788 // Delimiter and enclosure greater than one character are forbidden.
789 if (drupal_strlen($options['delimiter']) != 1) {
790 $messages['delimiter'] = t('Delimiter should be a one character string.');
791 }
792 if (drupal_strlen($options['enclosure']) > 1) {
793 $messages['enclosure'] = t('Enclosure lenght cannot be greater than one character.');
794 }
795 if ($options['delimiter'] == $options['enclosure']) {
796 $messages['delimiter'] = t('Delimiter and enclosure cannot be same character.');
797 }
798
799 if (!in_array($options['line_ending'], array(
800 'Unix',
801 'Mac',
802 'Microsoft DOS',
803 ))) {
804 $messages['line_ending'] = t('Line ending should be "Unix", "Mac" or "Microsoft DOS".');
805 }
806
807 if (!in_array($options['order'], array(
808 'name',
809 'tid',
810 'weight',
811 ))) {
812 $messages['order'] = t('Order should be "name", "tid" or "weight".');
813 }
814
815 if (!in_array($options['fields_links_terms_ids'], array(
816 'name_if_needed',
817 'name',
818 'tid',
819 ))) {
820 $messages['fields_links_terms_ids'] = t('Terms identifiants should be "name_if_needed", "name" or "tid".');
821 }
822
823 if (!in_array($options['fields_links_vocabularies_ids'], array(
824 'none',
825 'name',
826 'vid',
827 ))) {
828 $messages['fields_links_vocabularies_ids'] = t('Vocabularies identifiants should be "none", "name" or "vid".');
829 }
830
831 // When multiple vocabularies are exported, vocabulary names or tids is always needed.
832 if ($options['fields_links_vocabularies_ids'] == 'none') {
833 if ($options['vocabulary_id'] == 0) {
834 $options['fields_links_vocabularies_ids'] = 'name';
835 }
836 else {
837 // Check if all related terms are related to main vocabulary.
838 ///TODO. Currently, check is made for each term in line_export.
839 }
840 }
841
842 // Calculates number of terms to be exported.
843 $sql = "
844 SELECT COUNT(*)
845 FROM {term_data}
846 ";
847 $args = array();
848 if ($options['vocabulary_id']) {
849 $sql .= ' WHERE vid = %d';
850 $args[] = $options['vocabulary_id'];
851 }
852 $options['total_terms'] = array_shift(db_fetch_array(db_query($sql, $args)));
853 if (!$options['total_terms']) {
854 $message['vocabulary_id'] = t('No term to export. Export finished.');
855 }
856
857 return $messages;
858 }
859
860 /**
861 * Batch process of vocabulary export.
862 *
863 * @param $options
864 * Array of batch options.
865 * @param &$context
866 * Batch context to keep results and messages.
867 * @return
868 * NULL because use of &$context.
869 */
870 function _taxonomy_csv_vocabulary_export_process($options, &$context) {
871 // First callback.
872 if (empty($context['sandbox'])) {
873 // Remember options as batch_set can't use form_storage.
874 // It allows too that first line in result is numbered 1 and not 0.
875 $context['results'][0] = $options;
876
877 // Initialize some variables.
878 $context['results'][0]['current_term'] = 0;
879 $context['results'][0]['current_name'] = '';
880 $context['results'][0]['worst_term'] = 0;
881 $context['results'][0]['worst_name'] = '';
882 $context['results'][0]['worst_message'] = 999;
883 // No pointer because new line is appended to file.
884 $context['results'][0]['handle'] = fopen($options['file']->filepath, 'a+');
885 $context['sandbox']['max'] = $options['total_terms'];
886
887 // Prepare terms to be exported.
888 $sql = "
889 SELECT *
890 FROM {term_data}
891 ";
892 $args = array();
893 if ($options['vocabulary_id']) {
894 $sql .= ' WHERE vid = %d';
895 $args[] = $options['vocabulary_id'];
896 }
897 // Use descendant order, because each term will be poped (quicker than shift).
898 $sql .= ' ORDER BY %s DESC';
899 $args[] = $options['order'];
900 $result = db_query($sql, $args);
901 while ($term = db_fetch_array($result)) {
902 $context['sandbox']['terms'][] = $term;
903 }
904
905 // Prepare list of duplicate terms for fields_links export.
906 $context['results'][0]['duplicate_terms'] = array();
907 $sql = '
908 SELECT t1.tid, t1.name
909 FROM {term_data} t1
910 LEFT OUTER JOIN {term_data} t2 ON t1.tid != t2.tid AND LOWER(t1.name) = LOWER(t2.name)
911 WHERE t2.tid IS NOT NULL
912 ';
913 $args = array();
914 if ($options['vocabulary_id']) {
915 $sql .= ' AND t1.vid = %d';
916 $args[] = $options['vocabulary_id'];
917 }
918 $sql .= '
919 ORDER BY {t1.tid} ASC
920 ';
921 $result = db_query($sql, $args);
922 while ($term = db_fetch_object($result)) {
923 $context['results'][0]['duplicate_terms'][$term->tid] = $term->name;
924 }
925 }
926 elseif (!is_resource($context['results'][0]['handle'])) {
927 // Recall file in case of memory or time out. No pointer, because terms are appended.
928 $context['results'][0]['handle'] = fopen($options['file']->filepath, 'a+');
929 }
930
931 // Load and process one term.
932 $worst_term = &$context['results'][0]['worst_term'];
933 $worst_name = &$context['results'][0]['worst_name'];
934 $worst_message = &$context['results'][0]['worst_message'];
935 $handle = &$context['results'][0]['handle'];
936 $duplicate_terms = &$context['results'][0]['duplicate_terms'];
937 $term_number = &$context['results'][0]['current_term'];
938 $current_name = &$context['results'][0]['current_name'];
939 $term = array_pop($context['sandbox']['terms']);
940 if ($term) {
941 $term_number++;
942
943 // Remember current name in case of error.
944 $current_name = $term['name'];
945
946 // Process export of current term.
947 $result = taxonomy_csv_line_export($term, $options, $duplicate_terms);
948
949 // Check if separator, enclosure or line ending exist in line.
950 $check_line = implode('', $result['line']);
951 if ((strpos($check_line, $options['separator']) !== FALSE)
952 || (($options['enclosure'] != '')
953 && (strpos($check_line, $options['enclosure']) !== FALSE))
954 || (($options['enclosure'] == '')
955 && (strpos($check_line, $options['end_of_line']) !== FALSE))) {
956 $result['msg'][] = array(313); // Error delimiter or enclosure.
957 }
958
959 // Save line to file.
960 $line = $options['enclosure'] . implode($options['separator'], $result['line']) . $options['enclosure'] . $options['end_of_line'];
961 if (fwrite($handle, $line) === FALSE) {
962 $result['msg'][] = array(312); // Unable to write to file.
963 }
964
965 // Remember worst message of exported terms.
966 $worst_message_new = _taxonomy_csv_message_get_worst_message($result['msg']);
967 if ($worst_message_new < $worst_message) {
968 $worst_term = $term_number;
969 $worst_name = $current_name;
970 $worst_message = $worst_message_new;
971 };
972
973 // Inform about progress.
974 $context['message'] = t('Term !term_number of !total_terms processed: %term', array(
975 '!term_number' => $term_number,
976 '!total_terms' => $options['total_terms'],
977 '%term' => $current_name,
978 ));
979
980 // Check worst message of exported lines and update progress.
981 if (_taxonomy_csv_message_get_level($worst_message) > TAXONOMY_CSV_WATCHDOG_ERROR) {
982 $context['finished'] = $term_number / $context['sandbox']['max'];
983 }
984 else {
985 $context['finished'] = 1;
986 }
987 }
988 }
989
990 /**
991 * Callback for finished batch export and display result informations.
992 */
993 function _taxonomy_csv_vocabulary_export_finished($success, $results, $operations) {
994 // $results[0] is used to save options and some infos, as batch process can't use $form_state.
995 $options = &$results[0];
996 unset($results[0]);
997
998 // Close exported file.
999 if ($options['handle']) {
1000 fclose($options['handle']);
1001 }
1002
1003 // General infos.
1004 $message = t('!term_count / !total_terms terms of chosen vocabularies have been exported to file <a href="!filepath">!filename</a> (!filesize KB). Click on link to view it or right click to download it.', array(
1005 '!term_count' => $options['current_term'],
1006 '!total_terms' => $options['total_terms'],
1007 '!filepath' => $options['file']->filepath,
1008 '!filename' => $options['file']->filename,
1009 '!filesize' => number_format(filesize($options['file']->filepath) / 1000, 1),
1010 )) .'<br />';
1011 // Main check of end process.
1012 $status = (($options['current_term'] == $options['total_terms']) && ($options['current_term'] > 0)) ? 'status' : 'error';
1013 drupal_set_message($message, $status);
1014
1015 // Infos on duplicate term names.
1016 $duplicate_count = count($options['duplicate_terms']);
1017 // Duplicate term names exist. Array flip allows to display only duplicate names.
1018 if ($duplicate_count) {
1019 $message = t('!count duplicate term names have been found. Error can occur when you will import this list, except if you choose "fields and links" export format or import option "ignore existing items".', array('!count' => $duplicate_count)) .'<br />'.
1020 t('List of duplicate term names:') .'<br />"'.
1021 implode('", "', array_keys(array_flip($options['duplicate_terms']))) .'"';
1022 $status = 'warning';
1023 }
1024 else {
1025 // No duplicate term names.
1026 $message = t('No duplicate term name has been found.');
1027 $status = 'status';
1028 }
1029 drupal_set_message($message, $status);
1030
1031 // Prepare batch result message.
1032 if (!$success) {
1033 $message = t('Exportation failed');
1034 $message .= '<br />'. t('This issue is related to export process and may be caused by a memory overrun of the database.');
1035 if ($options['current_term'] == $options['total_terms']) {
1036 $message .= '<br />'. t('All terms were exported, but a unknown error appears after the end of process.');
1037 }
1038 else {
1039 $message .= '<br />'. t('Export process was successful until the term !term_count (%term_name) of a total of !total_terms.', array(
1040 '!term_count' => $options['current_term'],
1041 '%term_name' => $options['current_name'],
1042 '!total_terms' => $options['total_terms'],
1043 ));
1044 }
1045 $message .= '<br />'. t('You can reinstall module from a fresh release or submit an issue on <a href="!link">Taxonomy CSV import/export module</a>.', array(
1046 '!link' => url('http://drupal.org/project/issues/taxonomy_csv/'),
1047 ));
1048
1049 drupal_set_message($message, 'error');
1050 }
1051 else {
1052 // Short summary information.
1053 switch (_taxonomy_csv_message_get_level($options['worst_message'])) {
1054 case TAXONOMY_CSV_WATCHDOG_ERROR:
1055 drupal_set_message(t('Errors have been reported during export process. Process failed at term !worst_count (%worst_name) with error: !message', array(
1056 '!worst_count' => $options['worst_term'],
1057 '%worst_name' => $options['worst_name'],
1058 '!message' => _taxonomy_csv_info_result_text($options['worst_message']),
1059 )), 'error');
1060 break;
1061 case TAXONOMY_CSV_WATCHDOG_WARNING:
1062 drupal_set_message(t('Warnings have been reported during export process (bad formatted terms). First term skipped is term !worst_count (%worst_name) with message: !message', array(
1063 '!worst_count' => $options['worst_term'],
1064 '%worst_name' => $options['worst_name'],
1065 '!message' => _taxonomy_csv_info_result_text($options['worst_message']),
1066 )), 'error');
1067 break;
1068 case TAXONOMY_CSV_WATCHDOG_NOTICE:
1069 drupal_set_message(t('Notices have been reported during export process (bad formatted or empty terms). Terms processed. First notice on term !worst_count (%worst_name) with message: !message', array(
1070 '!worst_count' => $options['worst_term'],
1071 '%worst_name' => $options['worst_name'],
1072 '!message' => _taxonomy_csv_info_result_text($options['worst_message']),
1073 )), 'warning');
1074 break;
1075 case TAXONOMY_CSV_WATCHDOG_INFO:
1076 default:
1077 drupal_set_message(t('No error, warnings or notices have been reported during export process.'), 'status');
1078 break;
1079 }
1080 }
1081 }
1082
1083 /**
1084 * Creates vocabulary by its name and returns an array with its vid and its name.
1085 */
1086 function taxonomy_csv_vocabulary_create($vocabulary_name = '') {
1087 // Create an empty vocabulary. Relations and hierarchy are updated after import.
1088 $vocabulary = array(
1089 'name' => _taxonomy_csv_vocabulary_create_name($vocabulary_name),
1090 'description' => t('Auto created vocabulary by taxonomy_csv module'),
1091 'help' => '',
1092 'relations' => TRUE,
1093 'hierarchy' => 2,
1094 'multiple' => TRUE,
1095 'required' => FALSE,
1096 'tags' => FALSE,
1097 'module' => 'taxonomy',
1098 'weight' => 0,
1099 'nodes' => array(),
1100 );
1101
1102 taxonomy_save_vocabulary($vocabulary);
1103
1104 return array(
1105 'vid' => taxonomy_csv_vocabulary_get_id($vocabulary['name']),
1106 'name' => $vocabulary['name'],
1107 );
1108 }
1109
1110 /**
1111 * Duplicates a vocabulary by its vid and returns an array with vid and name.
1112 * If not exist, creates an empty vocabulary.
1113 */
1114 function taxonomy_csv_vocabulary_duplicate($vocabulary_id) {
1115 $original_vocabulary = taxonomy_vocabulary_load($vocabulary_id);
1116 if ($original_vocabulary) {
1117 // Creates an unused name. Check if name begins with 'Copy of #name' in order to serialize name.
1118 $name = t('Copy of [!vocabulary_name]', array('!vocabulary_name' => $original_vocabulary->name));
1119 $name = _taxonomy_csv_vocabulary_create_name((strpos($original_vocabulary->name, $name) === FALSE) ? $name : $original_vocabulary->name);
1120
1121 // Duplicate original vocabulary, except relations and hierarchy, updated after import.
1122 $duplicated_vocabulary = array(
1123 'name' => $name,
1124 'description' => $original_vocabulary->description,
1125 'help' => $original_vocabulary->help,
1126 'relations' => $original_vocabulary->relations,
1127 'hierarchy' => $original_vocabulary->hierarchy,
1128 'multiple' => $original_vocabulary->multiple,
1129 'required' => $original_vocabulary->required,
1130 'tags' => $original_vocabulary->tags,
1131 'module' => $original_vocabulary->module,
1132 'weight' => $original_vocabulary->weight,
1133 'nodes' => array(),
1134 );
1135
1136 taxonomy_save_vocabulary($duplicated_vocabulary);
1137
1138 $duplicated_vocabulary['vid'] = taxonomy_csv_vocabulary_get_id($duplicated_vocabulary['name']);
1139
1140 // Get all terms and attributes of original vocabulary
1141 // and copy them in the new one in two steps.
1142 $original_terms = taxonomy_get_tree($original_vocabulary->vid);
1143
1144 // First step: copy each term except relations and parents.
1145 $duplicated_terms = array();
1146 foreach ($original_terms as $original_term) {
1147 $duplicated_terms[$original_term->tid] = array(
1148 'vid' => $duplicated_vocabulary['vid'],
1149 'name' => $original_term->name,
1150 'description' => $original_term->description,
1151 'weight' => $original_term->weight,
1152 'synonyms' => taxonomy_get_synonyms($original_term->tid),