PageRenderTime 72ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/class.backend.php

https://bitbucket.org/djl/flyspray-mirror
PHP | 1561 lines | 1037 code | 246 blank | 278 comment | 236 complexity | 7dfae22087c3b5d8c3448289dc5b245b MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, BSD-3-Clause, MPL-2.0-no-copyleft-exception

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Flyspray
  4. *
  5. * Backend class
  6. *
  7. * This script contains reusable functions we use to modify
  8. * various things in the Flyspray database tables.
  9. *
  10. * @license http://opensource.org/licenses/lgpl-license.php Lesser GNU Public License
  11. * @package flyspray
  12. * @author Tony Collins, Florian Schmitz, Cristian Rodriguez
  13. */
  14. if (!defined('IN_FS')) {
  15. die('Do not access this file directly.');
  16. }
  17. // Include the notifications class
  18. require_once BASEDIR . '/includes/class.notify.php';
  19. class Backend
  20. {
  21. /**
  22. * Adds the user $user_id to the notifications list of $tasks
  23. * @param integer $user_id
  24. * @param array $tasks
  25. * @param bool $do Force execution independent of user permissions
  26. * @access public
  27. * @return bool
  28. * @version 1.0
  29. */
  30. function add_notification($user_id, $tasks, $do = false)
  31. {
  32. global $db, $user;
  33. settype($tasks, 'array');
  34. $user_id = Flyspray::ValidUserId($user_id);
  35. if (!$user_id || !count($tasks)) {
  36. return false;
  37. }
  38. $sql = $db->x->getAll(' SELECT *
  39. FROM {tasks}
  40. WHERE task_id IN(' . implode(',', array_map('intval', $tasks)) . ')');
  41. foreach ($sql as $row) {
  42. // -> user adds himself
  43. if ($user->id == $user_id) {
  44. if (!$user->can_view_task($row) && !$do) {
  45. continue;
  46. }
  47. // -> user is added by someone else
  48. } else {
  49. if (!$user->perms('manage_project', $row['project_id']) && !$do) {
  50. continue;
  51. }
  52. }
  53. $notif = $db->x->GetOne('SELECT notify_id
  54. FROM {notifications}
  55. WHERE task_id = ? and user_id = ?', null,
  56. array($row['task_id'], $user_id));
  57. if (!$notif) {
  58. $notif = $db->x->autoExecute('{notifications}', array('task_id'=> $row['task_id'], 'user_id'=> $user_id));
  59. Flyspray::logEvent($row['task_id'], 9, $user_id);
  60. }
  61. }
  62. return isset($notif); // only indicates whether or not there have been tries to add a notification
  63. }
  64. /**
  65. * Removes a user $user_id from the notifications list of $tasks
  66. * @param integer $user_id
  67. * @param array $tasks
  68. * @access public
  69. * @return void
  70. * @version 1.0
  71. */
  72. function remove_notification($user_id, $tasks)
  73. {
  74. global $db, $user;
  75. settype($tasks, 'array');
  76. if (!count($tasks)) {
  77. return;
  78. }
  79. $sql = $db->query(' SELECT *
  80. FROM {tasks}
  81. WHERE task_id IN ('. implode(',', array_map('intval', $tasks)) .')');
  82. while ($row = $sql->FetchRow()) {
  83. // -> user removes himself
  84. if ($user->id == $user_id) {
  85. if (!$user->can_view_task($row)) {
  86. continue;
  87. }
  88. // -> user is removed by someone else
  89. } else {
  90. if (!$user->perms('manage_project', $row['project_id'])) {
  91. continue;
  92. }
  93. }
  94. $num = $db->x->execParam('DELETE FROM {notifications}
  95. WHERE task_id = ? AND user_id = ?',
  96. array($row['task_id'], $user_id));
  97. if ($num) {
  98. Flyspray::logEvent($row['task_id'], 10, $user_id);
  99. }
  100. }
  101. }
  102. /**
  103. * Transforms a komma-separated list of user names to an user ID array
  104. * @param string $list komma separated list of of user names
  105. * @access public
  106. * @return array user IDs
  107. * @version 1.0
  108. */
  109. function UserIdsFromUserNameList($list)
  110. {
  111. $users = preg_split('/[\s,;]+/', $list, -1, PREG_SPLIT_NO_EMPTY);
  112. $result = array();
  113. foreach ($users as $user) {
  114. $id = Flyspray::UserNameToId($user);
  115. if ($id) {
  116. $result[] = $id;
  117. }
  118. }
  119. return $result;
  120. }
  121. /**
  122. * Adds one or more users to a group (few permission checks)
  123. * @param array $users list of user IDs
  124. * @param integer $group_id of new group
  125. * @access public
  126. * @return integer 0:failure 1:added 2:removed from project -1:perms error
  127. * @version 1.0
  128. */
  129. function add_user_to_group($users, $group_id, $proj_id = 0)
  130. {
  131. global $db, $user;
  132. $return = 9;
  133. foreach ($users as $uid) {
  134. $uid = Flyspray::ValidUserId($uid);
  135. if (!$uid) {
  136. $return = min($return, 0);
  137. continue;
  138. }
  139. // Delete from project?
  140. if (!$group_id && $proj_id) {
  141. $db->x->execParam('DELETE uig FROM {users_in_groups} uig
  142. LEFT JOIN {groups} g ON uig.group_id = g.group_id
  143. WHERE uig.user_id = ? AND g.project_id = ?',
  144. array($uid, $proj_id));
  145. $return = min($return, 1);
  146. continue;
  147. }
  148. // If user is already a member of one of the project's groups, **move** (not add) him to the new group
  149. $group_project = $db->x->GetOne('SELECT project_id FROM {groups} WHERE group_id = ?', null, $group_id);
  150. if (!$user->perms('manage_project', $group_project)) {
  151. $return = min($return, -1);
  152. continue;
  153. }
  154. $oldid = $db->x->GetOne('SELECT g.group_id
  155. FROM {users_in_groups} uig, {groups} g
  156. WHERE g.group_id = uig.group_id AND uig.user_id = ? AND project_id = ?',
  157. null, array($uid, $group_project));
  158. if ($oldid) {
  159. $db->x->execParam('UPDATE {users_in_groups} SET group_id = ? WHERE user_id = ? AND group_id = ?',
  160. array($group_id, $uid, $oldid));
  161. } else {
  162. $db->x->autoExecute('{users_in_groups}', array('group_id' => $group_id, 'user_id' => $uid));
  163. }
  164. $return = min($return, 2);
  165. continue;
  166. }
  167. return $return;
  168. }
  169. /**
  170. * Assigns one or more $tasks only to a user $user_id
  171. * @param integer $user_id
  172. * @param array $tasks
  173. * @access public
  174. * @return void
  175. * @version 1.0
  176. */
  177. function assign_to_me($user_id, $tasks)
  178. {
  179. global $db;
  180. $user = $GLOBALS['user'];
  181. if ($user_id != $user->id) {
  182. $user = new User($user_id);
  183. }
  184. settype($tasks, 'array');
  185. if (!count($tasks)) {
  186. return;
  187. }
  188. $sql = $db->query(' SELECT *
  189. FROM {tasks}
  190. WHERE task_id IN ('. implode(',', array_map('intval', $tasks)) .')');
  191. $del_assignees = $db->x->autoPrepare('{assigned}', null, MDB2_AUTOQUERY_DELETE, $where = 'task_id = ?');
  192. $insert_assigned = $db->x->autoPrepare('{assigned}', array('task_id', 'user_id'));
  193. while ($row = $sql->FetchRow()) {
  194. if (!$user->can_take_ownership($row)) {
  195. continue;
  196. }
  197. $num = $del_assignees->execute($row['task_id']);
  198. $insert_assigned->execute(array($row['task_id'], $user->id));
  199. if ($num) {
  200. Flyspray::logEvent($row['task_id'], 19, $user->id, implode(' ', Flyspray::GetAssignees($row['task_id'])));
  201. Notifications::send($row['task_id'], ADDRESS_TASK, NOTIFY_OWNERSHIP);
  202. }
  203. }
  204. $del_assignees->free();
  205. $insert_assigned->free();
  206. }
  207. /**
  208. * Adds a user $user_id to the assignees of one or more $tasks
  209. * @param integer $user_id
  210. * @param array $tasks
  211. * @param bool $do Force execution independent of user permissions
  212. * @access public
  213. * @return void
  214. * @version 1.0
  215. */
  216. function add_to_assignees($user_id, $tasks, $do = false)
  217. {
  218. global $db;
  219. $user = $GLOBALS['user'];
  220. if ($user_id != $user->id) {
  221. $user = new User($user_id);
  222. }
  223. settype($tasks, 'array');
  224. if (!count($tasks)) {
  225. return;
  226. }
  227. $sql = $db->query(' SELECT *
  228. FROM {tasks}
  229. WHERE task_id IN ('. implode(',', array_map('intval', $tasks)) .')');
  230. while ($row = $sql->FetchRow()) {
  231. if (!$user->can_add_to_assignees($row) && !$do) {
  232. continue;
  233. }
  234. $fields = array('user_id'=> array('value' => $user->id, 'key' => true),
  235. 'task_id'=> array('value' => $row['task_id'], 'key' => true) );
  236. $r = $db->Replace('{assigned}', $fields);
  237. if ($r > 0) {
  238. Flyspray::logEvent($row['task_id'], 29, $user->id, implode(' ', Flyspray::GetAssignees($row['task_id'])));
  239. Notifications::send($row['task_id'], ADDRESS_TASK, NOTIFY_ADDED_ASSIGNEES);
  240. }
  241. }
  242. }
  243. /**
  244. * Adds a vote from $user_id to the task $task_id
  245. * @param integer $user_id
  246. * @param integer $task_id
  247. * @access public
  248. * @return bool
  249. * @version 1.0
  250. */
  251. function add_vote($user_id, $task_id)
  252. {
  253. global $db;
  254. $user = $GLOBALS['user'];
  255. if ($user_id != $user->id) {
  256. $user = new User($user_id);
  257. }
  258. $task = Flyspray::GetTaskDetails($task_id);
  259. if (!$task) {
  260. return false;
  261. }
  262. if ($user->can_vote($task) > 0) {
  263. if ($db->x->autoExecute('{votes}', array('user_id' => $user->id, 'task_id' => $task_id, 'date_time' => time()))) {
  264. // [RED] Update vote count
  265. $votes = $db->x->GetOne('SELECT count(*) FROM {votes} WHERE task_id = ?', null, $task_id);
  266. $db->x->execParam('UPDATE {redundant} SET vote_count = ? WHERE task_id = ?', array($votes, $task_id));
  267. return true;
  268. }
  269. }
  270. return false;
  271. }
  272. /**
  273. * Edits the task in $task using the parameters in $args
  274. * @param array $task a task array
  275. * @param array $args usually $_POST
  276. * @access public
  277. * @return array array(STATUS_CODE, msg)
  278. */
  279. function edit_task($task, $args)
  280. {
  281. global $user, $db, $fs, $proj;
  282. if ($proj->id != Post::val('project_id', $task['project_id'])) {
  283. $proj = new Project(Post::val('project_id', $task['project_id']));
  284. }
  285. if (!$proj->id) {
  286. return array(ERROR_INPUT, L('cannotedittaskglobally'));
  287. }
  288. if (!$user->can_edit_task($task) && !$user->can_correct_task($task)) {
  289. return array(ERROR_PERMS);
  290. }
  291. // check missing fields
  292. if (!array_get($args, 'item_summary') || !array_get($args, 'detailed_desc')) {
  293. return array(ERROR_RECOVER, L('summaryanddetails'));
  294. }
  295. foreach ($proj->fields as $field) {
  296. if ($field->prefs['value_required'] && !array_get($args, 'field' . $field->id)
  297. && !($field->prefs['force_default'] && !$user->perms('modify_all_tasks'))) {
  298. return array(ERROR_RECOVER, L('missingrequired') . ' (' . $field->prefs['field_name'] . ')');
  299. }
  300. }
  301. $time = time();
  302. $plugins = trim(implode(' ', array_get($args, 'detailed_desc_syntax_plugins', array())));
  303. if (!$plugins) {
  304. $plugins = $proj->prefs['syntax_plugins'];
  305. }
  306. $db->x->autoExecute('{tasks}', array('project_id' => $proj->id,
  307. 'item_summary' => array_get($args, 'item_summary'),
  308. 'detailed_desc' => array_get($args, 'detailed_desc'),
  309. 'mark_private' => intval($user->can_change_private($task) && array_get($args, 'mark_private')),
  310. 'last_edited_by' => intval($user->id),
  311. 'last_edited_time' => $time,
  312. 'syntax_plugins' => $plugins,
  313. 'percent_complete' => array_get($args, 'percent_complete')),
  314. MDB2_AUTOQUERY_UPDATE, sprintf('task_id = %d', $task['task_id']));
  315. // Now the custom fields
  316. foreach ($proj->fields as $field) {
  317. $field_value = $field->read(array_get($args, 'field' . $field->id));
  318. $fields = array('field_id'=> array('value' => $field->id, 'key' => true),
  319. 'task_id'=> array('value' => $task['task_id'], 'key' => true),
  320. 'field_value' => array('value' => $field_value));
  321. $db->Replace('{field_values}', $fields);
  322. }
  323. // [RED] Update last changed date and user
  324. $db->x->execParam('UPDATE {redundant} SET last_changed_time = ?,
  325. last_changed_by_real_name = ?, last_changed_by_user_name = ?,
  326. last_edited_by_real_name = ?, last_edited_by_user_name = ?
  327. WHERE task_id = ?',
  328. array($time, $user->infos['real_name'], $user->infos['user_name'],
  329. $user->infos['real_name'], $user->infos['user_name'], $task['task_id']));
  330. // Prepare assignee list
  331. $assignees = explode(';', trim(array_get($args, 'assigned_to')));
  332. $assignees = array_map(array('Flyspray', 'UserNameToId'), $assignees);
  333. $assignees = array_filter($assignees, create_function('$x', 'return ($x > 0);'));
  334. // Update the list of users assigned this task, if changed
  335. if ($user->perms('edit_assignments') && count(array_diff($task['assigned_to'], $assignees)) + count(array_diff($assignees, $task['assigned_to']))) {
  336. // Delete the current assignees for this task
  337. $db->x->execParam('DELETE FROM {assigned}
  338. WHERE task_id = ?', $task['task_id']);
  339. // Store them in the 'assigned' table
  340. foreach ($assignees as $val)
  341. {
  342. $fields = array('user_id'=> array('value' => $val, 'key' => true),
  343. 'task_id'=> array('value' => $task['task_id'], 'key' => true));
  344. $db->Replace('{assigned}', $fields);
  345. }
  346. // Log to task history
  347. Flyspray::logEvent($task['task_id'], 14, implode(' ', $assignees), implode(' ', $task['assigned_to']), '', $time);
  348. // Notify the new assignees what happened. This obviously won't happen if the task is now assigned to no-one.
  349. if (count($assignees)) {
  350. $new_assignees = array_diff($assignees, $task['assigned_to']);
  351. // Remove current user from notification list
  352. if (!$user->infos['notify_own']) {
  353. $new_assignees = array_filter($new_assignees, create_function('$u', 'global $user; return $user->id != $u;'));
  354. }
  355. if (count($new_assignees)) {
  356. Notifications::send($new_assignees, ADDRESS_USER, NOTIFY_NEW_ASSIGNEE, array('task_id' => $task['task_id']));
  357. }
  358. }
  359. }
  360. // Get the details of the task we just updated
  361. // To generate the changed-task message
  362. $new_details_full = Flyspray::GetTaskDetails($task['task_id']);
  363. $changes = Flyspray::compare_tasks($task, $new_details_full);
  364. foreach ($changes as $change) {
  365. if ($change[4] == 'assigned_to_name') {
  366. continue;
  367. }
  368. Flyspray::logEvent($task['task_id'], 3, $change[6], $change[5], $change[4], $time);
  369. }
  370. if (count($changes) > 0) {
  371. Notifications::send($task['task_id'], ADDRESS_TASK, NOTIFY_TASK_CHANGED, array('changes' => $changes));
  372. }
  373. Backend::add_comment($task, array_get($args, 'comment_text'), $time);
  374. Backend::delete_files(array_get($args, 'delete_att'));
  375. Backend::upload_files($task['task_id'], '0', 'usertaskfile');
  376. return array(SUBMIT_OK, L('taskupdated'));
  377. }
  378. /**
  379. * [RED] Takes care of updating a user's redundant data
  380. * @param string $username user ID of the changed user
  381. * @access public
  382. * @version 1.0
  383. */
  384. function UpdateRedudantUserData($username)
  385. {
  386. global $db;
  387. // Get the new user- and real-name
  388. $userinfo = $db->x->getRow('SELECT user_name, real_name FROM {users} WHERE user_name = ?', null, $username);
  389. // Possibly user is deleted
  390. if ($userinfo == null) {
  391. $userinfo['user_name'] = $userinfo['real_name'] = '';
  392. }
  393. $args = array($userinfo['real_name'], $username);
  394. // Opened by
  395. $db->x->execParam('UPDATE {redundant} SET opened_by_real_name = ? WHERE opened_by_user_name = ?', $args);
  396. // Closed by
  397. $db->x->execParam('UPDATE {redundant} SET closed_by_real_name = ? WHERE closed_by_user_name = ?', $args);
  398. // Last edited by
  399. $db->x->execParam('UPDATE {redundant} SET last_edited_by_real_name = ? WHERE last_edited_by_user_name = ?', $args);
  400. // Last changed by
  401. $db->x->execParam('UPDATE {redundant} SET last_changed_by_real_name = ? WHERE last_changed_by_user_name = ?', $args);
  402. }
  403. /**
  404. * Adds a comment to $task
  405. * @param array $task
  406. * @param string $comment_text
  407. * @param integer $time for synchronisation with other functions
  408. * @param array array of used syntax plugins
  409. * @access public
  410. * @return int
  411. * @version 1.0
  412. */
  413. function add_comment($task, $comment_text, $time = null, $syntax_plugins = array())
  414. {
  415. global $db, $user, $proj;
  416. if (!($user->perms('add_comments', $task['project_id']) && (!$task['is_closed'] || $user->perms('comment_closed', $task['project_id'])))) {
  417. return 0;
  418. }
  419. if (!is_string($comment_text) || !strlen($comment_text)) {
  420. return 0;
  421. }
  422. if ($proj->id != Post::val('project_id', $task['project_id'])) {
  423. if (!($proj = new Project(Post::val('project_id', $task['project_id'])))) return 0;
  424. }
  425. $time = !is_numeric($time) ? time() : $time ;
  426. if (!count($syntax_plugins)) {
  427. $syntax_plugins = explode(' ', $proj->prefs['syntax_plugins']);
  428. }
  429. $db->x->autoExecute('{comments}', array('task_id' => $task['task_id'],
  430. 'date_added'=> $time,
  431. 'last_edited_time'=> $time,
  432. 'user_id' => $user->id,
  433. 'comment_text' => $comment_text,
  434. 'syntax_plugins' => implode(' ', $syntax_plugins)
  435. )
  436. );
  437. $cid = $db->lastInsertID();
  438. // [RED] Update comment count
  439. $comments = $db->x->GetOne('SELECT count(*) FROM {comments} WHERE task_id = ?', null, $task['task_id']);
  440. $db->x->execParam('UPDATE {redundant} SET comment_count = ? WHERE task_id = ?', array($comments, $task['task_id']));
  441. // [RED] And update last changed date
  442. $db->x->execParam('UPDATE {redundant} SET last_changed_time = ?,
  443. last_changed_by_real_name = ?, last_changed_by_user_name = ?
  444. WHERE task_id = ?',
  445. array($time, $user->infos['real_name'], $user->infos['user_name'], $task['task_id']));
  446. Flyspray::logEvent($task['task_id'], 4, $cid);
  447. if (Backend::upload_files($task['task_id'], $cid)) {
  448. Notifications::send($task['task_id'], ADDRESS_TASK, NOTIFY_COMMENT_ADDED, array('files' => true, 'cid' => $cid));
  449. } else {
  450. Notifications::send($task['task_id'], ADDRESS_TASK, NOTIFY_COMMENT_ADDED, array('cid' => $cid));
  451. }
  452. return $cid;
  453. }
  454. /**
  455. * Upload files for a comment or a task
  456. * @param integer $task_id
  457. * @param integer $comment_id if it is 0, the files will be attached to the task itself
  458. * @param string $source name of the file input
  459. * @access public
  460. * @return bool
  461. * @version 1.0
  462. */
  463. function upload_files($task_id, $comment_id = 0, $source = 'userfile')
  464. {
  465. global $db, $conf, $user;
  466. $task = Flyspray::GetTaskDetails($task_id);
  467. if (!$user->perms('create_attachments', $task['project_id'])) {
  468. return false;
  469. }
  470. $res = false;
  471. if (!isset($_FILES[$source]['error'])) {
  472. return false;
  473. }
  474. $attachstmt = $db->x->autoPrepare('{attachments}', array('task_id', 'comment_id',
  475. 'file_name','file_type',
  476. 'file_size', 'orig_name',
  477. 'added_by', 'date_added'));
  478. foreach ($_FILES[$source]['error'] as $key => $error) {
  479. if ($error != UPLOAD_ERR_OK) {
  480. continue;
  481. }
  482. $fname = md5(uniqid(mt_rand(), true));
  483. $path = FS_ATTACHMENTS_DIR. DIRECTORY_SEPARATOR. $fname ;
  484. $tmp_name = $_FILES[$source]['tmp_name'][$key];
  485. // Then move the uploaded file and remove exe permissions
  486. if (!move_uploaded_file($tmp_name, $path)) {
  487. return false;
  488. }
  489. @chmod($path, 0644);
  490. $res = true;
  491. // Use a different MIME type
  492. $fileparts = explode('.', $_FILES[$source]['name'][$key]);
  493. $extension = end($fileparts);
  494. if (isset($conf['attachments'][$extension])) {
  495. $_FILES[$source]['type'][$key] = $conf['attachments'][$extension];
  496. //actually, try really hard to get the real filetype, not what the browser reports.
  497. } elseif($type = Flyspray::check_mime_type($path)) {
  498. $_FILES[$source]['type'][$key] = $type;
  499. }// we can try even more, however, far too much code is needed.
  500. $attachstmt->execute(array($task_id, $comment_id, $fname, $_FILES[$source]['type'][$key],
  501. $_FILES[$source]['size'][$key], $_FILES[$source]['name'][$key],
  502. $user->id, time()));
  503. // Fetch the attachment id for the history log
  504. $aid = $db->lastInsertID();
  505. Flyspray::logEvent($task_id, 7, $aid, $_FILES[$source]['name'][$key]);
  506. }
  507. $attachstmt->free();
  508. // [RED] Update attachment count
  509. $atts = $db->x->GetOne('SELECT count(*) FROM {attachments} WHERE task_id = ?', null, $task['task_id']);
  510. $db->x->execParam('UPDATE {redundant} SET attachment_count = ? WHERE task_id = ?', array($atts, $task['task_id']));
  511. return $res;
  512. }
  513. /**
  514. * Delete one or more attachments of a task or comment
  515. * @param array $attachments
  516. * @access public
  517. * @return void
  518. * @version 1.0
  519. */
  520. function delete_files($attachments)
  521. {
  522. global $db, $user;
  523. settype($attachments, 'array');
  524. if (!count($attachments)) {
  525. return;
  526. }
  527. $sql = $db->query(' SELECT t.*, a.*
  528. FROM {attachments} a
  529. LEFT JOIN {tasks} t ON t.task_id = a.task_id
  530. WHERE attachment_id IN('. implode(',', array_map('intval', $attachments)) .')');
  531. while ($task = $sql->FetchRow()) {
  532. if (!$user->perms('delete_attachments', $task['project_id'])) {
  533. continue;
  534. }
  535. $db->x->execParam('DELETE FROM {attachments} WHERE attachment_id = ?',
  536. $task['attachment_id']);
  537. @unlink(FS_ATTACHMENTS_DIR . DIRECTORY_SEPARATOR . $task['file_name']);
  538. // [RED] Update attachment count
  539. $atts = $db->x->GetOne('SELECT count(*) FROM {attachments} WHERE task_id = ?', null, $task['task']);
  540. $db->x->execParam('UPDATE {redundant} SET attachment_count = ? WHERE task_id = ?', array($atts, $task['task']));
  541. Flyspray::logEvent($task['task_id'], 8, $task['orig_name']);
  542. }
  543. }
  544. /**
  545. * Cleans a username (length, special chars, spaces)
  546. * @param string $user_name
  547. * @access public
  548. * @return string
  549. */
  550. function clean_username($user_name)
  551. {
  552. // Limit length
  553. $user_name = substr(trim($user_name), 0, 32);
  554. // Remove doubled up spaces and control chars
  555. $user_name = preg_replace('![\x00-\x1f\s]+!u', ' ', $user_name);
  556. // Strip special chars
  557. return utf8_keepalphanum($user_name);
  558. }
  559. /**
  560. * Creates a new user
  561. * @param string $user_name
  562. * @param string $password
  563. * @param string $real_name
  564. * @param string $jabber_id
  565. * @param string $email
  566. * @param integer $notify_type
  567. * @param integer $time_zone
  568. * @param integer $group_in
  569. * @access public
  570. * @return mixed false if username is already taken, otherwise integer uid
  571. * @version 1.0
  572. * @notes This function does not have any permission checks (checked elsewhere)
  573. */
  574. function create_user($user_name, $password, $real_name, $jabber_id, $email, $notify_type, $time_zone, $group_in)
  575. {
  576. global $fs, $db, $baseurl;
  577. $user_name = Backend::clean_username($user_name);
  578. // Limit lengths
  579. $real_name = substr(trim($real_name), 0, 100);
  580. // Remove doubled up spaces and control chars
  581. $real_name = preg_replace('![\x00-\x1f\s]+!u', ' ', $real_name);
  582. // Check to see if the username is available
  583. $username_exists = $db->x->GetOne('SELECT COUNT(*) FROM {users} WHERE user_name = ?', null, $user_name);
  584. if ($username_exists) {
  585. return false;
  586. }
  587. $auto = false;
  588. // Autogenerate a password
  589. if (!$password) {
  590. $auto = true;
  591. $password = substr(md5(uniqid(mt_rand(), true)), 0, mt_rand(8, 12));
  592. }
  593. $salt = md5(uniqid(mt_rand() , true));
  594. $userdata = array('user_name'=> $user_name,
  595. 'user_pass'=> Flyspray::cryptPassword($password, $salt),
  596. 'password_salt'=> $salt,
  597. 'real_name'=> $real_name,
  598. 'jabber_id'=> $jabber_id,
  599. 'email_address'=> $email,
  600. 'notify_type'=> $notify_type,
  601. 'time_zone'=> $time_zone,
  602. 'register_date'=> time(),
  603. 'account_enabled'=> 1,
  604. );
  605. $db->x->autoExecute('{users}', $userdata);
  606. // Get this user's id for the record
  607. $uid = Flyspray::UserNameToId($user_name);
  608. // Now, create a new record in the users_in_groups table
  609. $db->x->autoExecute('{users_in_groups}', array('user_id'=> $uid, 'group_id'=> $group_in));
  610. Flyspray::logEvent(0, 30, serialize(Flyspray::getUserDetails($uid)));
  611. // Add user to project groups
  612. $sql = $db->x->getAll('SELECT anon_group FROM {projects} WHERE anon_group != 0');
  613. if (count($sql)) {
  614. $stmt = $db->x->autoPrepare('{users_in_groups}', array('user_id', 'group_id'));
  615. foreach($sql as $row) {
  616. $stmt->execute(array($uid, $row['anon_group']));
  617. }
  618. $stmt->free();
  619. }
  620. $varnames = array('iwatch','atome','iopened');
  621. $toserialize = array('string' => null,
  622. 'type' => array (''),
  623. 'sev' => array (''),
  624. 'due' => array (''),
  625. 'dev' => null,
  626. 'cat' => array (''),
  627. 'status' => array ('open'),
  628. 'order' => null,
  629. 'sort' => null,
  630. 'percent' => array (''),
  631. 'opened' => null,
  632. 'search_in_comments' => null,
  633. 'search_for_all' => null,
  634. 'reported' => array (''),
  635. 'only_primary' => null,
  636. 'only_watched' => null);
  637. foreach ($varnames as $tmpname) {
  638. if ($tmpname == 'iwatch') {
  639. $tmparr = array('only_watched' => '1');
  640. } elseif ($tmpname == 'atome') {
  641. $tmparr = array('dev'=> $uid);
  642. } elseif ($tmpname == 'iopened') {
  643. $tmparr = array('opened'=> $uid);
  644. }
  645. $$tmpname = $tmparr + $toserialize;
  646. }
  647. // Now give him his default searches
  648. $stmt = $db->x->autoPrepare('{searches}', array('user_id', 'name', 'search_string', 'time'));
  649. $params = array(array($uid, L('taskswatched'), serialize($iwatch), time()),
  650. array($uid, L('assignedtome'), serialize($atome), time()),
  651. array($uid, L('tasksireported'), serialize($iopened), time()));
  652. $db->x->executeMultiple($stmt, $params);
  653. $stmt->free();
  654. if ($jabber_id) {
  655. Notifications::JabberRequestAuth($jabber_id);
  656. }
  657. // Send a user his details (his username might be altered, password auto-generated)
  658. if ($fs->prefs['notify_registration']) {
  659. $admins = $db->x->GetCol('SELECT user_id
  660. FROM {users_in_groups}
  661. WHERE group_id = 1');
  662. Notifications::send($admins, ADDRESS_USER, NOTIFY_NEW_USER,
  663. array($baseurl, $user_name, $real_name, $email, $jabber_id, $password, $auto));
  664. }
  665. return $uid;
  666. }
  667. /**
  668. * Deletes a user
  669. * @param integer $uid
  670. * @access public
  671. * @return bool
  672. * @version 1.0
  673. */
  674. function delete_user($uid)
  675. {
  676. global $db, $user;
  677. if (!$user->perms('is_admin')) {
  678. return false;
  679. }
  680. $user_data = Flyspray::getUserDetails($uid);
  681. $tables = array('users', 'users_in_groups', 'searches',
  682. 'notifications', 'assigned');
  683. foreach ($tables as $table) {
  684. if (!$db->x->execParam('DELETE FROM ' .'{' . $table .'}' . ' WHERE user_id = ?', $uid)) {
  685. return false;
  686. }
  687. }
  688. // for the unusual situuation that a user ID is re-used, make sure that the new user doesn't
  689. // get permissions for a task automatically
  690. $db->x->execParam('UPDATE {tasks} SET opened_by = 0 WHERE opened_by = ?', $uid);
  691. Backend::UpdateRedudantUserData($user_data['user_name']);
  692. Flyspray::logEvent(0, 31, serialize($user_data));
  693. return true;
  694. }
  695. /**
  696. * Deletes a project
  697. * @param integer $pid
  698. * @param integer $move_to to which project contents of the project are moved
  699. * @access public
  700. * @return bool
  701. * @version 1.0
  702. */
  703. function delete_project($pid, $move_to = 0)
  704. {
  705. global $db, $user;
  706. if (!$user->perms('manage_project', $pid)) {
  707. return false;
  708. }
  709. // Delete all project's tasks related information
  710. if (!$move_to) {
  711. $taskIds = $db->x->getCol('SELECT task_id FROM {tasks} WHERE project_id = ?', null, $pid);
  712. $tables = array('admin_requests', 'assigned', 'attachments', 'comments', 'dependencies', 'related',
  713. 'field_values', 'history', 'notification_threads', 'notifications', 'redundant', 'reminders', 'votes');
  714. foreach ($tables as $table) {
  715. if ($table == 'related') {
  716. $stmt = $db->prepare('DELETE FROM {' . $table . '} WHERE this_task = ? OR related_task = ? ', array('integer', 'integer'), MDB2_PREPARE_MANIP);
  717. } else {
  718. $stmt = $db->prepare('DELETE FROM {' . $table . '} WHERE task_id = ?', array('integer'), MDB2_PREPARE_MANIP);
  719. }
  720. foreach ($taskIds as $id) {
  721. $stmt->execute( ($table == 'related') ? array($id, $id) : $id);
  722. }
  723. $stmt->free();
  724. }
  725. }
  726. $tables = array('lists', 'admin_requests', 'fields',
  727. 'cache', 'projects', 'tasks');
  728. foreach ($tables as $table) {
  729. if ($move_to && $table !== 'projects') {
  730. $base_sql = 'UPDATE {' . $table . '} SET project_id = ?';
  731. $sql_params = array($move_to, $pid);
  732. } else {
  733. $base_sql = 'DELETE FROM {' . $table . '}';
  734. $sql_params = array($pid);
  735. }
  736. $db->x->execParam($base_sql . ' WHERE project_id = ?', $sql_params);
  737. }
  738. // groups are only deleted, not moved (it is likely
  739. // that the destination project already has all kinds
  740. // of groups which are also used by the old project)
  741. $sql = $db->x->getAll('SELECT group_id FROM {groups} WHERE project_id = ?', null, array($pid));
  742. if(count($sql)) {
  743. $stmt = $db->prepare('DELETE FROM {users_in_groups} WHERE group_id = ?', array('integer'), MDB2_PREPARE_MANIP);
  744. foreach ($sql as $row) {
  745. $stmt->execute($row['group_id']);
  746. }
  747. $stmt->free();
  748. }
  749. $db->x->execParam('DELETE FROM {groups} WHERE project_id = ?', $pid);
  750. //we have enough reasons .. the process is OK.
  751. return true;
  752. }
  753. /**
  754. * Adds a reminder to a task
  755. * @param integer $task_id
  756. * @param string $message
  757. * @param integer $how_often send a reminder every ~ seconds
  758. * @param integer $start_time time when the reminder starts
  759. * @param $user_id the user who is reminded. by default (null) all users assigned to the task are reminded.
  760. * @access public
  761. * @return bool
  762. * @version 1.0
  763. */
  764. function add_reminder($task_id, $message, $how_often, $start_time, $user_id = null)
  765. {
  766. global $user, $db;
  767. $task = Flyspray::GetTaskDetails($task_id);
  768. if (!$user->perms('manage_project', $task['project_id'])) {
  769. return false;
  770. }
  771. if (is_null($user_id)) {
  772. // Get all users assigned to a task
  773. $user_id = Flyspray::GetAssignees($task_id);
  774. } else {
  775. $user_id = array(Flyspray::ValidUserId($user_id));
  776. if (!reset($user_id)) {
  777. return false;
  778. }
  779. }
  780. foreach ($user_id as $id) {
  781. $fields = array('task_id'=> array('value' => $task_id, 'key' => true),
  782. 'to_user_id'=> array('value' => $id, 'key' => true),
  783. 'from_user_id' => array('value' => $user->id),
  784. 'start_time' => array('value' => $start_time),
  785. 'how_often' => array('value' => $how_often, 'key' => true),
  786. 'reminder_message' => array('value' => $message, 'key' => true));
  787. $sql = $db->Replace('{reminders}', $fields);
  788. if(!$sql) {
  789. // query has failed :(
  790. return false;
  791. }
  792. }
  793. // 2 = no record has found and was INSERT'ed correclty
  794. if (isset($sql) && $sql == 2) {
  795. Flyspray::logEvent($task_id, 17, $task_id);
  796. }
  797. return true;
  798. }
  799. /**
  800. * Adds a new task
  801. * @param array $args array containing all task properties. unknown properties will be ignored
  802. * @access public
  803. * @return array(error type, msg, false) or array(task ID, token, true)
  804. * @version 1.0
  805. * @notes $args is POST data, bad..bad user..
  806. */
  807. function create_task($args)
  808. {
  809. global $db, $user, $proj, $fs;
  810. if ($proj->id != $args['project_id']) {
  811. $proj = new Project($args['project_id']);
  812. }
  813. if (!$user->can_open_task($proj) || count($args) < 3) {
  814. return array(ERROR_RECOVER, L('missingrequired'), false);
  815. }
  816. // check required fields
  817. if (!(($item_summary = $args['item_summary']) && ($detailed_desc = $args['detailed_desc']))) {
  818. return array(ERROR_RECOVER, L('summaryanddetails'), false);
  819. }
  820. foreach ($proj->fields as $field) {
  821. if ($field->prefs['value_required'] && !array_get($args, 'field' . $field->id)
  822. && !($field->prefs['force_default'] && !$user->perms('modify_all_tasks'))) {
  823. return array(ERROR_RECOVER, L('missingrequired') . ' (' . $field->prefs['field_name'] . ')', false);
  824. }
  825. }
  826. if ($user->isAnon() && $fs->prefs['use_recaptcha']) {
  827. include_once BASEDIR . '/includes/external/recaptchalib.php';
  828. $solution =& new reCAPTCHA_Solution();
  829. $solution->privatekey = $fs->prefs['recaptcha_priv_key'];
  830. $solution->challenge = Post::val('recaptcha_challenge_field');
  831. $solution->response = Post::val('recaptcha_response_field');
  832. $solution->remoteip = $_SERVER['REMOTE_ADDR'];
  833. if(!$solution->isValid()) {
  834. return array(ERROR_RECOVER, $solution->error_code, false);
  835. }
  836. }
  837. $sql_values = array(time(), time(), $args['project_id'], $item_summary,
  838. $detailed_desc, intval($user->id), 0);
  839. $sql_params[] = 'mark_private';
  840. $sql_values[] = isset($args['mark_private']) && $args['mark_private'] == '1';
  841. $sql_params[] = 'closure_comment';
  842. $sql_values[] = '';
  843. $sql_params[] = 'syntax_plugins';
  844. $plugins = trim(implode(' ', array_get($args, 'detailed_desc_syntax_plugins', array())));
  845. if (!$plugins) {
  846. $plugins = $proj->prefs['syntax_plugins'];
  847. }
  848. $sql_values[] = $plugins;
  849. // Token for anonymous users
  850. $token = '';
  851. if ($user->isAnon()) {
  852. $token = md5(uniqid(mt_rand(), true));
  853. $sql_params[] = 'task_token';
  854. $sql_values[] = $token;
  855. }
  856. $sql_params[] = 'anon_email';
  857. $sql_values[] = array_get($args, 'anon_email', '');
  858. $sql_cols = array_merge(array('date_opened', 'last_edited_time', 'project_id',
  859. 'item_summary', 'detailed_desc', 'opened_by', 'percent_complete'), $sql_params);
  860. $db->x->autoExecute('{tasks}', array_combine($sql_cols, $sql_values));
  861. $task_id = $db->lastInsertID();
  862. // [RED] Add task to redundancy table (opened by, last_changed_time)
  863. $db->x->autoExecute('{redundant}', array('task_id'=> $task_id,
  864. 'last_changed_time' => time(),
  865. 'opened_by_real_name' => $user->infos['real_name'],
  866. 'opened_by_user_name' => $user->infos['user_name'],
  867. 'last_changed_by_real_name' => $user->infos['real_name'],
  868. 'last_changed_by_user_name' => $user->infos['user_name']));
  869. // Per project task ID
  870. $prefix_id = $db->x->GetOne('SELECT MAX(prefix_id)+1 FROM {tasks} WHERE project_id = ?', null, $proj->id);
  871. $db->x->execParam('UPDATE {tasks} SET prefix_id = ? WHERE task_id = ?', array($prefix_id, $task_id));
  872. // Now the custom fields
  873. if(count($proj->fields)) {
  874. $stmt = $db->x->autoPrepare('{field_values}', array('task_id', 'field_id', 'field_value'));
  875. foreach ($proj->fields as $field) {
  876. $stmt->execute(array($task_id, $field->id, $field->read(array_get($args, 'field' . $field->id, 0))));
  877. }
  878. $stmt->free();
  879. }
  880. $assignees = array();
  881. if(isset($args['assigned_to'])) {
  882. // Prepare assignee list
  883. $assignees = explode(';', trim($args['assigned_to']));
  884. $assignees = array_map(array('Flyspray', 'UserNameToId'), $assignees);
  885. $assignees = array_filter($assignees, create_function('$x', 'return ($x > 0);'));
  886. // Log the assignments and send notifications to the assignees
  887. if (count($assignees)) {
  888. // Convert assigned_to and store them in the 'assigned' table
  889. foreach ($assignees as $val) {
  890. $fields = array('user_id'=> array('value' => $val, 'key' => true),
  891. 'task_id'=> array('value' => $task_id, 'key' => true));
  892. $db->Replace('{assigned}', $fields);
  893. }
  894. Flyspray::logEvent($task_id, 14, implode(' ', $assignees));
  895. // Notify the new assignees what happened. This obviously won't happen if the task is now assigned to no-one.
  896. Notifications::send($assignees, ADDRESS_USER, NOTIFY_NEW_ASSIGNEE, array('task_id' => $task_id));
  897. }
  898. }
  899. // Log that the task was opened
  900. Flyspray::logEvent($task_id, 1);
  901. // find category owners
  902. $owners = array();
  903. foreach ($proj->fields as $field) {
  904. if ($field->prefs['list_type'] != LIST_CATEGORY) {
  905. continue;
  906. }
  907. $cat = $db->x->getRow('SELECT *
  908. FROM {list_category}
  909. WHERE category_id = ?', null,
  910. array_get($args, 'field' . $field->id, 0));
  911. if ($cat['category_owner']) {
  912. $owners[] = $cat['category_owner'];
  913. } else {
  914. // check parent categories
  915. $sql = $db->x->getAll('SELECT *
  916. FROM {list_category}
  917. WHERE lft < ? AND rgt > ? AND list_id = ?
  918. ORDER BY lft DESC', null,
  919. array($cat['lft'], $cat['rgt'], $cat['list_id']));
  920. foreach ($sql as $row) {
  921. // If there's a parent category owner, send to them
  922. if ($row['category_owner']) {
  923. $owners[] = $row['category_owner'];
  924. break;
  925. }
  926. }
  927. }
  928. }
  929. // last try...
  930. if (!count($owners) && $proj->prefs['default_cat_owner']) {
  931. $owners[] = $proj->prefs['default_cat_owner'];
  932. }
  933. if (count($owners)) {
  934. foreach ($owners as $owner) {
  935. if ($proj->prefs['auto_assign'] && !in_array($owner, $assignees)) {
  936. Backend::add_to_assignees($owner, $task_id, true);
  937. }
  938. Backend::add_notification($owner, $task_id, true);
  939. }
  940. }
  941. // Create the Notification
  942. if (Backend::upload_files($task_id)) {
  943. Notifications::send($task_id, ADDRESS_TASK, NOTIFY_TASK_OPENED, array('files' => true));
  944. } else {
  945. Notifications::send($task_id, ADDRESS_TASK, NOTIFY_TASK_OPENED);
  946. }
  947. // If the reporter wanted to be added to the notification list
  948. if (isset($args['notifyme']) && $args['notifyme'] == '1' && !in_array($user->id, $owners)) {
  949. Backend::add_notification($user->id, $task_id, true);
  950. }
  951. // this is relaxed, if the anonymous email is not valid, just dont bother..
  952. if ($user->isAnon() && Flyspray::check_email($args['anon_email'])) {
  953. Notifications::send($args['anon_email'], ADDRESS_EMAIL, NOTIFY_ANON_TASK, array('task_id' => $task_id, 'token' => $token));
  954. }
  955. return array($task_id, $token, true);
  956. }
  957. /**
  958. * Closes a task
  959. * @param integer $task_id
  960. * @param integer $reason
  961. * @param string $comment
  962. * @param bool $mark100
  963. * @access public
  964. * @return bool
  965. * @version 1.0
  966. */
  967. function close_task($task_id, $reason, $comment, $mark100 = true)
  968. {
  969. global $db, $user, $fs;
  970. $task = Flyspray::GetTaskDetails($task_id);
  971. if (!$user->can_close_task($task)) {
  972. return false;
  973. }
  974. if ($task['is_closed']) {
  975. return false;
  976. }
  977. $db->x->autoExecute('{tasks}', array('date_closed'=> time(),
  978. 'closed_by'=> $user->id,
  979. 'closure_comment'=> $comment,
  980. 'is_closed'=> 1,
  981. 'resolution_reason'=> $reason,
  982. 'last_edited_time'=> time(),
  983. 'last_edited_by'=> $user->id,
  984. 'percent_complete'=> ((bool) $mark100) * 100),
  985. MDB2_AUTOQUERY_UPDATE, sprintf('task_id = %d', $task_id));
  986. if ($mark100) {
  987. Flyspray::logEvent($task_id, 3, 100, $task['percent_complete'], 'percent_complete');
  988. }
  989. // [RED] Update last changed date
  990. $db->x->execParam('UPDATE {redundant} SET last_changed_time = ?,
  991. last_changed_by_real_name = ?, last_changed_by_user_name = ?,
  992. closed_by_real_name = ?, closed_by_user_name = ?
  993. WHERE task_id = ?',
  994. array(time(), $user->infos['real_name'], $user->infos['user_name'],
  995. $user->infos['real_name'], $user->infos['user_name'], $task_id));
  996. Notifications::send($task_id, ADDRESS_TASK, NOTIFY_TASK_CLOSED);
  997. Flyspray::logEvent($task_id, 2, $reason, $comment);
  998. // If there's an admin request related to this, close it
  999. $db->x->autoExecute('{admin_requests}', array('resolved_by'=> $user->id,
  1000. 'time_resolved'=> time()),
  1001. MDB2_AUTOQUERY_UPDATE, sprintf('task_id = %d AND request_type = 1', $task_id));
  1002. // duplicate
  1003. if ($reason == $fs->prefs['resolution_dupe']) {
  1004. $look = array('FS#', 'bug ');
  1005. foreach ($fs->projects as $project) {
  1006. $look[] = preg_quote($project['project_prefix'] . '#', '/');
  1007. }
  1008. preg_match("/\b(" . implode('|', $look) . ")(\d+)\b/", $comment, $dupe_of);
  1009. if (count($dupe_of) >= 2) {
  1010. $existing = $db->x->getOne('SELECT count(*) FROM {related} WHERE this_task = ? AND related_task = ? AND related_type = 1',
  1011. null, array($task_id, $dupe_of[1]));
  1012. if (!$existing) {
  1013. $db->x->autoExecute('{related}', array('this_task'=> $task_id, 'related_task'=> $dupe_of[1], 'related_type' => 1));
  1014. }
  1015. Backend::add_vote($task['opened_by'], $dupe_of[1]);
  1016. }
  1017. }
  1018. return true;
  1019. }
  1020. /**
  1021. * Returns an array of tasks (respecting pagination) and an ID list (all tasks)
  1022. * @param array $args call by reference because we have to modifiy $_GET if we use default values from a user profile
  1023. * @param array $visible
  1024. * @param integer $offset
  1025. * @param integer $comment
  1026. * @param bool $perpage
  1027. * @access public
  1028. * @return array
  1029. * @version 1.0
  1030. */
  1031. function get_task_list(&$args, $visible, $offset = 0, $perpage = null)
  1032. {
  1033. global $proj, $db, $user, $conf, $fs;
  1034. /* build SQL statement {{{ */
  1035. // Original SQL courtesy of Lance Conry http://www.rhinosw.com/
  1036. $where = $sql_params = array();
  1037. $select = '';
  1038. $groupby = 't.task_id, ';
  1039. $from = ' {tasks} t
  1040. LEFT JOIN {projects} p ON t.project_id = p.project_id
  1041. LEFT JOIN {list_items} lr ON t.resolution_reason = lr.list_item_id
  1042. LEFT JOIN {redundant} r ON t.task_id = r.task_id ';
  1043. // Only join tables which are really necessary to speed up the db-query
  1044. $from .= ' LEFT JOIN {assigned} ass ON t.task_id = ass.task_id ';
  1045. $from .= ' LEFT JOIN {users} u ON ass.user_id = u.user_id ';
  1046. if (array_get($args, 'dev') || in_array('assignedto', $visible)) {
  1047. $select .= ' MIN(u.real_name) AS assigned_to_name, ';
  1048. $select .= ' COUNT(ass.user_id) AS num_assigned, ';
  1049. }
  1050. if (array_get($args, 'only_primary')) {
  1051. $from .= ' LEFT JOIN {dependencies} dep ON dep.dep_task_id = t.task_id ';
  1052. $where[] = 'dep.depend_id IS null';
  1053. }
  1054. if (array_get($args, 'has_attachment')) {
  1055. $where[] = 'attachment_count > 0';
  1056. }
  1057. // sortable default fields
  1058. $order_keys = array (
  1059. 'id' => 't.task_id %s',
  1060. 'project' => 'project_title %s',
  1061. 'dateopened' => 'date_opened %s',
  1062. 'summary' => 'item_summary %s',
  1063. 'progress' => 'percent_complete %s',
  1064. 'lastedit' => 'la…

Large files files are truncated, but you can click here to view the full file