user.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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. "nl" => \ISO::LC_nl . " (Nederlands)",
  15. "de" => \ISO::LC_de . " (Deutsch)",
  16. "cs" => \ISO::LC_cs . " (Češka)",
  17. "zh" => \ISO::LC_zh . " (中国)",
  18. "ja" => \ISO::LC_ja . " (日本語)",
  19. );
  20. }
  21. /**
  22. * @param \Base $f3
  23. */
  24. public function index($f3) {
  25. $f3->reroute("/user");
  26. }
  27. /**
  28. * GET /user/dashboard User dashboard
  29. *
  30. * @param \Base $f3
  31. * @param array $params
  32. * @throws \Exception
  33. */
  34. public function dashboard($f3, $params) {
  35. $dashboard = $f3->get("user_obj")->option("dashboard");
  36. if(!$dashboard) {
  37. $dashboard = array(
  38. "left" => array("projects", "subprojects", "bugs", "repeat_work", "watchlist"),
  39. "right" => array("tasks")
  40. );
  41. }
  42. // Load dashboard widget data
  43. $allWidgets = array("projects", "subprojects", "tasks", "bugs", "repeat_work", "watchlist", "my_comments", "recent_comments");
  44. $helper = \Helper\Dashboard::instance();
  45. foreach($dashboard as $pos=>$widgets) {
  46. foreach($widgets as $widget) {
  47. if(is_callable(array($helper, $widget))) {
  48. $f3->set($widget, $helper->$widget());
  49. } else {
  50. $f3->set("error", "Widget '{$widget}' is not available.");
  51. }
  52. unset($allWidgets[array_search($widget, $allWidgets)]);
  53. }
  54. }
  55. $f3->set("unused_widgets", $allWidgets);
  56. // Get current sprint if there is one
  57. $sprint = new \Model\Sprint;
  58. $sprint->load(array("? BETWEEN start_date AND end_date", date("Y-m-d")));
  59. $f3->set("sprint", $sprint);
  60. $f3->set("dashboard", $dashboard);
  61. $f3->set("menuitem", "index");
  62. $this->_render("user/dashboard.html");
  63. }
  64. /**
  65. * POST /user/dashboard
  66. *
  67. * @param \Base $f3
  68. */
  69. public function dashboardPost($f3) {
  70. $user = $f3->get("user_obj");
  71. if($f3->get("POST.action") == "add") {
  72. $widgets = $user->option("dashboard");
  73. foreach($f3->get("POST.widgets") as $widget) {
  74. $widgets["left"][] = $widget;
  75. }
  76. } else {
  77. $widgets = json_decode($f3->get("POST.widgets"));
  78. }
  79. $user->option("dashboard", $widgets);
  80. $user->save();
  81. if($f3->get("AJAX")) {
  82. $this->_printJson($widgets);
  83. } else {
  84. $f3->reroute("/");
  85. }
  86. }
  87. private function _loadThemes() {
  88. $f3 = \Base::instance();
  89. // Get theme list
  90. $hidden_themes = array("backlog", "style", "taskboard", "datepicker", "jquery-ui-1.10.3", "bootstrap-tagsinput", "emote", "fontawesome");
  91. $themes = array();
  92. foreach (glob("css/*.css") as $file) {
  93. $name = pathinfo($file, PATHINFO_FILENAME);
  94. if(!in_array($name, $hidden_themes)) {
  95. $themes[] = $name;
  96. }
  97. }
  98. $f3->set("themes", $themes);
  99. return $themes;
  100. }
  101. /**
  102. * GET /user
  103. *
  104. * @param \Base $f3
  105. * @param array $params
  106. */
  107. public function account($f3, $params) {
  108. $f3->set("title", $f3->get("dict.my_account"));
  109. $f3->set("menuitem", "user");
  110. $f3->set("languages", $this->_languages);
  111. $this->_loadThemes();
  112. $this->_render("user/account.html");
  113. }
  114. /**
  115. * POST /user
  116. *
  117. * @param \Base $f3
  118. * @param array $params
  119. * @throws \Exception
  120. */
  121. public function save($f3, $params) {
  122. $f3 = \Base::instance();
  123. $post = array_map("trim", $f3->get("POST"));
  124. $user = new \Model\User();
  125. $user->load($this->_userId);
  126. if(!empty($post["old_pass"])) {
  127. $security = \Helper\Security::instance();
  128. // Update password
  129. if($security->hash($post["old_pass"], $user->salt) == $user->password) {
  130. $min = $f3->get("security.min_pass_len");
  131. if(strlen($post["new_pass"]) >= $min) {
  132. if($post["new_pass"] == $post["new_pass_confirm"]) {
  133. $user->salt = $security->salt();
  134. $user->password = $security->hash($post["new_pass"], $user->salt);
  135. $f3->set("success", "Password updated successfully.");
  136. } else {
  137. $f3->set("error", "New passwords do not match");
  138. }
  139. } else {
  140. $f3->set("error", "New password must be at least {$min} characters.");
  141. }
  142. } else {
  143. $f3->set("error", "Current password entered is not valid.");
  144. }
  145. } elseif(!empty($post["action"]) && $post["action"] == "options") {
  146. // Update option values
  147. $user->option("disable_mde", !empty($post["disable_mde"]));
  148. } else {
  149. // Update profile
  150. if(!empty($post["name"])) {
  151. $user->name = filter_var($post["name"], FILTER_SANITIZE_STRING);
  152. } else {
  153. $error = "Please enter your name.";
  154. }
  155. if(preg_match("/^([\p{L}\.\-\d]+)@([\p{L}\-\.\d]+)((\.(\p{L})+)+)$/im", $post["email"])) {
  156. $user->email = $post["email"];
  157. } else {
  158. $error = $post["email"] . " is not a valid email address.";
  159. }
  160. if(empty($error) && ctype_xdigit(ltrim($post["task_color"], "#"))) {
  161. $user->task_color = ltrim($post["task_color"], "#");
  162. } elseif(empty($error)) {
  163. $error = $post["task_color"] . " is not a valid color code.";
  164. }
  165. if(empty($post["theme"])) {
  166. $user->theme = null;
  167. } else {
  168. $user->theme = $post["theme"];
  169. }
  170. if(empty($post["language"])) {
  171. $user->language = null;
  172. } else {
  173. $user->language = $post["language"];
  174. }
  175. if(empty($error)) {
  176. $f3->set("success", "Profile updated successfully.");
  177. } else {
  178. $f3->set("error", $error);
  179. }
  180. }
  181. $user->save();
  182. $f3->set("title", $f3->get("dict.my_account"));
  183. $f3->set("menuitem", "user");
  184. // Use new user values for page
  185. $user->loadCurrent();
  186. $f3->set("languages", $this->_languages);
  187. $this->_loadThemes();
  188. $this->_render("user/account.html");
  189. }
  190. /**
  191. * POST /user/avatar
  192. *
  193. * @param \Base $f3
  194. * @param array $params
  195. * @throws \Exception
  196. */
  197. public function avatar($f3, $params) {
  198. $f3 = \Base::instance();
  199. $user = new \Model\User();
  200. $user->load($this->_userId);
  201. if(!$user->id) {
  202. $f3->error(404);
  203. return;
  204. }
  205. $web = \Web::instance();
  206. $f3->set("UPLOADS",'uploads/avatars/');
  207. if(!is_dir($f3->get("UPLOADS"))) {
  208. mkdir($f3->get("UPLOADS"), 0777, true);
  209. }
  210. $overwrite = true;
  211. $slug = true;
  212. //Make a good name
  213. $parts = pathinfo($_FILES['avatar']['name']);
  214. $_FILES['avatar']['name'] = $user->id . "-" . substr(sha1($user->id), 0, 4) . "." . $parts["extension"];
  215. $f3->set("avatar_filename", $_FILES['avatar']['name']);
  216. $web->receive(
  217. function($file) use($f3, $user) {
  218. if($file['size'] > $f3->get("files.maxsize")) {
  219. return false;
  220. }
  221. $user->avatar_filename = $f3->get("avatar_filename");
  222. $user->save();
  223. return true;
  224. },
  225. $overwrite,
  226. $slug
  227. );
  228. // Clear cached profile picture data
  229. $cache = \Cache::instance();
  230. $cache->clear($f3->hash("GET /avatar/48/{$user->id}.png") . ".url");
  231. $cache->clear($f3->hash("GET /avatar/96/{$user->id}.png") . ".url");
  232. $cache->clear($f3->hash("GET /avatar/128/{$user->id}.png") . ".url");
  233. $f3->reroute("/user");
  234. }
  235. /**
  236. * GET /user/@username
  237. *
  238. * @param \Base $f3
  239. * @param array $params
  240. * @throws \Exception
  241. */
  242. public function single($f3, $params) {
  243. $this->_requireLogin();
  244. $user = new \Model\User;
  245. $user->load(array("username = ? AND deleted_date IS NULL", $params["username"]));
  246. if($user->id) {
  247. $f3->set("title", $user->name);
  248. $f3->set("this_user", $user);
  249. // Extra arrays required for bulk update
  250. $status = new \Model\Issue\Status;
  251. $f3->set("statuses", $status->find(null, null, $f3->get("cache_expire.db")));
  252. $f3->set("users", $user->getAll());
  253. $f3->set("groups", $user->getAllGroups());
  254. $priority = new \Model\Issue\Priority;
  255. $f3->set("priorities", $priority->find(null, array("order" => "value DESC"), $f3->get("cache_expire.db")));
  256. $type = new \Model\Issue\Type;
  257. $f3->set("types", $type->find(null, null, $f3->get("cache_expire.db")));
  258. $issue = new \Model\Issue\Detail;
  259. $f3->set("created_issues", $issue->paginate(0, 200, array("status_closed = '0' AND deleted_date IS NULL AND author_id = ?", $user->id),
  260. array("order" => "priority DESC, due_date DESC")));
  261. $f3->set("assigned_issues", $issue->paginate(0, 200, array("status_closed = '0' AND deleted_date IS NULL AND owner_id = ?", $user->id),
  262. array("order" => "priority DESC, due_date DESC")));
  263. $f3->set("overdue_issues", $issue->paginate(0, 200, array("status_closed = '0' AND deleted_date IS NULL AND owner_id = ? AND due_date IS NOT NULL AND due_date < ?",
  264. $user->id, date("Y-m-d", \Helper\View::instance()->utc2local())), array("order" => "due_date ASC")));
  265. $this->_render("user/single.html");
  266. } else {
  267. $f3->error(404);
  268. }
  269. }
  270. /**
  271. * Convert a flat issue array to a tree array. Child issues are added to
  272. * the 'children' key in each issue.
  273. * @param array $array Flat array of issues, including all parents needed
  274. * @return array Tree array where each issue contains its child issues
  275. */
  276. protected function _buildTree($array) {
  277. $tree = array();
  278. // Create an associative array with each key being the ID of the item
  279. foreach($array as $k => &$v) {
  280. $tree[$v['id']] = &$v;
  281. }
  282. // Loop over the array and add each child to their parent
  283. foreach($tree as $k => &$v) {
  284. if(empty($v['parent_id'])) {
  285. continue;
  286. }
  287. $tree[$v['parent_id']]['children'][] = &$v;
  288. }
  289. // Loop over the array again and remove any items that don't have a parent of 0;
  290. foreach($tree as $k => &$v) {
  291. if(empty($v['parent_id'])) {
  292. continue;
  293. }
  294. unset($tree[$k]);
  295. }
  296. return $tree;
  297. }
  298. /**
  299. * GET /user/@username/tree
  300. *
  301. * @param \Base $f3
  302. * @param array $params
  303. * @throws \Exception
  304. */
  305. public function single_tree($f3, $params) {
  306. $this->_requireLogin();
  307. $user = new \Model\User;
  308. $user->load(array("username = ? AND deleted_date IS NULL", $params["username"]));
  309. if($user->id) {
  310. $f3->set("title", $user->name);
  311. $f3->set("this_user", $user);
  312. // Load assigned issues
  313. $issue = new \Model\Issue\Detail;
  314. $assigned = $issue->find(array("closed_date IS NULL AND deleted_date IS NULL AND owner_id = ?", $user->id));
  315. // Build issue list
  316. $issues = array();
  317. $assigned_ids = array();
  318. $missing_ids = array();
  319. foreach($assigned as $iss) {
  320. $issues[] = $iss->cast();
  321. $assigned_ids[] = $iss->id;
  322. }
  323. foreach($issues as $iss) {
  324. if($iss["parent_id"] && !in_array($iss["parent_id"], $assigned_ids)) {
  325. $missing_ids[] = $iss["parent_id"];
  326. }
  327. }
  328. while(!empty($missing_ids)) {
  329. $parents = $issue->find("id IN (" . implode(",", $missing_ids) . ")");
  330. foreach($parents as $iss) {
  331. if (($key = array_search($iss->id, $missing_ids)) !== false) {
  332. unset($missing_ids[$key]);
  333. }
  334. $issues[] = $iss->cast();
  335. $assigned_ids[] = $iss->id;
  336. if($iss->parent_id && !in_array($iss->parent_id, $assigned_ids)) {
  337. $missing_ids[] = $iss->parent_id;
  338. }
  339. }
  340. }
  341. // Convert list to tree
  342. $tree = $this->_buildTree($issues);
  343. /**
  344. * Helper function for recursive tree rendering
  345. * @param array $issue
  346. * @var callable $renderTree This function, required for recursive calls
  347. */
  348. $renderTree = function(&$issue, $level = 0) use(&$renderTree) {
  349. if(!empty($issue['id'])) {
  350. $f3 = \Base::instance();
  351. $hive = array("issue" => $issue, "dict" => $f3->get("dict"), "BASE" => $f3->get("BASE"), "level" => $level, "issue_type" => $f3->get("issue_type"));
  352. echo \Helper\View::instance()->render("issues/project/tree-item.html", "text/html", $hive);
  353. if(!empty($issue['children'])) {
  354. foreach($issue['children'] as $item) {
  355. $renderTree($item, $level + 1);
  356. }
  357. }
  358. }
  359. };
  360. $f3->set("renderTree", $renderTree);
  361. // Render view
  362. $f3->set("issues", $tree);
  363. $this->_render($f3->get("AJAX") ? "user/single/tree/ajax.html" : "user/single/tree.html");
  364. } else {
  365. $f3->error(404);
  366. }
  367. }
  368. }