user.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <?php
  2. namespace Controller;
  3. class User extends \Controller {
  4. protected $_userId;
  5. private $_languages;
  6. public function __construct() {
  7. $this->_userId = $this->_requireLogin();
  8. $this->_languages = array(
  9. "en" => \ISO::LC_en,
  10. "en_GB" => \ISO::LC_en . " (Great Britain)",
  11. "es" => \ISO::LC_es . " (Español)",
  12. "pt" => \ISO::LC_pt . " (Português)",
  13. "ru" => \ISO::LC_ru . " (Pу́сский Язы́к)",
  14. );
  15. }
  16. public function index($f3) {
  17. $f3->reroute("/user");
  18. }
  19. public function dashboard($f3, $params) {
  20. $issue = new \Model\Issue\Detail();
  21. // Add user's group IDs to owner filter
  22. $owner_ids = array($this->_userId);
  23. $groups = new \Model\User\Group();
  24. foreach($groups->find(array("user_id = ?", $this->_userId)) as $r) {
  25. $owner_ids[] = $r->group_id;
  26. }
  27. $owner_ids = implode(",", $owner_ids);
  28. $order = "priority DESC, has_due_date ASC, due_date ASC";
  29. $projects = $issue->find(
  30. array(
  31. "owner_id IN ($owner_ids) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
  32. ":type" => $f3->get("issue_type.project"),
  33. ),array(
  34. "order" => $order
  35. )
  36. );
  37. $subprojects = array();
  38. foreach($projects as $i=>$project) {
  39. if($project->parent_id) {
  40. $subprojects[] = $project;
  41. unset($projects[$i]);
  42. }
  43. }
  44. $f3->set("projects", $projects);
  45. $f3->set("subprojects", $subprojects);
  46. $f3->set("bugs", $issue->find(
  47. array(
  48. "owner_id IN ($owner_ids) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
  49. ":type" => $f3->get("issue_type.bug"),
  50. ),array(
  51. "order" => $order
  52. )
  53. ));
  54. $f3->set("repeat_issues", $issue->find(
  55. array(
  56. "owner_id IN ($owner_ids) AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0 AND repeat_cycle NOT IN ('none', '')",
  57. ":type" => $f3->get("issue_type.bug"),
  58. ),array(
  59. "order" => $order
  60. )
  61. ));
  62. $watchlist = new \Model\Issue\Watcher();
  63. $f3->set("watchlist", $watchlist->findby_watcher($this->_userId, $order));
  64. $tasks = new \Model\Issue\Detail();
  65. $f3->set("tasks", $tasks->find(
  66. array(
  67. "owner_id IN ($owner_ids) AND type_id=:type AND deleted_date IS NULL AND closed_date IS NULL AND status_closed = 0",
  68. ":type" => $f3->get("issue_type.task"),
  69. ),array(
  70. "order" => $order
  71. )
  72. ));
  73. // Get current sprint if there is one
  74. $sprint = new \Model\Sprint;
  75. $sprint->load("NOW() BETWEEN start_date AND end_date");
  76. $f3->set("sprint", $sprint);
  77. $f3->set("menuitem", "index");
  78. $this->_render("user/dashboard.html");
  79. }
  80. private function _loadThemes() {
  81. $f3 = \Base::instance();
  82. // Get theme list
  83. $hidden_themes = array("backlog", "style", "taskboard", "datepicker", "jquery-ui-1.10.3", "bootstrap-tagsinput", "emote");
  84. $themes = array();
  85. foreach (glob("css/*.css") as $file) {
  86. $name = pathinfo($file, PATHINFO_FILENAME);
  87. if(!in_array($name, $hidden_themes)) {
  88. $themes[] = $name;
  89. }
  90. }
  91. $f3->set("themes", $themes);
  92. return $themes;
  93. }
  94. public function account($f3, $params) {
  95. $f3->set("title", "My Account");
  96. $f3->set("menuitem", "user");
  97. $f3->set("languages", $this->_languages);
  98. $this->_loadThemes();
  99. $this->_render("user/account.html");
  100. }
  101. public function save($f3, $params) {
  102. $f3 = \Base::instance();
  103. $post = array_map("trim", $f3->get("POST"));
  104. $user = new \Model\User();
  105. $user->load($this->_userId);
  106. if(!empty($post["old_pass"])) {
  107. $security = \Helper\Security::instance();
  108. // Update password
  109. if($security->hash($post["old_pass"], $user->salt) == $user->password) {
  110. if(strlen($post["new_pass"]) >= 6) {
  111. if($post["new_pass"] == $post["new_pass_confirm"]) {
  112. $user->salt = $security->salt();
  113. $user->password = $security->hash($post["new_pass"], $user->salt);
  114. $f3->set("success", "Password updated successfully.");
  115. } else {
  116. $f3->set("error", "New passwords do not match");
  117. }
  118. } else {
  119. $f3->set("error", "New password must be at least 6 characters.");
  120. }
  121. } else {
  122. $f3->set("error", "Current password entered is not valid.");
  123. }
  124. } else {
  125. // Update profile
  126. if(!empty($post["name"])) {
  127. $user->name = filter_var($post["name"], FILTER_SANITIZE_STRING);
  128. } else {
  129. $error = "Please enter your name.";
  130. }
  131. if(preg_match("/^([\p{L}\.\-\d]+)@([\p{L}\-\.\d]+)((\.(\p{L})+)+)$/im", $post["email"])) {
  132. $user->email = $post["email"];
  133. } else {
  134. $error = $post["email"] . " is not a valid email address.";
  135. }
  136. if(empty($error) && ctype_xdigit(ltrim($post["task_color"], "#"))) {
  137. $user->task_color = ltrim($post["task_color"], "#");
  138. } elseif(empty($error)) {
  139. $error = $post["task_color"] . " is not a valid color code.";
  140. }
  141. if(empty($post["theme"])) {
  142. $user->theme = null;
  143. } else {
  144. $user->theme = $post["theme"];
  145. }
  146. if(empty($post["language"])) {
  147. $user->language = null;
  148. } else {
  149. $user->language = $post["language"];
  150. }
  151. if(empty($error)) {
  152. $f3->set("success", "Profile updated successfully.");
  153. } else {
  154. $f3->set("error", $error);
  155. }
  156. }
  157. $user->save();
  158. $f3->set("title", "My Account");
  159. $f3->set("menuitem", "user");
  160. // Use new user values for page
  161. $user->loadCurrent();
  162. $f3->set("languages", $this->_languages);
  163. $this->_loadThemes();
  164. $this->_render("user/account.html");
  165. }
  166. public function avatar($f3, $params) {
  167. $f3 = \Base::instance();
  168. $user = new \Model\User();
  169. $user->load($this->_userId);
  170. if(!$user->id) {
  171. $f3->error(404);
  172. return;
  173. }
  174. $web = \Web::instance();
  175. $f3->set("UPLOADS",'uploads/avatars/');
  176. if(!is_dir($f3->get("UPLOADS"))) {
  177. mkdir($f3->get("UPLOADS"), 0777, true);
  178. }
  179. $overwrite = true;
  180. $slug = true;
  181. //Make a good name
  182. $parts = pathinfo($_FILES['avatar']['name']);
  183. $_FILES['avatar']['name'] = $user->id . "-" . substr(sha1($user->id), 0, 4) . "." . $parts["extension"];
  184. $f3->set("avatar_filename", $_FILES['avatar']['name']);
  185. $web->receive(
  186. function($file) use($f3, $user) {
  187. if($file['size'] > $f3->get("files.maxsize")) {
  188. return false;
  189. }
  190. $user->avatar_filename = $f3->get("avatar_filename");
  191. $user->save();
  192. return true;
  193. },
  194. $overwrite,
  195. $slug
  196. );
  197. // Clear cached profile picture data
  198. $cache = \Cache::instance();
  199. $cache->clear($f3->hash("GET /avatar/48/{$user->id}.png") . ".url");
  200. $cache->clear($f3->hash("GET /avatar/96/{$user->id}.png") . ".url");
  201. $cache->clear($f3->hash("GET /avatar/128/{$user->id}.png") . ".url");
  202. $f3->reroute("/user");
  203. }
  204. public function single($f3, $params) {
  205. $this->_requireLogin();
  206. $user = new \Model\User;
  207. $user->load(array("username = ? AND deleted_date IS NULL", $params["username"]));
  208. if($user->id) {
  209. $f3->set("title", $user->name);
  210. $f3->set("this_user", $user);
  211. // Extra arrays required for bulk update
  212. $status = new \Model\Issue\Status;
  213. $f3->set("statuses", $status->find(null, null, $f3->get("cache_expire.db")));
  214. $f3->set("users", $user->getAll());
  215. $f3->set("groups", $user->getAllGroups());
  216. $priority = new \Model\Issue\Priority;
  217. $f3->set("priorities", $priority->find(null, array("order" => "value DESC"), $f3->get("cache_expire.db")));
  218. $sprint = new \Model\Sprint;
  219. $f3->set("sprints", $sprint->find(array("end_date >= ?", $this->now(false)), array("order" => "start_date ASC")));
  220. $type = new \Model\Issue\Type;
  221. $f3->set("types", $type->find(null, null, $f3->get("cache_expire.db")));
  222. $issue = new \Model\Issue\Detail;
  223. $issues = $issue->paginate(0, 100, array("status_closed = '0' AND deleted_date IS NULL AND (owner_id = ? OR author_id = ?)", $user->id, $user->id));
  224. $f3->set("issues", $issues);
  225. // Get current sprint if there is one
  226. $sprint = new \Model\Sprint;
  227. $sprint->load("NOW() BETWEEN start_date AND end_date");
  228. $f3->set("sprint", $sprint);
  229. $this->_render("user/single.html");
  230. } else {
  231. $f3->error(404);
  232. }
  233. }
  234. /**
  235. * Convert a flat issue array to a tree array. Child issues are added to
  236. * the 'children' key in each issue.
  237. * @param array $array Flat array of issues, including all parents needed
  238. * @return array Tree array where each issue contains its child issues
  239. */
  240. protected function _buildTree($array) {
  241. $tree = array();
  242. // Create an associative array with each key being the ID of the item
  243. foreach($array as $k => &$v) {
  244. $tree[$v['id']] = &$v;
  245. }
  246. // Loop over the array and add each child to their parent
  247. foreach($tree as $k => &$v) {
  248. if(empty($v['parent_id'])) {
  249. continue;
  250. }
  251. $tree[$v['parent_id']]['children'][] = &$v;
  252. }
  253. // Loop over the array again and remove any items that don't have a parent of 0;
  254. foreach($tree as $k => &$v) {
  255. if(empty($v['parent_id'])) {
  256. continue;
  257. }
  258. unset($tree[$k]);
  259. }
  260. return $tree;
  261. }
  262. public function single_tree($f3, $params) {
  263. $this->_requireLogin();
  264. $user = new \Model\User;
  265. $user->load(array("username = ? AND deleted_date IS NULL", $params["username"]));
  266. if($user->id) {
  267. $f3->set("title", $user->name);
  268. $f3->set("this_user", $user);
  269. // Load assigned issues
  270. $issue = new \Model\Issue\Detail;
  271. $assigned = $issue->find(array("closed_date IS NULL AND deleted_date IS NULL AND owner_id = ?", $user->id));
  272. // Build issue list
  273. $issues = array();
  274. $assigned_ids = array();
  275. $missing_ids = array();
  276. foreach($assigned as $iss) {
  277. $issues[] = $iss->cast();
  278. $assigned_ids[] = $iss->id;
  279. }
  280. foreach($issues as $iss) {
  281. if($iss["parent_id"] && !in_array($iss["parent_id"], $assigned_ids)) {
  282. $missing_ids[] = $iss["parent_id"];
  283. }
  284. }
  285. while(!empty($missing_ids)) {
  286. $parents = $issue->find("id IN (" . implode(",", $missing_ids) . ")");
  287. foreach($parents as $iss) {
  288. if (($key = array_search($iss->id, $missing_ids)) !== false) {
  289. unset($missing_ids[$key]);
  290. }
  291. $issues[] = $iss->cast();
  292. $assigned_ids[] = $iss->id;
  293. if($iss->parent_id && !in_array($iss->parent_id, $assigned_ids)) {
  294. $missing_ids[] = $iss->parent_id;
  295. }
  296. }
  297. }
  298. // Convert list to tree
  299. $tree = $this->_buildTree($issues);
  300. // Helper function for recursive tree rendering
  301. $recurDisplay = function($issue) use(&$recurDisplay) {
  302. $url = \Base::instance()->get("site.url");
  303. echo "<li>";
  304. if(!empty($issue["id"])) {
  305. echo '<a href="'.$url.'issues/'.$issue['id'].'">#'.$issue["id"].' - '.$issue["name"].'</a> ';
  306. if($issue["author_name"]) {
  307. echo '<small class="text-muted">&ndash; '.$issue["author_name"].'</small>';
  308. }
  309. }
  310. if(!empty($issue["children"])) {
  311. echo "<ul>";
  312. foreach($issue["children"] as $iss) {
  313. $recurDisplay($iss);
  314. }
  315. echo "</ul>";
  316. }
  317. echo "</li>";
  318. };
  319. $f3->set("recurDisplay", $recurDisplay);
  320. // Render view
  321. $f3->set("issues", $tree);
  322. $this->_render("user/single/tree.html");
  323. } else {
  324. $f3->error(404);
  325. }
  326. }
  327. public function single_overdue($f3, $params) {
  328. $this->_requireLogin();
  329. $user = new \Model\User;
  330. $user->load(array("username = ? AND deleted_date IS NULL", $params["username"]));
  331. if($user->id) {
  332. $f3->set("title", $user->name);
  333. $f3->set("this_user", $user);
  334. $issue = new \Model\Issue\Detail;
  335. $view = \Helper\View::instance();
  336. $issues = $issue->find(array("owner_id = ? AND status_closed = 0 AND deleted_date IS NULL AND due_date IS NOT NULL AND due_date < ?", $user->id, date("Y-m-d", $view->utc2local())), array("order" => "due_date ASC"));
  337. $f3->set("issues.subset", $issues);
  338. $this->_render("user/single/overdue.html");
  339. } else {
  340. $f3->error(404);
  341. }
  342. }
  343. }