2 // +----------------------------------------------------------------------+
3 // | Anuko Time Tracker
4 // +----------------------------------------------------------------------+
5 // | Copyright (c) Anuko International Ltd. (https://www.anuko.com)
6 // +----------------------------------------------------------------------+
7 // | LIBERAL FREEWARE LICENSE: This source code document may be used
8 // | by anyone for any purpose, and freely redistributed alone or in
9 // | combination with other software, provided that the license is obeyed.
11 // | There are only two ways to violate the license:
13 // | 1. To redistribute this code in source form, with the copyright
14 // | notice or license removed or altered. (Distributing in compiled
15 // | forms without embedded copyright notices is permitted).
17 // | 2. To redistribute modified versions of this code in *any* form
18 // | that bears insufficient indications that the modifications are
19 // | not the work of the original author(s).
21 // | This license applies to this document only, not any other software
22 // | that it may be combined with.
24 // +----------------------------------------------------------------------+
26 // | https://www.anuko.com/time_tracker/credits.htm
27 // +----------------------------------------------------------------------+
29 import('ttUserHelper');
30 import('ttRoleHelper');
31 import('ttTaskHelper');
32 import('ttProjectHelper');
33 import('ttClientHelper');
34 import('ttInvoiceHelper');
36 // ttOrgImportHelper - this class is a future replacement for ttImportHelper.
37 // Currently, it is work in progress.
38 // When done, it should handle import of complex groups consisting of other groups.
39 class ttOrgImportHelper {
40 var $errors = null; // Errors go here. Set in constructor by reference.
41 var $conflicting_entities = null; // A comma-separated list of entity names we cannot import.
42 var $canImport = true; // False if we cannot import data due to a conflict such as login collision.
43 var $firstPass = true; // True during first pass through the file.
44 var $org_id = null; // Organization id (same as top group_id).
45 var $current_group_id = null; // Current group id during parsing.
46 var $current_parent_group_id = null; // Current parent group id during parsing.
47 // Set when we create a new group.
48 var $top_role_id = 0; // Top role id.
50 // Entity maps for current group. They map XML ids with database ids.
51 var $currentGroupRoleMap = array();
52 var $currentGroupTaskMap = array();
53 var $currentGroupProjectMap = array();
54 var $currentGroupClientMap = array();
55 var $currentGroupUserMap = array();
56 var $currentGroupInvoiceMap = array();
57 var $currentGroupLogMap = array();
60 function __construct(&$errors) {
61 $this->errors = &$errors;
62 $this->top_role_id = ttRoleHelper::getRoleByRank(512, 0);
65 // startElement - callback handler for opening tag of an XML element in the file.
66 function startElement($parser, $name, $attrs) {
69 // First pass. We only check user logins for potential collisions with existing.
70 if ($this->firstPass) {
71 if ($name == 'USER' && $this->canImport) {
72 $login = $attrs['LOGIN'];
73 if ('' != $attrs['STATUS'] && ttUserHelper::getUserByLogin($login)) {
74 // We have a login collision. Append colliding login to a list of things we cannot import.
75 $this->conflicting_entities .= ($this->conflicting_entities ? ", $login" : $login);
80 // Second pass processing. We import data here, one tag at a time.
81 if (!$this->firstPass && $this->canImport && $this->errors->no()) {
82 $mdb2 = getConnection();
84 // We are in second pass and can import data.
85 if ($name == 'GROUP') {
86 // Create a new group.
87 $this->current_group_id = $this->createGroup(array(
88 'parent_id' => $this->current_parent_group_id,
89 'org_id' => $this->org_id,
90 'name' => $attrs['NAME'],
91 'currency' => $attrs['CURRENCY'],
92 'decimal_mark' => $attrs['DECIMAL_MARK'],
93 'lang' => $attrs['LANG'],
94 'date_format' => $attrs['DATE_FORMAT'],
95 'time_format' => $attrs['TIME_FORMAT'],
96 'week_start' => $attrs['WEEK_START'],
97 'tracking_mode' => $attrs['TRACKING_MODE'],
98 'project_required' => $attrs['PROJECT_REQUIRED'],
99 'task_required' => $attrs['TASK_REQUIRED'],
100 'record_type' => $attrs['RECORD_TYPE'],
101 'bcc_email' => $attrs['BCC_EMAIL'],
102 'allow_ip' => $attrs['ALLOW_IP'],
103 'password_complexity' => $attrs['PASSWORD_COMPLEXITY'],
104 'plugins' => $attrs['PLUGINS'],
105 'lock_spec' => $attrs['LOCK_SPEC'],
106 'workday_minutes' => $attrs['WORKDAY_MINUTES'],
107 'custom_logo' => $attrs['CUSTOM_LOGO'],
108 'config' => $attrs['CONFIG']));
110 // Special handling for top group.
111 if (!$this->org_id && $this->current_group_id) {
112 $this->org_id = $this->current_group_id;
113 $sql = "update tt_groups set org_id = $this->current_group_id where org_id is NULL and id = $this->current_group_id";
114 $affected = $mdb2->exec($sql);
116 // Set parent group to create subgroups with this group as parent at next entry here.
117 $this->current_parent_group_id = $this->current_group_id;
120 if ($name == 'ROLES') {
121 // If we get here, we have to recycle $currentGroupRoleMap.
122 unset($this->currentGroupRoleMap);
123 $this->currentGroupRoleMap = array();
124 // Role map is reconstructed after processing <role> elements in XML. See below.
127 if ($name == 'ROLE') {
128 // We get here when processing <role> tags for the current group.
129 $role_id = ttRoleHelper::insert(array(
130 'group_id' => $this->current_group_id,
131 'org_id' => $this->org_id,
132 'name' => $attrs['NAME'],
133 'description' => $attrs['DESCRIPTION'],
134 'rank' => $attrs['RANK'],
135 'rights' => $attrs['RIGHTS'],
136 'status' => $attrs['STATUS']));
139 $this->currentGroupRoleMap[$attrs['ID']] = $role_id;
140 } else $this->errors->add($i18n->get('error.db'));
143 if ($name == 'TASKS') {
144 // If we get here, we have to recycle $currentGroupTaskMap.
145 unset($this->currentGroupTaskMap);
146 $this->currentGroupTaskMap = array();
147 // Task map is reconstructed after processing <task> elements in XML. See below.
150 if ($name == 'TASK') {
151 // We get here when processing <task> tags for the current group.
152 $task_id = ttTaskHelper::insert(array(
153 'group_id' => $this->current_group_id,
154 'org_id' => $this->org_id,
155 'name' => $attrs['NAME'],
156 'description' => $attrs['DESCRIPTION'],
157 'status' => $attrs['STATUS']));
160 $this->currentGroupTaskMap[$attrs['ID']] = $task_id;
161 } else $this->errors->add($i18n->get('error.db'));
164 if ($name == 'PROJECTS') {
165 // If we get here, we have to recycle $currentGroupProjectMap.
166 unset($this->currentGroupProjectMap);
167 $this->currentGroupProjectMap = array();
168 // Project map is reconstructed after processing <project> elements in XML. See below.
171 if ($name == 'PROJECT') {
172 // We get here when processing <project> tags for the current group.
174 // Prepare a list of task ids.
175 $tasks = explode(',', $attrs['TASKS']);
176 foreach ($tasks as $id)
177 $mapped_tasks[] = $this->currentGroupTaskMap[$id];
179 $project_id = ttProjectHelper::insert(array(
180 'group_id' => $this->current_group_id,
181 'org_id' => $this->org_id,
182 'name' => $attrs['NAME'],
183 'description' => $attrs['DESCRIPTION'],
184 'tasks' => $mapped_tasks,
185 'status' => $attrs['STATUS']));
188 $this->currentGroupProjectMap[$attrs['ID']] = $project_id;
189 } else $this->errors->add($i18n->get('error.db'));
192 if ($name == 'CLIENTS') {
193 // If we get here, we have to recycle $currentGroupClientMap.
194 unset($this->currentGroupClientMap);
195 $this->currentGroupClientMap = array();
196 // Client map is reconstructed after processing <client> elements in XML. See below.
199 if ($name == 'CLIENT') {
200 // We get here when processing <client> tags for the current group.
202 // Prepare a list of project ids.
203 $projects = explode(',', $attrs['PROJECTS']);
204 foreach ($projects as $id)
205 $mapped_projects[] = $this->currentGroupProjectMap[$id];
207 $client_id = ttClientHelper::insert(array(
208 'group_id' => $this->current_group_id,
209 'org_id' => $this->org_id,
210 'name' => $attrs['NAME'],
211 'address' => $attrs['ADDRESS'],
212 'tax' => $attrs['TAX'],
213 'projects' => $mapped_projects,
214 'status' => $attrs['STATUS']));
217 $this->currentGroupClientMap[$attrs['ID']] = $client_id;
218 } else $this->errors->add($i18n->get('error.db'));
221 if ($name == 'USERS') {
222 // If we get here, we have to recycle $currentGroupUserMap.
223 unset($this->currentGroupUserMap);
224 $this->currentGroupUserMap = array();
225 // User map is reconstructed after processing <user> elements in XML. See below.
228 if ($name == 'USER') {
229 // We get here when processing <user> tags for the current group.
231 $role_id = $attrs['ROLE_ID'] === '0' ? $this->top_role_id : $this->currentGroupRoleMap[$attrs['ROLE_ID']]; // 0 (not null) means top manager role.
233 $user_id = ttUserHelper::insert(array(
234 'group_id' => $this->current_group_id,
235 'org_id' => $this->org_id,
236 'role_id' => $role_id,
237 'client_id' => $this->currentGroupClientMap[$attrs['CLIENT_ID']],
238 'name' => $attrs['NAME'],
239 'login' => $attrs['LOGIN'],
240 'password' => $attrs['PASSWORD'],
241 'rate' => $attrs['RATE'],
242 'email' => $attrs['EMAIL'],
243 'status' => $attrs['STATUS']), false);
246 $this->currentGroupUserMap[$attrs['ID']] = $user_id;
247 } else $this->errors->add($i18n->get('error.db'));
250 if ($name == 'USER_PROJECT_BIND') {
251 if (!ttUserHelper::insertBind(array(
252 'user_id' => $this->currentGroupUserMap[$attrs['USER_ID']],
253 'project_id' => $this->currentGroupProjectMap[$attrs['PROJECT_ID']],
254 'group_id' => $this->current_group_id,
255 'org_id' => $this->org_id,
256 'rate' => $attrs['RATE'],
257 'status' => $attrs['STATUS']))) {
258 $this->errors->add($i18n->get('error.db'));
262 if ($name == 'INVOICES') {
263 // If we get here, we have to recycle $currentGroupInvoiceMap.
264 unset($this->currentGroupInvoiceMap);
265 $this->currentGroupInvoiceMap = array();
266 // Invoice map is reconstructed after processing <invoice> elements in XML. See below.
269 if ($name == 'INVOICE') {
270 // We get here when processing <invoice> tags for the current group.
271 $invoice_id = ttInvoiceHelper::insert(array(
272 'group_id' => $this->current_group_id,
273 'org_id' => $this->org_id,
274 'name' => $attrs['NAME'],
275 'date' => $attrs['DATE'],
276 'client_id' => $this->currentGroupClientMap[$attrs['CLIENT_ID']],
277 'status' => $attrs['STATUS']));
280 $this->currentGroupInvoiceMap[$attrs['ID']] = $invoice_id;
281 } else $this->errors->add($i18n->get('error.db'));
284 if ($name == 'LOG') {
285 // If we get here, we have to recycle $currentGroupLogMap.
286 unset($this->currentGroupLogMap);
287 $this->currentGroupLogMap = array();
288 // Log map is reconstructed after processing <log_item> elements in XML. See below.
291 if ($name == 'LOG_ITEM') {
292 // We get here when processing <log_item> tags for the current group.
293 $log_item_id = ttTimeHelper::insert(array(
294 'user_id' => $this->currentGroupUserMap[$attrs['USER_ID']],
295 'group_id' => $this->current_group_id,
296 'org_id' => $this->org_id,
297 'date' => $attrs['DATE'],
298 'start' => $attrs['START'],
299 'finish' => $attrs['FINISH'],
300 'duration' => $attrs['DURATION'],
301 'client' => $this->currentGroupClientMap[$attrs['CLIENT_ID']],
302 'project' => $this->currentGroupProjectMap[$attrs['PROJECT_ID']],
303 'task' => $this->currentGroupTaskMap[$attrs['TASK_ID']],
304 'invoice' => $this->currentGroupInvoiceMap[$attrs['INVOICE_ID']],
305 'note' => (isset($attrs['COMMENT']) ? $attrs['COMMENT'] : ''),
306 'billable' => $attrs['BILLABLE'],
307 'paid' => $attrs['PAID'],
308 'status' => $attrs['STATUS']));
311 $this->currentGroupLogMap[$attrs['ID']] = $log_item_id;
312 } else $this->errors->add($i18n->get('error.db'));
317 // importXml - uncompresses the file, reads and parses its content. During parsing,
318 // startElement, endElement, and dataElement functions are called as many times as necessary.
319 // Actual import occurs in the endElement handler.
320 function importXml() {
323 // Do we have a compressed file?
325 $file_ext = substr($_FILES['xmlfile']['name'], strrpos($_FILES['xmlfile']['name'], '.') + 1);
326 if (in_array($file_ext, array('bz','tbz','bz2','tbz2'))) {
330 // Create a temporary file.
331 $dirName = dirname(TEMPLATE_DIR . '_c/.');
332 $filename = tempnam($dirName, 'import_');
334 // If the file is compressed - uncompress it.
336 if (!$this->uncompress($_FILES['xmlfile']['tmp_name'], $filename)) {
337 $this->errors->add($i18n->get('error.sys'));
340 unlink($_FILES['xmlfile']['tmp_name']);
342 if (!move_uploaded_file($_FILES['xmlfile']['tmp_name'], $filename)) {
343 $this->errors->add($i18n->get('error.upload'));
348 // Initialize XML parser.
349 $parser = xml_parser_create();
350 xml_set_object($parser, $this);
351 xml_set_element_handler($parser, 'startElement', false);
353 // We need to parse the file 2 times:
354 // 1) First pass: determine if import is possible - there must be no login collisions.
355 // 2) Second pass: if we can import, then do import in a second pass.
356 // This is different from earlier approach for single group import, where we could
357 // do both things in one pass because user info was in the beginning of XML file.
358 // Now, with subgroups, users can be located anywhere in the file.
360 // Read and parse the content of the file. During parsing, startElement, endElement, and dataElement functions are called.
361 $file = fopen($filename, 'r');
362 while ($data = fread($file, 4096)) {
363 if (!xml_parse($parser, $data, feof($file))) {
364 $this->errors->add(sprintf($i18n->get('error.xml'),
365 xml_get_current_line_number($parser),
366 xml_error_string(xml_get_error_code($parser))));
369 if ($this->conflicting_entities) {
370 $this->canImport = false;
371 $this->errors->add($i18n->get('error.user_exists'));
372 $this->errors->add(sprintf($i18n->get('error.cannot_import'), $this->conflicting_entities));
375 $this->firstPass = false; // We are done with 1st pass.
376 xml_parser_free($parser);
377 if ($file) fclose($file);
378 if (!$this->canImport) {
382 if ($this->errors->yes()) return; // Exit if we have errors.
384 // Now we can do a second pass, where real work is done.
385 $parser = xml_parser_create();
386 xml_set_object($parser, $this);
387 xml_set_element_handler($parser, 'startElement', false);
389 // Read and parse the content of the file. During parsing, startElement, endElement, and dataElement functions are called.
390 $file = fopen($filename, 'r');
391 while ($data = fread($file, 4096)) {
392 if (!xml_parse($parser, $data, feof($file))) {
393 $this->errors->add(sprintf($i18n->get('error.xml'),
394 xml_get_current_line_number($parser),
395 xml_error_string(xml_get_error_code($parser))));
398 xml_parser_free($parser);
399 if ($file) fclose($file);
403 // uncompress - uncompresses the content of the $in file into the $out file.
404 function uncompress($in, $out) {
405 // Do we have the uncompress function?
406 if (!function_exists('bzopen'))
409 // Initial checks of file names and permissions.
410 if (!file_exists($in) || !is_readable ($in))
412 if ((!file_exists($out) && !is_writable(dirname($out))) || (file_exists($out) && !is_writable($out)))
415 if (!$out_file = fopen($out, 'wb'))
417 if (!$in_file = bzopen ($in, 'r'))
420 while (!feof($in_file)) {
421 $buffer = bzread($in_file, 4096);
422 fwrite($out_file, $buffer, 4096);
429 // createGroup function creates a new group.
430 private function createGroup($fields) {
433 $mdb2 = getConnection();
435 $columns = '(parent_id, org_id, name, currency, decimal_mark, lang, date_format, time_format'.
436 ', week_start, tracking_mode, project_required, task_required, record_type, bcc_email'.
437 ', allow_ip, password_complexity, plugins, lock_spec'.
438 ', workday_minutes, config, created, created_ip, created_by)';
440 $values = ' values (';
441 $values .= $mdb2->quote($fields['parent_id']);
442 $values .= ', '.$mdb2->quote($fields['org_id']);
443 $values .= ', '.$mdb2->quote(trim($fields['name']));
444 $values .= ', '.$mdb2->quote(trim($fields['currency']));
445 $values .= ', '.$mdb2->quote($fields['decimal_mark']);
446 $values .= ', '.$mdb2->quote($fields['lang']);
447 $values .= ', '.$mdb2->quote($fields['date_format']);
448 $values .= ', '.$mdb2->quote($fields['time_format']);
449 $values .= ', '.(int)$fields['week_start'];
450 $values .= ', '.(int)$fields['tracking_mode'];
451 $values .= ', '.(int)$fields['project_required'];
452 $values .= ', '.(int)$fields['task_required'];
453 $values .= ', '.(int)$fields['record_type'];
454 $values .= ', '.$mdb2->quote($fields['bcc_email']);
455 $values .= ', '.$mdb2->quote($fields['allow_ip']);
456 $values .= ', '.$mdb2->quote($fields['password_complexity']);
457 $values .= ', '.$mdb2->quote($fields['plugins']);
458 $values .= ', '.$mdb2->quote($fields['lock_spec']);
459 $values .= ', '.(int)$fields['workday_minutes'];
460 $values .= ', '.$mdb2->quote($fields['config']);
461 $values .= ', now(), '.$mdb2->quote($_SERVER['REMOTE_ADDR']).', '.$mdb2->quote($user->id);
464 $sql = 'insert into tt_groups '.$columns.$values;
465 $affected = $mdb2->exec($sql);
466 if (is_a($affected, 'PEAR_Error')) {
467 $this->errors->add($i18n->get('error.db'));
471 $group_id = $mdb2->lastInsertID('tt_groups', 'id');