From 490b64c2b3bc96bef5183344e80341b5214e1269 Mon Sep 17 00:00:00 2001 From: maxpozdeev Date: Sun, 10 Jul 2022 15:23:53 +0300 Subject: [PATCH] * change HTTP API --- src/.htaccess | 12 + src/ajax.php | 970 ------------------------ src/api.php | 227 ++++++ src/content/themes/default/index.php | 4 +- src/includes/api/AuthController.php | 63 ++ src/includes/api/ListsController.php | 293 +++++++ src/includes/api/TagsController.php | 83 ++ src/includes/api/TasksController.php | 662 ++++++++++++++++ src/includes/common.php | 5 +- src/includes/mytinytodo.js | 25 +- src/includes/mytinytodo_ajax_storage.js | 179 ----- src/includes/mytinytodo_api.js | 385 ++++++++++ src/index.php | 1 + src/init.php | 11 +- 14 files changed, 1758 insertions(+), 1162 deletions(-) create mode 100644 src/.htaccess delete mode 100644 src/ajax.php create mode 100644 src/api.php create mode 100644 src/includes/api/AuthController.php create mode 100644 src/includes/api/ListsController.php create mode 100644 src/includes/api/TagsController.php create mode 100644 src/includes/api/TasksController.php delete mode 100644 src/includes/mytinytodo_ajax_storage.js create mode 100644 src/includes/mytinytodo_api.js diff --git a/src/.htaccess b/src/.htaccess new file mode 100644 index 0000000..194f78e --- /dev/null +++ b/src/.htaccess @@ -0,0 +1,12 @@ +# For Apache +# +# RewriteEngine On +# RewriteCond %{REQUEST_FILENAME} !-f +# RewriteCond %{REQUEST_FILENAME} !-d +# RewriteRule ^api/(.*)$ api.php/$1 [L,QSA] +# + +# For Nginx set something like this: +# location /api/ { +# rewrite ^/api/(.*) /api.php/$1 last; +# } diff --git a/src/ajax.php b/src/ajax.php deleted file mode 100644 index 14729d5..0000000 --- a/src/ajax.php +++ /dev/null @@ -1,970 +0,0 @@ - - Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. -*/ - -require_once('./init.php'); -require_once(MTTINC. 'markup.php'); - -if (MTT_DEBUG) { - set_error_handler('myErrorHandler'); //catch Notices, Warnings - set_exception_handler('myExceptionHandler'); -} -else { - ini_set('display_errors', '0'); -} - -$db = DBConnection::instance(); - -if(isset($_GET['loadLists'])) -{ - check_token(); - $t = array(); - $t['total'] = 0; - if (!is_logged()) $sqlWhere = 'WHERE published=1'; - else { - $sqlWhere = ''; - $t['list'][] = prepareAllTasksList(); // show alltasks lists only for authorized user - $t['total'] = 1; - } - $q = $db->dq("SELECT * FROM {$db->prefix}lists $sqlWhere ORDER BY ow ASC, id ASC"); - while($r = $q->fetchAssoc()) - { - $t['total']++; - $t['list'][] = prepareList($r); - } - jsonExit($t); -} -elseif(isset($_GET['loadTasks'])) -{ - $listId = (int)_get('list'); - check_read_access($listId); - - $sqlWhere = $inner = $sqlWhereListId = $sqlInnerWhereListId = ''; - if ($listId == -1) { - $userLists = getUserListsSimple(); - $sqlWhereListId = "{$db->prefix}todolist.list_id IN (". implode(',', array_keys($userLists)). ") "; - $sqlInnerWhereListId = "list_id IN (". implode(',', array_keys($userLists)). ") "; - } - else { - $sqlWhereListId = "{$db->prefix}todolist.list_id=". $listId; - $sqlInnerWhereListId = "list_id=$listId "; - } - if (_get('compl') == 0) { - $sqlWhere .= ' AND compl=0'; - } - - $tag = trim(_get('t')); - if($tag != '') - { - $at = explode(',', $tag); - $tagIds = array(); - $tagExIds = array(); - foreach($at as $i=>$atv) { - $atv = trim($atv); - if($atv == '' || $atv == '^') continue; - if(substr($atv,0,1) == '^') { - $tagExIds[] = getTagId(substr($atv,1)); - } else { - $tagIds[] = getTagId($atv); - } - } - - // Include tags: All - if (sizeof($tagIds) > 1) { - $inner .= "INNER JOIN (SELECT task_id, COUNT(tag_id) AS c FROM {$db->prefix}tag2task WHERE $sqlInnerWhereListId AND tag_id IN (". - implode(',',$tagIds). ") GROUP BY task_id) AS t2t ON id=t2t.task_id"; - $sqlWhere .= " AND c=". sizeof($tagIds); - } - elseif ($tagIds) { - $inner .= "INNER JOIN {$db->prefix}tag2task ON id=task_id"; - $sqlWhere .= " AND tag_id = {$tagIds[0]}"; - } - - // Exclude tags - if (sizeof($tagExIds) > 0) { - $sqlWhere .= " AND {$db->prefix}todolist.id NOT IN (SELECT DISTINCT task_id FROM {$db->prefix}tag2task WHERE $sqlInnerWhereListId AND tag_id IN (". - implode(',',$tagExIds). "))"; - } - //no optimization for single exTag - } - - $s = trim(_get('s')); - if ($s != '') { - if (preg_match("|^#(\d+)$|", $s, $m)) $sqlWhere .= " AND {$db->prefix}todolist.id = ". (int)$m[1]; - else $sqlWhere .= " AND (title LIKE ". $db->quoteForLike("%%%s%%",$s). " OR note LIKE ". $db->quoteForLike("%%%s%%",$s). ")"; - } - - $sort = (int)_get('sort'); - $sqlSort = "ORDER BY compl ASC, "; - if($sort == 1) $sqlSort .= "prio DESC, ddn ASC, duedate ASC, ow ASC"; // byPrio - elseif($sort == 101) $sqlSort .= "prio ASC, ddn DESC, duedate DESC, ow DESC"; // byPrio (reverse) - elseif($sort == 2) $sqlSort .= "ddn ASC, duedate ASC, prio DESC, ow ASC"; // byDueDate - elseif($sort == 102) $sqlSort .= "ddn DESC, duedate DESC, prio ASC, ow DESC";// byDueDate (reverse) - elseif($sort == 3) $sqlSort .= "d_created ASC, prio DESC, ow ASC"; // byDateCreated - elseif($sort == 103) $sqlSort .= "d_created DESC, prio ASC, ow DESC"; // byDateCreated (reverse) - elseif($sort == 4) $sqlSort .= "d_edited ASC, prio DESC, ow ASC"; // byDateModified - elseif($sort == 104) $sqlSort .= "d_edited DESC, prio ASC, ow DESC"; // byDateModified (reverse) - else $sqlSort .= "ow ASC"; - - $t = array(); - $t['total'] = 0; - $t['list'] = array(); - $q = $db->dq("SELECT *, duedate IS NULL AS ddn FROM {$db->prefix}todolist $inner WHERE $sqlWhereListId $sqlWhere $sqlSort"); - while($r = $q->fetchAssoc()) - { - $t['total']++; - $t['list'][] = prepareTaskRow($r); - } - if(_get('setCompl') && have_write_access($listId)) { - $bitwise = (_get('compl') == 0) ? 'taskview & ~1' : 'taskview | 1'; - $db->dq("UPDATE {$db->prefix}lists SET taskview=$bitwise WHERE id=$listId"); - } - jsonExit($t); -} -elseif(isset($_GET['newTask'])) -{ - $listId = (int)_post('list'); - check_write_access($listId); - $t = array(); - $t['total'] = 0; - $title = trim(_post('title')); - $prio = 0; - $tags = ''; - if(Config::get('smartsyntax') != 0) - { - $a = parse_smartsyntax($title); - if($a === false) { - jsonExit($t); - } - $title = $a['title']; - $prio = $a['prio']; - $tags = $a['tags']; - } - if($title == '') { - jsonExit($t); - } - if(Config::get('autotag')) $tags .= ','._post('tag'); - $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=0"); - $db->ex("BEGIN"); - $db->dq("INSERT INTO {$db->prefix}todolist (uuid,list_id,title,d_created,d_edited,ow,prio) VALUES (?,?,?,?,?,?,?)", - array(generateUUID(), $listId, $title, time(), time(), $ow, $prio) ); - $id = $db->lastInsertId(); - if($tags != '') - { - $aTags = prepareTags($tags); - if($aTags) { - addTaskTags($id, $aTags['ids'], $listId); - $db->ex("UPDATE {$db->prefix}todolist SET tags=?,tags_ids=? WHERE id=$id", array(implode(',',$aTags['tags']), implode(',',$aTags['ids']))); - } - } - $db->ex("COMMIT"); - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - $t['list'][] = prepareTaskRow($r); - $t['total'] = 1; - jsonExit($t); -} -elseif(isset($_GET['fullNewTask'])) -{ - $listId = (int)_post('list'); - check_write_access($listId); - $title = trim(_post('title')); - $note = str_replace("\r\n", "\n", _post('note')); - $prio = (int)_post('prio'); - if($prio < -1) $prio = -1; - elseif($prio > 2) $prio = 2; - $duedate = parse_duedate(trim(_post('duedate'))); - $t = array(); - $t['total'] = 0; - if($title == '') { - jsonExit($t); - } - $tags = trim(_post('tags')); - if(Config::get('autotag')) $tags .= ','._post('tag'); - $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=0"); - $db->ex("BEGIN"); - $db->dq("INSERT INTO {$db->prefix}todolist (uuid,list_id,title,d_created,d_edited,ow,prio,note,duedate) VALUES(?,?,?,?,?,?,?,?,?)", - array(generateUUID(), $listId, $title, time(), time(), $ow, $prio, $note, $duedate) ); - $id = $db->lastInsertId(); - if($tags != '') - { - $aTags = prepareTags($tags); - if($aTags) { - addTaskTags($id, $aTags['ids'], $listId); - $db->ex("UPDATE {$db->prefix}todolist SET tags=?,tags_ids=? WHERE id=$id", array(implode(',',$aTags['tags']), implode(',',$aTags['ids']))); - } - } - $db->ex("COMMIT"); - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - $t['list'][] = prepareTaskRow($r); - $t['total'] = 1; - jsonExit($t); -} -elseif(isset($_GET['deleteTask'])) -{ - $id = (int)_post('id'); - $deleted = deleteTask($id); - $t = array(); - $t['total'] = $deleted; - $t['list'][] = array('id'=>$id); - jsonExit($t); -} -elseif(isset($_GET['completeTask'])) -{ - check_write_access(); - $id = (int)_post('id'); - $compl = _post('compl') ? 1 : 0; - $listId = (int)$db->sq("SELECT list_id FROM {$db->prefix}todolist WHERE id=$id"); - if($compl) $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=1"); - else $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=0"); - $dateCompleted = $compl ? time() : 0; - $db->dq("UPDATE {$db->prefix}todolist SET compl=$compl,ow=$ow,d_completed=?,d_edited=? WHERE id=$id", - array($dateCompleted, time()) ); - $t = array(); - $t['total'] = 1; - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - $t['list'][] = prepareTaskRow($r); - jsonExit($t); -} -elseif(isset($_GET['editNote'])) -{ - check_write_access(); - $id = (int)_post('id'); - $note = str_replace("\r\n", "\n", _post('note')); - $db->dq("UPDATE {$db->prefix}todolist SET note=?,d_edited=? WHERE id=$id", array($note, time()) ); - $t = array(); - $t['total'] = 1; - $t['list'][] = array('id'=>$id, 'note'=> noteMarkup($note), 'noteText'=>(string)$note); - jsonExit($t); -} -elseif(isset($_GET['editTask'])) -{ - check_write_access(); - $id = (int)_post('id'); - $title = trim(_post('title')); - $note = str_replace("\r\n", "\n", _post('note')); - $prio = (int)_post('prio'); - if($prio < -1) $prio = -1; - elseif($prio > 2) $prio = 2; - $duedate = parse_duedate(trim(_post('duedate'))); - $t = array(); - $t['total'] = 0; - if($title == '') { - jsonExit($t); - } - $listId = $db->sq("SELECT list_id FROM {$db->prefix}todolist WHERE id=$id"); - $tags = trim(_post('tags')); - $db->ex("BEGIN"); - $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id=$id"); - $aTags = prepareTags($tags); - if($aTags) { - $tags = implode(',', $aTags['tags']); - $tags_ids = implode(',',$aTags['ids']); - addTaskTags($id, $aTags['ids'], $listId); - } - $db->dq("UPDATE {$db->prefix}todolist SET title=?,note=?,prio=?,tags=?,tags_ids=?,duedate=?,d_edited=? WHERE id=$id", - array($title, $note, $prio, $tags, $tags_ids, $duedate, time()) ); - $db->ex("COMMIT"); - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); - if($r) { - $t['list'][] = prepareTaskRow($r); - $t['total'] = 1; - } - jsonExit($t); -} -elseif(isset($_GET['changeOrder'])) -{ - check_write_access(); - $s = _post('order'); - parse_str($s, $order); - $t = array(); - $t['total'] = 0; - if($order) - { - $ad = array(); - foreach($order as $id=>$diff) { - $ad[(int)$diff][] = (int)$id; - } - $db->ex("BEGIN"); - foreach($ad as $diff=>$ids) { - if($diff >=0) $set = "ow=ow+".$diff; - else $set = "ow=ow-".abs($diff); - $db->dq("UPDATE {$db->prefix}todolist SET $set,d_edited=? WHERE id IN (".implode(',',$ids).")", array(time()) ); - } - $db->ex("COMMIT"); - $t['total'] = 1; - } - jsonExit($t); -} -elseif(isset($_POST['login'])) -{ - check_token(); - $t = array('logged' => 0); - if (!need_auth()) { - $t['disabled'] = 1; - jsonExit($t); - } - if ( isPasswordEqualsToHash(_post('password'), Config::get('password')) ) { - updateSessionLogged(true); - $t['token'] = update_token(); - $t['logged'] = 1; - } - jsonExit($t); -} -elseif(isset($_POST['logout'])) -{ - check_token(); - updateSessionLogged(false); - update_token(); - session_regenerate_id(1); - $t = array('logged' => 0); - jsonExit($t); -} -elseif(isset($_GET['suggestTags'])) -{ - $listId = (int)_get('list'); - check_read_access($listId); - $begin = trim(_get('q')); - $limit = 8; - $q = $db->dq("SELECT name,id FROM {$db->prefix}tags INNER JOIN {$db->prefix}tag2task ON id=tag_id WHERE list_id=$listId AND name LIKE ". - $db->quoteForLike('%s%%',$begin) ." GROUP BY tag_id ORDER BY name LIMIT $limit"); - $t = array(); - while($r = $q->fetchRow()) { - $t[] = $r[0]; - } - jsonExit($t); -} -elseif(isset($_GET['setTaskPriority'])) -{ - check_write_access(); - $id = (int)_post('id'); - $prio = (int)_post('priority'); - if($prio < -1) $prio = -1; - elseif($prio > 2) $prio = 2; - $db->ex("UPDATE {$db->prefix}todolist SET prio=$prio,d_edited=? WHERE id=$id", array(time()) ); - $t = array(); - $t['total'] = 1; - $t['list'][] = array('id'=>$id, 'prio'=>$prio); - jsonExit($t); -} -elseif(isset($_GET['tagCloud'])) -{ - $listId = (int)_get('list'); - check_read_access($listId); - - $q = $db->dq("SELECT name,tag_id,COUNT(tag_id) AS tags_count FROM {$db->prefix}tag2task INNER JOIN {$db->prefix}tags ON tag_id=id ". - "WHERE list_id=$listId GROUP BY (tag_id) ORDER BY tags_count ASC"); - $at = array(); - $ac = array(); - while($r = $q->fetchAssoc()) { - $at[] = array('name'=>$r['name'], 'id'=>$r['tag_id']); - $ac[] = $r['tags_count']; - } - - $t = array(); - $t['total'] = 0; - $count = sizeof($at); - if(!$count) { - jsonExit($t); - } - - $qmax = max($ac); - $qmin = min($ac); - if($count >= 10) $grades = 10; - else $grades = $count; - $step = ($qmax - $qmin)/$grades; - foreach($at as $i=>$tag) - { - $t['cloud'][] = array('tag'=>htmlarray($tag['name']), 'id'=>(int)$tag['id'], 'w'=> tag_size($qmin,$ac[$i],$step) ); - } - $t['total'] = $count; - jsonExit($t); -} -elseif(isset($_GET['addList'])) -{ - check_write_access(); - $t = array(); - $t['total'] = 0; - $name = str_replace( array('"',"'",'<','>','&'), '', trim(_post('name')) ); - $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}lists"); - $db->dq("INSERT INTO {$db->prefix}lists (uuid,name,ow,d_created,d_edited,taskview) VALUES (?,?,?,?,?,?)", - array(generateUUID(), $name, $ow, time(), time(), 1) ); - $id = $db->lastInsertId(); - $t['total'] = 1; - $r = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=$id"); - $t['list'][] = prepareList($r); - jsonExit($t); -} -elseif(isset($_GET['renameList'])) -{ - check_write_access(); - $t = array(); - $t['total'] = 0; - $id = (int)_post('list'); - $name = str_replace(array('"',"'",'<','>','&'),array('','','','',''),trim(_post('name'))); - $db->dq("UPDATE {$db->prefix}lists SET name=?,d_edited=? WHERE id=$id", array($name, time()) ); - $t['total'] = $db->affected(); - $r = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=$id"); - $t['list'][] = prepareList($r); - jsonExit($t); -} -elseif(isset($_GET['deleteList'])) -{ - check_write_access(); - $t = array(); - $t['total'] = 0; - $id = (int)_post('list'); - $db->ex("BEGIN"); - $db->ex("DELETE FROM {$db->prefix}lists WHERE id=$id"); - $t['total'] = $db->affected(); - if($t['total']) { - $db->ex("DELETE FROM {$db->prefix}tag2task WHERE list_id=$id"); - $db->ex("DELETE FROM {$db->prefix}todolist WHERE list_id=$id"); - } - $db->ex("COMMIT"); - jsonExit($t); -} -elseif(isset($_GET['setSort'])) -{ - check_write_access(); - $listId = (int)_post('list'); - $sort = (int)_post('sort'); - if($sort < 0 || $sort > 104) $sort = 0; - elseif($sort < 101 && $sort > 4) $sort = 0; - if ($listId == -1) { - $opts = Config::requestDomain('alltasks.json'); - $opts['sort'] = $sort; - Config::saveDomain('alltasks.json', $opts); - } - else { - $db->ex("UPDATE {$db->prefix}lists SET sorting=$sort,d_edited=? WHERE id=$listId", array(time())); - } - jsonExit(array('total'=>1)); -} -elseif(isset($_GET['publishList'])) -{ - check_write_access(); - $listId = (int)_post('list'); - $publish = (int)_post('publish'); - $db->ex("UPDATE {$db->prefix}lists SET published=?,d_created=? WHERE id=$listId", array($publish ? 1 : 0, time())); - jsonExit(array('total'=>1)); -} -elseif(isset($_GET['moveTask'])) -{ - check_write_access(); - $id = (int)_post('id'); - $fromId = (int)_post('from'); - $toId = (int)_post('to'); - $result = moveTask($id, $toId); - $t = array('total' => $result ? 1 : 0); - if($fromId == -1 && $result && $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id")) { - $t['list'][] = prepareTaskRow($r); - } - jsonExit($t); -} -elseif(isset($_GET['changeListOrder'])) -{ - check_write_access(); - $order = (array)_post('order'); - $t = array(); - $t['total'] = 0; - if($order) - { - $a = array(); - $setCase = ''; - foreach($order as $ow=>$id) { - $id = (int)$id; - $a[] = $id; - $setCase .= "WHEN id=$id THEN $ow\n"; - } - $ids = implode(',', $a); - $db->dq("UPDATE {$db->prefix}lists SET d_edited=?, ow = CASE\n $setCase END WHERE id IN ($ids)", - array(time()) ); - $t['total'] = 1; - } - jsonExit($t); -} -elseif(isset($_GET['parseTaskStr'])) -{ - check_write_access(); - $t = array( - 'title' => trim(_post('title')), - 'prio' => 0, - 'tags' => '' - ); - if(Config::get('smartsyntax') != 0 && (false !== $a = parse_smartsyntax($t['title']))) - { - $t['title'] = $a['title']; - $t['prio'] = $a['prio']; - $t['tags'] = $a['tags']; - } - jsonExit($t); -} -elseif(isset($_GET['clearCompletedInList'])) -{ - check_write_access(); - $t = array(); - $t['total'] = 0; - $listId = (int)_post('list'); - $db->ex("BEGIN"); - $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id IN (SELECT id FROM {$db->prefix}todolist WHERE list_id=? and compl=1)", array($listId)); - $db->ex("DELETE FROM {$db->prefix}todolist WHERE list_id=$listId and compl=1"); - $t['total'] = $db->affected(); - $db->ex("COMMIT"); - jsonExit($t); -} -elseif(isset($_GET['setShowNotesInList'])) -{ - check_write_access(); - $listId = (int)_post('list'); - $flag = (int)_post('shownotes'); - $bitwise = ($flag == 0) ? 'taskview & ~2' : 'taskview | 2'; - $db->dq("UPDATE {$db->prefix}lists SET taskview=$bitwise WHERE id=$listId"); - jsonExit(array('total'=>1)); -} -elseif(isset($_GET['setHideList'])) -{ - check_write_access(); - $listId = (int)_post('list'); - $flag = (int)_post('hide') ? 1 : 0; - if ($listId == -1) { - $opts = Config::requestDomain('alltasks.json'); - $opts['hidden'] = $flag; - Config::saveDomain('alltasks.json', $opts); - } - else { - $bitwise = ($flag == 0) ? 'taskview & ~4' : 'taskview | 4'; - $db->dq("UPDATE {$db->prefix}lists SET taskview=$bitwise WHERE id=$listId"); - } - jsonExit(array('total'=>1)); -} -elseif (isset($_POST['createSession'])) -{ - $t = array(); - if (!need_auth()) { - $t['disabled'] = 1; - jsonExit($t); - } - if (access_token() == '') { - update_token(); - } - $t['token'] = access_token(); - $t['session'] = session_id(); - jsonExit($t); -} -else { - jsonExit(['total' => 0]); -} - - -################################################################################################### - -function prepareTaskRow($r) -{ - $lang = Lang::instance(); - $dueA = prepare_duedate($r['duedate']); - $formatCreatedInline = $formatCompletedInline = Config::get('dateformatshort'); - if(date('Y') != date('Y',$r['d_created'])) $formatCreatedInline = Config::get('dateformat2'); - if($r['d_completed'] && date('Y') != date('Y',$r['d_completed'])) $formatCompletedInline = Config::get('dateformat2'); - - $dCreated = timestampToDatetime($r['d_created']); - $dCompleted = $r['d_completed'] ? timestampToDatetime($r['d_completed']) : ''; - - return array( - 'id' => $r['id'], - 'title' => titleMarkup( $r['title'] ), - 'titleText' => (string)$r['title'], - 'listId' => $r['list_id'], - 'date' => htmlarray($dCreated), - 'dateInt' => (int)$r['d_created'], - 'dateInline' => htmlarray(formatTime($formatCreatedInline, $r['d_created'])), - 'dateInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_created'), $dCreated)), - 'dateEditedInt' => (int)$r['d_edited'], - 'dateCompleted' => htmlarray($dCompleted), - 'dateCompletedInline' => $r['d_completed'] ? htmlarray(formatTime($formatCompletedInline, $r['d_completed'])) : '', - 'dateCompletedInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_completed'), $dCompleted)), - 'compl' => (int)$r['compl'], - 'prio' => $r['prio'], - 'note' => noteMarkup($r['note']), - 'noteText' => (string)$r['note'], - 'ow' => (int)$r['ow'], - 'tags' => htmlarray($r['tags']), - 'tags_ids' => htmlarray($r['tags_ids']), - 'duedate' => $dueA['formatted'], - 'dueClass' => $dueA['class'], - 'dueStr' => htmlarray($r['compl'] && $dueA['timestamp'] ? formatTime($formatCompletedInline, $dueA['timestamp']) : $dueA['str']), - 'dueInt' => date2int($r['duedate']), - 'dueTitle' => htmlarray(sprintf($lang->get('taskdate_inline_duedate'), $dueA['formattedlong'])), - ); -} - -function check_read_access($listId = null) -{ - check_token(); - $db = DBConnection::instance(); - if(is_logged()) return true; - if($listId !== null) - { - $id = $db->sq("SELECT id FROM {$db->prefix}lists WHERE id=? AND published=1", array($listId)); - if($id) return; - } - jsonExit( array('total'=>0, 'list'=>array(), 'denied'=>1) ); -} - -function have_write_access($listId = null) -{ - if(is_readonly()) return false; - // check list exist - if($listId !== null) - { - $db = DBConnection::instance(); - $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}lists WHERE id=?", array($listId)); - if(!$count) return false; - } - return true; -} - -function check_write_access($listId = null) -{ - check_token(); - if(have_write_access($listId)) return; - jsonExit( array('total'=>0, 'list'=>array(), 'denied'=>1) ); -} - -/* -function inputTaskParams() -{ - $a = array( - 'id' => _post('id'), - 'title'=> trim(_post('title')), - 'note' => str_replace("\r\n", "\n", _post('note')), - 'prio' => (int)_post('prio'), - 'duedate' => '', - 'tags' => trim(_post('tags')), - 'listId' => (int)_post('list'), - - ); - if($a['prio'] < -1) $a['prio'] = -1; - elseif($a['prio'] > 2) $a['prio'] = 2; - return $a; -} -*/ - -function prepareTags($tagsStr) -{ - $tags = explode(',', $tagsStr); - if(!$tags) return 0; - - $aTags = array('tags'=>array(), 'ids'=>array()); - foreach($tags as $tag) - { - $tag = str_replace(array('^','#'),'',trim($tag)); - if($tag == '') continue; - - $aTag = getOrCreateTag($tag); - if($aTag && !in_array($aTag['id'], $aTags['ids'])) { - $aTags['tags'][] = $aTag['name']; - $aTags['ids'][] = $aTag['id']; - } - } - return $aTags; -} - -function getOrCreateTag($name) -{ - $db = DBConnection::instance(); - $tagId = $db->sq("SELECT id FROM {$db->prefix}tags WHERE name=?", array($name)); - if($tagId) return array('id'=>$tagId, 'name'=>$name); - - $db->ex("INSERT INTO {$db->prefix}tags (name) VALUES (?)", array($name)); - return array('id'=>$db->lastInsertId(), 'name'=>$name); -} - -function getTagId($tag) -{ - $db = DBConnection::instance(); - $id = $db->sq("SELECT id FROM {$db->prefix}tags WHERE name=?", array($tag)); - return $id ? $id : 0; -} - -function get_task_tags($id) -{ - $db = DBConnection::instance(); - $q = $db->dq("SELECT tag_id FROM {$db->prefix}tag2task WHERE task_id=?", $id); - $a = array(); - while($r = $q->fetchRow()) { - $a[] = $r[0]; - } - return $a; -} - - -function addTaskTags($taskId, $tagIds, $listId) -{ - $db = DBConnection::instance(); - if(!$tagIds) return; - foreach($tagIds as $tagId) - { - $db->ex("INSERT INTO {$db->prefix}tag2task (task_id,tag_id,list_id) VALUES (?,?,?)", array($taskId,$tagId,$listId)); - } -} - -function parse_smartsyntax($title) -{ - $a = [ - 'prio' => 0, - 'title' => $title, - 'tags' => '' - ]; - if ( preg_match("|^([-+]{1}\d+)(.+)|", $a['title'], $m) ) { - $a['prio'] = (int) $m[1]; - if ( $a['prio'] < -1 ) $a['prio'] = -1; - elseif ( $a['prio'] > 2 ) $a['prio'] = 2; - $a['title'] = trim($m[2]); - } - $tags = []; - $a['title'] = trim( preg_replace_callback( - "/(?:^|\s+)#([^#\s]+)/", - function ($matches) use (&$tags) { - $tags[] = $matches[1]; - return ''; - }, - $a['title'] - ) ); - if (count($tags) > 0) { - $a['tags'] = implode( ',' , $tags ); - } - return $a; -} - - -function tag_size($qmin, $q, $step) -{ - if($step == 0) return 1; - $v = ceil(($q - $qmin)/$step); - if($v == 0) return 0; - else return $v-1; - -} - -function parse_duedate($s) -{ - $df2 = Config::get('dateformat2'); - if(max((int)strpos($df2,'n'), (int)strpos($df2,'m')) > max((int)strpos($df2,'d'), (int)strpos($df2,'j'))) $formatDayFirst = true; - else $formatDayFirst = false; - - $y = $m = $d = 0; - if(preg_match("|^(\d+)-(\d+)-(\d+)\b|", $s, $ma)) { - $y = (int)$ma[1]; $m = (int)$ma[2]; $d = (int)$ma[3]; - } - elseif(preg_match("|^(\d+)\/(\d+)\/(\d+)\b|", $s, $ma)) - { - if($formatDayFirst) { - $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; - } else { - $m = (int)$ma[1]; $d = (int)$ma[2]; $y = (int)$ma[3]; - } - } - elseif(preg_match("|^(\d+)\.(\d+)\.(\d+)\b|", $s, $ma)) { - $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; - } - elseif(preg_match("|^(\d+)\.(\d+)\b|", $s, $ma)) { - $d = (int)$ma[1]; $m = (int)$ma[2]; - $a = explode(',', date('Y,m,d')); - if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; - else $y = (int)$a[0]; - } - elseif(preg_match("|^(\d+)\/(\d+)\b|", $s, $ma)) - { - if($formatDayFirst) { - $d = (int)$ma[1]; $m = (int)$ma[2]; - } else { - $m = (int)$ma[1]; $d = (int)$ma[2]; - } - $a = explode(',', date('Y,m,d')); - if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; - else $y = (int)$a[0]; - } - else return null; - if($y < 100) $y = 2000 + $y; - elseif($y < 1000 || $y > 2099) $y = 2000 + (int)substr((string)$y, -2); - if($m > 12) $m = 12; - $maxdays = daysInMonth($m,$y); - if($m < 10) $m = '0'.$m; - if($d > $maxdays) $d = $maxdays; - elseif($d < 10) $d = '0'.$d; - return "$y-$m-$d"; -} - -function prepare_duedate($duedate) -{ - $lang = Lang::instance(); - - $a = array( 'class'=>'', 'str'=>'', 'formatted'=>'', 'formattedlong'=>'', 'timestamp'=>0 ); - if($duedate == '') { - return $a; - } - $ad = explode('-', $duedate); - $at = explode('-', date('Y-m-d')); - $a['timestamp'] = mktime(0,0,0,$ad[1],$ad[2],$ad[0]); - $diff = mktime(0,0,0,$ad[1],$ad[2],$ad[0]) - mktime(0,0,0,$at[1],$at[2],$at[0]); - - if($diff < -604800 && $ad[0] == $at[0]) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformatshort'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } - elseif($diff < -604800) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformat2'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } - elseif($diff < -86400) { $a['class'] = 'past'; $a['str'] = sprintf($lang->get('daysago'),ceil(abs($diff)/86400)); } - elseif($diff < 0) { $a['class'] = 'past'; $a['str'] = $lang->get('yesterday'); } - elseif($diff < 86400) { $a['class'] = 'today'; $a['str'] = $lang->get('today'); } - elseif($diff < 172800) { $a['class'] = 'today'; $a['str'] = $lang->get('tomorrow'); } - elseif($diff < 691200) { $a['class'] = 'soon'; $a['str'] = sprintf($lang->get('indays'),ceil($diff/86400)); } - elseif($ad[0] == $at[0]) { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformatshort'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } - else { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformat2'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } - - #avoid short year - $fmt = str_replace('y', 'Y', Config::get('dateformat2')); - $a['formatted'] = formatTime($fmt, $a['timestamp']); - $a['formattedlong'] = formatTime(Config::get('dateformat'), $a['timestamp']); - - return $a; -} - -function date2int($d) -{ - if(!$d) return 33330000; - $ad = explode('-', $d); - $s = $ad[0]; - if(strlen($ad[1]) < 2) $s .= "0$ad[1]"; else $s .= $ad[1]; - if(strlen($ad[2]) < 2) $s .= "0$ad[2]"; else $s .= $ad[2]; - return (int)$s; -} - -function daysInMonth($m, $y=0) -{ - if($y == 0) $y = (int)date('Y'); - $a = array(1=>31,(($y-2000)%4?28:29),31,30,31,30,31,31,30,31,30,31); - if(isset($a[$m])) return $a[$m]; else return 0; -} - -function myErrorHandler($errno, $errstr, $errfile, $errline) -{ - if ($errno==E_ERROR || $errno==E_CORE_ERROR || $errno==E_COMPILE_ERROR || $errno==E_USER_ERROR || $errno==E_PARSE) { - $error = 'Error'; - } - elseif ($errno==E_WARNING || $errno==E_CORE_WARNING || $errno==E_COMPILE_WARNING || $errno==E_USER_WARNING || $errno==E_STRICT) { - if (error_reporting() & $errno) $error = 'Warning'; else return; - } - elseif ($errno==E_NOTICE || $errno==E_USER_NOTICE || $errno==E_DEPRECATED || $errno==E_USER_DEPRECATED) { - if (error_reporting() & $errno) $error = 'Notice'; else return; - } - else $error = "Error ($errno)"; # here may be E_RECOVERABLE_ERROR - throw new Exception("$error: '$errstr' in $errfile:$errline", -1); -} - -function myExceptionHandler($e) -{ - // to avoid Exception thrown without a stack frame - try - { - if (-1 == $e->getCode()) { - //thrown in myErrorHandler - logAndDie( $e->getMessage() ); - } - - $c = get_class($e); - $errText = "Exception ($c): '". $e->getMessage(). "' in ". $e->getFile(). ":". $e->getLine() ; - - if (MTT_DEBUG) { - if ( count($e->getTrace()) > 0 ) { - $errText .= "\n". $e->getTraceAsString() ; - } - } - logAndDie($errText); - } - catch (Exception $e) { - logAndDie('Exception in ExceptionHandler: \''. $e->getMessage() .'\' in '. $e->getFile() .':'. $e->getLine()); - } - exit; -} - -function deleteTask($id) -{ - check_write_access(); - $db = DBConnection::instance(); - $db->ex("BEGIN"); - $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id=$id"); - //TODO: delete unused tags? - $db->dq("DELETE FROM {$db->prefix}todolist WHERE id=$id"); - $affected = $db->affected(); - $db->ex("COMMIT"); - return $affected; -} - -function moveTask($id, $listId) -{ - check_write_access(); - $db = DBConnection::instance(); - - // Check task exists and not in target list - $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=?", array($id)); - if(!$r || $listId == $r['list_id']) return false; - - // Check target list exists - if(!$db->sq("SELECT COUNT(*) FROM {$db->prefix}lists WHERE id=?", $listId)) - return false; - - $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=? AND compl=?", array($listId, $r['compl']?1:0)); - - $db->ex("BEGIN"); - $db->ex("UPDATE {$db->prefix}tag2task SET list_id=? WHERE task_id=?", array($listId, $id)); - $db->dq("UPDATE {$db->prefix}todolist SET list_id=?, ow=?, d_edited=? WHERE id=?", array($listId, $ow, time(), $id)); - $db->ex("COMMIT"); - return true; -} - -function prepareList($row) -{ - $taskview = (int)$row['taskview']; - return array( - 'id' => $row['id'], - 'name' => htmlarray($row['name']), - 'sort' => (int)$row['sorting'], - 'published' => $row['published'] ? 1 :0, - 'showCompl' => $taskview & 1 ? 1 : 0, - 'showNotes' => $taskview & 2 ? 1 : 0, - 'hidden' => $taskview & 4 ? 1 : 0, - ); -} - -function prepareAllTasksList() -{ - //default values - $hidden = 1; - $sort = 3; - - $opts = Config::requestDomain('alltasks.json'); - if ( isset($opts['hidden']) ) $hidden = (int)$opts['hidden'] ? 1 : 0; - if ( isset($opts['sort']) ) $sort = (int)$opts['sort']; - - return array( - 'id' => -1, - 'name' => htmlarray(__('alltasks')), - 'sort' => $sort, - 'published' => 0, - 'showCompl' => 1, - 'showNotes' => 0, - 'hidden' => $hidden, - ); -} - -function getUserListsSimple() -{ - $db = DBConnection::instance(); - $a = array(); - $q = $db->dq("SELECT id,name FROM {$db->prefix}lists ORDER BY id ASC"); - while($r = $q->fetchRow()) { - $a[$r[0]] = $r[1]; - } - return $a; -} diff --git a/src/api.php b/src/api.php new file mode 100644 index 0000000..ce5d2ee --- /dev/null +++ b/src/api.php @@ -0,0 +1,227 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +require_once('./init.php'); + +if (MTT_DEBUG) { + set_error_handler('myErrorHandler'); //catch Notices, Warnings + set_exception_handler('myExceptionHandler'); +} +else { + ini_set('display_errors', '0'); +} + +require_once(MTTINC. 'api/ListsController.php'); +require_once(MTTINC. 'api/TasksController.php'); +require_once(MTTINC. 'api/TagsController.php'); +require_once(MTTINC. 'api/AuthController.php'); + +$req = new ApiRequest(); + +$endpoints = array( + '/lists' => [ + 'GET' => [ ListsController::class , 'get' ], + 'POST' => [ ListsController::class , 'post' ], + 'PUT' => [ ListsController::class , 'put' ], + ], + '/lists/(-?\d+)' => [ + 'GET' => [ ListsController::class , 'getId' ], + 'PUT' => [ ListsController::class , 'putId' ], + 'DELETE' => [ ListsController::class , 'deleteId' ], + ], + '/tasks' => [ + 'GET' => [ TasksController::class , 'get' ], + 'POST' => [ TasksController::class , 'post' ], + 'PUT' => [ TasksController::class , 'put' ], + ], + '/tasks/(-?\d+)' => [ + 'PUT' => [ TasksController::class , 'putId' ], + 'DELETE' => [ TasksController::class , 'deleteId' ], + ], + '/tasks/parseTitle' => [ + 'POST' => [ TasksController::class , 'postTitleParse' ], + ], + '/tagCloud/(-?\d+)' => [ + 'GET' => [ TagsController::class , 'getCloud' ], + ], + '/suggestTags' => [ + 'GET' => [ TagsController::class , 'getSuggestions' ], + ], + '/(login|logout|session)' => [ + 'POST' => [ AuthController::class , 'postAction' ], + ], +); + +$executed = false; +$data = null; +foreach ($endpoints as $search => $methods) { + $m = array(); + if (preg_match("#^$search$#", $req->path, $m)) { + $classDescr = $methods[$req->method] ?? null; + // check if http method is supported for path + if ( is_null($classDescr) ) { + http_response_code(500); + die ("Unknown method for resource"); + } + if ( !is_array($classDescr) || count($classDescr) != 2) { + http_response_code(500); + die ("Incorrect method definition"); + } + // check if class method exists + $class = $classDescr[0]; + $classMethod = $classDescr[1]; + $param = null; + if (count($m) >= 2) { + $param = $m[1]; + } + if (method_exists($class, $classMethod)) { // test for static with ReflectionMethod? + if ($req->method != 'GET' && $req->contentType == 'application/json') { + if ($req->decodeJsonBody() === false) { + http_response_code(500); + die ("Failed to parse JSON body"); + } + } + $instance = new $class($req); + $data = $instance->$classMethod($param); + $executed = true; + break; + } + else { + http_response_code(405); + if (MTT_DEBUG) { + die ("Class method $class:$classMethod() not found"); + } + die ("Class method not found"); + } + } +} + +if ($executed) { + if (is_null($data)) { + http_response_code(404); + } + jsonExit($data); +} +else { + http_response_code(404); + die ("Unknown command"); +} + + + + +function myErrorHandler($errno, $errstr, $errfile, $errline) +{ + if ($errno==E_ERROR || $errno==E_CORE_ERROR || $errno==E_COMPILE_ERROR || $errno==E_USER_ERROR || $errno==E_PARSE) { + $error = 'Error'; + } + elseif ($errno==E_WARNING || $errno==E_CORE_WARNING || $errno==E_COMPILE_WARNING || $errno==E_USER_WARNING || $errno==E_STRICT) { + if (error_reporting() & $errno) $error = 'Warning'; else return; + } + elseif ($errno==E_NOTICE || $errno==E_USER_NOTICE || $errno==E_DEPRECATED || $errno==E_USER_DEPRECATED) { + if (error_reporting() & $errno) $error = 'Notice'; else return; + } + else $error = "Error ($errno)"; # here may be E_RECOVERABLE_ERROR + throw new Exception("$error: '$errstr' in $errfile:$errline", -1); +} + +function myExceptionHandler(Throwable $e) +{ + // to avoid Exception thrown without a stack frame + try + { + if (-1 == $e->getCode()) { + //thrown in myErrorHandler + http_response_code(500); + logAndDie( $e->getMessage() ); + } + + $c = get_class($e); + $errText = "Exception ($c): '". $e->getMessage(). "' in ". $e->getFile(). ":". $e->getLine() ; + + if (MTT_DEBUG) { + if ( count($e->getTrace()) > 0 ) { + $errText .= "\n". $e->getTraceAsString() ; + } + } + http_response_code(500); + logAndDie($errText); + } + catch (Exception $e) { + http_response_code(500); + logAndDie('Exception in ExceptionHandler: \''. $e->getMessage() .'\' in '. $e->getFile() .':'. $e->getLine()); + } + exit; +} + +function checkReadAccess(?int $listId = null) +{ + check_token(); + $db = DBConnection::instance(); + if (is_logged()) return true; + if ($listId !== null) + { + $id = $db->sq("SELECT id FROM {$db->prefix}lists WHERE id=? AND published=1", array($listId)); + if ($id) return; + } + jsonExit( array('total'=>0, 'list'=>array(), 'denied'=>1) ); +} + +function checkWriteAccess(?int $listId = null) +{ + check_token(); + if (haveWriteAccess($listId)) return; + http_response_code(403); + jsonExit( array('total'=>0, 'list'=>array(), 'denied'=>1) ); +} + +function haveWriteAccess(?int $listId = null) +{ + if (is_readonly()) { + return false; + } + // check list exist + if ($listId !== null) + { + $db = DBConnection::instance(); + $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}lists WHERE id=?", array($listId)); + if (!$count) return false; + } + return true; +} + +class ApiRequest +{ + public $path; + public $method; + public $contentType; + public $jsonBody; + + function __construct() { + $this->path = $_SERVER['PATH_INFO'] ?? ''; + $this->method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + $this->contentType = $_SERVER['CONTENT_TYPE'] ?? ''; + } + + function decodeJsonBody() { + $this->jsonBody = json_decode( file_get_contents('php://input'), true, 10, JSON_INVALID_UTF8_SUBSTITUTE ); + return $this->jsonBody; + } +} + +abstract class ApiController +{ + /** + * @var ApiRequest + */ + protected $req; + + function __construct(ApiRequest $req) { + $this->req = $req; + } +} diff --git a/src/content/themes/default/index.php b/src/content/themes/default/index.php index dbf6f46..427bdca 100644 --- a/src/content/themes/default/index.php +++ b/src/content/themes/default/index.php @@ -26,11 +26,11 @@ - + diff --git a/src/includes/api/AuthController.php b/src/includes/api/AuthController.php new file mode 100644 index 0000000..57d59bc --- /dev/null +++ b/src/includes/api/AuthController.php @@ -0,0 +1,63 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +class AuthController extends ApiController { + + function postAction($action) + { + switch ($action) { + case 'login': return $this->login(); break; + case 'logout': return $this->logout(); break; + case 'session': return $this->createSession(); break; + default: return ['total' => 0]; // error 400 ? + } + } + + private function login() + { + check_token(); + $t = array('logged' => 0); + if (!need_auth()) { + $t['disabled'] = 1; + return $t; + } + $password = $this->req->jsonBody['password'] ?? ''; + if ( isPasswordEqualsToHash($password, Config::get('password')) ) { + updateSessionLogged(true); + $t['token'] = update_token(); + $t['logged'] = 1; + } + return $t; + } + + private function logout() + { + check_token(); + updateSessionLogged(false); + update_token(); + session_regenerate_id(true); + $t = array('logged' => 0); + return $t; + } + + private function createSession() + { + $t = array(); + if (!need_auth()) { + $t['disabled'] = 1; + return $t; + } + if (access_token() == '') { + update_token(); + } + $t['token'] = access_token(); + $t['session'] = session_id(); + return $t; + } + +} diff --git a/src/includes/api/ListsController.php b/src/includes/api/ListsController.php new file mode 100644 index 0000000..84900e0 --- /dev/null +++ b/src/includes/api/ListsController.php @@ -0,0 +1,293 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +class ListsController extends ApiController { + + /** + * Get all lists + * @return array + * @throws Exception + */ + function get() + { + $db = DBConnection::instance(); + check_token(); + $t = array(); + $t['total'] = 0; + if (!is_logged()) { + $sqlWhere = 'WHERE published=1'; + } + else { + $sqlWhere = ''; + $t['list'][] = $this->prepareAllTasksList(); // show alltasks lists only for authorized user + $t['total'] = 1; + } + $q = $db->dq("SELECT * FROM {$db->prefix}lists $sqlWhere ORDER BY ow ASC, id ASC"); + while ($r = $q->fetchAssoc()) + { + $t['total']++; + $t['list'][] = $this->prepareList($r); + } + return $t; + } + + + /** + * Create new list + * Code 201 on success + * @return array + * @throws Exception + */ + function post() + { + checkWriteAccess(); + $db = DBConnection::instance(); + $t = array(); + $t['total'] = 0; + $name = str_replace( + array('"',"'",'<','>','&'), + '', + trim( $this->req->jsonBody['name'] ?? '' ) + ); + $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}lists"); + $db->dq("INSERT INTO {$db->prefix}lists (uuid,name,ow,d_created,d_edited,taskview) VALUES (?,?,?,?,?,?)", + array(generateUUID(), $name, $ow, time(), time(), 1) ); + $id = $db->lastInsertId(); + $t['total'] = 1; + $r = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=$id"); + $t['list'][] = $this->prepareList($r); + return $t; + } + + /** + * Actions with all lists + * @return array + * @throws Exception + */ + function put() + { + checkWriteAccess(); + $action = $this->req->jsonBody['action'] ?? ''; + switch ($action) { + case 'order': return $this->changeListOrder(); break; + default: return ['total' => 0]; // error 400 ? + } + } + + + /* Single list */ + + /** + * Get single list by Id + * @param mixed $id + * @return null|array + * @throws Exception + */ + function getId($id) + { + checkReadAccess($id); + $db = DBConnection::instance(); + $r = $db->sqa( "SELECT * FROM {$db->prefix}lists WHERE id=?", array($id) ); + if (!$r) { + return null; + } + $t = $this->prepareList($r); + return $t; + } + + /** + * Delete list by Id + * @param mixed $id + * @return array + * @throws Exception + */ + function deleteId($id) + { + checkWriteAccess(); + $db = DBConnection::instance(); + $t = array(); + $t['total'] = 0; + $id = (int)$id; + $db->ex("BEGIN"); + $db->ex("DELETE FROM {$db->prefix}lists WHERE id=$id"); + $t['total'] = $db->affected(); + if ($t['total']) { + $db->ex("DELETE FROM {$db->prefix}tag2task WHERE list_id=$id"); + $db->ex("DELETE FROM {$db->prefix}todolist WHERE list_id=$id"); + } + $db->ex("COMMIT"); + return $t; + } + + + /** + * Edit some properties of List + * Actions: rename + * @param mixed $id + * @return array + * @throws Exception + */ + function putId($id) + { + checkWriteAccess(); + $id = (int)$id; + + $action = $this->req->jsonBody['action'] ?? ''; + switch ($action) { + case 'rename': return $this->renameList($id); break; + case 'sort': return $this->sortList($id); break; + case 'publish': return $this->publishList($id); break; + case 'showNotes': return $this->showNotes($id); break; + case 'hide': return $this->hideList($id); break; + case 'clearCompleted': return $this->clearCompleted($id); break; + default: return ['total' => 0]; + } + } + + + /* Private Functions */ + + private function prepareAllTasksList() + { + //default values + $hidden = 1; + $sort = 3; + + $opts = Config::requestDomain('alltasks.json'); + if ( isset($opts['hidden']) ) $hidden = (int)$opts['hidden'] ? 1 : 0; + if ( isset($opts['sort']) ) $sort = (int)$opts['sort']; + + return array( + 'id' => -1, + 'name' => htmlarray(__('alltasks')), + 'sort' => $sort, + 'published' => 0, + 'showCompl' => 1, + 'showNotes' => 0, + 'hidden' => $hidden, + ); + } + + private function prepareList($row) + { + $taskview = (int)$row['taskview']; + return array( + 'id' => $row['id'], + 'name' => htmlarray($row['name']), + 'sort' => (int)$row['sorting'], + 'published' => $row['published'] ? 1 :0, + 'showCompl' => $taskview & 1 ? 1 : 0, + 'showNotes' => $taskview & 2 ? 1 : 0, + 'hidden' => $taskview & 4 ? 1 : 0, + ); + } + + private function renameList(int $id) + { + $db = DBConnection::instance(); + $t = array(); + $t['total'] = 0; + $name = str_replace( + array('"',"'",'<','>','&'), + array('','','','',''), + trim($this->req->jsonBody['name'] ?? '') + ); + $db->dq("UPDATE {$db->prefix}lists SET name=?,d_edited=? WHERE id=$id", array($name, time()) ); + $t['total'] = $db->affected(); + $r = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=$id"); + $t['list'][] = $this->prepareList($r); + return $t; + } + + private function sortList(int $listId) + { + $db = DBConnection::instance(); + $sort = (int)($this->req->jsonBody['sort'] ?? 0); + if ($sort < 0 || $sort > 104) $sort = 0; + elseif ($sort < 101 && $sort > 4) $sort = 0; + if ($listId == -1) { + $opts = Config::requestDomain('alltasks.json'); + $opts['sort'] = $sort; + Config::saveDomain('alltasks.json', $opts); + } + else { + $db->ex("UPDATE {$db->prefix}lists SET sorting=$sort,d_edited=? WHERE id=$listId", array(time())); + } + return ['total'=>1]; + } + + private function publishList(int $listId) + { + $db = DBConnection::instance(); + $publish = (int)($this->req->jsonBody['publish'] ?? 0); + $db->ex("UPDATE {$db->prefix}lists SET published=?,d_created=? WHERE id=$listId", array($publish ? 1 : 0, time())); + return ['total'=>1]; + } + + private function showNotes(int $listId) + { + $db = DBConnection::instance(); + $flag = (int)($this->req->jsonBody['shownotes'] ?? 0); + $bitwise = ($flag == 0) ? 'taskview & ~2' : 'taskview | 2'; + $db->dq("UPDATE {$db->prefix}lists SET taskview=$bitwise WHERE id=$listId"); + return ['total'=>1]; + } + + private function hideList(int $listId) + { + $db = DBConnection::instance(); + $flag = (int)($this->req->jsonBody['hide'] ?? 0); + if ($listId == -1) { + $opts = Config::requestDomain('alltasks.json'); + $opts['hidden'] = $flag ? 1 : 0; + Config::saveDomain('alltasks.json', $opts); + } + else { + $bitwise = ($flag == 0) ? 'taskview & ~4' : 'taskview | 4'; + $db->dq("UPDATE {$db->prefix}lists SET taskview=$bitwise WHERE id=$listId"); + } + return ['total'=>1]; + } + + private function clearCompleted(int $listId) + { + $db = DBConnection::instance(); + $t = array(); + $t['total'] = 0; + $db->ex("BEGIN"); + $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id IN (SELECT id FROM {$db->prefix}todolist WHERE list_id=? and compl=1)", array($listId)); + $db->ex("DELETE FROM {$db->prefix}todolist WHERE list_id=$listId and compl=1"); + $t['total'] = $db->affected(); + $db->ex("COMMIT"); + return $t; + } + + private function changeListOrder() + { + $t = array(); + $t['total'] = 0; + if (!is_array($this->req->jsonBody['order'])) { + return $t; + } + $db = DBConnection::instance(); + $order = $this->req->jsonBody['order']; + $a = array(); + $setCase = ''; + foreach ($order as $ow => $id) { + $id = (int)$id; + $a[] = $id; + $setCase .= "WHEN id=$id THEN $ow\n"; + } + $ids = implode(',', $a); + $db->dq("UPDATE {$db->prefix}lists SET d_edited=?, ow = CASE\n $setCase END WHERE id IN ($ids)", + array(time()) ); + $t['total'] = 1; + return $t; + } + +} diff --git a/src/includes/api/TagsController.php b/src/includes/api/TagsController.php new file mode 100644 index 0000000..493d921 --- /dev/null +++ b/src/includes/api/TagsController.php @@ -0,0 +1,83 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +class TagsController extends ApiController { + + /** + * Get tag cloud + * @return array + * @throws Exception + */ + function getCloud($listId) + { + $listId = (int)$listId; + checkReadAccess($listId); + $db = DBConnection::instance(); + + $q = $db->dq("SELECT name,tag_id,COUNT(tag_id) AS tags_count FROM {$db->prefix}tag2task INNER JOIN {$db->prefix}tags ON tag_id=id ". + "WHERE list_id=$listId GROUP BY (tag_id) ORDER BY tags_count ASC"); + $at = array(); + $ac = array(); + while ($r = $q->fetchAssoc()) { + $at[] = array( + 'name' => $r['name'], + 'id' => $r['tag_id'] + ); + $ac[] = (int) $r['tags_count']; + } + + $t = array(); + $t['total'] = 0; + $count = sizeof($at); + if (!$count) { + return $t; + } + + $qmax = max($ac); + $qmin = min($ac); + if ($count >= 10) $grades = 10; + else $grades = $count; + $step = ($qmax - $qmin)/$grades; + foreach ($at as $i => $tag) + { + $t['cloud'][] = array( + 'tag' => htmlspecialchars($tag['name']), + 'id' => (int)$tag['id'], + 'w' => $this->tagWeight($qmin, $ac[$i], $step) + ); + } + $t['total'] = $count; + return $t; + } + + function getSuggestions($listId) + { + $listId = (int)_get('list'); + checkWriteAccess($listId); + $db = DBConnection::instance(); + $begin = trim(_get('q')); + $limit = 8; + $q = $db->dq("SELECT name,id FROM {$db->prefix}tags INNER JOIN {$db->prefix}tag2task ON id=tag_id WHERE list_id=$listId AND name LIKE ". + $db->quoteForLike('%s%%',$begin) ." GROUP BY tag_id ORDER BY name LIMIT $limit"); + $t = array(); + while ($r = $q->fetchRow()) { + $t[] = $r[0]; + } + return $t; + } + + private function tagWeight(int $qmin, int $q, float $step) + { + if ($step == 0) return 1; + $v = ceil(($q - $qmin)/$step); + if ($v == 0) return 0; + else return $v-1; + } + + +} diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php new file mode 100644 index 0000000..7915b4b --- /dev/null +++ b/src/includes/api/TasksController.php @@ -0,0 +1,662 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +require_once(MTTINC. 'markup.php'); + +class TasksController extends ApiController { + + /** + * Get tasks. + * Filters are set with query parameters. + * @return array + * @throws Exception + */ + function get() + { + $listId = (int)_get('list'); + checkReadAccess($listId); + $db = DBConnection::instance(); + + $sqlWhere = $inner = $sqlWhereListId = $sqlInnerWhereListId = ''; + if ($listId == -1) { + $userLists = $this->getUserListsSimple(); + $sqlWhereListId = "{$db->prefix}todolist.list_id IN (". implode(',', array_keys($userLists)). ") "; + $sqlInnerWhereListId = "list_id IN (". implode(',', array_keys($userLists)). ") "; + } + else { + $sqlWhereListId = "{$db->prefix}todolist.list_id=". $listId; + $sqlInnerWhereListId = "list_id=$listId "; + } + if (_get('compl') == 0) { + $sqlWhere .= ' AND compl=0'; + } + + $tag = trim(_get('t')); + if ($tag != '') + { + $at = explode(',', $tag); + $tagIds = array(); + $tagExIds = array(); + foreach ($at as $i=>$atv) { + $atv = trim($atv); + if ($atv == '' || $atv == '^') continue; + if (substr($atv,0,1) == '^') { + $tagExIds[] = $this->getTagId(substr($atv,1)); + } else { + $tagIds[] = $this->getTagId($atv); + } + } + + // Include tags: All + if (sizeof($tagIds) > 1) { + $inner .= "INNER JOIN (SELECT task_id, COUNT(tag_id) AS c FROM {$db->prefix}tag2task WHERE $sqlInnerWhereListId AND tag_id IN (". + implode(',',$tagIds). ") GROUP BY task_id) AS t2t ON id=t2t.task_id"; + $sqlWhere .= " AND c=". sizeof($tagIds); + } + elseif ($tagIds) { + $inner .= "INNER JOIN {$db->prefix}tag2task ON id=task_id"; + $sqlWhere .= " AND tag_id = {$tagIds[0]}"; + } + + // Exclude tags + if (sizeof($tagExIds) > 0) { + $sqlWhere .= " AND {$db->prefix}todolist.id NOT IN (SELECT DISTINCT task_id FROM {$db->prefix}tag2task WHERE $sqlInnerWhereListId AND tag_id IN (". + implode(',',$tagExIds). "))"; + } + //no optimization for single exTag + } + + $s = trim(_get('s')); + if ($s != '') { + if (preg_match("|^#(\d+)$|", $s, $m)) $sqlWhere .= " AND {$db->prefix}todolist.id = ". (int)$m[1]; + else $sqlWhere .= " AND (title LIKE ". $db->quoteForLike("%%%s%%",$s). " OR note LIKE ". $db->quoteForLike("%%%s%%",$s). ")"; + } + + $sort = (int)_get('sort'); + $sqlSort = "ORDER BY compl ASC, "; + if ($sort == 1) $sqlSort .= "prio DESC, ddn ASC, duedate ASC, ow ASC"; // byPrio + elseif ($sort == 101) $sqlSort .= "prio ASC, ddn DESC, duedate DESC, ow DESC"; // byPrio (reverse) + elseif ($sort == 2) $sqlSort .= "ddn ASC, duedate ASC, prio DESC, ow ASC"; // byDueDate + elseif ($sort == 102) $sqlSort .= "ddn DESC, duedate DESC, prio ASC, ow DESC"; // byDueDate (reverse) + elseif ($sort == 3) $sqlSort .= "d_created ASC, prio DESC, ow ASC"; // byDateCreated + elseif ($sort == 103) $sqlSort .= "d_created DESC, prio ASC, ow DESC"; // byDateCreated (reverse) + elseif ($sort == 4) $sqlSort .= "d_edited ASC, prio DESC, ow ASC"; // byDateModified + elseif ($sort == 104) $sqlSort .= "d_edited DESC, prio ASC, ow DESC"; // byDateModified (reverse) + else $sqlSort .= "ow ASC"; + + $t = array(); + $t['total'] = 0; + $t['list'] = array(); + $q = $db->dq("SELECT *, duedate IS NULL AS ddn FROM {$db->prefix}todolist $inner WHERE $sqlWhereListId $sqlWhere $sqlSort"); + while ($r = $q->fetchAssoc()) + { + $t['total']++; + $t['list'][] = $this->prepareTaskRow($r); + } + if (_get('setCompl') && haveWriteAccess($listId)) { + $bitwise = (_get('compl') == 0) ? 'taskview & ~1' : 'taskview | 1'; + $db->dq("UPDATE {$db->prefix}lists SET taskview=$bitwise WHERE id=$listId"); + } + return $t; + } + + /** + * Create new task + * action: simple or full + * @return mixed + * @throws Exception + */ + function post() + { + $listId = (int)($this->req->jsonBody['list'] ?? 0); + checkWriteAccess($listId); + $action = $this->req->jsonBody['action'] ?? ''; + if ($action == 'full') { + return $this->fullNewTaskInList($listId); + } + return $this->newTaskInList($listId); + } + + /** + * Actions with multiple tasks + * @return array + * @throws Exception + */ + function put() + { + checkWriteAccess(); + $action = $this->req->jsonBody['action'] ?? ''; + switch ($action) { + case 'order': return $this->changeTaskOrder(); break; + default: return ['total' => 0]; // error 400 ? + } + } + + + /** + * Delete task by Id + * @param mixed $id + * @return array + * @throws Exception + */ + function deleteId($id) + { + checkWriteAccess(); + $id = (int)$id; + $db = DBConnection::instance(); + $db->ex("BEGIN"); + $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id=$id"); + //TODO: delete unused tags? + $db->dq("DELETE FROM {$db->prefix}todolist WHERE id=$id"); + $deleted = $db->affected(); + $db->ex("COMMIT"); + $t = array(); + $t['total'] = $deleted; + $t['list'][] = array('id' => $id); + return $t; + } + + /** + * Edit some properties of Task + * @param mixed $id + * @return array + * @throws Exception + */ + function putId($id) + { + checkWriteAccess(); + $id = (int)$id; + + $action = $this->req->jsonBody['action'] ?? ''; + switch ($action) { + case 'edit': return $this->editTask($id); break; + case 'complete': return $this->completeTask($id); break; + case 'note': return $this->editNote($id); break; + case 'move': return $this->moveTask($id); break; + case 'priority': return $this->priorityTask($id); break; + default: return ['total' => 0]; + } + } + + + /** + * Parse task input string to components for representing in edit/add form + * @return array + * @throws Exception + */ + function postTitleParse() + { + checkWriteAccess(); + $t = array( + 'title' => trim( $this->req->jsonBody['title'] ?? '' ), + 'prio' => 0, + 'tags' => '' + ); + if (Config::get('smartsyntax') != 0 && (false !== $a = $this->parseSmartSyntax($t['title']))) + { + $t['title'] = $a['title']; + $t['prio'] = $a['prio']; + $t['tags'] = $a['tags']; + } + return $t; + } + + /* Private Functions */ + + private function newTaskInList(int $listId) + { + $db = DBConnection::instance(); + $t = array(); + $t['total'] = 0; + $title = trim($this->req->jsonBody['title'] ?? ''); + $prio = 0; + $tags = ''; + if (Config::get('smartsyntax') != 0) + { + $a = $this->parseSmartSyntax($title); + if ($a === false) { + return $t; + } + $title = $a['title']; + $prio = $a['prio']; + $tags = $a['tags']; + } + if ($title == '') { + return $t; + } + if (Config::get('autotag')) { + $tags .= ',' . ($this->req->jsonBody['tag'] ?? ''); + } + $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=0"); + $db->ex("BEGIN"); + $db->dq("INSERT INTO {$db->prefix}todolist (uuid,list_id,title,d_created,d_edited,ow,prio) VALUES (?,?,?,?,?,?,?)", + array(generateUUID(), $listId, $title, time(), time(), $ow, $prio) ); + $id = (int) $db->lastInsertId(); + if ($tags != '') + { + $aTags = $this->prepareTags($tags); + if ($aTags) { + $this->addTaskTags($id, $aTags['ids'], $listId); + $db->ex("UPDATE {$db->prefix}todolist SET tags=?,tags_ids=? WHERE id=$id", array(implode(',',$aTags['tags']), implode(',',$aTags['ids']))); + } + } + $db->ex("COMMIT"); + $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); + $t['list'][] = $this->prepareTaskRow($r); + $t['total'] = 1; + return $t; + } + + private function fullNewTaskInList(int $listId) + { + $db = DBConnection::instance(); + $title = trim($this->req->jsonBody['title'] ?? ''); + $note = str_replace("\r\n", "\n", $this->req->jsonBody['note'] ?? ''); + $prio = (int)($this->req->jsonBody['prio'] ?? 0); + if ($prio < -1) $prio = -1; + elseif ($prio > 2) $prio = 2; + $duedate = $this->parseDuedate(trim( $this->req->jsonBody['duedate'] ?? '' )); + $t = array(); + $t['total'] = 0; + if ($title == '') { + return $t; + } + $tags = $this->req->jsonBody['tags'] ?? ''; + if (Config::get('autotag')) + $tags .= ',' . ($this->req->jsonBody['tag'] ?? ''); + $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=0"); + $db->ex("BEGIN"); + $db->dq("INSERT INTO {$db->prefix}todolist (uuid,list_id,title,d_created,d_edited,ow,prio,note,duedate) VALUES (?,?,?,?,?,?,?,?,?)", + array(generateUUID(), $listId, $title, time(), time(), $ow, $prio, $note, $duedate) ); + $id = (int) $db->lastInsertId(); + if ($tags != '') + { + $aTags = $this->prepareTags($tags); + if ($aTags) { + $this->addTaskTags($id, $aTags['ids'], $listId); + $db->ex("UPDATE {$db->prefix}todolist SET tags=?,tags_ids=? WHERE id=$id", array(implode(',',$aTags['tags']), implode(',',$aTags['ids']))); + } + } + $db->ex("COMMIT"); + $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); + $t['list'][] = $this->prepareTaskRow($r); + $t['total'] = 1; + return $t; + } + + private function editTask(int $id) + { + $db = DBConnection::instance(); + $title = trim($this->req->jsonBody['title'] ?? ''); + $note = str_replace("\r\n", "\n", $this->req->jsonBody['note'] ?? ''); + $prio = (int)($this->req->jsonBody['prio'] ?? 0); + if ($prio < -1) $prio = -1; + elseif ($prio > 2) $prio = 2; + $duedate = $this->parseDuedate(trim( $this->req->jsonBody['duedate'] ?? '' )); + $t = array(); + $t['total'] = 0; + if ($title == '') { + return $t; + } + $listId = (int) $db->sq("SELECT list_id FROM {$db->prefix}todolist WHERE id=$id"); + $tags = trim( $this->req->jsonBody['tags'] ?? '' ); + $db->ex("BEGIN"); + $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id=$id"); + $aTags = $this->prepareTags($tags); + if($aTags) { + $tags = implode(',', $aTags['tags']); + $tags_ids = implode(',',$aTags['ids']); + $this->addTaskTags($id, $aTags['ids'], $listId); + } + $db->dq("UPDATE {$db->prefix}todolist SET title=?,note=?,prio=?,tags=?,tags_ids=?,duedate=?,d_edited=? WHERE id=$id", + array($title, $note, $prio, $tags, $tags_ids, $duedate, time()) ); + $db->ex("COMMIT"); + $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); + if ($r) { + $t['list'][] = $this->prepareTaskRow($r); + $t['total'] = 1; + } + return $t; + } + + private function moveTask(int $id) + { + $db = DBConnection::instance(); + $fromId = (int)($this->req->jsonBody['from'] ?? 0); + $toId = (int)($this->req->jsonBody['to'] ?? 0); + $result = $this->doMoveTask($id, $toId); + $t = array('total' => $result ? 1 : 0); + if ($fromId == -1 && $result && $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id")) { + $t['list'][] = $this->prepareTaskRow($r); + } + return $t; + } + + private function doMoveTask(int $id, int $listId) + { + $db = DBConnection::instance(); + + // Check task exists and not in target list + $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=?", array($id)); + if (!$r || $listId == $r['list_id']) return false; + + // Check target list exists + if (!$db->sq("SELECT COUNT(*) FROM {$db->prefix}lists WHERE id=?", $listId)) + return false; + + $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=? AND compl=?", array($listId, $r['compl']?1:0)); + + $db->ex("BEGIN"); + $db->ex("UPDATE {$db->prefix}tag2task SET list_id=? WHERE task_id=?", array($listId, $id)); + $db->dq("UPDATE {$db->prefix}todolist SET list_id=?, ow=?, d_edited=? WHERE id=?", array($listId, $ow, time(), $id)); + $db->ex("COMMIT"); + return true; + } + + private function completeTask(int $id) + { + $db = DBConnection::instance(); + $compl = (int)($this->req->jsonBody['compl'] ?? 0); + $listId = (int)$db->sq("SELECT list_id FROM {$db->prefix}todolist WHERE id=$id"); + if ($compl) $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=1"); + else $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}todolist WHERE list_id=$listId AND compl=0"); + $dateCompleted = $compl ? time() : 0; + $db->dq("UPDATE {$db->prefix}todolist SET compl=$compl,ow=$ow,d_completed=?,d_edited=? WHERE id=$id", + array($dateCompleted, time()) ); + $t = array(); + $t['total'] = 1; + $r = $db->sqa("SELECT * FROM {$db->prefix}todolist WHERE id=$id"); + $t['list'][] = $this->prepareTaskRow($r); + return $t; + } + + private function editNote(int $id) + { + $db = DBConnection::instance(); + $note = $this->req->jsonBody['note'] ?? ''; + $note = str_replace("\r\n", "\n", $note); + $db->dq("UPDATE {$db->prefix}todolist SET note=?,d_edited=? WHERE id=$id", array($note, time()) ); + $t = array(); + $t['total'] = 1; + $t['list'][] = array('id'=>$id, 'note'=> noteMarkup($note), 'noteText'=>(string)$note); + return $t; + } + + private function priorityTask(int $id) + { + $db = DBConnection::instance(); + $prio = (int)($this->req->jsonBody['prio'] ?? 0); + if ($prio < -1) $prio = -1; + elseif ($prio > 2) $prio = 2; + $db->ex("UPDATE {$db->prefix}todolist SET prio=$prio,d_edited=? WHERE id=$id", array(time()) ); + $t = array(); + $t['total'] = 1; + $t['list'][] = array('id'=>$id, 'prio'=>$prio); + return $t; + } + + + private function changeTaskOrder() + { + $db = DBConnection::instance(); + $order = $this->req->jsonBody['order'] ?? null; + $t = array(); + $t['total'] = 0; + if (is_array($order)) + { + $ad = array(); + foreach ($order as $obj) { + $id = $obj['id'] ?? 0; + $diff = $obj['diff'] ?? 0; + $ad[(int)$diff][] = (int)$id; + } + $db->ex("BEGIN"); + foreach ($ad as $diff=>$ids) { + if ($diff >=0) $set = "ow=ow+".$diff; + else $set = "ow=ow-".abs($diff); + $db->dq("UPDATE {$db->prefix}todolist SET $set,d_edited=? WHERE id IN (".implode(',',$ids).")", array(time()) ); + } + $db->ex("COMMIT"); + $t['total'] = 1; + } + return $t; + } + + private function getUserListsSimple() + { + $db = DBConnection::instance(); + $a = array(); + $q = $db->dq("SELECT id,name FROM {$db->prefix}lists ORDER BY id ASC"); + while($r = $q->fetchRow()) { + $a[$r[0]] = $r[1]; + } + return $a; + } + + private function prepareTaskRow(array $r) + { + $lang = Lang::instance(); + $dueA = $this->prepareDuedate($r['duedate']); + $formatCreatedInline = $formatCompletedInline = Config::get('dateformatshort'); + if (date('Y') != date('Y', (int)$r['d_created'])) + $formatCreatedInline = Config::get('dateformat2'); + if ($r['d_completed'] && date('Y') != date('Y', (int)$r['d_completed'])) + $formatCompletedInline = Config::get('dateformat2'); + + $dCreated = timestampToDatetime($r['d_created']); + $dCompleted = $r['d_completed'] ? timestampToDatetime($r['d_completed']) : ''; + + return array( + 'id' => $r['id'], + 'title' => titleMarkup( $r['title'] ), + 'titleText' => (string)$r['title'], + 'listId' => $r['list_id'], + 'date' => htmlarray($dCreated), + 'dateInt' => (int)$r['d_created'], + 'dateInline' => htmlarray(formatTime($formatCreatedInline, $r['d_created'])), + 'dateInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_created'), $dCreated)), + 'dateEditedInt' => (int)$r['d_edited'], + 'dateCompleted' => htmlarray($dCompleted), + 'dateCompletedInline' => $r['d_completed'] ? htmlarray(formatTime($formatCompletedInline, $r['d_completed'])) : '', + 'dateCompletedInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_completed'), $dCompleted)), + 'compl' => (int)$r['compl'], + 'prio' => $r['prio'], + 'note' => noteMarkup($r['note']), + 'noteText' => (string)$r['note'], + 'ow' => (int)$r['ow'], + 'tags' => htmlarray($r['tags']), + 'tags_ids' => htmlarray($r['tags_ids']), + 'duedate' => $dueA['formatted'], + 'dueClass' => $dueA['class'], + 'dueStr' => htmlarray($r['compl'] && $dueA['timestamp'] ? formatTime($formatCompletedInline, $dueA['timestamp']) : $dueA['str']), + 'dueInt' => $this->date2int($r['duedate']), + 'dueTitle' => htmlarray(sprintf($lang->get('taskdate_inline_duedate'), $dueA['formattedlong'])), + ); + } + + + private function parseDuedate($s) + { + $df2 = Config::get('dateformat2'); + if (max((int)strpos($df2,'n'), (int)strpos($df2,'m')) > max((int)strpos($df2,'d'), (int)strpos($df2,'j'))) $formatDayFirst = true; + else $formatDayFirst = false; + + $y = $m = $d = 0; + if (preg_match("|^(\d+)-(\d+)-(\d+)\b|", $s, $ma)) { + $y = (int)$ma[1]; $m = (int)$ma[2]; $d = (int)$ma[3]; + } + elseif (preg_match("|^(\d+)\/(\d+)\/(\d+)\b|", $s, $ma)) + { + if($formatDayFirst) { + $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; + } else { + $m = (int)$ma[1]; $d = (int)$ma[2]; $y = (int)$ma[3]; + } + } + elseif (preg_match("|^(\d+)\.(\d+)\.(\d+)\b|", $s, $ma)) { + $d = (int)$ma[1]; $m = (int)$ma[2]; $y = (int)$ma[3]; + } + elseif (preg_match("|^(\d+)\.(\d+)\b|", $s, $ma)) { + $d = (int)$ma[1]; $m = (int)$ma[2]; + $a = explode(',', date('Y,m,d')); + if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; + else $y = (int)$a[0]; + } + elseif (preg_match("|^(\d+)\/(\d+)\b|", $s, $ma)) + { + if($formatDayFirst) { + $d = (int)$ma[1]; $m = (int)$ma[2]; + } else { + $m = (int)$ma[1]; $d = (int)$ma[2]; + } + $a = explode(',', date('Y,m,d')); + if( $m<(int)$a[1] || ($m==(int)$a[1] && $d<(int)$a[2]) ) $y = (int)$a[0]+1; + else $y = (int)$a[0]; + } + else return null; + if ($y < 100) $y = 2000 + $y; + elseif ($y < 1000 || $y > 2099) $y = 2000 + (int)substr((string)$y, -2); + if ($m > 12) $m = 12; + $maxdays = $this->daysInMonth($m,$y); + if ($m < 10) $m = '0'.$m; + if ($d > $maxdays) $d = $maxdays; + elseif ($d < 10) $d = '0'.$d; + return "$y-$m-$d"; + } + + private function prepareDuedate($duedate) + { + $lang = Lang::instance(); + + $a = array( 'class'=>'', 'str'=>'', 'formatted'=>'', 'formattedlong'=>'', 'timestamp'=>0 ); + if ($duedate == '') { + return $a; + } + $ad = explode('-', $duedate); + $at = explode('-', date('Y-m-d')); + $a['timestamp'] = mktime(0,0,0, (int)$ad[1], (int)$ad[2], (int)$ad[0]); + $diff = mktime(0,0,0, (int)$ad[1], (int)$ad[2], (int)$ad[0]) - mktime(0,0,0, (int)$at[1], (int)$at[2], (int)$at[0]); + + if ($diff < -604800 && $ad[0] == $at[0]) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformatshort'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } + elseif ($diff < -604800) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformat2'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } + elseif ($diff < -86400) { $a['class'] = 'past'; $a['str'] = sprintf($lang->get('daysago'),ceil(abs($diff)/86400)); } + elseif ($diff < 0) { $a['class'] = 'past'; $a['str'] = $lang->get('yesterday'); } + elseif ($diff < 86400) { $a['class'] = 'today'; $a['str'] = $lang->get('today'); } + elseif ($diff < 172800) { $a['class'] = 'today'; $a['str'] = $lang->get('tomorrow'); } + elseif ($diff < 691200) { $a['class'] = 'soon'; $a['str'] = sprintf($lang->get('indays'),ceil($diff/86400)); } + elseif ($ad[0] == $at[0]) { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformatshort'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } + else { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformat2'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } + + #avoid short year + $fmt = str_replace('y', 'Y', Config::get('dateformat2')); + $a['formatted'] = formatTime($fmt, $a['timestamp']); + $a['formattedlong'] = formatTime(Config::get('dateformat'), $a['timestamp']); + + return $a; + } + + private function date2int($d) + { + if (!$d) return 33330000; + $ad = explode('-', $d); + $s = $ad[0]; + if (strlen($ad[1]) < 2) $s .= "0$ad[1]"; else $s .= $ad[1]; + if (strlen($ad[2]) < 2) $s .= "0$ad[2]"; else $s .= $ad[2]; + return (int)$s; + } + + private function daysInMonth(int $m, int $y = 0) + { + if ($y == 0) $y = (int)date('Y'); + $a = array(1=>31,(($y-2000)%4?28:29),31,30,31,30,31,31,30,31,30,31); + if (isset($a[$m])) return $a[$m]; + else return 0; + } + + private function getTagId($tag) + { + $db = DBConnection::instance(); + $id = $db->sq("SELECT id FROM {$db->prefix}tags WHERE name=?", array($tag)); + return $id ? $id : 0; + } + + private function getOrCreateTag($name) + { + $db = DBConnection::instance(); + $tagId = $db->sq("SELECT id FROM {$db->prefix}tags WHERE name=?", array($name)); + if ($tagId) + return array('id'=>$tagId, 'name'=>$name); + + $db->ex("INSERT INTO {$db->prefix}tags (name) VALUES (?)", array($name)); + return array( + 'id' => $db->lastInsertId(), + 'name' => $name + ); + } + + private function prepareTags(string $tagsStr) + { + $tags = explode(',', $tagsStr); + if (!$tags) return 0; + + $aTags = array('tags'=>array(), 'ids'=>array()); + foreach ($tags as $tag) + { + $tag = str_replace(array('^','#'),'',trim($tag)); + if ($tag == '') continue; + + $aTag = $this->getOrCreateTag($tag); + if ($aTag && !in_array($aTag['id'], $aTags['ids'])) { + $aTags['tags'][] = $aTag['name']; + $aTags['ids'][] = $aTag['id']; + } + } + return $aTags; + } + + private function addTaskTags(int $taskId, array $tagIds, int $listId) + { + $db = DBConnection::instance(); + if (!$tagIds) return; + foreach ($tagIds as $tagId) { + $db->ex( + "INSERT INTO {$db->prefix}tag2task (task_id,tag_id,list_id) VALUES (?,?,?)", + array($taskId, $tagId, $listId) + ); + } + } + + private function parseSmartSyntax($title) + { + $a = [ + 'prio' => 0, + 'title' => $title, + 'tags' => '' + ]; + if ( preg_match("|^([-+]{1}\d+)(.+)|", $a['title'], $m) ) { + $a['prio'] = (int) $m[1]; + if ( $a['prio'] < -1 ) $a['prio'] = -1; + elseif ( $a['prio'] > 2 ) $a['prio'] = 2; + $a['title'] = trim($m[2]); + } + $tags = []; + $a['title'] = trim( preg_replace_callback( + "/(?:^|\s+)#([^#\s]+)/", + function ($matches) use (&$tags) { + $tags[] = $matches[1]; + return ''; + }, + $a['title'] + ) ); + if (count($tags) > 0) { + $a['tags'] = implode( ',' , $tags ); + } + return $a; + } + +} diff --git a/src/includes/common.php b/src/includes/common.php index b90e189..87b7d12 100644 --- a/src/includes/common.php +++ b/src/includes/common.php @@ -84,7 +84,10 @@ function getRequestUri() { // Do not use HTTP_X_REWRITE_URL due to CVE-2018-14773 // SCRIPT_NAME or PATH_INFO ? - if (isset($_SERVER['REQUEST_URI'])) { + if (isset($_SERVER['SCRIPT_NAME'])) { + return $_SERVER['SCRIPT_NAME']; + } + elseif (isset($_SERVER['REQUEST_URI'])) { return $_SERVER['REQUEST_URI']; } else if (isset($_SERVER['ORIG_PATH_INFO'])) // IIS 5.0 CGI diff --git a/src/includes/mytinytodo.js b/src/includes/mytinytodo.js index f8094d0..d78852b 100644 --- a/src/includes/mytinytodo.js +++ b/src/includes/mytinytodo.js @@ -50,6 +50,7 @@ var mytinytodo = window.mytinytodo = _mtt = { menus: {}, mttUrl: '', homeUrl: '', + apiUrl: '', options: { token: '', title: '', @@ -119,9 +120,9 @@ var mytinytodo = window.mytinytodo = _mtt = { lastHistoryState: null, // procs - setApi: function(storage) + setApiDriver: function(driver) { - this.db = new storage(this); + this.db = new driver(_mtt); return this; }, @@ -136,6 +137,13 @@ var mytinytodo = window.mytinytodo = _mtt = { this.mttUrl = options.mttUrl; delete options.mttUrl; } + if (options.hasOwnProperty('apiUrl')) { + this.apiUrl = options.apiUrl; + delete options.apiUrl; + } + else { + this.apiUrl = this.mttUrl + 'api.php/'; + } if (options.hasOwnProperty('db')) { delete options.db; } @@ -546,9 +554,9 @@ var mytinytodo = window.mytinytodo = _mtt = { $(document).ajaxError(function(event, request, settings){ var errtxt; - if(request.status == 0) errtxt = 'Bad connection'; + if (request.status == 0) errtxt = 'Bad connection'; else if(request.status == 403) errtxt = request.responseText; - else if(request.status != 200) errtxt = 'HTTP: '+request.status+'/'+request.statusText; + else if (request.status != 200) errtxt = 'HTTP: '+request.status+'/'+request.statusText + "\n" + request.responseText; else errtxt = request.responseText; flashError(_mtt.lang.get('error'), errtxt); }); @@ -1729,6 +1737,7 @@ function loadTags(listId, callback) else tagsList = json.cloud; var cloud = ''; $.each(tagsList, function(i,item){ + // item.tag is escaped with htmlspecialchars() cloud += ' '+item.tag+''; }); $('#tagcloudcontent').html(cloud) @@ -2487,7 +2496,7 @@ function showLogin() function doAuth(form) { - $.post(mytinytodo.mttUrl+'ajax.php?login', { login:1, password: form.password.value }, function(json){ + _mtt.db.request( 'login', { password: form.password.value }, function(json) { form.password.value = ''; if(json.logged) { @@ -2498,16 +2507,16 @@ function doAuth(form) flashError(_mtt.lang.get('invalidpass')); $('#password').focus(); } - }, 'json'); + }); } function logout() { - $.post(mytinytodo.mttUrl+'ajax.php?logout', { logout:1 }, function(json){ + _mtt.db.request( 'logout', {}, function(json) { flag.isLogged = false; window.location.hash = ''; window.location.reload(); - }, 'json'); + }); return false; } diff --git a/src/includes/mytinytodo_ajax_storage.js b/src/includes/mytinytodo_ajax_storage.js deleted file mode 100644 index de5ad37..0000000 --- a/src/includes/mytinytodo_ajax_storage.js +++ /dev/null @@ -1,179 +0,0 @@ -/* - This file is a part of myTinyTodo. - (C) Copyright 2010,2020 Max Pozdeev - Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. -*/ - -// AJAX myTinyTodo Storage - -(function(){ - -"use strict"; - -var mtt; - -function mytinytodoStorageAjax(amtt) -{ - this.mtt = mtt = amtt; -} - -window.mytinytodoStorageAjax = mytinytodoStorageAjax; - -mytinytodoStorageAjax.prototype = -{ - /* required method */ - request:function(action, params, callback) - { - if(!this[action]) throw "Unknown storage action: "+action; - - this[action](params, function(json){ - if(json.denied) mtt.errorDenied(); - if(callback) callback.call(mtt, json) - }); - }, - - - loadLists: function(params, callback) - { - $.getJSON(this.mtt.mttUrl+'ajax.php?loadLists', callback); - }, - - - loadTasks: function(params, callback) - { - var q = ''; - if(params.search && params.search != '') q += '&s='+encodeURIComponent(params.search); - if(params.tag && params.tag != '') q += '&t='+encodeURIComponent(params.tag); - if(params.setCompl && params.setCompl != 0) q += '&setCompl=1'; - - $.getJSON(this.mtt.mttUrl+'ajax.php?loadTasks&list='+params.list+'&compl='+params.compl+'&sort='+params.sort+q, callback); - }, - - - newTask: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?newTask', - { list:params.list, title: params.title, tag:params.tag }, callback, 'json'); - }, - - - fullNewTask: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?fullNewTask', - { list:params.list, title:params.title, note:params.note, prio:params.prio, tags:params.tags, duedate:params.duedate }, - callback, 'json'); - }, - - - editTask: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?editTask='+params.id, - { id:params.id, title:params.title, note:params.note, prio:params.prio, tags:params.tags, duedate:params.duedate }, - callback, 'json'); - }, - - - editNote: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?editNote='+params.id, {id:params.id, note: params.note}, callback, 'json'); - }, - - - completeTask: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?completeTask='+params.id, { id:params.id, compl:params.compl }, callback, 'json'); - }, - - - deleteTask: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?deleteTask='+params.id, { id:params.id }, callback, 'json'); - }, - - - setTaskPriority: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?setTaskPriority='+params.id, { id:params.id, priority:params.priority }, callback, 'json'); - }, - - - setSort: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?setSort', { list:params.list, sort:params.sort }, callback, 'json'); - }, - - changeOrder: function(params, callback) - { - var order = ''; - for(var i in params.order) { - order += params.order[i].id +'='+ params.order[i].diff + '&'; - } - $.post(this.mtt.mttUrl+'ajax.php?changeOrder', { order:order }, callback, 'json'); - }, - - suggestTags: function(params, callback) - { - $.getJSON(this.mtt.mttUrl+'ajax.php?suggestTags', {list:params.list, q:params.q}, callback); - }, - - tagCloud: function(params, callback) - { - $.getJSON(this.mtt.mttUrl+'ajax.php?tagCloud&list='+params.list, callback); - }, - - moveTask: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?moveTask', { id:params.id, from:params.from, to:params.to }, callback, 'json'); - }, - - parseTaskStr: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?parseTaskStr', { list:params.list, title:params.title, tag:params.tag }, callback, 'json'); - }, - - - // Lists - addList: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?addList', { name:params.name }, callback, 'json'); - - }, - - renameList: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?renameList', { list:params.list, name:params.name }, callback, 'json'); - }, - - deleteList: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?deleteList', { list:params.list }, callback, 'json'); - }, - - publishList: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?publishList', { list:params.list, publish:params.publish }, callback, 'json'); - }, - - setShowNotesInList: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?setShowNotesInList', { list:params.list, shownotes:params.shownotes }, callback, 'json'); - }, - - setHideList: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?setHideList', { list:params.list, hide:params.hide }, callback, 'json'); - }, - - changeListOrder: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?changeListOrder', { order:params.order }, callback, 'json'); - }, - - clearCompletedInList: function(params, callback) - { - $.post(this.mtt.mttUrl+'ajax.php?clearCompletedInList', { list:params.list }, callback, 'json'); - } - -}; - -})(); diff --git a/src/includes/mytinytodo_api.js b/src/includes/mytinytodo_api.js new file mode 100644 index 0000000..5788360 --- /dev/null +++ b/src/includes/mytinytodo_api.js @@ -0,0 +1,385 @@ +/* + This file is a part of myTinyTodo. + (C) Copyright 2010,2020,2022 Max Pozdeev + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +(function(){ + +"use strict"; + +var mtt; + +function MytinytodoAjaxApi(amtt) +{ + mtt = amtt; +} + +window.MytinytodoAjaxApi = MytinytodoAjaxApi; + +MytinytodoAjaxApi.prototype = +{ + /* required method */ + request: function(action, params, callback) + { + if (!this[action]) throw "Unknown ApiDriver action: " + action; + + this[action] (params, function(json){ + if (json.denied) mtt.errorDenied(); + if (callback) callback.call(mtt, json) + }); + }, + + + loadTasks: function(params, callback) + { + var q = ''; + if (params.search && params.search != '') q += '&s=' + encodeURIComponent(params.search); + if (params.tag && params.tag != '') q += '&t=' + encodeURIComponent(params.tag); + if (params.setCompl && params.setCompl != 0) q += '&setCompl=1'; + + $.getJSON(mtt.apiUrl + 'tasks?list='+params.list+'&compl='+params.compl+'&sort='+params.sort+q, callback); + }, + + + newTask: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks', + method: 'POST', + contentType : 'application/json', + data: JSON.stringify({ + action: 'simple', + list: params.list, + title: params.title, + tag: params.tag, + }), + success: callback, + dataType: 'json' + }); + }, + + + fullNewTask: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks', + method: 'POST', + contentType : 'application/json', + data: JSON.stringify({ + action: 'full', + list: params.list, + title: params.title, + note: params.note, + prio: params.prio, + tags: params.tags, + duedate: params.duedate, + /* tag: params.tag, // We do not send current tag filter, autotag should set it in the form and include in tags */ + }), + success: callback, + dataType: 'json' + }); + }, + + + editTask: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks/' + encodeURIComponent(params.id), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'edit', + title: params.title, + note: params.note, + prio: params.prio, + tags: params.tags, + duedate: params.duedate, + }), + success: callback, + dataType: 'json' + }); + }, + + + editNote: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks/' + encodeURIComponent(params.id), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'note', + note: params.note + }), + success: callback, + dataType: 'json' + }); + }, + + + completeTask: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks/' + encodeURIComponent(params.id), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'complete', + compl: params.compl + }), + success: callback, + dataType: 'json' + }); + }, + + + deleteTask: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks/' + encodeURIComponent(params.id), + method: 'DELETE', + success: callback, + dataType: 'json' + }); + }, + + + setTaskPriority: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks/' + encodeURIComponent(params.id), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'priority', + prio: params.priority, + }), + success: callback, + dataType: 'json' + }); + }, + + changeOrder: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks', + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'order', + order: params.order, + }), + success: callback, + dataType: 'json' + }); + }, + + suggestTags: function(params, callback) + { + $.getJSON(mtt.apiUrl + 'suggestTags', {list:params.list, q:params.q}, callback); + }, + + tagCloud: function(params, callback) + { + $.getJSON(mtt.apiUrl + 'tagCloud/' + encodeURIComponent(params.list), callback); + }, + + moveTask: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks/' + encodeURIComponent(params.id), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'move', + from: params.from, + to: params.to + }), + success: callback, + dataType: 'json' + }); + }, + + parseTaskStr: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'tasks/parseTitle', + method: 'POST', + contentType : 'application/json', + data: JSON.stringify({ + list: params.list, + title: params.title, + tag: params.tag , + }), + success: callback, + dataType: 'json' + }); + }, + + + // Lists + loadLists: function(params, callback) + { + $.getJSON(mtt.apiUrl + 'lists', callback); + }, + + addList: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists', + method: 'POST', + contentType : 'application/json', + data: JSON.stringify({ + name: params.name, + }), + success: callback, + dataType: 'json' + }); + + }, + + deleteList: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists/' + encodeURIComponent(params.list), + method: 'DELETE', + success: callback, + dataType: 'json' + }); + }, + + renameList: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists/' + encodeURIComponent(params.list), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'rename', + name: params.name, + }), + success: callback, + dataType: 'json' + }); + }, + + setSort: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists/' + encodeURIComponent(params.list), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'sort', + name: params.name, + sort: params.sort + }), + success: callback, + dataType: 'json' + }); + }, + + publishList: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists/' + encodeURIComponent(params.list), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'publish', + publish: params.publish, + }), + success: callback, + dataType: 'json' + }); + }, + + setShowNotesInList: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists/' + encodeURIComponent(params.list), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'showNotes', + shownotes: params.shownotes, + }), + success: callback, + dataType: 'json' + }); + }, + + setHideList: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists/' + encodeURIComponent(params.list), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'hide', + hide: params.hide, + }), + success: callback, + dataType: 'json' + }); + }, + + changeListOrder: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists', + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'order', + order: params.order + }), + success: callback, + dataType: 'json' + }); + }, + + clearCompletedInList: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'lists/' + encodeURIComponent(params.list), + method: 'PUT', + contentType : 'application/json', + data: JSON.stringify({ + action: 'clearCompleted', + }), + success: callback, + dataType: 'json' + }); + }, + + /* Auth */ + + login: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'login', + method: 'POST', + contentType : 'application/json', + data: JSON.stringify({ + password: params.password, + }), + success: callback, + dataType: 'json' + }); + }, + + logout: function(params, callback) + { + $.ajax({ + url: mtt.apiUrl + 'logout', + method: 'POST', + success: callback, + dataType: 'json' + }); + } + +}; + +})(); diff --git a/src/index.php b/src/index.php index e685122..83f454b 100644 --- a/src/index.php +++ b/src/index.php @@ -73,6 +73,7 @@ function js_options() "lang" => Lang::instance()->jsStrings(), "mttUrl" => get_mttinfo('mtt_url'), "homeUrl" => get_mttinfo('url'), +// "apiUrl" => get_mttinfo('api_url'), "needAuth" => need_auth() ? true : false, "isLogged" => is_logged() ? true : false, "showdate" => Config::get('showdate') ? true : false, diff --git a/src/init.php b/src/init.php index 65b6c23..629b0f7 100644 --- a/src/init.php +++ b/src/init.php @@ -157,7 +157,7 @@ function check_token() $token = access_token(); if ($token == '' || !isset($_SERVER['HTTP_MTT_TOKEN']) || $_SERVER['HTTP_MTT_TOKEN'] != $token) { http_response_code(403); - die("Access denied! Try to reload the page."); + die("Access denied! You must authenticate first."); } } @@ -268,12 +268,19 @@ function get_unsafe_mttinfo($v) } return $_mttinfo['url']; case 'mtt_url': - /* Directory with ajax.php. No need to set if you use default directory structure. */ + /* Directory with settings.php. No need to set if you use default directory structure. */ $_mttinfo['mtt_url'] = Config::getUrl('mtt_url'); // need to have a trailing slash if ($_mttinfo['mtt_url'] == '') { $_mttinfo['mtt_url'] = url_dir( get_unsafe_mttinfo('url'), 0 ); } return $_mttinfo['mtt_url']; + case 'api_url': + /* URL for API, like http://localhost/mytinytodo/api/. No need to set by default. */ + $_mttinfo['api_url'] = Config::getUrl('api_url'); // need to have a trailing slash + if ($_mttinfo['api_url'] == '') { + $_mttinfo['api_url'] = get_unsafe_mttinfo('mtt_url'). 'api/'; + } + return $_mttinfo['api_url']; case 'title': $_mttinfo['title'] = (Config::get('title') != '') ? Config::get('title') : __('My Tiny Todolist'); return $_mttinfo['title'];