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'];