Repository: maxpozdeev/mytinytodo Branch: master Commit: 1b00932aa16b Files: 140 Total size: 748.4 KB Directory structure: gitextract_6m_aa8ts/ ├── .editorconfig ├── .gitignore ├── README.md ├── buildtar.php ├── composer.json ├── composer.sh └── src/ ├── .htaccess ├── COPYRIGHT ├── LICENSE ├── api.php ├── config-sample.php ├── content/ │ ├── index.html │ ├── js/ │ │ ├── index.html │ │ └── jquery.ui.touch-punch.js │ ├── mytinytodo.js │ ├── mytinytodo_api.js │ └── theme/ │ ├── dark.css │ ├── images/ │ │ ├── COPYRIGHT │ │ ├── index.html │ │ └── svg2base64.php │ ├── index.html │ ├── markdown.css │ ├── print.css │ ├── style.css │ └── style_rtl.css ├── db/ │ └── .htaccess ├── docker-config.php ├── export.php ├── ext/ │ ├── .htaccess │ ├── CustomCSS/ │ │ ├── .htaccess │ │ ├── extension.json │ │ ├── lang/ │ │ │ ├── en.json │ │ │ ├── pl.json │ │ │ └── ru.json │ │ └── loader.php │ ├── _examples/ │ │ └── CustomSmartSyntax/ │ │ ├── .htaccess │ │ ├── extension.json │ │ └── loader.php │ ├── backup/ │ │ ├── .htaccess │ │ ├── class.backup.php │ │ ├── class.check.php │ │ ├── class.controller.php │ │ ├── class.download.php │ │ ├── class.restore.php │ │ ├── extension.json │ │ ├── lang/ │ │ │ ├── en.json │ │ │ └── ru.json │ │ └── loader.php │ ├── index.html │ ├── notifications/ │ │ ├── .htaccess │ │ ├── class.controller.php │ │ ├── class.observer.php │ │ ├── class.sender.php │ │ ├── class.telegramapi.php │ │ ├── cli-notify.php │ │ ├── extension.json │ │ ├── lang/ │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ └── ru.json │ │ └── loader.php │ └── updater/ │ ├── .htaccess │ ├── class.controller.php │ ├── class.updater.php │ ├── extension.json │ ├── lang/ │ │ ├── de.json │ │ ├── en.json │ │ └── ru.json │ └── loader.php ├── feed.php ├── includes/ │ ├── .htaccess │ ├── api/ │ │ ├── AuthController.php │ │ ├── ExtSettingsController.php │ │ ├── ListsController.php │ │ ├── TagsController.php │ │ └── TasksController.php │ ├── class.config.php │ ├── class.db.mysql.php │ ├── class.db.mysqli.php │ ├── class.db.postgres.php │ ├── class.db.sqlite3.php │ ├── class.dbconnection.php │ ├── class.dbcore.php │ ├── class.lang.php │ ├── class.sessionhandler.php │ ├── classes.php │ ├── common.php │ ├── filters.php │ ├── index.html │ ├── lang/ │ │ ├── _percent.php │ │ ├── ar.json │ │ ├── bg.json │ │ ├── ca.json │ │ ├── cz.json │ │ ├── da.json │ │ ├── de.json │ │ ├── el.json │ │ ├── en-rtl.json │ │ ├── en.json │ │ ├── es-mx.json │ │ ├── es.json │ │ ├── et.json │ │ ├── fa.json │ │ ├── fr.json │ │ ├── he.json │ │ ├── hu.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── lt.json │ │ ├── mk.json │ │ ├── nl.json │ │ ├── no.json │ │ ├── pl.json │ │ ├── pt-br.json │ │ ├── pt-pt.json │ │ ├── readme.md │ │ ├── ro.json │ │ ├── ru.json │ │ ├── sk.json │ │ ├── sl.json │ │ ├── sr.json │ │ ├── sv.json │ │ ├── th.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── vi.json │ │ ├── zh-cn.json │ │ └── zh-tw.json │ ├── markup.commonmark.php │ ├── markup.parsedown.php │ ├── markup.php │ ├── notifications.php │ ├── smartsyntax.php │ ├── theme.php │ └── version.php ├── index.php ├── init.php ├── mtt-edit-settings.php ├── mtt-emergency.php ├── settings.php └── setup.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true # 'insert_final_newline = false' should not remove last empty line [*] charset = utf-8 end_of_line = lf # tab_width here is only used to represent tabs if indent_size is not set tab_width = 4 trim_trailing_whitespace = true insert_final_newline = true [*.php] indent_style = space indent_size = 4 [*.js] indent_style = space tab_width = 4 [*.{css,yml,html,svg,xml}] indent_style = space indent_size = 2 [*.md] trim_trailing_whitespace = false insert_final_newline = false [/src/includes/theme.php] indent_style = space indent_size = 2 ================================================ FILE: .gitignore ================================================ .DS_Store src/db/todolist.db* src/db/config.php src/db/config-* src/db/backup.xml* src/config.php src/includes/vendor/ src/content/theme/custom.css tests/ ================================================ FILE: README.md ================================================ # myTinyTodo Your tiny todo list Original website - http://www.mytinytodo.net/ ### System requirements - PHP 7.2 or greater - PHP extensions: - mbstring - pdo_sqlite, intl (SQLite version) - pdo_mysql or mysqli (MySQL version) - pdo_pgsql (PostgreSQL version) - One of databases: - MySQL 5.7 or greater / MariaDB 10.2 or greater - PostgreSQL 10 or greater - SQLite (system library) Supported browsers: Chrome 49, Safari 10, Firefox 53. Internet Explorer and Opera with Presto engine are not supported. ================================================ FILE: buildtar.php ================================================ #!/usr/bin/env php [-o source.tar.gz] [-v VERSION]\n"); } $repo = $argv[1]; $dir = sys_get_temp_dir(). DIRECTORY_SEPARATOR. "mytinytodo.build"; $curdir = getcwd(); $archive = $curdir. DIRECTORY_SEPARATOR. 'mytinytodo-v@VERSION-@REV.tar.gz'; $ver = 0; while ($arg = next($argv)) { if ($arg == '-o') { $archive = next($argv); } elseif ($arg == '-v') { $ver = next($argv); } } deleteTreeIfDir($dir); $out = `git clone $repo $dir 2>&1`; if (!is_dir($dir)) { die("Error while clone: $out\n"); } print "> Repository was cloned to temp dir: $dir\n"; #get current version number if not specified if (!$ver) { require_once(__DIR__ . '/src/includes/version.php'); $ver = mytinytodo\Version::VERSION; } chdir($dir. DIRECTORY_SEPARATOR. 'src'); $rev = trim(`git show --format=format:%H --summary`); $rev = substr($rev, 0, 8); ##$ver = str_replace('@REV', $rev, $ver); print "> Version is $ver\n"; unlink('./docker-config.php'); unlink('./includes/lang/en-rtl.json'); unlink('./includes/lang/_percent.php'); unlink('./mtt-edit-settings.php'); unlink('./mtt-emergency.php'); unlink('./content/theme/images/svg2base64.php'); chdir('..'); # to the root of repo assert( strpos(getcwd(), ':') === false ); # FIXME: if path contains a colon ':' echo("> Run Composer\n"); $retval = 0; if (false === system( "./composer.sh install --no-dev --no-interaction --optimize-autoloader", $retval) || $retval != 0) { die("Failed to install composer libs via docker\n"); } # ext if (is_dir('src/ext')) { mkdir('src/ext2'); chdir('src/ext'); deleteTreeIfDir('_examples'); $extCount = 0; $exts = array_diff(scandir('.') ?? [], ['.', '..']); foreach ($exts as $ext) { if (is_dir($ext)) { rename($ext, "../ext2/$ext"); $extCount++; } } chdir('../ext2'); if ($extCount > 0) { `tar --no-xattrs -czf ../ext/extensions.tar.gz *`; #OS dep.!!! } chdir('../..'); deleteTreeIfDir('src/ext2'); echo("> Extensions were packed\n"); } rename('src', 'mytinytodo') or die("Cant rename 'src'\n"); `tar --no-xattrs -czf mytinytodo.tar.gz mytinytodo`; #OS dep.!!! if (!file_exists('mytinytodo.tar.gz')) { die("Failed to pack files (no output tar.gz file)\n"); } $archive = str_replace('@VERSION', $ver, $archive); $archive = str_replace('@REV', $rev, $archive); chdir($curdir); if ( ! rename("$dir/mytinytodo.tar.gz", $archive) ) { die("Failed to move mytinytodo.tar.gz to $archive"); } deleteTreeIfDir($dir); echo("> Temp dir was cleaned\n"); echo("> Build is stored in $archive\n"); function deleteTreeIfDir($dir) { if ( !is_dir($dir) ) { return; } switch (PHP_OS) { case 'Darwin': case 'Linux': system("rm -rf ". escapeshellarg($dir)); break; case 'Windows': system("rmdir /s /q ". escapeshellarg($dir)); break; default: die("Unknown system ". PHP_OS. "\n"); } } ================================================ FILE: composer.json ================================================ { "name": "maxpozdeev/mytinytodo", "type": "project", "license": "GPL-2.0-or-later", "homepage": "https://mytinytodo.net", "authors": [ { "name": "Max Pozdeev", "role": "Developer" } ], "config": { "vendor-dir": "src/includes/vendor" }, "require": { "php": ">=7.2", "ext-mbstring": "*", "erusev/parsedown": "1.7.x-dev#f7285e7", "symfony/polyfill-intl-normalizer": "^1.31" }, "require-dev": { "league/commonmark": "^2.6" } } ================================================ FILE: composer.sh ================================================ #!/bin/sh #dir="$( dirname -- "$( readlink -f -- "$0"; )"; )" dir="$PWD" app=$(which podman) if [ -z $app ]; then app="docker" fi $app run -it --rm -v "$dir:/app" composer $@ ================================================ FILE: src/.htaccess ================================================ # For REST API in Apache # # RewriteEngine On # RewriteCond %{REQUEST_FILENAME} !-f # RewriteCond %{REQUEST_FILENAME} !-d # RewriteRule ^api/(.*)$ api.php/$1 [L,QSA] # # # Allow from all # # In Nginx set something like this: # Deny access to some files and folders #location ~ ^/(db|includes)/ { # deny all; #} #location ~ /\.ht { # deny all; #} #location ~* ^/ext/.*\.(json|md)$ { # deny all; #} # Optional # location /api/ { # rewrite ^/api/(.*) /api.php/$1 last; # } ================================================ FILE: src/COPYRIGHT ================================================ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program as the file LICENSE; if not, please see https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. myTinyTodo -------------- Url: https://www.mytinytodo.net/ Copyright: 2009-2011,2019-2025 Max Pozdeev License: GPL version 2 or any later (see LICENSE file) myTinyTodo uses other works: jQuery -------------- Url: http://jquery.com/ Copyright: (c) JS Foundation and other contributors | https://jquery.org/license/ License: MIT license. Compatible with GNU GPL (see https://blog.jquery.com/2012/09/10/jquery-licensing-changes/) jQuery UI -------------- Url: http://jqueryui.com/ Copyright: Copyright jQuery Foundation and other contributors License: MIT license. Compatible with GNU GPL. jQuery UI Touch Punch (fork by RWAP Software) --------------------------------------------- Url: https://github.com/RWAP/jquery-ui-touch-punch based on original touchpunch Original: https://github.com/furf/jquery-ui-touch-punch Copyright: Copyright 2011–2014, Dave Furfero License: Dual licensed under the MIT or GPL Version 2 licenses. Parsedown -------------- Url: https://parsedown.org/ Copyright: (c) 2013-2018 Emanuil Rusev, erusev.com License: MIT license. Compatible with GNU GPL. Other libraries (in includes/vendor) ---------------------------------------------- symfony/polyfill-intl-normalizer league/commonmark Images -------------- This software contains images by 3d-parties. See file content/theme/images/COPYRIGHT for details. ================================================ FILE: src/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: src/api.php ================================================ 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'); require_once(MTTINC. 'api/ExtSettingsController.php'); $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' ], 'POST' => [ ListsController::class , 'putId' ], //compatibility ], '/tasks' => [ 'GET' => [ TasksController::class , 'get' ], 'POST' => [ TasksController::class , 'post' ], 'PUT' => [ TasksController::class , 'put' ], ], '/tasks/(-?\d+)' => [ 'PUT' => [ TasksController::class , 'putId' ], 'DELETE' => [ TasksController::class , 'deleteId' ], 'POST' => [ TasksController::class , 'putId' ], //compatibility ], '/tasks/parseTitle' => [ 'POST' => [ TasksController::class , 'postTitleParse' ], ], '/tasks/newCounter' => [ 'POST' => [ TasksController::class , 'postNewCounter' ], ], '/tagCloud/(-?\d+)' => [ 'GET' => [ TagsController::class , 'getCloud' ], ], '/suggestTags' => [ 'GET' => [ TagsController::class , 'getSuggestions' ], ], '/(login|logout|session)' => [ 'POST' => [ AuthController::class , 'postAction' ], ], '/ext-settings/(.+)' => [ 'GET' => [ ExtSettingsController::class , 'get' ], 'PUT' => [ ExtSettingsController::class , 'put' ], 'POST' => [ ExtSettingsController::class , 'put' ], //compatibility ] ); // look for extensions foreach (MTTExtensionLoader::loadedExtensions() as $instance) { if ($instance instanceof MTTHttpApiExtender) { $newRoutes = $instance->extendHttpApi(); foreach ($newRoutes as $endpoint => $methods) { $endpoint = '/ext/'. $instance::bundleId. $endpoint; foreach ($methods as $k => &$v) { $v[3] = true; // Mark extension method } $endpoints[$endpoint] = $methods; } } } $req = new ApiRequest(); $response = new ApiResponse(); $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) ) { $response->htmlContent("Unknown method for resource", 500) ->exit(); } if ( !is_array($classDescr) || count($classDescr) < 2) { $response->htmlContent("Incorrect method definition", 500) ->exit(); } // check if class method exists $class = $classDescr[0]; $classMethod = $classDescr[1]; $isExtMethod = $classDescr[3] ?? false; if ($isExtMethod) { if (false == ($classDescr[2] ?? false)) { //TODO: describe $classDescr[2] // By default all extension methods require write access rights checkWriteAccess(); } } $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) { $response->htmlContent("Failed to parse JSON body", 500) ->exit(); } } $instance = new $class($req, $response); $instance->$classMethod($param); $executed = true; break; } else { if (MTT_DEBUG) { $response->htmlContent("Class method $class:$classMethod() not found", 405) ->exit(); } $response->htmlContent("Class method not found", 405) ->exit(); } } } if (!$executed) { if (MTT_DEBUG) { $response->htmlContent("Unknown endpoint: {$req->method} {$req->path}", 404) ->exit(); } $response->htmlContent("Unknown endpoint", 404); } $response->exit(); 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) { 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; } http_response_code(403); 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) : bool { if (is_readonly()) { return false; } // check list exist if ($listId !== null && $listId != -1) { $db = DBConnection::instance(); $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}lists WHERE id=?", array($listId)); if (!$count) return false; } return true; } ================================================ FILE: src/config-sample.php ================================================ 0 || navigator.msMaxTouchPoints > 0 ); // Ignore browsers without touch or mouse support if ((!$.touch && !$.mspointer) || !$.ui.mouse) { return; } let mouseProto = $.ui.mouse.prototype, _mouseInit = mouseProto._mouseInit, _mouseDestroy = mouseProto._mouseDestroy, touchHandled, lastClickTime = 0; let delay = 300, delayTimer, delayEvent, delayStarted = false, delayFinished = false, lastClickCoord; /** * Get the x,y position of a touch event * @param {Object} event A touch event */ function getTouchCoords (event) { return { x: event.originalEvent.changedTouches[0].pageX, y: event.originalEvent.changedTouches[0].pageY }; } /** * Simulate a mouse event based on a corresponding touch event * @param {Object} event A touch event * @param {String} simulatedType The corresponding mouse event */ function simulateMouseEvent (event, simulatedType) { // Ignore multi-touch events if (event.originalEvent.touches.length > 1) { return; } //Ignore input or textarea elements so user can still enter text if ($(event.target).is("input") || $(event.target).is("textarea")) { return; } // Prevent "Ignored attempt to cancel a touchmove event with cancelable=false" errors if (event.cancelable) { event.preventDefault(); } let touch = event.originalEvent.changedTouches[0], simulatedEvent = new MouseEvent(simulatedType, { bubbles: true, cancelable: true, view:window, screenX:touch.screenX, screenY:touch.screenY, clientX:touch.clientX, clientY:touch.clientY }); // Dispatch the simulated event to the target element event.target.dispatchEvent(simulatedEvent); } function startDelayTimer (event) { clearTimeout(delayTimer); delayEvent = event; delayTimer = setTimeout(function() { fireMouseDown.call(this); }, delay); delayStarted = true; delayFinished = false; } function fireMouseDown () { const self = this; delayFinished = true; // Set the flag to prevent other widgets from inheriting the touch event touchHandled = true; // Track movement to determine if interaction was a click self._touchMoved = false; // Simulate the mouseover event simulateMouseEvent(delayEvent, 'mouseover'); // Simulate the mousemove event simulateMouseEvent(delayEvent, 'mousemove'); // Simulate the mousedown event simulateMouseEvent(delayEvent, 'mousedown'); } /** * Handle the jQuery UI widget's touchstart events * @param {Object} event The widget element's touchstart event */ mouseProto._touchStart = function (event) { let self = this; // Interaction time this._startedMove = event.timeStamp; // Track movement to determine if interaction was a click self._startPos = getTouchCoords(event); // Ignore the event if another widget is already being handled if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) { return; } if (!delayStarted) { startDelayTimer.call(self, event); } }; /** * Handle the jQuery UI widget's touchmove events * @param {Object} event The document's touchmove event */ mouseProto._touchMove = function (event) { // if (!delayFinished) { delayStarted = false; clearTimeout(delayTimer); return; } // Ignore event if not handled if (!touchHandled) { return; } // Interaction was moved this._touchMoved = true; // Simulate the mousemove event simulateMouseEvent(event, 'mousemove'); }; /** * Handle the jQuery UI widget's touchend events * @param {Object} event The document's touchend event */ mouseProto._touchEnd = function (event) { // if (delayStarted) { clearTimeout(delayTimer); delayStarted = false; if (!delayFinished) { fireMouseDown(); } } // Ignore event if not handled if (!touchHandled) { return; } // Simulate the mouseup event simulateMouseEvent(event, 'mouseup'); // Simulate the mouseout event simulateMouseEvent(event, 'mouseout'); // If the touch interaction did not move, it should trigger a click // Check for this in two ways - length of time of simulation and distance moved // Allow for Apple Stylus to be used also let timeMoving = event.timeStamp - this._startedMove; if (!this._touchMoved || timeMoving < 500) { // Simulate the dblclick event if last click was not far away from the previous one if ( event.timeStamp - lastClickTime < 400 && Math.abs(lastClickCoord.x - this._startPos.x) < 10 && Math.abs(lastClickCoord.y - this._startPos.y) < 10) { simulateMouseEvent(event, 'dblclick'); } // Simulate the click event else simulateMouseEvent(event, 'click'); lastClickTime = event.timeStamp lastClickCoord = this._startPos; } else { let endPos = getTouchCoords(event); if ((Math.abs(endPos.x - this._startPos.x) < 10) && (Math.abs(endPos.y - this._startPos.y) < 10)) { // If the touch interaction did not move, it should trigger a click if (!this._touchMoved || event.originalEvent.changedTouches[0].touchType === 'stylus') { // Simulate the click event simulateMouseEvent(event, 'click'); } } } // Unset the flag to determine the touch movement stopped this._touchMoved = false; // Unset the flag to allow other widgets to inherit the touch event touchHandled = false; }; let _touchStartBound; let _touchMoveBound; let _touchEndBound /** * A duck punch of the $.ui.mouse _mouseInit method to support touch events. * This method extends the widget with bound touch event handlers that * translate touch events to mouse events and pass them to the widget's * original mouse event handling methods. */ mouseProto._mouseInit = function () { let self = this; // Microsoft Surface Support = remove original touch Action if ($.mspointer) { self.element[0].style.msTouchAction = 'none'; } _touchStartBound = mouseProto._touchStart.bind(self); _touchMoveBound = mouseProto._touchMove.bind(self); _touchEndBound = mouseProto._touchEnd.bind(self); // Delegate the touch handlers to the widget's element self.element.on({ touchstart: _touchStartBound, touchmove: _touchMoveBound, touchend: _touchEndBound }); // Call the original $.ui.mouse init method _mouseInit.call(self); }; /** * Remove the touch event handlers */ mouseProto._mouseDestroy = function () { let self = this; // Delegate the touch handlers to the widget's element self.element.off({ touchstart: _touchStartBound, touchmove: _touchMoveBound, touchend: _touchEndBound }); // Call the original $.ui.mouse destroy method _mouseDestroy.call(self); // clearTimeout(delayTimer); delayEvent = null }; })); ================================================ FILE: src/content/mytinytodo.js ================================================ /* This file is a part of myTinyTodo. (C) Copyright 2009-2010,2020-2025 Max Pozdeev Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ (function(){ "use strict"; var taskList = new Array(), taskOrder = new Array(); var filter = { compl:0, search:'', due:'' }; var sortOrder; //save task order before dragging var searchTimer; var objPrio = {}; var flag = { needAuth: false, isLogged: false, tagsChanged: true, readOnly: false, editFormChanged: false, firstLoad: true, dontChangeHistoryOnce: false, showTagsFromAllLists: false }; var taskCnt = { total:0, past: 0, today:0, soon:0 }; var tabLists = { _lists: {}, _length: 0, _order: [], _alltasks: {}, lastTime: 0, clear: function(){ this._lists = {}; this._length = 0; this._order = []; this._alltasks = { id:-1, showCompl:0, sort:3, name:_mtt.lang.get('alltasks') }; }, length: function(){ return this._length; }, exists: function(id){ if(this._lists[id] || id==-1) return true; else return false; }, add: function(list){ this._lists[list.id] = list; this._length++; this._order.push(list.id); }, replace: function(list){ this._lists[list.id] = list; }, get: function(id){ if(id==-1) return this._alltasks; else return this._lists[id]; }, getAll: function(){ var r = []; for(var i in this._order) { r.push(this._lists[this._order[i]]); }; return r; }, reorder: function(order){ this._order = order; } }; var curList = 0; var tagsList = []; var _mtt; /* internal alias for window.mytinytodo */ var mytinytodo = window.mytinytodo = _mtt = { theme: { newTaskFlashColor: '#ffffaa', editTaskFlashColor: '#bbffaa', deleteTaskFlashColor: '#ffaaaa', msgFlashColor: '#ffffff' }, actions: {}, menus: {}, mttUrl: '', homeUrl: '', apiUrl: '', options: { token: '', title: '', openList: 0, autotag: false, instantSearch: true, tagPreview: false, tagPreviewDelay: 700, //milliseconds ajaxAnimationDelay: 200, saveShowNotes: false, showdate: false, showdateInline: false, firstdayofweek: 1, touchDevice: false, calendarIcon: 'calendar.png', // need themeUrl+icon history: true, markdown: true, viewTaskOnClick: false, newTaskCounter: false, newTaskCounterIcon: false, }, timers: { previewtag: 0, ajaxAnimation: 0, newTaskCounter: 0, searchTags: 0, }, lang: { __lang: null, daysMin: [], daysLong: [], monthsShort: [], monthsLong: [], get: function(v) { if(this.__lang[v]) return this.__lang[v]; else return v; }, init: function(lang) { this.__lang = lang; this.daysMin = this.__lang.daysMin; this.daysLong = this.__lang.daysLong; this.monthsShort = this.__lang.monthsShort; this.monthsLong = this.__lang.monthsLong; }, isRTL: function() { return this.get('_rtl') > 0 ? true : false; } }, pages: { current: null, prev: [] }, pageDefault: { page: 'tasks', pageClass: '', lastScrollTop: 0, onOpen: function() { this.loadLists(); } }, curList: function(){ return curList; }, flag: flag, lastHistoryState: null, // procs setApiDriver: function(driver) { this.db = new driver({ useREST: false }); return this; }, init: function(options) { // required properties if (options.hasOwnProperty('lang')) { this.lang.init(options.lang); delete options.lang; } if (options.hasOwnProperty('mttUrl')) { 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?_path=/'; } if (options.hasOwnProperty('db')) { delete options.db; } if (options.hasOwnProperty('homeUrl')) { this.homeUrl = options.homeUrl; delete options.homeUrl; } else { this.homeUrl = this.mttUrl; } if ( ! options.hasOwnProperty('touchDevice') ) { this.options.touchDevice = ('ontouchend' in document); } jQuery.extend(this.options, options); if (this.options.token) { jQuery.ajaxSetup( { headers: { "MTT-Token": this.options.token } } ) } flag.needAuth = options.needAuth ? true : false; flag.isLogged = options.isLogged ? true : false; if (this.options.showdate) { $('#mtt').addClass('show-date'); } if (this.options.showdateInline) { $('#mtt').addClass('date-inline'); } // handlers $('.mtt-tabs-new-button').click(function(){ addList(); }); $('.mtt-tabs-select-button').click(function(event){ if (!_mtt.menus.selectlist) { _mtt.menus.selectlist = new mttMenu( 'slmenucontainer', { onclick:slmenuSelect, alignRight: true } ); } _mtt.menus.selectlist.show(this); }); $('#newtask_form').submit(function(){ submitNewTask(this); return false; }); $('#newtask_submit').mousedown(function(e){ e.preventDefault(); //keep the focus in #task $('#newtask_form').submit(); }); $('#newtask_adv').click(function(){ showEditForm(1); return false; }); $('#task').keydown(function(event){ if(event.keyCode == 27) { $(this).val(''); } }).focusin(function(){ $('#task_placeholder').removeClass('placeholding'); $('#toolbar').addClass('mtt-intask'); }).focusout(function(){ if('' == $(this).val()) $('#task_placeholder').addClass('placeholding'); $('#toolbar').removeClass('mtt-intask'); }); $('#search_close').click(function(){ liveSearchToggle(0); return false; }); $('#search').keyup(function(event){ if(event.keyCode == 27) return; if($(this).val() == '') $('#search_close').hide(); //actual value is only on keyup else $('#search_close').show(); if (_mtt.options.instantSearch) { clearTimeout(searchTimer); searchTimer = setTimeout(function(){searchTasks()}, 300); } }) .keydown(function(event){ if(event.keyCode == 27) { // cancel on Esc (NB: no esc event on keypress in Chrome and on keyup in Opera) if($(this).val() != '') { $(this).val(''); $('#search_close').hide(); searchTasks(); } else { liveSearchToggle(0); } return false; //need to return false in firefox (for AJAX?) } else if ( event.keyCode == 13 ) { searchTasks(1); return false; } }).focusin(function(){ $('#toolbar').addClass('mtt-insearch'); }).focusout(function(){ $('#toolbar').removeClass('mtt-insearch'); }); $('#taskview').click(function(){ if(!_mtt.menus.taskview) _mtt.menus.taskview = new mttMenu('taskviewcontainer'); _mtt.menus.taskview.show(this); }); $('#mtt-tag-filters').on('click', '.mtt-filter-close', function(){ cancelTagFilter($(this).attr('tagid')); }); $('#mtt-tag-toolbar-close').click(function(){ cancelTagFilter(0); }); $('#tagcloudbtn').click(function(){ if (flag.readOnly) { $('#tagcloudAllLists').prop('checked', false).prop('disabled', true); } else if (curList.id == -1) { $('#tagcloudAllLists').prop('checked', true).prop('disabled', true); } else { $('#tagcloudAllLists').prop('checked', flag.showTagsFromAllLists).prop('disabled', false); } if (!_mtt.menus.tagcloud) _mtt.menus.tagcloud = new mttMenu('tagcloud', { beforeShow: function(){ if (flag.tagsChanged) { $('#tagcloudcontent').html(''); $('#tagcloudload').show(); loadTags(curList.id, function() { $('#tagcloudload').hide(); document.getElementById('tagcloudSearch').value = ''; }); } }, alignRight: true, onClose: function(){ document.getElementById('tagcloudSearch').value = ''; searchTags(); } }); _mtt.menus.tagcloud.show(this); }); $('#tagcloudSearch').keyup(function(event) { if (event.keyCode == 27) return; clearTimeout(_mtt.timers.searchTags); _mtt.timers.searchTags = setTimeout(function(){searchTags()}, 400); }) .keydown(function(event){ if (event.keyCode == 27) { // Cancel on Esc if (this.value === '') return; //allow to close the popup this.value = ''; clearTimeout(_mtt.timers.searchTags); searchTags(); return false; } }) $('#tagcloudcancel').click(function(){ if(_mtt.menus.tagcloud) _mtt.menus.tagcloud.close(); }); $('#tagcloudcontent').on('click', '.tag', function(event){ //tag is not escaped addFilterTag( this.dataset.tag, this.dataset.tagId, (event.metaKey || event.ctrlKey ? true : false) ); if (_mtt.menus.tagcloud) _mtt.menus.tagcloud.close(); return false; }); $('#tagcloudAllLists').click(function(){ flag.showTagsFromAllLists = this.checked; $('#tagcloudcontent').html(''); $('#tagcloudload').show(); loadTags(curList.id, function(){ $('#tagcloudload').hide(); $('#tagcloudSearch').val(''); }); }); $('#mtt-notes-show').click(function(e){ toggleAllNotes(1, e); this.blur(); return false; }); $('#mtt-notes-hide').click(function(e){ toggleAllNotes(0, e); this.blur(); return false; }); $('#taskviewcontainer li').click(function(){ if(this.id == 'view_tasks') setTaskview(0); else if(this.id == 'view_past') setTaskview('past'); else if(this.id == 'view_today') setTaskview('today'); else if(this.id == 'view_soon') setTaskview('soon'); }); // Tabs $('#lists').on('click', 'li.mtt-tab', function(event) { var listId = this.id.split('_', 2)[1]; if (listId === 'all') listId = -1; if(event.metaKey || event.ctrlKey) { // hide the tab hideList(listId); return false; } tabSelect(listId); return false; }); $('#lists').on('click', 'li.mtt-tab .list-action', function(){ listMenu(this); return false; //stop bubble to tab click }); //Priority popup $('#priopopup .prio-neg-1').click(function(){ prioClick(-1,this); }); $('#priopopup .prio-zero').click(function(){ prioClick(0,this); }); $('#priopopup .prio-pos-1').click(function(){ prioClick(1,this); }); $('#priopopup .prio-pos-2').click(function(){ prioClick(2,this); }); $('#priopopup').mouseleave(function(){ $(this).hide()} ); // edit form handlers $('#alltags_show').click(function(){ toggleEditAllTags(1); return false; }); $('#alltags_hide').click(function(){ toggleEditAllTags(0); return false; }); $('#taskedit_form').submit(function(){ return saveTask(this); }); $('#alltags').on('click', '.tag', function(){ addEditTag(this.dataset.tag); return false; }); $("#duedate").datepicker({ dateFormat: _mtt.duedatepickerformat(), firstDay: _mtt.options.firstdayofweek, showOn: 'button', buttonImage: _mtt.options.calendarIcon, buttonImageOnly: true, constrainInput: false, duration:'', dayNamesMin:_mtt.lang.daysMin, dayNames:_mtt.lang.daysLong, monthNamesShort:_mtt.lang.monthsShort, monthNames:_mtt.lang.monthsLong, changeMonth: true, changeYear: true, isRTL: _mtt.lang.isRTL() }); function ac_split( val ) { return val.split( /,\s*/ ); } function ac_extractLast( term ) { return ac_split( term ).pop(); } $("#edittags").autocomplete({ source: function(request, response) { var taskId = document.getElementById('taskedit_form').id.value; var listId = (taskId != '') ? taskList[taskId].listId : curList.id; _mtt.db.request('suggestTags', {list:listId, q:ac_extractLast(request.term)}, function(json){ response(json); }) },/* search: function() { // custom minLength var term = ac_extractLast( this.value ); if ( term.length < 2 ) { return false; } },*/ focus: function() { // prevent value inserted on focus using keyboard return false; }, select: function( event, ui ) { var terms = ac_split( this.value ); terms.pop(); // remove the current input terms.push( ui.item.value ); // add the selected item terms.push( "" ); // add placeholder to get the comma-and-space at the end this.value = terms.join( ", " ); return false; } }); $('#taskedit_form').find('select,input,textarea').bind('change keypress', function(){ flag.editFormChanged = true; }); $('#taskviewer_edit_btn').on('click', function() { const id = document.getElementById('page_taskviewer').dataset.id; editTask(id); }); if (this.options.touchDevice) { this.options.viewTaskOnClick = true; } if (this.options.viewTaskOnClick) { $('#mtt').addClass('view-task-on-click'); } // tasklist handlers $("#tasklist").on('click', '> li.task-row .task-title', function(e) { if ( findParentNode(e.target, 'A') ) { return; //ignore clicks on links } const li = findParentNode(this, 'LI'); if (li && li.id) { if (e.altKey) { viewTask(li.dataset.id); return; } if (_mtt.options.viewTaskOnClick) { viewTask(li.dataset.id); } } }); $('#tasklist').on('dblclick', '> li.task-row .task-middle, > li.task-row .task-note-block', function(){ let id = parseInt(getLiTaskId(this)); if (id) { //clear selection if (document.selection && document.selection.empty && document.selection.createRange().text) document.selection.empty(); else if (window.getSelection) window.getSelection().removeAllRanges(); editTask(id); } }); $('#tasklist').on('click', '.taskactionbtn', function(){ var id = parseInt(getLiTaskId(this)); if(id) taskContextMenu(this, id); return false; }); $('#tasklist').on('click', 'input[type=checkbox]', function(){ var id = parseInt(getLiTaskId(this)); if(id) completeTask(id, this); //return false; }); $('#tasklist').on('click', '.task-toggle', function(){ var id = getLiTaskId(this); if(id) $('#taskrow_'+id).toggleClass('task-expanded'); return false; }); $('#tasklist').on('click', '.tag', function(event){ clearTimeout(_mtt.timers.previewtag); $('#tasklist li').removeClass('not-in-tagpreview'); //tag is not escaped addFilterTag(this.dataset.tag, this.dataset.tagId, (event.metaKey || event.ctrlKey ? true : false) ); return false; }); if(!this.options.touchDevice) { $('#tasklist').on('mouseover mouseout', '.task-prio', function(event){ var id = parseInt(getLiTaskId(this)); if(!id) return; if(event.type == 'mouseover') prioPopup(1, this, id); else prioPopup(0, this); }); } $('#tasklist').on('click', '.mtt-action-note-cancel', function(){ var id = parseInt(getLiTaskId(this)); if(id) cancelTaskNote(id); return false; }); $('#tasklist').on('click', '.mtt-action-note-save', function(){ var id = parseInt(getLiTaskId(this)); if(id) saveTaskNote(id); return false; }); if (this.options.tagPreview && !this.options.touchDevice) { $('#tasklist').on('mouseover mouseout', '.tag', function(event){ const cl = 'tag-id-' + this.dataset.tagId; const sel = (event.metaKey || event.ctrlKey) ? 'li.'+cl : 'li:not(.'+cl+')'; if (event.type == 'mouseover') { _mtt.timers.previewtag = setTimeout( function(){ $('#tasklist '+sel).addClass('not-in-tagpreview'); }, _mtt.options.tagPreviewDelay); } else { clearTimeout(_mtt.timers.previewtag); $('#tasklist li').removeClass('not-in-tagpreview'); } }); } $("#tasklist").sortable({ items: '> :not(.task-completed)', cancel: 'span,input,a,textarea,.task-note-block', delay: 150, start: tasklistSortStart, update: tasklistSortUpdated, placeholder: 'mtt-task-placeholder', cursor: 'grabbing' }); $("#lists ul").sortable({ delay: 150, update: listOrderChanged, items: '> :not(#list_all)', forcePlaceholderSize : true, placeholder: 'mtt-tab mtt-tab-sort-placeholder', cursor: 'grabbing' }); if (this.options.touchDevice) { $("#tasklist").disableSelection(); $("#tasklist").sortable('option', { axis: 'y', delay: 50, cancel: 'input', distance: 0 }); /*$('#cmenu_note').hide();*/ $("#lists ul").sortable('disable'); $("#mtt").addClass("touch-device"); } // AJAX Errors $(document).ajaxSend(function(r,s){ hideAlert(); clearTimeout(_mtt.timers.ajaxAnimation); _mtt.timers.ajaxAnimation = setTimeout( function(){ $("#mtt").addClass("ajax-loading"); }, _mtt.options.ajaxAnimationDelay ); }); $(document).ajaxStop(function(r,s){ clearTimeout(_mtt.timers.ajaxAnimation); $("#mtt").removeClass("ajax-loading"); }); $(document).ajaxError(function(event, request, settings){ var errtxt; 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 + "\n" + request.responseText; else errtxt = request.responseText; flashError(_mtt.lang.get('error'), errtxt); }); // Error Message details $("#msg>.msg-text").click(function(){ $("#msg>.msg-details").toggle(); }); // Authentication $('#login_btn').click(function(){ showLogin(); return false; }); $('#logout_btn').click(function(){ logout(); return false; }); $('#login_form').submit(function(){ doAuth(this); return false; }); // Settings $(document).on('click', 'a[data-settings-link]', function(event) { var settingsPage = this.dataset.settingsLink; if (settingsPage == 'index') { showSettings( (event.metaKey || event.ctrlKey) ? 1 : 0 ); } else if (settingsPage == 'ext-activate' || settingsPage == 'ext-deactivate') { activateExtension(settingsPage == 'ext-activate' ? true : false, this.dataset.ext); } else if (settingsPage == 'ext-index') { showExtensionSettings(this.dataset.ext); } return false; }); $("#page_ajax").on('submit', '#settings_form', function() { saveSettings(this); return false; }); $("#page_ajax").on('submit', '#ext_settings_form', function() { saveExtensionSettings(this); return false; }); $(document).on('click', '.mtt-back-button', function() { _mtt.pageBack(true); this.blur(); return false; }); $(window).bind('beforeunload', function() { if (_mtt.pages.current && _mtt.pages.current.page == 'taskedit' && flag.editFormChanged) { return _mtt.lang.get('confirmLeave'); } }); $("#page_ajax").on('click', 'a[data-ext-settings-action],button[data-ext-settings-action]', function() { extensionSettingsAction(this.dataset.extSettingsAction, this.dataset.ext); return false; }); // tab menu this.addAction('listSelected', tabmenuOnListSelected); // task context menu this.addAction('listsLoaded', cmenuOnListsLoaded); this.addAction('listRenamed', cmenuOnListRenamed); this.addAction('listAdded', cmenuOnListAdded); this.addAction('listSelected', cmenuOnListSelected); this.addAction('listOrderChanged', cmenuOnListOrderChanged); this.addAction('listHidden', cmenuOnListHidden); // select list menu this.addAction('listsLoaded', slmenuOnListsLoaded); this.addAction('listRenamed', slmenuOnListRenamed); this.addAction('listAdded', slmenuOnListAdded); this.addAction('listSelected', slmenuOnListSelected); this.addAction('listHidden', slmenuOnListHidden); //History if (this.options.history) { window.onpopstate = historyOnPopState; window.history.scrollRestoration = 'manual'; } // Appearance mode for CSS if (window.matchMedia) { document.documentElement.setAttribute('data-system-appearance', window.matchMedia("(prefers-color-scheme: dark)").matches ? 'dark' : 'light'); // TODO: use MediaQueryList.onchange since Safari 14 (macos 10.14) is min target window.matchMedia('(prefers-color-scheme: dark)').addListener(function (e) { document.documentElement.setAttribute('data-system-appearance', e.matches ? 'dark' : 'light'); }); } // Counter if (this.options.newTaskCounter /* TODO: && !flag.readOnly */) { this.addAction('listsLoaded', newTaskCounterStart); this.addAction('listSelected', newTaskCounterOnListSelected) if (this.options.newTaskCounterIcon) { this.addAction('newTaskCounterUpdated', newTaskCounterUpdated); } } this.doAction( 'init' ); return this; }, log: function(v) { console.log.apply(this, arguments); }, addAction: function(action, proc) { if(!this.actions[action]) this.actions[action] = new Array(); this.actions[action].push(proc); }, doAction: function(action, opts) { if(!this.actions[action]) return; for(var i in this.actions[action]) { this.actions[action][i](opts); } }, setOptions: function(opts) { jQuery.extend(this.options, opts); }, run: function() { var path = this.parseAnchor(); updateAccessStatus(); if (path.settings) { showSettings(path.settings == 'json' ? 1 : 0); } else if (path.search && path.list) { filter.search = path.search; this.pageSet('tasks', ''); this.loadLists(); } else { this.pageSet('tasks', ''); this.loadLists(); } }, loadLists: function() { if(filter.search != '') { //filter.search = '' will be in tabSelect $('#searchbarkeyword').text(''); $('#searchbar').hide(); } $('#page_tasks').hide(); $('#tasklist').html(''); $('#tasks_info').hide(); tabLists.clear(); this.db.loadLists(null, function(res) { var ti = ''; var openListId = 0; if (res && res.total && res.list) { // open required or last opened or first non-hidden list let list; if (_mtt.options.openList) { list = res.list.find( item => _mtt.options.openList == item.id ); } else { const lastOpenList = getLocalStorageItem('lastList'); if (lastOpenList && !flag.readOnly) { list = res.list.find( item => !item.hidden && lastOpenList == item.id ); } if (!list) { list = res.list.find( item => !item.hidden ); } } if (list) { openListId = list.id; } tabLists.lastTime = res.time; res.list.forEach( (item) => { item.lastTime = res.time; if ( item.id == -1 ) { tabLists._alltasks = item; ti += prepareListHtml(item); } else { tabLists.add(item); ti += prepareListHtml(item); } }); } if (openListId == 0) { curList = 0; } if (_mtt.options.markdown == true) { $('#mtt').addClass('markdown-enabled'); } if (tabLists.length() > 0) { $('#mtt').removeClass('no-lists'); } else { $('#mtt').addClass('no-lists'); } if (_mtt.options.openList != 0 && openListId == 0) { // cant open list - not found $('#tasks_info .v').text(_mtt.lang.get('listNotFound')) $('#tasks_info').show(); } else if (tabLists.length() == 0) { if (flag.readOnly) $('#tasks_info .v').text(_mtt.lang.get('noPublicLists')); else $('#tasks_info .v').text(_mtt.lang.get('listNotFound')) $('#tasks_info').show(); } _mtt.options.openList = 0; $('#lists .mtt-tab-selected').removeClass('mtt-tab-selected'); $('#mtt').addClass('no-list-selected'); $('#lists ul').html(ti); $('#lists').show(); _mtt.doAction('listsLoaded'); if (tabLists.length() > 0 && openListId != 0) { tabSelect(openListId); } $('#page_tasks').show(); }); }, duedatepickerformat: function() { if (!this.options.duedatepickerformat) return 'yy-mm-dd'; const s = this.options.duedatepickerformat.replace(/(.)/g, function(t,s) { switch(t) { case 'Y': return 'yy'; case 'y': return 'yy'; case 'd': return 'dd'; case 'j': return 'd'; case 'm': return 'mm'; case 'M': return 'M'; case 'n': return 'm'; case ' ': case '/': case '.': case '-': return t; default: return ''; } }); if (s == '') return 'yy-mm-dd'; return s; }, errorDenied: function() { flashError(this.lang.get('denied')); }, pageSet: function(page, pageClass) { if (this.pages.current) { var prev = this.pages.current; prev.lastScrollTop = $(window).scrollTop(); this.pages.prev.push(this.pages.current); $('#mtt').removeClass('page-' + prev.page); $('#page_'+ prev.page).removeClass('mtt-page-'+prev.page.pageClass).hide(); } $(window).scrollTop(0); this.pages.current = { page:page, pageClass:pageClass }; $('#mtt').addClass('page-' + page); $('#page_'+ this.pages.current.page).show().addClass('mtt-page-'+ this.pages.current.pageClass); }, pageBack: function(clicked) { hideAlert(); $(document).off('keydown.mttback'); // If clicked on back button in settings or taskviewer we'll use history navigation if ( clicked && this.pages.current && this.pages.prev.length > 0 && ((_mtt.pages.current.page == 'ajax' && _mtt.pages.current.pageClass == 'settings') || _mtt.pages.current.page == 'taskviewer') ) { window.history.back(); return; } if (this.pages.current.page == 'tasks') { return; } if (this.pages.current) { var prev = this.pages.current; $('#mtt').removeClass('page-' + prev.page); $('#page_'+ prev.page).removeClass('mtt-page-'+prev.pageClass); $('#page_'+ prev.page).hide(); } var cur = this.pages.prev.pop(); this.pages.current = cur ? cur : this.pageDefault; $('#mtt').addClass('page-' + this.pages.current.page); $('#page_'+ this.pages.current.page).addClass('mtt-page-'+ this.pages.current.pageClass).show(); $(window).scrollTop(this.pages.current.lastScrollTop); if (!cur && this.pages.current.onOpen) { this.pages.current.onOpen.call(this); } }, filter: { _filters: [], clear() { this._filters = []; $('#mtt-tag-toolbar').hide(); $('#mtt-tag-filters').html(''); }, addTag(tagId, tag, exclude) { //Catch 'any tag' filter if (tagId == -2) { tagId = -1; tag = '^'; exclude = true } for (const filter of this._filters) { if (filter.tagId && filter.tagId == tagId) return false; } this._filters.push({tagId:tagId, tag:tag, exclude:exclude}); if (tagId == -1) { // for display purposes only tag = exclude ? _mtt.lang.get('withAnyTag') : _mtt.lang.get('withoutTags'); exclude = false; } const tagHtml = this.prepareTagHtml(tagId, tag, ['tag-filter', 'tag-id-'+tagId, exclude ? 'tag-filter-exclude' : '']) ; $('#mtt-tag-filters').append(tagHtml); $('#mtt-tag-toolbar').show(); return true; }, cancelTag(tagId) { for (let i in this._filters) { if (this._filters[i].tagId && this._filters[i].tagId == tagId) { this._filters.splice(i, 1); $('#mtt-tag-filters .tag-filter.tag-id-'+tagId).remove(); if (this._filters.length == 0) { $('#mtt-tag-toolbar').hide(); } return true; } } return false; }, getTags(withExcluded) { let a = []; for (const filter of this._filters) { if (filter.tagId) { if (filter.exclude && withExcluded) a.push('^'+ filter.tag); else if (!filter.exclude) a.push(filter.tag) } } return a.join(', '); }, prepareTagHtml(tagId, tag, classes) { // tag is not escaped return `${escapeHtml(tag)}`; } }, parseAnchor: function() { if(location.hash == '') return false; var h = location.hash.substr(1); var a = h.split("/"); var p = {}; var s = ''; for(var i=0; i 0) { $('#lists ul').append(prepareListHtml(item)); mytinytodo.doAction('listAdded', item); } else { _mtt.loadLists(); } }); }); }; function renameCurList() { if (!curList) return; mttPrompt( _mtt.lang.get('renameList'), dehtml(curList.name), function(r) { _mtt.db.request('renameList', {list:curList.id, name:r}, function(json){ if (!parseInt(json.total)) return; var item = json.list[0]; curList = item; tabLists.replace(item); $('#list_'+curList.id).replaceWith(prepareListHtml(curList, true)); mytinytodo.doAction('listRenamed', item); }); }); }; function deleteCurList() { if (!curList) return false; mttConfirm( _mtt.lang.get('deleteList'), function() { _mtt.db.request('deleteList', {list:curList.id}, function(json){ if (!parseInt(json.total)) return; _mtt.loadLists(); }); }); }; function publishCurList() { if(!curList) return false; _mtt.db.request('publishList', { list:curList.id, publish:curList.published?0:1 }, function(json){ if(!parseInt(json.total)) return; curList.published = curList.published?0:1; if(curList.published) { $('#btnPublish').addClass('mtt-item-checked'); $('#btnRssFeed').removeClass('mtt-item-disabled'); } else { $('#btnPublish').removeClass('mtt-item-checked'); $('#btnRssFeed').addClass('mtt-item-disabled'); } }); }; function enableFeedKeyInCurList() { if (!curList) return false; _mtt.db.request('enableFeedKey', { list: curList.id, enable: (curList.feedKey === undefined || curList.feedKey === '') ? 1 : 0 }, function(json){ if (!parseInt(json.total)) return; var item = json.list[0]; curList.feedKey = item.feedKey; if (curList.feedKey) { $('#btnFeedKey').addClass('mtt-item-checked'); $('#btnShowFeedKey').removeClass('mtt-item-disabled'); mttAlert(curList.feedKey); } else { $('#btnFeedKey').removeClass('mtt-item-checked'); $('#btnShowFeedKey').addClass('mtt-item-disabled'); } }); }; function showFeedKeyInCurList() { if (!curList) return false; if (curList.feedKey === undefined || curList.feedKey === '') return false; mttAlert(curList.feedKey); }; function loadTasks(opts) { if(!curList) return false; updateSortUI(curList.sort); opts = opts || {}; if(opts.clearTasklist) { $('#tasklist').html(''); $('#total').html('0'); } _mtt.db.request('loadTasks', { list: curList.id, compl: curList.showCompl, sort: curList.sort, search: filter.search, tag: _mtt.filter.getTags(true), setCompl: opts.setCompl, saveSort: opts.saveSort }, function(json){ taskList.length = 0; taskOrder.length = 0; taskCnt.total = taskCnt.past = taskCnt.today = taskCnt.soon = 0; var tasks = ''; $.each(json.list, function(i,item){ tasks += _mtt.prepareTaskStr(item); taskList[item.id] = item; taskOrder.push(parseInt(item.id)); changeTaskCnt(item, 1); }); curList.lastTime = json.time; setNewTaskCounterForList(curList.id, 0); _mtt.doAction("newTaskCounterUpdated", curList.id); if(opts.beforeShow && opts.beforeShow.call) { opts.beforeShow(); } refreshTaskCnt(); $('#tasklist').html(tasks); }); }; function prepareListHtml(list, isSelected) { const classSelected = isSelected ? 'mtt-tab-selected' : ''; const classHidden = list.hidden ? 'mtt-tab-hidden' : ''; const liId = list.id == -1 ? 'list_all' : 'list_' + list.id; return `
  • ` + ''+ '
    '+ '' + list.name + '
    ' + '
    '+ '
  • '; } function prepareTaskStr(item, noteExp) { return '
  • ' + prepareTaskBlocks(item) + "
  • \n"; }; _mtt.prepareTaskStr = prepareTaskStr; function prepareTaskBlocks(item) { const id = item.id; let markdown = ''; if (_mtt.options.markdown == true) markdown = 'markdown-note'; return '' + '
    ' + '
    ' + '
    ' + '' + "
    \n" + '
    ' + '
    ' + '
    ' + preparePrio(item.prio,id) + '' + prepareTaskTitleInlineHtml(item.title) + ' ' + (curList.id == -1 ? prepareListNameInline(item) : '') + '' + prepareTagsStr(item) + '' + '
    ' + prepareInlineDate(item) + '
    ' + '
    ' + '
    ' + prepareDueDate(item) + "
    " + '
    ' + "
    " + '
    ' + '
    ' + '
    ' + '
    ' + prepareTaskNoteInlineHtml(item.note, item.noteText) + '
    ' + '' + '
    '; }; _mtt.prepareTaskBlocks = prepareTaskBlocks; function prepareTaskTitleInlineHtml(s) { // Task title is already escaped on back-end return s; } _mtt.prepareTaskTitleInlineHtml = prepareTaskTitleInlineHtml; function prepareListNameInline(item) { // Used in AllTasks list // List name is already escaped on back-end return ''+ item.listName +''; } _mtt.prepareListNameInline = prepareListNameInline; function prepareTaskNoteInlineHtml(s, rawText) { // Task note is already escaped on back-end return s; }; _mtt.prepareTaskNoteInlineHtml = prepareTaskNoteInlineHtml; function preparePrio(prio,id) { var cl =''; var v = ''; if(prio < 0) { cl = 'prio-neg prio-neg-'+Math.abs(prio); v = '−'+Math.abs(prio); } // − = − = − else if(prio > 0) { cl = 'prio-pos prio-pos-'+prio; v = '+'+prio; } else { cl = 'prio-zero'; v = '±0'; } // ± = ± = ± return ''+v+''; }; _mtt.preparePrio = preparePrio; function prepareTagsStr(item, delimiter = ', ') { if (!item.tags || item.tags == '') return ''; let a = item.tags.split(','); if (!a.length) return ''; const b = item.tags_ids.split(',') for (let i in a) { // tag is escaped a[i] = ''+a[i]+''; } return a.join(delimiter); }; _mtt.prepareTagsStr = prepareTagsStr; function prepareDomClassOfTags(ids) { if(!ids || ids == '') return ''; var a = ids.split(','); if(!a.length) return ''; for(var i in a) { a[i] = 'tag-id-'+a[i]; } return ' '+a.join(' '); }; _mtt.prepareDomClassOfTags = prepareDomClassOfTags; function prepareDueDate(item) { if(!item.duedate) return ''; return ''+item.dueStr+''; }; _mtt.prepareDueDate = prepareDueDate; function prepareInlineDate(item) { var inlineDate = item.dateInlineTitle; var title = item.dateFull; if (item.compl) { inlineDate = item.dateCompletedInlineTitle; title = item.dateCompletedFull; } else if ( item.isEdited && (curList.sort == 4 || curList.sort == 104) ) { inlineDate = item.dateEditedInlineTitle; title = item.dateEditedFull; } return '#' + item.id + ' ' + inlineDate + ''; } _mtt.prepareInlineDate = prepareInlineDate; function submitNewTask(form) { if(form.task.value == '') return false; _mtt.db.request('newTask', { list:curList.id, title: form.task.value, tag:_mtt.filter.getTags() }, function(json){ if(!json.total) return; $('#total').text( parseInt($('#total').text()) + 1 ); taskCnt.total++; form.task.value = ''; var item = json.list[0]; taskList[item.id] = item; taskOrder.push(parseInt(item.id)); $('#tasklist').append(_mtt.prepareTaskStr(item)); changeTaskOrder(item.id); $('#taskrow_'+item.id).effect("highlight", {color:_mtt.theme.newTaskFlashColor}, 2000); refreshTaskCnt(); }); flag.tagsChanged = true; return false; }; function changeTaskOrder(id) { id = parseInt(id); if (taskOrder.length < 2) { return; } if (id && (curList.sort == 5 || curList.sort == 105)) { // re-sort the whole list in case of database sorting is not the same due to collation changeTaskOrder(); } const oldOrder = taskOrder.slice(); function firstNonZero(order, compl, ...args) { const m = (order < 100) ? 1 : -1; if (compl != 0) return compl; for (const arg of args) { if (arg != 0) return arg * m; } return 0; } // sortByHand if (curList.sort == 0 || curList.sort == 100) { taskOrder.sort( (a, b) => firstNonZero( curList.sort, taskList[a].compl - taskList[b].compl, taskList[a].ow - taskList[b].ow )) } // sortByPrio and reverse else if (curList.sort == 1 || curList.sort == 101) { taskOrder.sort( (a, b) => firstNonZero( curList.sort, taskList[a].compl - taskList[b].compl, taskList[b].prio - taskList[a].prio, taskList[a].dueInt - taskList[b].dueInt, taskList[a].ow - taskList[b].ow )); } // sortByDueDate and reverse else if (curList.sort == 2 || curList.sort == 102) { taskOrder.sort( (a, b) => firstNonZero( curList.sort, taskList[a].compl - taskList[b].compl, taskList[a].dueInt - taskList[b].dueInt, taskList[b].prio - taskList[a].prio, taskList[a].ow - taskList[b].ow )) } // sortByDateCreated and reverse else if (curList.sort == 3 || curList.sort == 103) { taskOrder.sort( (a, b) => firstNonZero( curList.sort, taskList[a].compl - taskList[b].compl, taskList[a].dateInt - taskList[b].dateInt, taskList[b].prio - taskList[a].prio, taskList[a].ow - taskList[b].ow )); } // sortByDateModified and reverse else if (curList.sort == 4 || curList.sort == 104) { taskOrder.sort( (a, b) => firstNonZero( curList.sort, taskList[a].compl - taskList[b].compl, taskList[a].dateEditedInt - taskList[b].dateEditedInt, taskList[b].prio - taskList[a].prio, taskList[a].ow - taskList[b].ow )) } // sortByTitle and reverse else if (curList.sort == 5 || curList.sort == 105) { taskOrder.sort( (a, b) => firstNonZero( curList.sort, taskList[a].compl - taskList[b].compl, taskList[a].title.localeCompare(taskList[b].title, 'en', {sensitivity: 'base'}), taskList[b].prio - taskList[a].prio, taskList[a].ow - taskList[b].ow )) } else { return; } if (oldOrder.toString() == taskOrder.toString()) { return; } if (id && taskList[id]) { // optimization: determine where to insert task: top or after some task const indx = $.inArray(id, taskOrder); if (indx == 0) { $('#tasklist').prepend($('#taskrow_'+id)) } else { const after = taskOrder[indx-1]; $('#taskrow_' + after).after($('#taskrow_'+id)); } } else { const o = $('#tasklist'); for (const i in taskOrder) { o.append($('#taskrow_' + taskOrder[i])); } } }; function prioPopup(act, el, id) { if(act == 0) { clearTimeout(objPrio.timer); return; } var offset = $(el).offset(); $('#priopopup').css({ position: 'absolute', top: offset.top + 1, left: offset.left + 1 }); objPrio.taskId = id; objPrio.el = el; objPrio.timer = setTimeout("$('#priopopup').show()", 300); }; function prioClick(prio, el) { el.blur(); prio = parseInt(prio); $('#priopopup').fadeOut('fast'); //.hide(); setTaskPrio(objPrio.taskId, prio); }; function setTaskPrio(id, prio) { _mtt.db.request('setTaskPriority', {id:id, priority:prio}); taskList[id].prio = prio; var $t = $('#taskrow_'+id); $t.find('.task-prio').replaceWith(preparePrio(prio, id)); if (curList.sort != 0 && curList.sort != 100) changeTaskOrder(id); $t.effect("highlight", {color:_mtt.theme.editTaskFlashColor}, 'normal'); }; function setSort(v, init) { if (v < 0 || (v > 5 && v < 100) || v > 105) { return; } curList.sort = v; loadTasks({saveSort:1}); }; function updateSortUI(v) { $('#listmenucontainer .sort-item').removeClass('mtt-item-checked').children('.mtt-sort-direction').text(''); if (v == 0 || v == 100) $('#sortByHand').addClass('mtt-item-checked').children('.mtt-sort-direction').text(v==0 ? '↓' : '↑'); else if(v==1 || v==101) $('#sortByPrio').addClass('mtt-item-checked').children('.mtt-sort-direction').text(v==1 ? '↑' : '↓'); else if(v==2 || v==102) $('#sortByDueDate').addClass('mtt-item-checked').children('.mtt-sort-direction').text(v==2 ? '↑' : '↓'); else if(v==3 || v==103) $('#sortByDateCreated').addClass('mtt-item-checked').children('.mtt-sort-direction').text(v==3 ? '↓' : '↑'); else if(v==4 || v==104) $('#sortByDateModified').addClass('mtt-item-checked').children('.mtt-sort-direction').text(v==4 ? '↓' : '↑'); else if(v==5 || v==105) $('#sortByTitle').addClass('mtt-item-checked').children('.mtt-sort-direction').text(v==5 ? '↓' : '↑'); else return; curList.sort = v; if ( (v == 0 || v == 100) && !flag.readOnly) $("#tasklist").sortable('enable'); else $("#tasklist").sortable('disable'); }; function changeTaskCnt(task, dir, old) { if(dir > 0) dir = 1; else if(dir < 0) dir = -1; if(dir == 0 && old != null && task.dueClass != old.dueClass) //on saveTask { if(old.dueClass != '') taskCnt[old.dueClass]--; if(task.dueClass != '') taskCnt[task.dueClass]++; } else if(dir == 0 && old == null) //on comleteTask { if(!curList.showCompl && task.compl) taskCnt.total--; if(task.dueClass != '') taskCnt[task.dueClass] += task.compl ? -1 : 1; } if(dir != 0) { if(task.dueClass != '' && !task.compl) taskCnt[task.dueClass] += dir; taskCnt.total += dir; } }; function refreshTaskCnt() { $('#cnt_total').text(taskCnt.total); $('#cnt_past').text(taskCnt.past); $('#cnt_today').text(taskCnt.today); $('#cnt_soon').text(taskCnt.soon); if(filter.due == '') $('#total').text(taskCnt.total); else if(taskCnt[filter.due] != null) $('#total').text(taskCnt[filter.due]); }; function setTaskview(v) { if(v == 0) { if(filter.due == '') return; $('#taskview .btnstr').text(_mtt.lang.get('tasks')); $('#tasklist').removeClass('filter-'+filter.due); filter.due = ''; $('#total').text(taskCnt.total); } else if(v=='past' || v=='today' || v=='soon') { if(filter.due == v) return; else if(filter.due != '') { $('#tasklist').removeClass('filter-'+filter.due); } $('#tasklist').addClass('filter-'+v); $('#taskview .btnstr').text(_mtt.lang.get('f_'+v)); $('#total').text(taskCnt[v]); filter.due = v; } }; function toggleAllNotes(show, event) { for (let id in taskList) { if (taskList[id].note == '') continue; if (show) $('#taskrow_'+id).addClass('task-expanded'); else $('#taskrow_'+id).removeClass('task-expanded'); } curList.showNotes = show; if (_mtt.options.saveShowNotes || (event && (event.metaKey || event.ctrlKey)) ) { _mtt.db.request('setShowNotesInList', {list:curList.id, shownotes:show}, function(json){}); } }; function tabSelect(elementOrId) { let id; if (typeof elementOrId == 'number') id = elementOrId; else if(typeof elementOrId == 'string') id = parseInt(elementOrId); else { id = $(elementOrId).attr('id'); if (!id) return; id = id.split('_', 2)[1]; if (id === 'all') id = -1; } if ( !tabLists.exists(id) ) { $('#tasks_info .v').text(_mtt.lang.get('listNotFound')) $('#tasks_info').show(); $('.mtt-need-list').addClass('mtt-item-disabled'); return; } else { $('#tasks_info').hide(); $('.mtt-need-list').removeClass('mtt-item-disabled'); $('#mtt').removeClass('no-list-selected'); } var prevList = curList; curList = tabLists.get(id); $('#lists .mtt-tab-selected').removeClass('mtt-tab-selected'); if (id == -1) { $('#list_all').addClass('mtt-tab-selected').removeClass('mtt-tab-hidden'); $('#listmenucontainer .mtt-need-real-list').addClass('mtt-item-hidden'); } else { $('#list_'+id).addClass('mtt-tab-selected').removeClass('mtt-tab-hidden'); $('#listmenucontainer .mtt-need-real-list').removeClass('mtt-item-hidden'); } if (prevList.id != id) { if (id == -1) $('#mtt').addClass('show-all-tasks'); else $('#mtt').removeClass('show-all-tasks'); if (filter.search != '') liveSearchToggle(0, 1); mytinytodo.doAction('listSelected', { 'list': curList, 'prevList':prevList }); } const newTitle = dehtml(curList.name) + ' - ' + _mtt.options.title; const isFirstLoad = flag.firstLoad; //replaceHistoryState( 'list', { list:id }, _mtt.urlForList(curList), newTitle ); updateHistoryState( { list:id }, _mtt.urlForList(curList), newTitle ); if (!flag.readOnly) { setLocalStorageItem('lastList', ''+id); } if (curList.hidden && flag.readOnly != true) { curList.hidden = false; _mtt.db.request('setHideList', {list:curList.id, hide:0}); } flag.tagsChanged = true; cancelTagFilter(0, 1); setTaskview(0); if (isFirstLoad && filter.search != '') { $('#search').val(filter.search); $('#search_close').show(); searchTasks(true); } else { filter.search = ''; loadTasks({clearTasklist:1}); } }; function listMenu(el) { if(!mytinytodo.menus.listMenu) mytinytodo.menus.listMenu = new mttMenu('listmenucontainer', {onclick:listMenuClick, onhover:listMenuHover}); mytinytodo.menus.listMenu.show(el); }; function listMenuClick(el, menu) { if(!el.id) return; switch(el.id) { case 'btnAddList': addList(); break; case 'btnRenameList': renameCurList(); break; case 'btnDeleteList': deleteCurList(); break; case 'btnPublish': publishCurList(); break; case 'btnFeedKey': enableFeedKeyInCurList(); break; case 'btnShowFeedKey': showFeedKeyInCurList(); break; case 'btnHideList': hideList(curList.id); break; case 'btnExportCSV': return true; case 'btnExportICAL': return true; case 'btnRssFeed': return true; case 'btnShowCompleted': showCompletedToggle(); break; case 'btnClearCompleted': clearCompleted(); break; case 'sortByHand': setSort(curList.sort==0 ? 100 : 0); break; case 'sortByPrio': setSort(curList.sort==1 ? 101 : 1); break; case 'sortByDueDate': setSort(curList.sort==2 ? 102 : 2); break; case 'sortByDateCreated': setSort(curList.sort==3 ? 103 : 3); break; case 'sortByDateModified': setSort(curList.sort==4 ? 104 : 4); break; case 'sortByTitle': setSort(curList.sort==5 ? 105 : 5); break; } return false; }; function listMenuHover(el, menu) { if(!el.id) return; switch(el.id) { case 'btnExportCSV': $('#'+el.id+'>a').attr('href', _mtt.urlForExport('csv')) ; break; case 'btnExportICAL': $('#'+el.id+'>a').attr('href', _mtt.urlForExport('ical')) ; break; case 'btnRssFeed': $('#'+el.id+'>a').attr('href', _mtt.urlForFeed()) ; break; } } function deleteTask(id) { mttConfirm( _mtt.lang.get('confirmDelete'), function() { flag.tagsChanged = true; _mtt.db.request('deleteTask', {id:id}, function(json){ if (!parseInt(json.total)) return; var item = json.list[0]; taskOrder.splice($.inArray(id,taskOrder), 1); $('#taskrow_'+id).effect("highlight", {color:_mtt.theme.deleteTaskFlashColor}, 'normal', function(){ $(this).remove() }); changeTaskCnt(taskList[id], -1); refreshTaskCnt(); delete taskList[id]; }); }) return false; }; function completeTask(id, ch) { if(!taskList[id]) return; //click on already removed from the list while anim. effect var compl = 0; if(ch.checked) compl = 1; _mtt.db.request('completeTask', {id:id, compl:compl, list:curList.id}, function(json){ if(!parseInt(json.total)) return; var item = json.list[0]; if(item.compl) $('#taskrow_'+id).addClass('task-completed'); else $('#taskrow_'+id).removeClass('task-completed'); taskList[id] = item; changeTaskCnt(taskList[id], 0); if(item.compl && !curList.showCompl) { delete taskList[id]; taskOrder.splice($.inArray(id,taskOrder), 1); $('#taskrow_'+id).fadeOut('normal', function(){ $(this).remove() }); } else if(curList.showCompl) { $('#taskrow_'+item.id).replaceWith(_mtt.prepareTaskStr(taskList[id])); $('#taskrow_'+id).fadeOut('fast', function(){ changeTaskOrder(id); $(this).effect("highlight", {color:_mtt.theme.editTaskFlashColor}, 'normal', function(){$(this).css('display','')}); }); } refreshTaskCnt(); }); return false; }; function toggleTaskNote(id) { var aArea = '#tasknotearea'+id; if($(aArea).css('display') == 'none') { $('#notetext'+id).val(taskList[id].noteText); $(aArea).show(); $('#tasknote'+id).hide(); $('#taskrow_'+id).addClass('task-expanded'); $('#notetext'+id).focus(); } else { cancelTaskNote(id) } return false; }; function cancelTaskNote(id) { if(taskList[id].note == '') $('#taskrow_'+id).removeClass('task-expanded'); $('#tasknotearea'+id).hide(); $('#tasknote'+id).show(); return false; }; function saveTaskNote(id) { _mtt.db.request('editNote', {id:id, note:$('#notetext'+id).val()}, function(json){ if(!parseInt(json.total)) return; var item = json.list[0]; taskList[id].note = item.note; taskList[id].noteText = item.noteText; $('#tasknote'+id).html(prepareTaskNoteInlineHtml(item.note, item.noteText)); if(item.note == '') $('#taskrow_'+id).removeClass('task-has-note task-expanded'); else $('#taskrow_'+id).addClass('task-has-note task-expanded'); cancelTaskNote(id); }); return false; }; function fillTaskViewer(id) { const item = taskList[id]; if (!item) return false; $('#page_taskviewer').attr('data-id', item.id); $('#taskviewer_id').text('#' + item.id); $('#page_taskviewer .title').html(item.title); $('#page_taskviewer .note').html(item.note); $('#page_taskviewer .prio .content').html(preparePrio(item.prio,item.id)); $('#page_taskviewer .due .content').html(item.duedate); $('#page_taskviewer .tags .content').html(prepareTagsStr(item, '')); $('#page_taskviewer .list .content').text(curList.id == -1 ? item.listName : curList.name); if (item.note == '') { $('#page_taskviewer').addClass('no-note'); } else { $('#page_taskviewer').removeClass('no-note'); } return item; } function viewTask(id) { const item = fillTaskViewer(id); if (!item) return; _mtt.pageSet('taskviewer'); updateHistoryState({ task: item.id, list: item.listId }, '#task/'+item.id, dehtml(item.title) + ' - ' + dehtml(curList.name) + ' - ' + _mtt.options.title); } function editTask(id) { var item = taskList[id]; if(!item) return false; // no need to clear form var form = document.getElementById('taskedit_form'); form.task.value = item.titleText; form.note.value = item.noteText; form.id.value = item.id; form.tags.value = dehtml(item.tags).split(',').join(', '); form.duedate.value = item.duedate; form.prio.value = item.prio; $('#taskedit_id').text('#' + item.id); $('#taskedit_info .date-created-value').text(item.date).attr('title', item.dateFull);; if (item.isEdited && !item.compl) { $('#taskedit_info .date-edited-value').text(item.dateEdited).attr('title', item.dateEditedFull); $('#taskedit_info .date-edited').show() } else { $('#taskedit_info .date-edited').hide(); } if (item.compl) { $('#taskedit_info .date-completed-value').text(item.dateCompleted).attr('title', item.dateCompletedFull);; $('#taskedit_info .date-completed').show() } else { $('#taskedit_info .date-completed').hide(); } toggleEditAllTags(0); showEditForm(); return false; }; function clearEditForm() { var form = document.getElementById('taskedit_form'); form.task.value = ''; form.note.value = ''; form.tags.value = ''; form.duedate.value = ''; form.prio.value = '0'; form.id.value = ''; toggleEditAllTags(0); }; function showEditForm(isAdd) { let form = document.getElementById('taskedit_form'); if (isAdd) { clearEditForm(); $('#page_taskedit').removeClass('mtt-inedit').addClass('mtt-inadd'); form.isadd.value = 1; if (_mtt.options.autotag) form.tags.value = _mtt.filter.getTags(); if ($('#task').val() != '') { _mtt.db.request('parseTaskStr', { list:curList.id, title:$('#task').val(), tag:_mtt.filter.getTags() }, function(json){ if(!json) return; form.task.value = json.title form.tags.value = (form.tags.value != '') ? form.tags.value +', '+ json.tags : json.tags; form.prio.value = json.prio; form.duedate.value = json.duedate; $('#task').val(''); }); } } else { $('#page_taskedit').removeClass('mtt-inadd').addClass('mtt-inedit'); form.isadd.value = 0; } $(document).on('keydown.mttback', function(event) { if (event.keyCode == 27) { //Esc pressed _mtt.pageBack(true); } }); flag.editFormChanged = false; _mtt.pageSet('taskedit'); }; function saveTask(form) { $("#edittags").autocomplete('close'); if (flag.readOnly) return false; if (form.isadd.value != 0) return submitFullTask(form); let duedate = $("#duedate").datepicker('getDate'); if (duedate) { duedate = duedate.getFullYear() + '-' + (duedate.getMonth() + 1) + '-' + duedate.getDate(); } _mtt.db.request('editTask', {id:form.id.value, title: form.task.value, note:form.note.value, prio:form.prio.value, tags:form.tags.value, duedate:duedate}, function(json) { if (!parseInt(json.total)) return; const item = json.list[0]; changeTaskCnt(item, 0, taskList[item.id]); taskList[item.id] = item; const noteExpanded = (item.note != '' && $('#taskrow_'+item.id).is('.task-expanded')) ? 1 : 0; $('#taskrow_'+item.id).replaceWith(_mtt.prepareTaskStr(item, noteExpanded)); if (curList.sort != 0 && curList.sort != 100) { changeTaskOrder(item.id); } refreshTaskCnt(); _mtt.pageBack(); //back to list or viewer if (_mtt.pages.current.page == 'taskviewer') { fillTaskViewer(item.id); } else { $('#taskrow_'+item.id).effect("highlight", {color:_mtt.theme.editTaskFlashColor}, 'normal', function(){$(this).css('display','')}); } }); flag.tagsChanged = true; return false; }; function toggleEditAllTags(show) { if (show) { if (curList.id == -1) { const taskId = document.getElementById('taskedit_form').id.value; loadTags(taskList[taskId].listId, fillEditAllTags); } else if (flag.tagsChanged) loadTags(curList.id, fillEditAllTags); else fillEditAllTags(); showhide($('#alltags_hide'), $('#alltags_show')); } else { $('#alltags').hide(); showhide($('#alltags_show'), $('#alltags_hide')) } }; function fillEditAllTags() { const a = []; tagsList.forEach( (item) => { a.push('' + item.tag + ''); }); const content = (a.length == 0) ? _mtt.lang.get('noTags') : a.join(''); $('#alltags').html(content); $('#alltags').show(); }; function addEditTag(tag) { var v = $('#edittags').val(); if(v == '') { $('#edittags').val(tag); return; } var r = v.search(new RegExp('(^|,)\\s*'+tag+'\\s*(,|$)')); if(r < 0) $('#edittags').val(v+', '+tag); }; function loadTags(listId, callback) { if (flag.showTagsFromAllLists) listId = -1; _mtt.db.request('tagCloud', {list:listId}, function(json){ if (!parseInt(json.total)) tagsList = []; else tagsList = json.items; flag.tagsChanged = false; _mtt.doAction('tagsLoaded', tagsList); setTagcloudContent(tagsList); callback(); }); }; function setTagcloudContent(tags, isFiltered = false) { let cloud = ''; tags.forEach( item => { // item.tag is escaped with htmlspecialchars() cloud += ` ${item.tag}`; }); if (cloud == '') { cloud = _mtt.lang.get('noTags'); } else if (!isFiltered) { cloud = `${_mtt.lang.get('withoutTags')}` + `${_mtt.lang.get('withAnyTag')}` + cloud; } $('#tagcloudcontent').html(cloud) } function cancelTagFilter(tagId, dontLoadTasks) { if(tagId) _mtt.filter.cancelTag(tagId); else _mtt.filter.clear(); if(dontLoadTasks==null || !dontLoadTasks) loadTasks(); }; function addFilterTag(tag, tagId, exclude) { if (!_mtt.filter.addTag(tagId, tag, exclude)) return false; loadTasks(); }; function searchTags() { const filter = document.getElementById('tagcloudSearch').value.toLocaleLowerCase(); if (filter === '') { setTagcloudContent(tagsList); return; } const filtered = []; tagsList.forEach( item => { if (item.tagText.toLocaleLowerCase().search(filter) === -1) return; filtered.push(item); }); setTagcloudContent(filtered, true); } function liveSearchToggle(toSearch, dontLoad) { if(toSearch) { $('#search').focus(); } else { if($('#search').val() != '') { filter.search = ''; $('#search').val(''); $('#searchbarkeyword').text(''); $('#searchbar').hide(); $('#search_close').hide(); if(!dontLoad) loadTasks(); } $('#search').blur(); } }; function searchTasks(force) { var newkeyword = $('#search').val(); if(newkeyword == filter.search && !force) return false; filter.search = newkeyword; if (filter.search != '') { $('#searchbarkeyword').text(filter.search); $('#searchbar').fadeIn('fast'); } else $('#searchbar').fadeOut('fast'); loadTasks(); return false; }; function submitFullTask(form) { if(flag.readOnly) return false; let duedate = $("#duedate").datepicker('getDate'); if (duedate) { duedate = duedate.getFullYear() + '-' + (duedate.getMonth() + 1) + '-' + duedate.getDate(); } _mtt.db.request( 'fullNewTask', { list: curList.id, tag: _mtt.filter.getTags(), title: form.task.value, note: form.note.value, prio: form.prio.value, tags: form.tags.value, duedate: duedate }, function(json) { if (!parseInt(json.total)) return; form.task.value = ''; var item = json.list[0]; taskList[item.id] = item; taskOrder.push(parseInt(item.id)); curList.lastTaskCreatedTime = item.dateInt; $('#tasklist').append(_mtt.prepareTaskStr(item)); changeTaskOrder(item.id); _mtt.pageBack(); $('#taskrow_'+item.id).effect("highlight", {color:_mtt.theme.newTaskFlashColor}, 2000); changeTaskCnt(item, 1); refreshTaskCnt(); } ); flag.tagsChanged = true; return false; }; function tasklistSortStart(event, ui) { // remember initial order before sorting sortOrder = $(this).sortable('toArray'); }; function tasklistSortUpdated(event, ui) { if (!ui.item[0]) { return; } const itemId = ui.item[0].id; const n = $(this).sortable('toArray'); // remove possible empty id's for (let i = 0; i < sortOrder.length; i++) { if (sortOrder[i] == '') { sortOrder.splice(i,1); i--; } } if (n.toString() == sortOrder.toString()) { return; } // make index: id=>position const posBefore = {}; for (let j = 0; j < sortOrder.length; j++) { posBefore[sortOrder[j]] = j; } const posAfter = {}; for (let j = 0; j < n.length; j++) { posAfter[n[j]] = j; taskOrder[j] = parseInt(n[j].split('_')[1]); } // prepare params const o = []; const newWeight = taskList[sortOrder[posAfter[itemId]].split('_')[1]].ow; let diff; for (const j in posBefore) { diff = posAfter[j] - posBefore[j]; // depends on position if (curList.sort == 100) { diff *= -1; } if (diff != 0) { const taskId = j.split('_')[1]; if (j == itemId) { diff = newWeight - taskList[taskId].ow; // just for new weight } o.push({id:taskId, diff:diff}); taskList[taskId].ow += diff; } } _mtt.db.request('changeOrder', {order:o}); }; function mttMenu(container, options) { var menu = this; this.container = document.getElementById(container); this.$container = $(this.container); this.isOpen = false; this.options = options || {}; this.submenu = []; this.curSubmenu = null; this.showTimer = null; this.ts = (new Date).getTime(); this.container.mttmenu = this.ts; if (!this.options.hasOwnProperty('isRTL')) { this.options.isRTL = ($('body').css('direction') == 'rtl') ? true : false; } if (!this.options.hasOwnProperty('alignRight')) { this.options.alignRight = false; } if (!this.options.hasOwnProperty('adjustWidth')) { this.options.adjustWidth = true; } this.$container.find('li').click(function(){ var r = menu.onclick(this, menu); return (typeof r === 'undefined') ? false : r; }) .each(function(){ var submenu = 0; if($(this).is('.mtt-menu-indicator')) { submenu = new mttMenu($(this).attr('submenu'), menu.options); submenu.$caller = $(this); submenu.parent = menu; if(menu.root) submenu.root = menu.root; //!! be careful with circular references else submenu.root = menu; menu.submenu.push(submenu); submenu.ts = submenu.container.mttmenu = submenu.root.ts; } $(this).hover( function(){ if(!$(this).is('.mtt-menu-item-active')) menu.$container.find('li').removeClass('mtt-menu-item-active'); clearTimeout(menu.showTimer); if(menu.hideTimer && menu.parent) { clearTimeout(menu.hideTimer); menu.hideTimer = null; menu.$caller.addClass('mtt-menu-item-active'); clearTimeout(menu.parent.showTimer); } if(menu.curSubmenu && menu.curSubmenu.isOpen && menu.curSubmenu != submenu && !menu.curSubmenu.hideTimer) { menu.$container.find('li').removeClass('mtt-menu-item-active'); var curSubmenu = menu.curSubmenu; curSubmenu.hideTimer = setTimeout(function(){ curSubmenu.hide(); curSubmenu.hideTimer = null; }, 300); } if (menu.options.onhover) menu.options.onhover(this, menu); if(!submenu || menu.curSubmenu == submenu && menu.curSubmenu.isOpen) return; menu.showTimer = setTimeout(function(){ menu.curSubmenu = submenu; submenu.showSub(); }, 400); }, function(){} ); }); this.onclick = function(item, fromMenu) { if ($(item).is('.mtt-item-disabled,.mtt-menu-indicator,.mtt-item-hidden')) return; var r = undefined; if (this.options.onclick) r = this.options.onclick(item, fromMenu); if (menu.root) menu.root.close(); else menu.close(); return r; }; this.hide = function() { for(var i in this.submenu) this.submenu[i].hide(); clearTimeout(this.showTimer); this.$container.hide(); this.$container.find('li').removeClass('mtt-menu-item-active'); this.isOpen = false; }; this.close = function(event) { if(!this.isOpen) return; if(event) { // ignore if event (click) was on caller or container var t = event.target; if(t == this.caller || (t.mttmenu && t.mttmenu == this.ts)) return; while(t.parentNode) { if(t.parentNode == this.caller || (t.mttmenu && t.mttmenu == this.ts)) return; t = t.parentNode; } } this.hide(); $(this.caller).removeClass('mtt-menu-button-active'); $(document).off('mousedown.mttmenu'); $(document).off('keydown.mttmenu'); // onClose trigger if(this.options.onClose && this.options.onClose.call) { this.options.onClose.call(this); } }; this.show = function(caller) { if(this.isOpen) { this.close(); if(this.caller && this.caller == caller) return; } $(document).triggerHandler('mousedown.mttmenu'); //close any other open menu $(document).on('keydown.mttmenu', function(event) { if (event.keyCode == 27) { menu.close(); //close the menu on Esc pressed } }); this.caller = caller; var $caller = $(caller); // beforeShow trigger if(this.options.beforeShow && this.options.beforeShow.call) { this.options.beforeShow.call(this); } // adjust width if (this.options.adjustWidth) { this.$container.width(''); this.$container.removeClass('mtt-left-adjusted mtt-right-adjusted'); if ( this.$container.outerWidth(true) > $(window).width() ) { this.$container.addClass('mtt-left-adjusted mtt-right-adjusted'); this.$container.width( $(window).width() - (this.$container.outerWidth(true) - this.$container.width()) ); } } //round the width to avoid overflow issues this.$container.width( Math.ceil(this.$container.width()) ); $caller.addClass('mtt-menu-button-active'); var offset = $caller.offset(); var containerWidth = this.$container.outerWidth(true); var alignRight = this.options.isRTL ^ this.options.alignRight; //alignRight is not for submenu var x2 = $(window).width() + $(document).scrollLeft() - containerWidth - 1; // TODO: rtl? var x = alignRight ? offset.left + $caller.outerWidth() - containerWidth : offset.left; if (x > x2) { x = x2; //move left if container overflows right edge this.$container.addClass('mtt-right-adjusted'); } if (x < 0) { x = 0; //do not cross left edge this.$container.addClass('mtt-left-adjusted'); } var y = offset.top + caller.offsetHeight - 1; if(y + this.$container.outerHeight(true) > $(window).height() + $(document).scrollTop()) y = offset.top - this.$container.outerHeight(); if(y<0) y=0; this.$container.css({ position: 'absolute', top: y, left: x, width:this.$container.width() /*, 'min-width': $caller.width()*/ }).show(); var menu = this; $(document).on('mousedown.mttmenu', function(e) { menu.close(e) }); this.isOpen = true; }; this.showSub = function() { // adjust width if (this.options.adjustWidth) { this.$container.width(''); this.$container.removeClass('mtt-left-adjusted mtt-right-adjusted'); if ( this.$container.outerWidth(true) > $(window).width() ) { this.$container.addClass('mtt-left-adjusted mtt-right-adjusted'); this.$container.width( $(window).width() - (this.$container.outerWidth(true) - this.$container.width()) ); } } //round the width to avoid overflow issues this.$container.width( Math.ceil(this.$container.width()) ); this.$caller.addClass('mtt-menu-item-active'); var offset = this.$caller.offset(); var containerWidth = this.$container.outerWidth(true); var x = 0; if (this.options.isRTL) { x = offset.left - containerWidth - 1; if (x < 0) { x = offset.left + this.$caller.outerWidth(); } if ( x + containerWidth > $(window).width() + $(document).scrollLeft() ) { x = $(window).width() + $(document).scrollLeft() - containerWidth; // TODO: rtl? this.$container.addClass('mtt-right-adjusted'); } } else { x = offset.left + this.$caller.outerWidth(); if ( x + containerWidth > $(window).width() + $(document).scrollLeft() ) { // TODO: rtl? x = offset.left - containerWidth - 1; } if (x < 0) { x = 0; this.$container.addClass('mtt-left-adjusted'); } } var y = offset.top + this.parent.$container.offset().top-this.parent.$container.find('li:first').offset().top; if(y + this.$container.outerHeight(true) > $(window).height() + $(document).scrollTop()) y = $(window).height() + $(document).scrollTop()- this.$container.outerHeight(true) - 1; if(y<0) y=0; this.$container.css({ position: 'absolute', top: y, left: x, width:this.$container.width() /*, 'min-width': this.$caller.outerWidth()*/ }).show(); this.isOpen = true; }; this.destroy = function() { for(var i in this.submenu) { this.submenu[i].destroy(); delete this.submenu[i]; } this.$container.find('li').unbind(); //'click mouseenter mouseleave' }; }; function taskContextMenu(el, id) { if(!_mtt.menus.cmenu) _mtt.menus.cmenu = new mttMenu('taskcontextcontainer', { onclick: taskContextClick, beforeShow: function() { var taskId = this.tag; $('#taskrow_'+taskId).addClass('menu-active'); $('#cmenupriocontainer li').removeClass('mtt-item-checked'); $('#cmenu_prio\\:'+ taskList[taskId].prio).addClass('mtt-item-checked'); }, onClose: function() { $('#tasklist li').removeClass('menu-active'); }, alignRight: true }); _mtt.menus.cmenu.tag = id; _mtt.menus.cmenu.show(el); }; function taskContextClick(el, menu) { if(!el.id) return; var taskId = parseInt(_mtt.menus.cmenu.tag); var id = el.id, value; var a = id.split(':'); if(a.length == 2) { id = a[0]; value = a[1]; } switch(id) { case 'cmenu_edit': editTask(taskId); break; /*case 'cmenu_note': toggleTaskNote(taskId); break;*/ case 'cmenu_delete': deleteTask(taskId); break; case 'cmenu_prio': setTaskPrio(taskId, parseInt(value)); break; case 'cmenu_list': if(menu.$caller && menu.$caller.attr('id')=='cmenu_move') moveTaskToList(taskId, value); break; } }; function moveTaskToList(taskId, listId) { if(curList.id == listId) return; _mtt.db.request('moveTask', {id:taskId, from:curList.id, to:listId}, function(json){ if(!parseInt(json.total)) return; if(curList.id == -1) { // leave the task in current tab (all tasks tab) var item = json.list[0]; changeTaskCnt(item, 0, taskList[item.id]); taskList[item.id] = item; var noteExpanded = (item.note != '' && $('#taskrow_'+item.id).is('.task-expanded')) ? 1 : 0; $('#taskrow_'+item.id).replaceWith(_mtt.prepareTaskStr(item, noteExpanded)); if (curList.sort != 0 && curList.sort != 100) { changeTaskOrder(item.id); } refreshTaskCnt(); $('#taskrow_'+item.id).effect("highlight", {color:_mtt.theme.editTaskFlashColor}, 'normal', function(){$(this).css('display','')}); } else { // remove the task from currrent tab changeTaskCnt(taskList[taskId], -1) delete taskList[taskId]; taskOrder.splice($.inArray(taskId,taskOrder), 1); $('#taskrow_'+taskId).fadeOut('normal', function(){ $(this).remove() }); refreshTaskCnt(); } }); flag.tagsChanged = true; }; function cmenuOnListsLoaded() { if(_mtt.menus.cmenu) _mtt.menus.cmenu.destroy(); _mtt.menus.cmenu = null; var s = ''; var all = tabLists.getAll(); for(var i in all) { s += '
  • '+all[i].name+'
  • '; } $('#cmenulistscontainer ul').html(s); }; function cmenuOnListAdded(list) { if(_mtt.menus.cmenu) _mtt.menus.cmenu.destroy(); _mtt.menus.cmenu = null; $('#cmenulistscontainer ul').append('
  • '+list.name+'
  • '); }; function cmenuOnListRenamed(list) { $('#cmenu_list\\:'+list.id).text(list.name); }; function cmenuOnListSelected(a) { const list = a.list; $('#cmenulistscontainer li').removeClass('mtt-item-disabled'); $('#cmenu_list\\:'+list.id).addClass('mtt-item-disabled').removeClass('mtt-list-hidden'); }; function cmenuOnListOrderChanged() { cmenuOnListsLoaded(); $('#cmenu_list\\:'+curList.id).addClass('mtt-item-disabled'); }; function cmenuOnListHidden(list) { if (list.id == -1) return; $('#cmenu_list\\:'+list.id).addClass('mtt-list-hidden'); }; function tabmenuOnListSelected(a) { const list = a.list; if (list.published) { $('#btnPublish').addClass('mtt-item-checked'); $('#btnRssFeed').removeClass('mtt-item-disabled'); } else { $('#btnPublish').removeClass('mtt-item-checked'); $('#btnRssFeed').addClass('mtt-item-disabled'); } if (list.showCompl) { $('#btnShowCompleted').addClass('mtt-item-checked'); } else { $('#btnShowCompleted').removeClass('mtt-item-checked'); } if (list.feedKey !== undefined && list.feedKey !== '') { $('#btnFeedKey').addClass('mtt-item-checked'); $('#btnShowFeedKey').removeClass('mtt-item-disabled'); } else { $('#btnFeedKey').removeClass('mtt-item-checked'); $('#btnShowFeedKey').addClass('mtt-item-disabled'); } }; function listOrderChanged(event, ui) { var a = $(this).sortable("toArray"); var order = []; for(var i in a) { order.push(a[i].split('_')[1]); } tabLists.reorder(order); _mtt.db.request('changeListOrder', {order:order}); _mtt.doAction('listOrderChanged', {order:order}); }; function showCompletedToggle() { var act = curList.showCompl ? 0 : 1; curList.showCompl = tabLists.get(curList.id).showCompl = act; if(act) $('#btnShowCompleted').addClass('mtt-item-checked'); else $('#btnShowCompleted').removeClass('mtt-item-checked'); loadTasks({setCompl:1}); }; function clearCompleted() { if (!curList) return false; mttConfirm( _mtt.lang.get('clearCompleted'), function() { _mtt.db.request('clearCompletedInList', {list:curList.id}, function(json){ if(!parseInt(json.total)) return; flag.tagsChanged = true; if(curList.showCompl) loadTasks(); }); }); }; function showhide(a,b) { a.show(); b.hide(); }; function findParentNode(el, node) { // in html nodename is in uppercase, in xhtml nodename in in lowercase if (el.nodeName.toUpperCase() == node) return el; while (el.parentNode) { el = el.parentNode; if (el.nodeName.toUpperCase() == node) return el; } return null; }; function getLiTaskId(el) { var li = findParentNode(el, 'LI'); if(!li || !li.id) return 0; return li.id.split('_',2)[1]; }; function isParentId(el, id) { if(el.id && $.inArray(el.id, id) != -1) return true; if(!el.parentNode) return null; return isParentId(el.parentNode, id); }; function dehtml(str) { return str.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); }; function escapeHtml(str) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return str.replace(/[&<>"']/g, (m) => map[m]); } function slmenuOnListsLoaded() { if (_mtt.menus.selectlist) { _mtt.menus.selectlist.destroy(); _mtt.menus.selectlist = null; } let s = ''; tabLists.getAll().forEach( (list) => { const classChecked = (list.id == curList.id) ? 'mtt-item-checked' : ''; const classHidden = list.hidden ? 'mtt-list-hidden' : ''; s += `
  • ${list.name}
  • `; }) $('#slmenucontainer ul>.slmenu-lists-begin').nextAll().remove(); $('#slmenucontainer ul>.slmenu-lists-begin').after(s); }; function slmenuOnListRenamed(list) { $('#slmenucontainer li.list-id-'+list.id).find('a').html(list.name); }; function slmenuOnListAdded(list) { if(_mtt.menus.selectlist) { _mtt.menus.selectlist.destroy(); _mtt.menus.selectlist = null; } $('#slmenucontainer ul').append(`
  • ${list.name}
  • `); }; function slmenuOnListSelected(a) { const list = a.list; $('#slmenucontainer li').removeClass('mtt-item-checked'); $('#slmenucontainer li.list-id-'+list.id).addClass('mtt-item-checked').removeClass('mtt-list-hidden'); }; function slmenuOnListHidden(list) { if (list.id == -1) return; $('#slmenucontainer li.list-id-'+list.id).addClass('mtt-list-hidden'); }; function slmenuSelect(el, menu) { if(!el.id) return; var id = el.id, value; var a = id.split(':'); if(a.length == 2) { id = a[0]; value = a[1]; } if(id == 'slmenu_list') { tabSelect(parseInt(value)); } return false; }; function hideList(listId) { if (typeof listId === 'string') { listId = parseInt(listId); } else if (typeof listId !== 'number') { return; } if(!tabLists.get(listId)) return false; // if we hide current tab var listIdToSelect = 0; if(curList.id == listId) { var all = tabLists.getAll(); for(var i in all) { if(all[i].id != curList.id && !all[i].hidden) { listIdToSelect = all[i].id; break; } } // do not hide the tab if others are hidden if(!listIdToSelect) return false; } if(listId == -1) { $('#list_all').addClass('mtt-tab-hidden').removeClass('mtt-tab-selected'); } else { $('#list_'+listId).addClass('mtt-tab-hidden').removeClass('mtt-tab-selected'); } tabLists.get(listId).hidden = true; _mtt.db.request('setHideList', {list:listId, hide:1}); _mtt.doAction('listHidden', tabLists.get(listId)); if(listIdToSelect) { tabSelect(listIdToSelect); } } function getLocalStorageItem(key) { try { return localStorage.getItem(key); } catch (e) { console.log(e); } return null; } function setLocalStorageItem(key, value) { try { localStorage.setItem(key, value); } catch (e) { console.log(e); } } function newTaskCounterStart() { clearInterval(_mtt.timers.newTaskCounter); _mtt.timers.newTaskCounter = setInterval(newTaskCounter, 60*1000); //every 60 sec } function newTaskCounter() { const params = { list: curList.id, later: curList.lastTime, showCompl: curList.showCompl, lists: [], }; tabLists.getAll().forEach( (list) => { if (list.hidden || list.id == -1 || list.id == curList.id) { return; } params.lists.push({ listId: list.id, later: list.lastTime }); }); _mtt.db.request("newTaskCounter", params, json => { if (json && json.ok) { let counters = {}; let curCounter = 0; if (Array.isArray(json.tasks)) { json.tasks.forEach((id) => { if (!taskList[id]) { curCounter++; } }); } counters[curList.id] = curCounter; if (Array.isArray(json.lists)) { json.lists.forEach((item) => { counters["" + item.listId] = +item.counter; }); } tabLists.getAll().forEach( (list) => { if (!list.hidden || list.id != -1) { setNewTaskCounterForList(list.id, counters[list.id]); } }); _mtt.doAction("newTaskCounterUpdated"); } }); } function setNewTaskCounterForList(listId, counter) { const list = tabLists.get(listId); if (!list) return; if (list.newTaskCounterOld) { counter += list.newTaskCounterOld; } if (counter > 0) { $('#list_' + listId).find('.counter').text(counter).removeClass('hidden'); $('#slmenucontainer li.list-id-' + listId).find('.counter').text(counter).removeClass('hidden'); list.newTaskCounter = counter; } else { $('#list_' + listId).find('.counter').text('').addClass('hidden'); $('#slmenucontainer li.list-id-' + listId).find('.counter').text('').addClass('hidden'); list.newTaskCounter = 0; } } function newTaskCounterOnListSelected(a) { if (a.prevList && a.prevList.newTaskCounter) { a.prevList.newTaskCounterOld = a.prevList.newTaskCounter; } } /** * Set favicon with number of new tasks */ function newTaskCounterUpdated() { // Calc a total number of new tasks in visible tabs let total = 0; tabLists.getAll().forEach( (list) => { if (list.newTaskCounter && (!list.hidden || list.id != -1)) { total += list.newTaskCounter; } }); // Restore original icon if (total <= 0) { const o = document.querySelectorAll('link[rel="icon"]'); let oType, oHref; for (let i = 0; i < o.length; i++) { if (o[i].dataset.ohref) { oHref = o[i].dataset.ohref; oType = o[i].dataset.otype; o[i].parentNode.removeChild(o[i]); } } if (oHref) { const n = document.createElement('link'); n.setAttribute('rel', 'icon'); n.setAttribute('href', oHref); n.setAttribute('type', oType); document.querySelector('head').appendChild(n); } return; } // Draw new icon const c = document.createElement('canvas'); c.height = c.width = 64; const ctx = c.getContext('2d'); //filled circle in center ctx.lineWidth = 4; ctx.fillStyle = '#ff0000'; ctx.beginPath(); ctx.arc(c.width / 2, c.height / 2, c.width / 2 - 2, 0, 2 * Math.PI, false); ctx.fill(); //number in center ctx.font = '48px Helvetica'; ctx.fillStyle = '#ffffff'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText( total > 9 ? '9+' : total, 32, 32, 50); // Save params of original icon const o = document.querySelectorAll('link[rel="icon"]'); let oType, oHref; for (let i = 0; i < o.length; i++) { oHref = o[i].dataset.ohref; oType = o[i].dataset.otype; if (!oHref) { oHref = o[i].getAttribute('href'); oType = o[i].getAttribute('type') || ''; } o[i].parentNode.removeChild(o[i]); } // Set new icon const n = document.createElement('link'); n.setAttribute('rel', 'icon'); n.setAttribute('href', c.toDataURL()); //"data:image/png;base64,......" if (oHref) { n.dataset.ohref = oHref; n.dataset.otype = oType; } document.querySelector('head').appendChild(n); } /* Errors and Info messages */ function flashError(str, details) { if (details === undefined) details = ''; $("#msg>.msg-text").text(dehtml(str)) $("#msg>.msg-details").text(dehtml(details)); $("#loading").hide(); $("#msg").addClass('mtt-error').effect("highlight", {color:_mtt.theme.msgFlashColor}, 700); } function flashInfo(str, details) { if (details === undefined) details = ''; $("#msg>.msg-text").text(dehtml(str)) $("#msg>.msg-details").text(dehtml(details)); $("#loading").hide(); $("#msg").addClass('mtt-info').effect("highlight", {color:_mtt.theme.msgFlashColor}, 700); } function hideAlert() { $("#msg>.msg-text").text(''); $("#msg>.msg-details").text(''); $("#msg").hide().removeClass('mtt-error mtt-info').find('.msg-details').hide(); } /* Authorization */ function updateAccessStatus() { // flag.needAuth is not changed after pageload if(flag.needAuth) { if (flag.isLogged) { showhide( $("#logout_btn"), $("#login_btn") ); } else { showhide( $("#login_btn"), $("#logout_btn") ); } } else { $('#mtt').addClass('no-need-auth'); } if(flag.needAuth && !flag.isLogged) { flag.readOnly = true; $("#bar_public").show(); $('#mtt').addClass('readonly') liveSearchToggle(1); // remove some tab menu items $('#btnRenameList,#btnDeleteList,#btnClearCompleted,#btnPublish').remove(); } else { flag.readOnly = false; $('#mtt').removeClass('readonly') $("#bar_public").hide(); liveSearchToggle(0); } $('#page_ajax').hide(); } function showLogin() { if (_mtt.pages.current && _mtt.pages.current.page == 'login') { return false; } _mtt.pageSet('login', ''); $('#password').val('').focus(); } function doAuth(form) { _mtt.db.request( 'login', { password: form.password.value }, function(json) { form.password.value = ''; if(json.logged) { flag.isLogged = true; window.location.hash = ''; window.location.reload(); } else { flashError(_mtt.lang.get('invalidpass')); $('#password').focus(); } }); } function logout() { _mtt.db.request( 'logout', {}, function(json) { flag.isLogged = false; window.location.hash = ''; window.location.reload(); }); return false; } /* Settings */ function showSettings(json = 0) { let reload = false; if (_mtt.pages.current && _mtt.pages.current.page == 'ajax' && _mtt.pages.current.pageClass == 'settings') { reload = true; } const jsonParam = (json == 1) ? '&json=1' : ''; $('#page_ajax').load(_mtt.mttUrl + 'settings.php?ajax=yes' + jsonParam, null, function(){ if (!reload) { _mtt.pageSet('ajax','settings'); const newTitle = _mtt.lang.get('set_header') + ' - ' + _mtt.options.title; updateHistoryState( { settings:1, settingsJson:json }, _mtt.urlForSettings(json), newTitle ); _mtt.doAction('settingsLoaded'); } }) } function saveSettings(frm) { if(!frm) return false; var params = { save:'ajax' }; $(frm).find("input:hidden,input:text,input:password,input:checked,select,textarea").filter(":enabled").each(function() { params[this.name || '__'] = this.value; }); $(frm).find(":submit").attr('disabled','disabled').blur(); $.post(_mtt.mttUrl+'settings.php', params, function(json){ if(json.saved) { flashInfo(_mtt.lang.get('settingsSaved')); setTimeout( function(){ window.location.assign(_mtt.homeUrl); //window.location.reload(); }, 1000); } }, 'json'); } function activateExtension(activate, ext) { var params = { 'activate': activate ? 1 : 0, 'ext': ext } $.post(_mtt.mttUrl+'settings.php', params, function(json){ if(json.saved) { flashInfo(_mtt.lang.get('settingsSaved')); showSettings(0); } }, 'json'); } function showExtensionSettings(ext, callback, reload) { if (_mtt.pages.current && _mtt.pages.current.page == 'ajax' && _mtt.pages.current.pageClass == 'settings') { $('#page_ajax').load(_mtt.apiUrl + 'ext-settings/' + ext, null, function() { if (callback) callback(); if (!reload) { _mtt.pageSet('ajax','settings'); const newTitle = `${ext} - ${_mtt.lang.get('set_header')} - ${_mtt.options.title}`; replaceHistoryState('extSettings', { extSettings:true, ext:ext }, _mtt.urlForExtSettings(ext), newTitle ); } }); } } function saveExtensionSettings(frm) { if (!frm) return false; var ext = frm.dataset.ext; var params = {}; $(frm).find("input:hidden,input:text,input:password,input:checked,select,textarea").filter(":enabled").each(function() { params[this.name || '__'] = this.value; }); $.ajax({ url: _mtt.apiUrl + 'ext-settings/' + ext, method: 'PUT', contentType : 'application/json', data: JSON.stringify(params), dataType: 'json', success: function(json) { if (json.saved) { if (json.msg) showExtensionSettings(ext, function(){ flashInfo(json.msg); }, true); else showExtensionSettings(ext, null, true); } else if (json.msg) { flashError(json.msg); } } }); } function extensionSettingsAction(actionString, ext, formData) { if (actionString === undefined || ext === undefined) return false; const a = actionString.split(':', 2); if (a.length !== 2) return false; const method = a[0], action = a[1]; const success = function(json) { if (json.total && json.total > 0) { if (json.redirect) { window.location.assign(json.redirect); return; } if (json.html) { $('#page_ajax .mtt-settings-table').html(json.html); //FIXME: maybe whole page? return; } if (json.alertText) { mttAlert(json.alertText); return; } const callback = function() { if (json.alertTextOnLoad) { mttAlert(json.alertTextOnLoad); } else if (json.msg) { flashInfo(json.msg, json.details); } if (json.reload) { setTimeout( function(){ //window.location.hash = ''; window.location.reload(); }, 1000); } } showExtensionSettings(ext, callback, true); } else if (json.msg) { flashInfo(json.msg, json.details); } }; if (formData === undefined) { $.ajax({ url: _mtt.apiUrl + 'ext/' + ext + '/' + action, method: method.toUpperCase(), contentType : 'application/json', data: '{}', dataType: 'json', success: success }); } else { $.ajax({ url: _mtt.apiUrl + 'ext/' + ext + '/' + action, method: method.toUpperCase(), contentType : false, data: formData, processData: false, success: success }); } } _mtt.extensionSettingsAction = extensionSettingsAction; /* * Dialogs */ function mttConfirm(msg, callbackOk, callbackCancel) { mttModalDialog('confirm').message(msg).ok(callbackOk).cancel(callbackCancel).show(); } function mttPrompt(msg, defaultValue, callbackOk, callbackCancel) { mttModalDialog('prompt').message(msg).default(defaultValue).ok(callbackOk).cancel(callbackCancel).show(); } function mttAlert(msg, callbackOk) { mttModalDialog().ok(callbackOk).message(msg).show(); } function mttModalDialog(dialogType = 'alert') { if ( ! (this instanceof mttModalDialog) ) return new mttModalDialog(dialogType); let dialog = this; this.type = dialogType; let lastScrollTop = 0; this.close = function() { //restore scrolling $('body').css({ 'position': '', 'top': '' }); window.scrollTo(window.pageXOffset, lastScrollTop); $("html").removeClass('mtt-modal-dialog-active'); $("#modal_overlay, #modal").hide(); $("#btnModalOk").off('click'); $("#btnModalCancel").off('click'); $("#modalMessage").text(''); $("#modalTextInput").val('').off('keyup.mttmodal'); $(document).off('keydown.mttmodal'); } ; this.ok = function(callback) { $("#btnModalOk").on('click', function() { const value = $("#modalTextInput").val(); dialog.close(); if (typeof callback === 'function') callback( dialog.type === 'prompt' ? value : null ); }); return dialog; }; this.cancel = function(callback) { $("#btnModalCancel").on('click', function() { dialog.close(); if (typeof callback === 'function') callback(); }); return dialog; }; this.message = function(msg = '') { $("#modalMessage").text(msg); return dialog; }; this.default = function(value = '') { $("#modalTextInput").val(value); return dialog; } this.show = function() { let modalOverlay = document.getElementById("modal_overlay"); if (!modalOverlay) { modalOverlay = document.createElement("div"); modalOverlay.id = "modal_overlay"; modalOverlay.style.cssText = "position: fixed; z-index: 999; left: 0; top: 0; width: 100%; height: 100%; background-color: black; opacity: 0.6; display:none;"; document.getElementsByTagName('body')[0].appendChild(modalOverlay); } if (dialog.type === 'confirm') { $("#btnModalCancel").show(); $("#modalTextInput").hide(); } else if(dialog.type === 'prompt') { $("#btnModalCancel").show(); $("#modalTextInput").show(); $("#modalTextInput").on("keyup.mttmodal", function(e) { if (e.keyCode == 13) { $("#btnModalOk").click(); } }); } else { $("#btnModalCancel").hide(); $("#modalTextInput").hide(); } $(document).on('keydown.mttmodal', function(event) { if (event.keyCode == 27) { dialog.close(); } }); //disable background scrolling lastScrollTop = window.pageYOffset; $('body').css({ 'position': 'fixed', 'top': `-${lastScrollTop}px` }) $("html").addClass('mtt-modal-dialog-active'); $("#modal_overlay, #modal").show(); $("#modalTextInput").focus(); return dialog; }; } /* * History and Hash change */ /** * Manipulate browser history manually. * //TODO: use window.location and hashchange event ? * @param {object} state History Api state data * @param {string} url document url. appended to the state. * @param {string} title document title to set. appended to the state. */ function updateHistoryState(state, url, title) { if (!_mtt.options.history) { document.title = title; return; } if (flag.dontChangeHistoryOnce) { flag.dontChangeHistoryOnce = false; } else { if (_mtt.lastHistoryState) { //_mtt.lastHistoryState.title = document.title; window.history.pushState(_mtt.lastHistoryState, _mtt.lastHistoryState.title, _mtt.lastHistoryState.url); } state.url = url; state.title = title; window.history.replaceState(state, title, url); //also refresh visible URL } _mtt.lastHistoryState = history.state; flag.firstLoad = false; document.title = title; } function replaceHistoryState(param, _state, url, title) { if (!_mtt.options.history) { document.title = title; return; } if (flag.dontChangeHistoryOnce) { flag.dontChangeHistoryOnce = false; } const state = history.state; if (state && state[param]) { _state.url = url; _state.title = title; history.replaceState(_state, '', url); document.title = title; _mtt.lastHistoryState = history.state; } else { updateHistoryState(_state, url, title); } } function historyOnPopState(event) { if (!event.state) return; if (event.state.list && _mtt.pages.current && ((_mtt.pages.current.page == 'ajax' && _mtt.pages.current.pageClass == 'settings') || _mtt.pages.current.page == 'taskviewer') ) { // Here we go back to tasklist from settings or view task, no reload. // Just show and hide pages without history actions. _mtt.pageBack(); flag.dontChangeHistoryOnce = true; updateHistoryState( { list:event.state.list }, event.state.url, event.state.title ); } else if (event.state.task) { flag.dontChangeHistoryOnce = true; viewTask(event.state.task); } else if (event.state.list) { flag.dontChangeHistoryOnce = true; tabSelect(event.state.list); } else if (event.state.settings) { _mtt.pageBack(); // will do nothing if back from tasks flag.dontChangeHistoryOnce = true; _mtt.lastHistoryState = event.state; // will pageSet() if back from tasks // will not pageSet() if back from extSettings showSettings(event.state.settingsJson); } else if (event.state.extSettings) { flag.dontChangeHistoryOnce = true; showExtensionSettings(event.state.ext); } else { console.log("unexpected: nothing to pop"); } } })(); ================================================ FILE: src/content/mytinytodo_api.js ================================================ /* This file is a part of myTinyTodo. (C) Copyright 2010,2020-2025 Max Pozdeev Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ "use strict"; /** * @class */ function MytinytodoAjaxApi(props) { if (typeof mytinytodo !== 'object') { throw "mytinytodo global object is not found!"; } this.useREST = true; if (props.hasOwnProperty('useREST')) { this.useREST = !!props.useREST; } } MytinytodoAjaxApi.prototype = { /** * required method * @param {string} action * @param {Object.} [params] * @param {ApiDriverCallback} [callback] * * @callback ApiDriverCallback * @param {object} json */ request(action, params, callback) { if (typeof this[action] !== 'function') { throw "Unknown ApiDriver action: " + action; } this[action](params, function(json){ if (json.denied) { mytinytodo.errorDenied(); } if (callback) { callback.call(mytinytodo, json) } }); }, loadTasks(params, callback) { let 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'; if (params.saveSort && params.saveSort != 0) q += '&saveSort=1'; $.getJSON(mytinytodo.apiUrl + 'tasks' + (mytinytodo.apiUrl.indexOf('?') > -1 ? '&' : '?') + 'list='+params.list+'&compl='+params.compl+'&sort='+params.sort+q, callback); }, newTask(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks', method: 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'newSimple', list: params.list, title: params.title, tag: params.tag, }), success: callback, dataType: 'json' }); }, fullNewTask(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks', method: 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'newFull', 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(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks/' + encodeURIComponent(params.id), method: this.useREST ? 'PUT' : 'POST', 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(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks/' + encodeURIComponent(params.id), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'note', note: params.note }), success: callback, dataType: 'json' }); }, completeTask(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks/' + encodeURIComponent(params.id), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'complete', compl: params.compl }), success: callback, dataType: 'json' }); }, deleteTask(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks/' + encodeURIComponent(params.id), method: this.useREST ? 'DELETE' : 'POST', contentType : 'application/json', // contentType and data are required if method is POST data: JSON.stringify({ action: 'delete', }), success: callback, dataType: 'json' }); }, setTaskPriority(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks/' + encodeURIComponent(params.id), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'priority', prio: params.priority, }), success: callback, dataType: 'json' }); }, changeOrder(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks', method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'order', order: params.order, }), success: callback, dataType: 'json' }); }, suggestTags(params, callback) { $.getJSON(mytinytodo.apiUrl + 'suggestTags', {list:params.list, q:params.q}, callback); }, tagCloud(params, callback) { $.getJSON(mytinytodo.apiUrl + 'tagCloud/' + encodeURIComponent(params.list), callback); }, moveTask(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks/' + encodeURIComponent(params.id), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'move', from: params.from, to: params.to }), success: callback, dataType: 'json' }); }, parseTaskStr(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'tasks/parseTitle', method: 'POST', contentType : 'application/json', data: JSON.stringify({ list: params.list, title: params.title, tag: params.tag , }), success: callback, dataType: 'json' }); }, newTaskCounter(params, callback) { fetch(mytinytodo.apiUrl + 'tasks/newCounter', { method: 'POST', credentials: 'same-origin', // old browsers headers: { 'Content-Type': 'application/json', 'MTT-Token': mytinytodo.options.token, }, body: JSON.stringify(params) }) .then((response) => { if (!response.ok) throw response; return response.json(); }) .catch((e) => { if (e instanceof Error) console.log("newTaskCounter fetch error: ", e); else console.log("newTaskCounter fetch error, HTTP status code: " + e.status); }) .then(json => { callback(json) }); }, // Lists loadLists(params, callback) { $.getJSON(mytinytodo.apiUrl + 'lists', callback); }, addList(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists', method: 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'new', name: params.name, }), success: callback, dataType: 'json' }); }, deleteList(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'DELETE' : 'POST', contentType : 'application/json', // contentType and data are required if method is POST data: JSON.stringify({ action: 'delete', }), success: callback, dataType: 'json' }); }, renameList(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'rename', name: params.name, }), success: callback, dataType: 'json' }); }, setSort(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'sort', name: params.name, sort: params.sort }), success: callback, dataType: 'json' }); }, publishList(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'publish', publish: params.publish, }), success: callback, dataType: 'json' }); }, enableFeedKey(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'enableFeedKey', enable: params.enable, }), success: callback, dataType: 'json' }); }, setShowNotesInList(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'showNotes', shownotes: params.shownotes, }), success: callback, dataType: 'json' }); }, setHideList(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'hide', hide: params.hide, }), success: callback, dataType: 'json' }); }, changeListOrder(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists', method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'order', order: params.order }), success: callback, dataType: 'json' }); }, clearCompletedInList(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'lists/' + encodeURIComponent(params.list), method: this.useREST ? 'PUT' : 'POST', contentType : 'application/json', data: JSON.stringify({ action: 'clearCompleted', }), success: callback, dataType: 'json' }); }, /* Auth */ login(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'login', method: 'POST', contentType : 'application/json', data: JSON.stringify({ password: params.password, }), success: callback, dataType: 'json' }); }, logout(params, callback) { $.ajax({ url: mytinytodo.apiUrl + 'logout', method: 'POST', success: callback, dataType: 'json' }); } }; ================================================ FILE: src/content/theme/dark.css ================================================ /* This file is a part of myTinyTodo. */ /* Dark mode */ /* prefers-color-scheme media query is used in html link tag */ :root { color-scheme: dark; --color-bg: #151515; --color-text-default: #eee; --color-text-reduced: #e0e0e0; --color-text-reduced2: #999; --color-text-reduced3: #666; --color-link: #6495ED; /* CornflowerBlue */ --color-btn-reduced: #999; --color-btn-reduced-hover: #ddd; --color-btn-default: #fff; --color-btn-hover: #444; --color-submit: #444; --color-submit-hover: #555; --color-row-underlinig: #303030; --color-border-default: #555; --color-border-focus: #5a8df0; --color-error: #ff3333; --color-error-bg: var(--color-bg); --color-info: #EFC300; --color-info-bg: var(--color-bg); --color-input-bg: #1e1e1e; --color-toolbar: #3b3b3b; --color-btn-toolbar-hover: #555; --color-content-delimiter: #676767; --color-footer: var(--color-bg); /* Tabs */ --color-tab: #1b1b1b; --color-tab-selected: var(--color-toolbar); --color-tab-hover: #262626; --color-tab-border: #676767; --color-tab-text: #ddd; --color-btn-tab: #ccc; --color-btn-tab-hover: #fff; --color-btn-tab-hover-bg: #5a5a5a; /* Menu */ --color-menu: #252525; --color-menu-border: #555; --color-menu-hover: #5a8df0; --color-menu-text: #eaeaea; --color-menu-text-hover: #ebf0f8; --color-menu-text-disabled: #696969; --color-popup-shadow: 1px 2px 6px 1px rgba(85,85,85,0.1); /* Tasklist */ --color-tasklist-row: var(--color-bg); --color-tasklist-row-border: var(--color-row-underlinig); --color-tasklist-row-inter-border: var(--color-tasklist-row-border); --color-tasklist-row-expanded-border: #555; --color-tasklist-tag: var(--color-tab-text); --color-tasklist-note-link: #999; --color-tasklist-link-hover: var(--color-link); --color-tasklisk-hover-shadow: rgba(255,255,255,0.4); --color-taglist-tag: var(--color-text-reduced); --color-taglist-tag-bg: #444; --color-taglist-tag-hover: var(--color-taglist-tag); --color-taglist-tag-hover-bg: var(--color-taglist-tag-bg); --color-tasklist-listname: #bbb; --color-tasklist-listname-bg: #333; /* Priority */ --color-priority-none: #676767; --color-priority-text: var(--color-text-default); /* DueDates */ --color-duedate-default: var(--color-tab-text); --color-duedate-soon: #008000; --color-duedate-today: #FF0000; --color-duedate-past: #B52D2D; /* Markdown */ --color-md-border: #333; --color-md-bg-highlighted: #222; --color-md-text-blockquote: #777; /* Settings */ --color-settings-row: #222; /* */ --color-placeholder: #444; --color-placeholder-border: #555; --svg-select: var(--svg-select-dark); } ================================================ FILE: src/content/theme/images/COPYRIGHT ================================================ rss.svg, rss-disabled.svg - are (or based on) icons by Icons8 from https://icons8.com/icon/13841/rss calendar.svg - icon by Icons8 from https://icons8.com/icon/__LA9wZgJaqd/calendar loading48.gif - generated at Preloaders.net Other images in this directory were made by Max Pozdeev the author of myTinyTodo and licensed under the terms of GNU GPL version 2 or any later. ================================================ FILE: src/content/theme/images/index.html ================================================ Place for Images ================================================ FILE: src/content/theme/images/svg2base64.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ // call like: php -f svg2base64.php > ../images.css if (php_sapi_name() != 'cli') { error_log("Supports cli only"); exit(-1); } if (isset($argv[1])) { print base64file($argv[1]); exit(); } $files = []; $h = opendir(__DIR__); while ( false !== ($file = readdir($h)) ) { if ( preg_match('/(.+)\.svg$/', $file, $m) ) { $files[] = $m[1]; } } closedir($h); if (!$files) { exit; } sort($files); print ":root {\n"; foreach ($files as $name) { $b64 = base64file(__DIR__. "/$name.svg"); print " --svg-{$name}: url('data:image/svg+xml;base64,$b64');\n"; } print "}\n"; function base64file(string $filename): string { $content = file_get_contents($filename); //$content = str_replace(["\n","\r\n"], ['',''], $content); $content = cleanXml($content); return base64_encode($content); } function cleanXml(string $data): string { $dom = new DOMDocument; $dom->preserveWhiteSpace = false; $dom->loadXML($data); $xpath = new DOMXPath($dom); foreach ($xpath->query('//comment()') as $comment) { $comment->parentNode->removeChild($comment); } return $dom->saveXML(); } ================================================ FILE: src/content/theme/index.html ================================================ ================================================ FILE: src/content/theme/markdown.css ================================================ /* Markdown notes */ .markdown-note { line-height: 1.3; } .markdown-note > *:first-child { margin-top: 0 !important; } .markdown-note > *:last-child { margin-bottom: 0 !important; } .markdown-note h1 { font-size:2rem; } .markdown-note h2 { font-size:1.5rem; } .markdown-note h3 { font-size:1.2rem; } .markdown-note h4 { font-size:1rem; } .markdown-note h5 { font-size:1rem; } .markdown-note h6 { font-size:1rem; } .markdown-note hr { height: 1px; border: 0; background-color: var(--color-border-default); } .markdown-note p, .markdown-note blockquote, .markdown-note ul, .markdown-note ol, .markdown-note dl, .markdown-note table, .markdown-note pre { margin: 12px 0; } .markdown-note ul, .markdown-note ol { padding-left: 2.3rem; } .markdown-note ul.no-list, .markdown-note ol.no-list { list-style-type: none; padding: 0; } .markdown-note blockquote { margin:15px 0; border-left: 4px solid var(--color-md-border); padding: 0 15px; color: var(--color-md-text-blockquote, #777); } .markdown-note blockquote > :first-child { margin-top: 0px; } .markdown-note blockquote > :last-child { margin-bottom: 0px; } .markdown-note table { width: 100%; overflow: auto; display: block; border-spacing: 0; border-collapse: collapse; } .markdown-note table th { font-weight: bold; } .markdown-note table th, .markdown-note table td { border: 1px solid var(--color-md-border); padding: 6px 13px; } .markdown-note table tr { border-top: 1px solid var(--color-border-default); background-color: var(--color-tasklist-row); } .markdown-note table tr:nth-child(2n) { background-color: var(--color-md-bg-highlighted); } .markdown-note code, /* inline code */ .markdown-note tt { font-size: 13px; /* if main font is 14px */ font-family: ui-monospace,Consolas,"SF Mono",Menlo,"Noto Sans Mono","Liberation Mono",monospace; padding: 0px 5px; background-color: var(--color-md-bg-highlighted); border-radius: 3px; } .markdown-note code { white-space: break-spaces; } .markdown-note pre { font-size: 13px; font-family: ui-monospace,Consolas,"SF Mono",Menlo,"Noto Sans Mono","Liberation Mono",monospace; line-height: 1.2rem; padding: 10px; background-color: var(--color-md-bg-highlighted); overflow: auto; border-radius: 3px; } .markdown-note pre code, /* block of code */ .markdown-note pre tt { margin: 0; padding: 0; background-color: transparent; border: none; } .markdown-note pre > code { white-space: pre; } .markdown-note img { max-width: 100%; } /* narrow screens */ @media only screen and (max-width: 600px) { .markdown-note pre, .markdown-note code, .markdown-note tt { font-size: 14px; /* if main font is 16px */ } } ================================================ FILE: src/content/theme/print.css ================================================ @media print { html,body { height:auto; } h2 { display: none; } h3 { border-bottom:2px solid #777777; } #toolbar { display: none; } .topblock { display:none; } .mtt-tab { display:none; } .mtt-tab.mtt-tab-selected { display:block; border:none; background:none; margin:0; } .mtt-tab.mtt-tab-selected a { padding:0; display:inline-block; height:auto; } .mtt-tab.mtt-tab-selected .title-block { display:inline-block; } .mtt-tab.mtt-tab-selected .list-action { display:none; } .mtt-tab.mtt-tab-selected .title { text-align:left; padding:0; margin:0; max-width:none; font-size:1.3rem; color:#000; } .mtt-tabs-new-button { display:none; } #tabs_buttons { display:none; } #taskview { padding-left:0; font-weight:normal; } #taskview .arrdown { display:none; } #tasklist { list-style-type: decimal; list-style-position: outside; } #tasklist li.task-row { border-bottom:none; margin-left:2.5rem; /*border-bottom:1px solid #f0f0f0;*/ border: none; } #tasklist li.task-row:hover { box-shadow: none; } #tasklist li.task-row.task-has-note.task-expanded .task-block, #tasklist li.task-row.task-has-note.clicked .task-block, #tasklist li.task-row.task-expanded { border: none; } div.task-note-block { border-left:1px solid #777777; padding-left:10px; margin-left:3px; padding-top:0px; font-size:9pt; color:#333333; } .task-middle { margin-left:0px; margin-right:3px; } .task-left { display:none; } .task-actions { display:none; } .task-date { white-space:nowrap; margin-left:10px; } #tasklist li.today, #tasklist li.past { background-color:#ffffff; border-color:#dedede; } .task-prio { font-weight:bold; } li.task-completed { opacity:1; } #footer_content { border-top:1px solid #777777; background:none; } #footer_content a { text-decoration:none; color:#000000; } #tagcloudbtn { display:none; } .mtt-notes-showhide { display:none; } #taskview img { display:none; } } ================================================ FILE: src/content/theme/style.css ================================================ /* This file is a part of myTinyTodo. */ /* Browsers support: Flexbox layout - https://caniuse.com/flexbox CSS masks from SVG images - https://caniuse.com/mdn-css_properties_mask-image_svg_masks CSS variables - https://caniuse.com/css-variables */ /* light colors */ :root { --color-bg: #fff; --color-text-default: #000; --color-text-reduced: #222; --color-text-reduced2: #666; --color-text-reduced3: #999; --color-link: blue; --color-btn-reduced: #707070; --color-btn-reduced-hover: #4c4c4c; --color-btn-default: #000; --color-btn-hover: #efefef; --color-submit: #eee; --color-submit-hover: #ddd; --color-row-underlinig: #dedede; --color-border-default: #ccc; --color-border-focus: #5a8df0; --color-border-focus-shadow: rgba(90,141,240,0.7); --color-error: var(--color-text-reduced); --color-error-bg: #ff3333; --color-info: var(--color-text-reduced); --color-info-bg: #EFC300; --color-input-bg: #fff; --color-toolbar: #ededed; --color-btn-toolbar-hover: #ddd; --color-content-delimiter: #b5d5ff; --color-footer: #b5d5ff; /* Tabs */ --color-tab: #fbfbfb; --color-tab-selected: var(--color-toolbar); --color-tab-hover: #ddd; --color-tab-border: #ededed; --color-tab-text: #333333; --color-btn-tab: #888; --color-btn-tab-hover: #fff; --color-btn-tab-hover-bg: #999; /* Menu */ --color-menu: #f9f9f9; --color-menu-border: var(--color-border-default); --color-menu-hover: #5a8df0; --color-menu-text: #000; --color-menu-text-hover: #fff; --color-menu-text-disabled: #ACA899; --color-popup-shadow: 1px 2px 5px rgba(0,0,0,0.5); /* Tasklist */ --color-tasklist-row: var(--color-bg);; --color-tasklist-row-border: var(--color-row-underlinig); --color-tasklist-row-inter-border: #f0f0f0; --color-tasklist-row-expanded-border: var(--color-tasklist-row-border); --color-tasklist-tag: var(--color-tab-text); --color-tasklist-note-link: #777; --color-tasklist-link-hover: var(--color-link); /* #af0000 */ --color-tasklisk-hover-shadow: rgba(0,0,0,0.3); --color-taglist-tag: var(--color-text-reduced); --color-taglist-tag-bg: #e0e0e0; --color-taglist-tag-hover: var(--color-taglist-tag); --color-taglist-tag-hover-bg: var(--color-taglist-tag-bg); --color-tasklist-listname: #555; --color-tasklist-listname-bg: #eee; /* Priority */ --color-priority-none: #dedede; --color-priority-low: #3377ff; --color-priority-high: #ff7700; --color-priority-urgent: #ff3333; --color-priority-text: #fff; /* DueDates */ --color-duedate-default: var(--color-tab-text); --color-duedate-soon: #008000; --color-duedate-today: #FF0000; --color-duedate-past: #A52A2A; /* Markdown */ --color-md-border: #ddd; --color-md-bg-highlighted: #f8f8f8; --color-md-text-blockquote: #777; /* Settings */ --color-settings-row: #f5f5f5; /* */ --color-placeholder: #ddd; --color-placeholder-border: #aaa; /* svg images */ --svg-add: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwb2x5bGluZSBwb2ludHM9IjE1IDQsIDE1IDksIDIgOSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCIgZmlsbD0ibm9uZSIvPjxwb2x5bGluZSBwb2ludHM9IjYgNSwgMSA5LCA2IDEzIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSIjMDAwIiBmaWxsPSJub25lIi8+PC9zdmc+Cg=='); --svg-arr-left: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwb2x5Z29uIHBvaW50cz0iMTAgMywgNCA4LCAxMCAxMyIgZmlsbD0iIzAwMCIvPjwvc3ZnPgo='); --svg-arr-right: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwb2x5Z29uIHBvaW50cz0iNiAzLCAxMiA4LCA2IDEzIiBmaWxsPSIjMDAwIi8+PC9zdmc+Cg=='); --svg-arrdown2: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgOCA4Ij48cG9seWdvbiBwb2ludHM9IjAgMiwgOCAyLCA0IDYiIGZpbGw9ImJsYWNrIi8+PC9zdmc+Cg=='); --svg-back: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxwb2x5bGluZSBwb2ludHM9IjkgMiwgNCA3LCA5IDEyIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSIjMDAwIiBmaWxsPSJub25lIi8+PC9zdmc+Cg=='); --svg-calendar: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wLjUgMS41SDE1LjVWMTQuNUgwLjV6Ii8+PHBhdGggZmlsbD0iIzc4OGI5YyIgZD0iTTE1LDJ2MTJIMVYySDE1IE0xNiwxSDB2MTRoMTZWMUwxNiwxeiIvPjxwYXRoIGZpbGw9IiNmNzhmOGYiIGQ9Ik0wLjUgMS41SDE1LjVWNC41SDAuNXoiLz48cGF0aCBmaWxsPSIjYzc0MzQzIiBkPSJNMTUsMnYySDFWMkgxNSBNMTYsMUgwdjRoMTZWMUwxNiwxeiIvPjxwYXRoIGZpbGw9IiNjNWQ0ZGUiIGQ9Ik01IDdINlY4SDV6TTcgN0g4VjhIN3pNOSA3SDEwVjhIOXpNMTEgN0gxMlY4SDExek0zIDlINFYxMEgzek01IDlINlYxMEg1ek03IDlIOFYxMEg3ek05IDlIMTBWMTBIOXpNMTEgOUgxMlYxMEgxMXpNMyAxMUg0VjEySDN6TTUgMTFINlYxMkg1ek03IDExSDhWMTJIN3pNOSAxMUgxMFYxMkg5eiIvPjwvc3ZnPgo='); --svg-checkmark: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwb2x5bGluZSBwb2ludHM9IjQgOCwgNyAxMiwgMTMgNSIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiMwMDAiLz48L3N2Zz4K'); --svg-closetag: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxnIHN0cm9rZS13aWR0aD0iMS41IiBmaWxsPSJub25lIj48cGF0aCBkPSJNMTIuMCAxMi4wbC04LTgiIHN0cm9rZT0iIzAwMCIvPjxwYXRoIGQ9Ik00LjAgMTIuMGwrOC04IiBzdHJva2U9IiMwMDAiLz48L2c+PC9zdmc+Cg=='); --svg-newtask-ext: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxnIHN0cm9rZT0iIzAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxLjAiPjxwb2x5bGluZSBwb2ludHM9IjEwIDAuNSwgMC41IDAuNSwgMC41IDE1LjUsIDEzLjUgMTUuNSwgMTMuNSA3LjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cG9seWxpbmUgcG9pbnRzPSI3IDcsIDYgMTAsIDkgOSwgMTUgMywgMTMgMSwgNyA3LCA2IDEwIi8+PGxpbmUgeDE9IjciIHkxPSI3IiB4Mj0iOSIgeTI9IjkiLz48L2c+PC9zdmc+Cg=='); --svg-note-toggle: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxwb2x5bGluZSBwb2ludHM9IjUgMiwgMTAgNywgNSAxMiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCIgZmlsbD0ibm9uZSIvPjwvc3ZnPgo='); --svg-plus: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxsaW5lIHgxPSIyIiB5MT0iOCIgeDI9IjE0IiB5Mj0iOCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCIvPjxsaW5lIHgxPSI4IiB5MT0iMiIgeDI9IjgiIHkyPSIxNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZT0iIzAwMCIvPjwvc3ZnPgo='); --svg-rss: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4IDgiPjx0aXRsZT5SU1MgZmVlZCBpY29uPC90aXRsZT48c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLmJ1dHRvbiB7c3Ryb2tlOiBub25lOyBmaWxsOiBvcmFuZ2U7fQogICAgLnN5bWJvbCB7c3Ryb2tlOiBub25lOyBmaWxsOiB3aGl0ZTt9CiAgPC9zdHlsZT48cmVjdCBjbGFzcz0iYnV0dG9uIiB3aWR0aD0iOCIgaGVpZ2h0PSI4IiByeD0iMS41Ii8+PGNpcmNsZSBjbGFzcz0ic3ltYm9sIiBjeD0iMiIgY3k9IjYiIHI9IjEiLz48cGF0aCBjbGFzcz0ic3ltYm9sIiBkPSJtIDEsNCBhIDMsMyAwIDAgMSAzLDMgaCAxIGEgNCw0IDAgMCAwIC00LC00IHoiLz48cGF0aCBjbGFzcz0ic3ltYm9sIiBkPSJtIDEsMiBhIDUsNSAwIDAgMSA1LDUgaCAxIGEgNiw2IDAgMCAwIC02LC02IHoiLz48L3N2Zz4K'); --svg-rss-disabled: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4IDgiPjx0aXRsZT5SU1MgZmVlZCBpY29uPC90aXRsZT48c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLmJ1dHRvbiB7c3Ryb2tlOiBub25lOyBmaWxsOiAjYmJiO30KICAgIC5zeW1ib2wge3N0cm9rZTogbm9uZTsgZmlsbDogd2hpdGU7fQogIDwvc3R5bGU+PHJlY3QgY2xhc3M9ImJ1dHRvbiIgd2lkdGg9IjgiIGhlaWdodD0iOCIgcng9IjEuNSIvPjxjaXJjbGUgY2xhc3M9InN5bWJvbCIgY3g9IjIiIGN5PSI2IiByPSIxIi8+PHBhdGggY2xhc3M9InN5bWJvbCIgZD0ibSAxLDQgYSAzLDMgMCAwIDEgMywzIGggMSBhIDQsNCAwIDAgMCAtNCwtNCB6Ii8+PHBhdGggY2xhc3M9InN5bWJvbCIgZD0ibSAxLDIgYSA1LDUgMCAwIDEgNSw1IGggMSBhIDYsNiAwIDAgMCAtNiwtNiB6Ii8+PC9zdmc+Cg=='); --svg-search: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxnIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2U9IiMwMDAiIGZpbGw9Im5vbmUiPjxwYXRoIGQ9Ik0xNCAxNGwtNC00Ii8+PGNpcmNsZSBjeD0iNyIgY3k9IjciIHI9IjQuNSIvPjwvZz48L3N2Zz4K'); --svg-search-cancel: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Im04IDFhNyA3IDAgMCAwLTcgNyA3IDcgMCAwIDAgNyA3IDcgNyAwIDAgMCA3LTcgNyA3IDAgMCAwLTctN3ptLTIuNDY4OCAzLjQ2ODggMi40Njg4IDIuNDY4OCAyLjQ2ODgtMi40Njg4IDEuMDYyNSAxLjA2MjUtMi40Njg4IDIuNDY4OCAyLjQ2ODggMi40Njg4LTEuMDYyNSAxLjA2MjUtMi40Njg4LTIuNDY4OC0yLjQ2ODggMi40Njg4LTEuMDYyNS0xLjA2MjUgMi40Njg4LTIuNDY4OC0yLjQ2ODgtMi40Njg4IDEuMDYyNS0xLjA2MjV6IiBmaWxsPSIjMDAwIi8+PC9zdmc+Cg=='); --svg-select: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxwb2x5bGluZSBwb2ludHM9IjIgNiwgNyAxMSwgMTIgNiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiM0NDQiIGZpbGw9Im5vbmUiLz48L3N2Zz4K'); --svg-select-dark: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxwb2x5bGluZSBwb2ludHM9IjIgNiwgNyAxMSwgMTIgNiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiNhYWEiIGZpbGw9Im5vbmUiLz48L3N2Zz4K'); --svg-selectlist: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxjaXJjbGUgY3g9IjMiIGN5PSI4IiByPSIxLjUiIGZpbGw9IiMzMzMiLz48Y2lyY2xlIGN4PSI4IiBjeT0iOCIgcj0iMS41IiBmaWxsPSIjMzMzIi8+PGNpcmNsZSBjeD0iMTMiIGN5PSI4IiByPSIxLjUiIGZpbGw9IiMzMzMiLz48L3N2Zz4K'); --svg-selectlist2: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PGxpbmUgeDE9IjIiIHkxPSIzIiB4Mj0iMTQiIHkyPSIzIi8+PGxpbmUgeDE9IjIiIHkxPSI4IiB4Mj0iMTQiIHkyPSI4Ii8+PGxpbmUgeDE9IjQiIHkxPSIxMyIgeDI9IjE0IiB5Mj0iMTMiLz48L2c+PC9zdmc+Cg=='); --svg-task-menu: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxwb2x5bGluZSBwb2ludHM9IjIgNiwgNyAxMSwgMTIgNiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiMwMDAiIGZpbGw9Im5vbmUiLz48L3N2Zz4K'); --svg-task-menu2: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxnIGZpbGw9IiMwMDAiPjxjaXJjbGUgY3g9IjciIGN5PSIyIiByPSIxLjUiLz48Y2lyY2xlIGN4PSI3IiBjeT0iNyIgcj0iMS41Ii8+PGNpcmNsZSBjeD0iNyIgY3k9IjEyIiByPSIxLjUiLz48L2c+PC9zdmc+Cg=='); } /* default style */ html { height:100%; /*overflow-y:hidden; /* for modal overlay to disable scrolling, but breaks position:absolute */ font-size:14px; /* =1rem */ } body { margin: 0px; padding: 0px; width: 100%; height: 100%; background-color: var(--color-bg); color: var(--color-text-default); display:flex; flex-direction:column; align-items:center; overflow-y: scroll; /* always show scrollbar */ } body, td, th, input, textarea, select, button { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",/*Roboto,*/ Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; font-size: 1rem; } #mtt { flex: 1 0 auto; width:100%; max-width:1100px; padding:8px; margin-bottom: 1rem; box-sizing: border-box; } form { display: inline; } .topblock h2 { margin:0; font-size:1.5rem; padding-left: 30px; padding-right: 10px; background: url(images/logo.gif) no-repeat top left; background-size: 24px 24px; } #mtt.ajax-loading .topblock h2 { background-image: url(images/logo-loading.gif); } /* preload images */ body::after { position:absolute; width:0; height:0; overflow:hidden; z-index:-1; content:url(images/logo-loading.gif); } h3.page-title { margin:0; border-bottom:2px solid var(--color-content-delimiter); margin-bottom:1rem; padding:0.6rem 0; font-size:1.1rem; } a { color: var(--color-link); cursor:pointer; text-decoration:underline; } .topblock { display:flex; align-items:flex-start; margin-bottom:1rem; } #footer { flex-shrink:0; width:100%; max-width:1100px; } #footer_content { background-color: var(--color-footer); border-top: 1px solid var(--color-content-delimiter); padding:0.7rem; font-size:0.9rem; display:flex; justify-content:space-between; } #footer_content span:last-child { text-align:center; } #footer_content a { color: var(--color-text-default); } #footer_content a.powered-by-link { font-weight:bold; } .topblock-title { display:flex; align-items:center; min-width: 0; /* required for h2 ellipsis */ } .topblock-bar { flex-grow:1; display:flex; justify-content:flex-start; border-bottom:1px solid var(--color-content-delimiter); padding-bottom:5px; } .bar-menu { flex-grow:1; display:flex; justify-content:flex-end; text-align:right; white-space: nowrap; } .bar-menu > * { margin-left: 10px; } #mtt.no-need-auth .mtt-need-auth-enabled { display:none; } .form-input { padding: 3px; border: 1px solid var(--color-border-default); background-color: var(--color-input-bg); border-radius: 2px; transition: box-shadow .1s ease-in-out; } select.form-input { appearance: none; -webkit-appearance: none; -moz-appearance: none; background: var(--color-input-bg) var(--svg-select) no-repeat top 4px right 5px; background-size: 1rem 1rem; padding-right: calc(1rem + 10px); } .form-input:focus { outline: none; border-color: var(--color-border-focus); box-shadow: 0 0 0 2px var(--color-border-focus-shadow); } .form-bottom-buttons { display: flex; justify-content: center; padding: 1rem 0; } .form-bottom-buttons > * { min-width: 4rem; padding: 4px 6px; margin: 0 6px; background-color: var(--color-submit); border: 1px solid var(--color-border-default); border-radius: 3px; } .form-bottom-buttons > *:hover { background-color: var(--color-submit-hover); } #page_login { max-width: 250px; margin:0 auto; /* center */ margin-top: 100px; } #authform { background-color: var(--color-menu); border: 1px solid var(--color-menu-border); border-radius: 5px; } #authform .auth-content { padding: 1.5rem; } #authform div { padding:2px 0px; } #authform .form-bottom-buttons { border-top: 1px solid var(--color-border-default); padding: 0.8rem 0; } #authform div.h { font-weight:bold; } #authform input[type=password] { width: 100%; box-sizing: border-box; } #msg .msg-text { font-weight:bold; cursor:pointer; padding:0 1px; } #msg .msg-details { display:none; padding:1px 4px; background-color: var(--color-bg); max-width:800px; position:absolute; z-index:2; white-space: pre-line; } #msg.mtt-error .msg-text { background-color: var(--color-error-bg) } #msg.mtt-error .msg-details { border:1px solid var(--color-error-bg); } #msg.mtt-error .msg-text, #msg.mtt-error .msg-details { color: var(--color-error); } #msg.mtt-info .msg-text { background-color: var(--color-info-bg); } #msg.mtt-info .msg-details { border:1px solid var(--color-info-bg); } #msg.mtt-info .msg-text, #msg.mtt-info .msg-details { color: var(--color-info); } #lists { font-size:0.95rem; display:flex; align-items:flex-start; justify-content:flex-end; } #mtt.readonly.no-lists #lists > * { visibility: hidden; } .tabs-n-button { flex-grow:1; display:flex; align-items:flex-start; } .tab-height-wrapper { box-sizing:border-box; height:2.2rem; display:flex; align-items:center; } .mtt-tabs { list-style:none; padding:0; margin:0; display:flex; justify-content:flex-start; flex-wrap:wrap; } .mtt-tab { margin:1px 3px 0 0; background-color: var(--color-tab); border:1px solid var(--color-tab-border); border-bottom:none; border-top-right-radius:8px; transition:background-color 0.1s ease-in; max-width:230px; } .mtt-tab a { margin:0; text-decoration:none; white-space:nowrap; color: var(--color-tab-text); display:inline-block; outline:none; box-sizing:border-box; height:2.2rem; padding:1px 0.3rem 0 0.3rem; display:flex; align-items:center; } .mtt-tab a:active { outline:0; text-decoration:none; } .mtt-tab .title-block { display: flex; align-items: baseline; min-width: 75px; } .mtt-tab .title { display:inline-block; text-align:center; cursor:pointer; padding:0; overflow:hidden; text-overflow: ellipsis; margin-left:0.3rem; margin-right:0.3rem; flex-grow:1; } .mtt-tab .list-action { display:none; } .mtt-tab .mtt-img-button:hover, .mtt-tab .mtt-img-button.mtt-menu-button-active { background-color:var(--color-btn-tab-hover-bg); } .mtt-tab .mtt-img-button > span { width: 8px; height: 8px; mask: url(images/arrdown2.svg) no-repeat; -webkit-mask: url(images/arrdown2.svg) no-repeat; background-color: var(--color-btn-tab); transition: background-color 0.1s ease-in; /* animate together with button background */ } .mtt-tab .mtt-img-button:hover > span, .mtt-tab .mtt-img-button.mtt-menu-button-active > span { background-color: var(--color-btn-tab-hover); } .mtt-tab.mtt-tab-selected .list-action { display:block; } .mtt-tab.mtt-tab-selected { border-color: var(--color-tab-selected); background-color: var(--color-tab-selected); } .mtt-tab:not(.mtt-tab-selected):hover { background-color: var(--color-tab-hover); } .mtt-tab.mtt-tab-selected a { color: var(--color-text-default); } .mtt-tab-hidden { display:none; } .mtt-tab-sort-placeholder { background-color: var(--color-placeholder); border-color: var(--color-placeholder-border); } .mtt-tab .counter { display: inline-block; min-width: 1.2rem; height: 1rem; border-radius: 1rem; font-size: 0.8rem; text-align: center; background-color: #686868; /* #de5141 */ color: white; } .mtt-tab .counter.hidden { display: none; } #tabs_buttons { padding-left:2px; padding-right:2px; border-top:1px solid transparent; margin-top:1px; } .mtt-tabs-new-button { display:inline-block; margin-top:1px; border:1px solid var(--color-tab-border); border-bottom:none; border-top-right-radius: 8px; background-color: var(--color-tab); padding-left:3px; padding-right:3px; } .mtt-tabs-new-button span { display:block; width:16px; height:16px; mask: url(images/plus.svg) no-repeat; -webkit-mask: url(images/plus.svg) no-repeat; background-color: var(--color-tab-text); } .mtt-tabs-new-button:hover { cursor:pointer; background-color: var(--color-tab-hover); } #mtt.readonly .mtt-tabs-new-button { display:none; } .mtt-tabs-select-button > span { mask: url(images/selectlist2.svg) no-repeat; -webkit-mask: url(images/selectlist2.svg) no-repeat; background-color: var(--color-tab-text); } .mtt-img-button { padding: 5px; display:block; border-radius:4px; transition:background-color 0.1s ease-in; cursor:pointer; } .mtt-img-button:hover, .mtt-img-button.mtt-menu-button-active { background-color: var(--color-btn-hover); } .mtt-img-button span { display:block; width:16px; height:16px; } #mtt.no-lists #toolbar > * { visibility:hidden; } #mtt.no-list-selected #toolbar > * { visibility:hidden; } #mtt.readonly.no-lists #toolbar { visibility: hidden; } #toolbar { padding:8px; border-bottom:1px solid var(--color-row-underlinig); background: var(--color-toolbar); } #toolbar .mtt-img-button:hover { background-color: var(--color-btn-toolbar-hover); } .arrdown, .arrdown2 { display: inline-block; height: 9px; width:9px; mask: url(images/arrdown2.svg) no-repeat; -webkit-mask: url(images/arrdown2.svg) no-repeat; background-color: var(--color-btn-default); } .arrdown2 { height:7px; width:7px; } .newtask-n-search-container { display: flex; justify-content: flex-end; align-items: center; } /* Quick Task Add */ .taskbox-c { flex-grow:1; display:flex; align-items:center; } .mtt-taskbox { position:relative; padding-left:22px; /*input padding+border*/ flex-grow:1; max-width:430px; } #task { color: var(--color-text-reduced); background: var(--color-bg); height:1.35rem; padding:2px 4px; padding-right:20px; border:1px solid var(--color-border-default); border-radius:3px; width:100%; margin-left:-24px; } #mtt.show-all-tasks .taskbox-c, #mtt.readonly .taskbox-c { visibility: hidden; } .mtt-taskbox-icon { width:16px; height:16px; position:absolute; top:50%; right:2px; margin-top:-8px; cursor:pointer; mask: url(images/add.svg) no-repeat; -webkit-mask: url(images/add.svg) no-repeat; background-color: var(--color-btn-reduced); transition: background-color 0.1s ease-in; } .mtt-taskbox-icon:hover { background-color: var(--color-btn-reduced-hover); } #newtask_adv { margin-left:0.5rem; } #newtask_adv span { mask: url(images/newtask-ext.svg) no-repeat; -webkit-mask: url(images/newtask-ext.svg) no-repeat; background-color: var(--color-btn-reduced); } /* Live Search */ #search { color: var(--color-text-reduced); background: var(--color-bg); height:1.35rem; padding:2px 20px; width:100%; margin-left:-42px; /*padding+border*/ border:1px solid var(--color-border-default); border-radius:10px; } #search_close { display:none; } .mtt-searchbox { position:relative; padding-left:42px; /*input padding+border*/ } .mtt-searchbox-icon { width:16px; height:16px; position:absolute; top:50%; margin-top:-8px; } .mtt-searchbox-icon.mtt-icon-search { left:4px; mask: url(images/search.svg) no-repeat; -webkit-mask: url(images/search.svg) no-repeat; background-color: var(--color-btn-reduced); } .mtt-searchbox-icon.mtt-icon-cancelsearch { right:4px; cursor:pointer; mask: url(images/search-cancel.svg) no-repeat; -webkit-mask: url(images/search-cancel.svg) no-repeat; background-color: var(--color-btn-reduced); transition: background-color 0.1s ease-in; } .mtt-searchbox-icon.mtt-icon-cancelsearch:hover { background-color: var(--color-btn-reduced-hover); } #searchbar { font-size:1rem; font-weight:normal; display:none; margin-top:0.5rem; } #searchbarkeyword { font-weight:bold; } /* */ #page_tasks h3 { display:flex; align-items:baseline; } #mtt.no-lists #page_tasks h3 { visibility:hidden; } #mtt.no-list-selected #page_tasks h3 { visibility:hidden; } .mtt-notes-showhide { font-size:1rem; font-weight:normal; margin-left:5px; margin-right:5px; } .mtt-notes-showhide a { text-decoration:none; border-bottom:1px dotted; } /* Tag Toolbar */ #mtt-tag-toolbar { font-size:1.0rem; font-weight:normal; margin-top:0.5rem; line-height:1.5rem; display:flex; } .tag-toolbar-content { flex:auto; margin-bottom:-3px; } .tag-toolbar-close { flex-shrink:0; } .tag-toolbar-header { font-weight:normal; } .tag-filter { margin-left:3px; margin-right:3px; display:inline-flex; align-items:center; border:1px solid var(--color-border-default); border-radius:2rem; background-color: var(--color-bg); padding:0.1rem 0.5rem; margin-bottom:3px; cursor:pointer; } .tag-filter-exclude { text-decoration:line-through; } .mtt-filter-header { font-weight:bold; margin-right:.33rem; } .tag-filter-btn { margin-left:3px; display:inline-block; width:1em; height:1em; /* em! */ mask: var(--svg-closetag) no-repeat; -webkit-mask: var(--svg-closetag) no-repeat; background-color: var(--color-btn-reduced); transition: background-color 0.1s ease-in; } .tag-filter:hover .tag-filter-btn { background-color: var(--color-btn-reduced-hover); } #mtt-tag-toolbar-close span { mask: var(--svg-closetag) no-repeat; -webkit-mask: var(--svg-closetag) no-repeat; background-color: var(--color-btn-reduced); } a.mtt-back-button { font-size:1rem; } h3.page-title a.mtt-back-button { display:inline-block; width:1.3rem; min-width:1.3rem; height:1.3rem; padding-right:0.4rem; position: relative; top:4px; mask: url(images/back.svg) no-repeat; -webkit-mask: url(images/back.svg) no-repeat; background-color: var(--color-btn-default); } /* Tasklist */ .task-toggle { visibility: hidden; cursor: pointer; width: 1rem; height: 1rem; display: inline-block; margin-right: 4px; mask: var(--svg-note-toggle) no-repeat; -webkit-mask: var(--svg-note-toggle) no-repeat; background-color: var(--color-btn-reduced); transition: transform .1s linear, background-color .1s ease-in; } .task-toggle::after { /* for baseline */ content:'0'; color:transparent; } .task-toggle:hover { background-color: var(--color-btn-reduced-hover); } li.task-row.task-has-note .task-toggle { visibility:visible; } li.task-row.task-expanded .task-toggle { transform:rotate(90deg); } /* #tasklist input[type="checkbox"] { vertical-align:-1px; } /* Chrome */ #tasklist { list-style-type: none; margin: 0; padding: 0;} #tasklist li.task-row { border:1px solid transparent; /* allocate space for expanded border */ border-bottom:1px solid var(--color-tasklist-row-border); margin-bottom: 4px; min-height:20px; background-color: var(--color-tasklist-row); /* ?? */ position:relative; /* for z-index */ } /*#mtt:not(.touch-device) #tasklist li.task-row:hover,*/ #mtt:not(.touch-device) #tasklist li.task-row.menu-active { z-index: 1; box-shadow: 0 0 2px var(--color-tasklisk-hover-shadow); border-radius: 5px; } #mtt:not(.touch-device) #tasklist li.task-row.menu-active:not(.task-expanded) { border-bottom-color: transparent; } #tasklist .task-block { display: flex; justify-content: flex-start; align-items: stretch; padding: 0.5rem 3px; } #tasklist li.task-row.task-expanded { border:1px solid var(--color-tasklist-row-expanded-border); border-radius: 3px; } #tasklist li.task-row:not(.task-expanded):has(+ li.task-row.task-expanded) { border-bottom-color: transparent; } #tasklist li.task-row.task-has-note.task-expanded .task-block { border-bottom: 1px solid var(--color-tasklist-row-inter-border); } .task-left { display: flex; align-items: center; height: 1.2rem; /* same as line-height of task-middle */ /* border-radius: 0.01px; overflow: hidden;*/ /* to remove outline glitch in firefox, see https://bugzilla.mozilla.org/show_bug.cgi?id=1671784 */ } .task-middle { flex-grow:1; margin: -0.5rem 0px; padding: 0.5rem 0px; margin-left: 5px; align-items: baseline; line-height: 1.2rem; min-width: 0; /*for long text*/ } .task-middle-top { display: flex; } .task-actions { flex:0 0 1rem; margin-left:5px; } .task-left label { min-width:18px; text-align:center; } /* Safari has small checkboxes */ .task-date { color: var(--color-text-reduced3); font-size: 0.8rem; margin-top: 0.2rem; display: none; } .task-id::after { content: ' · '; /* '·' */ } #mtt.show-date .task-date { display: block; } #mtt.show-date.date-inline .task-date { display: inline-block; /* for RTL */ margin:0; margin-left:3px; } .task-through { overflow:hidden; flex-grow:1; } #mtt.view-task-on-click .task-title { cursor: pointer; } .task-title a { color: var(--color-text-default); } .task-title a:hover { color: var(--color-tasklist-link-hover); } #mtt.readonly #tasklist li.task-row .task-actions { display:none; } .task-listname { background-color: var(--color-tasklist-listname-bg); color: var(--color-tasklist-listname); padding: 0px 3px; margin: 0px 2px; } .task-tags { margin:0px 3px; display: inline; } .task-tags:empty { margin:0; } .task-tags .tag { font-size:0.9rem; font-weight:bold; color: var(--color-tasklist-tag); text-decoration:underline; cursor: pointer; } .duedate { color:var(--color-duedate-default); padding:0px; padding-left:1px; margin-left:5px; white-space:nowrap; } .duedate:before { content:'\2192\20'; font-family: -apple-system, "Segoe UI", "DejaVu Sans", sans-serif; } li.task-row.task-completed .duedate { /*font-size:0.8rem;*/ display:none; } #tasklist li.task-row.soon .duedate { color: var(--color-duedate-soon); } #tasklist li.task-row.today .duedate { color: var(--color-duedate-today); } #tasklist li.task-row.past .duedate { color: var(--color-duedate-past); } #tasklist li.task-row.task-completed { opacity:0.6; } #tasklist li.task-row.task-completed .task-through { text-decoration:line-through; } #tasklist li.task-row.task-completed:hover { opacity:1.0; } #tasklist li.task-row.not-in-tagpreview { opacity: 0.1; } #tasklist .mtt-task-placeholder { min-height: 0px; padding: 0px; height: 18px; line-height: 18px; background-color: var(--color-placeholder); border: 1px solid var(--color-placeholder-border); border-radius: 5px; } .taskactionbtn { height: 1rem; width: 1rem; visibility: hidden; /* allocate space */ mask: url(images/task-menu2.svg) no-repeat; -webkit-mask: url(images/task-menu2.svg) no-repeat; background-color: var(--color-btn-reduced); transition: background-color 0.1s ease-in; } .taskactionbtn::after { /* for baseline */ content:'0'; color:transparent; } li.task-row:hover .taskactionbtn { visibility:visible; } .taskactionbtn:hover, .taskactionbtn.mtt-menu-button-active { visibility:visible; cursor:pointer; background-color: var(--color-btn-reduced-hover); } #tasklist.filter-past li.task-row, #tasklist.filter-today li.task-row, #tasklist.filter-soon li.task-row { display:none; } #tasklist.filter-past li.task-row.past, #tasklist.filter-today li.task-row.today, #tasklist.filter-soon li.task-row.soon { display:block; } #tasklist.filter-past li.task-row.task-completed, #tasklist.filter-today li.task-row.task-completed, #tasklist.filter-soon li.task-row.task-completed { display:none; } .task-note-block { padding: 10px 2rem; color: var(--color-text-reduced); background-size:16px 16px; min-height:16px; display:none; white-space:normal; word-wrap:break-word; } li.task-row.task-expanded .task-note-block { display:block; } li.task-row.task-completed .task-note-block .task-note { text-decoration:line-through; } .task-note-area { display:none; margin-bottom:5px; } .task-note-area textarea { color:var(--color-text-reduced); width:100%; display:block; height:65px; } .task-note-actions { font-size:0.8rem; } .hidden { display:none; } .invisible { visibility:hidden; } .task-note a { color: var(--color-tasklist-note-link); } .task-note a:hover { color: var(--color-tasklist-link-hover) } .task-prio { padding-left:2px; padding-right:2px; margin-left:0px; margin-right:5px; cursor:default; } .prio-neg { background-color:var(--color-priority-low); color: var(--color-priority-text); } .prio-pos { background-color:var(--color-priority-urgent); color:var(--color-priority-text); } .prio-pos-1 { background-color:var(--color-priority-high); color:var(--color-priority-text); } .task-prio.prio-zero { display:none; } /* */ #tasks_info { display: flex; justify-content: center; align-content: center; flex-direction: column; min-height: 100px; border: 1px solid var(--color-border-default); border-radius: 5px; } #tasks_info .v { font-size: 1.1rem; font-weight: bold; text-align: center; } .form-container { display: flex; flex-wrap: wrap; } .form-row { margin-top:0.6rem; } .form-row-short { margin-right:12px; } .form-row:not(.form-row-short) { width:100%; } .form-row .h { font-weight:bold; color: var(--color-text-reduced); } .form-row div.h { margin-bottom:3px; } .form-input { color: var(--color-text-reduced); } .form-input.in100 { width:100px; } .form-input.inmax { width:100%; box-sizing:border-box; } textarea.form-input.inmax { height:280px; font-size:13px; font-family: ui-monospace,Consolas,"SF Mono",Menlo,"Liberation Mono",monospace; white-space: pre; padding: 5px; } .alltags-cell { width:1%; white-space:nowrap; padding-left:5px; } #page_taskedit.mtt-inadd .mtt-inedit { display:none; } #page_taskedit.mtt-inedit .mtt-inadd { display:none; } #taskedit_id, #taskviewer_id { color: var(--color-text-reduced2); font-weight: normal; margin-left: 5px; } #taskedit_info { font-size:1rem; font-weight:normal; color:var(--color-text-reduced2); margin-bottom:6px; } #taskedit_info > div { margin-top:6px; } #page_taskviewer h3 { display: flex; align-items: baseline; } #page_taskviewer .container { display: flex; max-width: 100%; } #page_taskviewer .container .left { flex-grow: 1; min-width: 0; /* to limit long text*/ border-right: 1px solid var(--color-tasklist-row-expanded-border); padding-right: 0.5rem; } #page_taskviewer .container .right { min-width:300px; max-width:300px; padding-left: 0.5rem; overflow: hidden; } #page_taskviewer .container .right .property { padding: 0.8rem 0.5rem; } #page_taskviewer .container .right .property:not(:last-child) { border-bottom: 1px solid var(--color-tasklist-row-expanded-border); } #page_taskviewer .container .right .form-bottom-buttons .mtt-back-button { display: none; } #page_taskviewer .title { word-break: break-word; } #page_taskviewer .note { line-height: 1.3; display: block; white-space: normal; word-wrap: break-word; } #page_taskviewer .no-note { color: var(--color-text-reduced3); display: none; } #page_taskviewer.no-note .no-note { display: block; } #page_taskviewer.no-note .note { display: none; } #page_taskviewer .property.tags .content { display: inline-flex; flex-wrap: wrap; max-width: 100%; } #mtt.readonly #taskviewer_edit_btn { display: none; } /* autocomplete */ .ui-helper-hidden-accessible { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } .ui-autocomplete { position: absolute; padding:0px; border:1px solid var(--color-border-default); background-color: var(--color-menu); overflow:hidden; z-index:99999; box-shadow: var(--color-popup-shadow); } .ui-autocomplete .ui-menu-item { margin: 0px; cursor:default; overflow: hidden; } .ui-autocomplete .ui-menu-item-wrapper { position: relative; padding:0.3rem 4px; } .ui-autocomplete .ui-menu-item-wrapper.ui-state-active { background-color:var(--color-menu-hover); color: var(--color-menu-text-hover); } #priopopup { overflow: hidden; z-index:100; background-color: var(--color-menu); border:1px solid var(--color-border-default); padding:5px; } #priopopup span { cursor:pointer; border:1px solid var(--color-menu); padding:1px; } #priopopup .prio-zero:hover { border-color: var(--color-priority-none); } #priopopup .prio-neg:hover { border-color: var(--color-priority-low); } #priopopup .prio-pos:hover { border-color: var(--color-priority-urgent); } #priopopup .prio-pos-1:hover { border-color: var(--color-priority-high); } #tagcloudbtn { font-size: 1rem; font-weight: normal; margin-left: auto; } #tagcloud { overflow: hidden; z-index:100; background-color:var(--color-menu); border:1px solid var(--color-border-default); width: 100%; max-width: 600px; text-align: center; box-shadow: var(--color-popup-shadow); border-radius: 5px; } #tagcloud.mtt-left-adjusted { margin-left:5px; } #tagcloud.mtt-right-adjusted { margin-right:5px; } #tagcloud.mtt-left-adjusted.mtt-right-adjusted { margin-bottom:5px; } #tagcloud > div { padding:5px; } #tagcloudload { display:none; height:24px; background:url(images/loading48.gif) center no-repeat; background-size:24px 24px; } #tagcloud .actions { display: flex; align-items: center; border-bottom: 1px solid var(--color-menu-border); } #tagcloud .actions > *:first-child { flex-grow: 1; } #tagcloudAllLists:disabled + label { opacity: 0.6; } #tagcloudSearch { margin-left: 0.5rem; } #tagcloudcancel span { mask: var(--svg-closetag) no-repeat; -webkit-mask: var(--svg-closetag) no-repeat; background-color: var(--color-btn-reduced); } #tagcloudcontent { overflow: hidden; text-overflow: ellipsis; } #tagcloud .tag, #alltags .tag, #page_taskviewer .property.tags .tag { display: inline-block; padding: 2px 0.5em; border-radius: 1em; color:var(--color-taglist-tag); background-color:var(--color-taglist-tag-bg); cursor: pointer; background-clip: padding-box; /* for transparent border */ border: 1px solid transparent; margin-bottom:2px; margin-right: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1em; font-weight: normal; } #alltags .tag { margin-right: 3px; } #page_taskviewer .property.tags .tag { overflow: hidden; text-overflow: ellipsis; margin-bottom:0; margin-top:3px; } #tagcloud .tag:hover, #alltags .tag:hover, #page_taskviewer .property.tags .tag:hover { /*background-color:var(--color-taglist-tag-hover-bg);*/ border-color: var(--color-taglist-tag-hover-bg);; color: var(--color-taglist-tag-hover); } .ui-datepicker { width: 250px; z-index: 202; border: 1px solid var(--color-menu-border); background: var(--color-menu); display: none; padding: 2px; box-shadow: var(--color-popup-shadow); border-radius: 5px; } .ui-datepicker-trigger { cursor:pointer; vertical-align:text-bottom; margin-left:4px; margin-right:4px; width:16px; height:16px; } .ui-datepicker-calendar { width:100%; border-collapse:collapse; } .ui-datepicker-calendar thead th { text-align:center; padding:1px; font-size:0.9rem; } .ui-datepicker-calendar tbody td { text-align:right; padding:1px; } .ui-datepicker-calendar td a { display:block; text-decoration:none; border:1px solid var(--color-border-default); background-color:var(--color-menu); color:var(--color-text-reduced); padding:4px; } .ui-datepicker-calendar td.ui-datepicker-current-day a { background-color: var(--color-menu-hover); color: var(--color-menu-text-hover); border-color: var(--color-menu-hover); } .ui-datepicker-calendar td.ui-datepicker-today a { color: var(--color-menu-text-hover); background-color: var(--color-border-default); } .ui-datepicker-calendar td a:hover { border-color: var(--color-link); } .ui-datepicker-header { padding:3px 0px; } .ui-datepicker-prev { position:absolute; left:2px; height:20px; text-decoration:none; } .ui-datepicker-next { position:absolute; right:2px; height:20px; text-decoration:none; } .ui-datepicker-title { text-align:center; line-height:20px; } .ui-icon { width:16px; height:16px; text-indent:-99999px; overflow:hidden; } .ui-datepicker .ui-icon-circle-triangle-w { display:block; position:absolute; top:50%; margin-top:-8px; left:50%; mask: url(images/arr-left.svg) no-repeat; -webkit-mask: url(images/arr-left.svg) no-repeat; background-color: var(--color-btn-default); } .ui-datepicker .ui-icon-circle-triangle-e { display:block; position:absolute; top:50%; margin-top:-8px; right:50%; mask: var(--svg-arr-right) no-repeat; -webkit-mask: var(--svg-arr-right) no-repeat; background-color: var(--color-btn-default); } .ui-datepicker select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background: transparent var(--svg-select) no-repeat top 2px right 4px; background-size: 1rem 1rem; padding: 1px 3px; padding-right: calc(1rem + 7px); border: 1px solid var(--color-border-default); border-radius: 2px; color: var(--color-text-default); margin: 0 2px; } .ui-datepicker select:focus { outline:none; /*border-color: var(--color-border-focus); box-shadow:0 0 0 2px var(--color-border-focus-shadow);*/ } .mtt-menu-button { user-select: none; cursor:pointer; padding:4px; transition:background-color 0.1s ease-in; } .mtt-menu-button:hover, .mtt-menu-button.mtt-menu-button-active { background-color: var(--color-btn-hover); border-radius:4px; } /* Menu */ .mtt-menu-container { overflow: hidden; max-width: 100vw; z-index: 100; background-color: var(--color-menu); border: 1px solid var(--color-menu-border); padding: 2px 0px; box-shadow: var(--color-popup-shadow); border-radius: 5px; user-select: none; } .mtt-menu-container.mtt-left-adjusted { margin-left:5px; } .mtt-menu-container.mtt-right-adjusted { margin-right:5px; } .mtt-menu-container.mtt-right-adjusted.mtt-left-adjusted { margin-bottom:5px; } .mtt-menu-container ul { list-style: none; padding:0; margin:0; } .mtt-menu-container li { margin:1px 0px; cursor:default; color: var(--color-menu-text); white-space:nowrap; padding-top:0.20rem; padding-bottom:0.20rem; padding-left:28px; padding-right:28px; position:relative; overflow:hidden; text-overflow:ellipsis; } .mtt-menu-container li:hover, .mtt-menu-container li.mtt-menu-item-active { background-color: var(--color-menu-hover); color: var(--color-menu-text-hover); } .mtt-menu-container li.mtt-item-disabled, .mtt-menu-container li.mtt-item-disabled a { color: var(--color-menu-text-disabled); } .mtt-menu-container a { display:block; cursor:default; text-decoration:none; outline:none; color: var(--color-menu-text); overflow:hidden; text-overflow:ellipsis; } .mtt-menu-container li:hover a { color: var(--color-menu-text-hover); } .mtt-menu-container li.mtt-menu-delimiter { height:0px; line-height:0; border-bottom:1px solid var(--color-menu-border); margin:2px -1px; padding:0px; font-size:0px; } .mtt-menu-container .menu-icon { width:16px; height:16px; position:absolute; left:6px; top:50%; margin-top:-8px; } li.mtt-item-checked .menu-icon { mask: var(--svg-checkmark) no-repeat; -webkit-mask: var(--svg-checkmark) no-repeat; background-color: var(--color-btn-default); } li.mtt-menu-indicator .submenu-icon { position:absolute; right:6px; top:50%; margin-top:-8px; width:16px; height:16px; mask: var(--svg-arr-right) no-repeat; -webkit-mask: var(--svg-arr-right) no-repeat; background-color: var(--color-btn-default); } .mtt-menu-container .counter { position:absolute; right:6px; top:50%; margin-top:-8px; height: 16px; min-width:16px; border-radius: 1rem; font-size: 0.8rem; text-align: center; background-color: #686868; /* #de5141 */ color: white; } li.mtt-item-hidden { display:none; } #slmenucontainer li.mtt-list-hidden a { font-style:italic; } #cmenulistscontainer li.mtt-list-hidden { font-style:italic; } #mtt.readonly .mtt-need-list { display:none; } #mtt.readonly .mtt-only-authorized { display:none; } /**/ #btnRssFeed .menu-icon { background:var(--svg-rss) no-repeat; } #btnRssFeed.mtt-item-disabled .menu-icon { background:var(--svg-rss-disabled) no-repeat; } #task, #search { transition: box-shadow .1s ease-in-out; } #task:focus, #search:focus { outline: none; border-color: var(--color-border-focus); box-shadow: 0 0 0 2px var(--color-border-focus-shadow); } .monospace, pre { font-family: ui-monospace,Consolas,"SF Mono",Menlo,"Liberation Mono",monospace; } .mtt-settings-table { width:100%; } .mtt-settings-table .tr { padding: 10px 0; vertical-align:top; display: flex; } .mtt-settings-table .tr.group-header { border-bottom: none !important; padding-bottom: 0 !important; background-color: unset !important; } .mtt-settings-table .group { margin-top: 0.75rem; margin-bottom: 0.5rem; border-radius: 1rem; background-color: var(--color-settings-row); } .mtt-settings-table .group .tr { margin: 0 10px; padding: 1rem 0; } .mtt-settings-table .group .th { padding-left: 0; } .mtt-settings-table .group .td { padding-right: 0; } .mtt-settings-table .tr:not(:last-child) { border-bottom:1px solid var(--color-row-underlinig); } .mtt-settings-table .th, .mtt-settings-table .td { padding: 0 10px; } .mtt-settings-table .th { width: 30%; font-weight: bold; font-weight: 600; } .mtt-settings-table .group-header .th { font-size: 0.9rem; } .mtt-settings-table .td { flex-grow: 1; line-height: 1.6em; } .mtt-settings-table .td.extensions { line-height: 1.7em; } .mtt-settings-table .descr { display: block; font-size:0.8rem; font-weight:normal; color:var(--color-text-reduced); padding-top: 4px; } .mtt-settings-table .in350 { min-width:350px; } .mtt-settings-table .inmax { width:100%; } .mtt-settings-table textarea.in350 { height:400px; } .mtt-settings-table textarea.inmax { height:400px; width:100%; } .mtt-settings-table input, .mtt-settings-table select, .mtt-settings-table button, .mtt-settings-table textarea, .mtt-settings-upload-button { padding: 3px; border: 1px solid var(--color-border-default); border-radius: 2px; background-color: var(--color-input-bg); color: var(--color-text-default); } .mtt-settings-table button:disabled { background-color: var(--color-btn-hover); color: var(--color-text-reduced2); } .mtt-settings-upload-button input[type=file] { visibility:hidden; position:absolute; z-index:-2; } .mtt-settings-table select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background: var(--color-input-bg) var(--svg-select) no-repeat top 4px right 5px; background-size: 1rem 1rem; padding-right: calc(1rem + 10px); } .mtt-settings-table input:focus, .mtt-settings-table select:focus { outline:none; border-color: var(--color-border-focus); box-shadow:0 0 0 2px var(--color-border-focus-shadow); } .mtt-settings-table a { color: var(--color-text-default); } .mtt-settings-table button, .mtt-settings-upload-button { padding: 3px 6px; border-radius: 3px; } .mtt-settings-table button:hover, .mtt-settings-upload-button:hover { background-color: var(--color-btn-hover); } .mtt-settings-table .form-bottom-buttons button { min-width: 4rem; padding: 4px 6px; margin: 0 6px; background-color: var(--color-submit); border: 1px solid var(--color-border-default); border-radius: 3px; } .mtt-settings-table .form-bottom-buttons button:hover { background-color: var(--color-submit-hover); } #modal { position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } #modal .modal-box { min-width: 350px; max-width: 1100px; /* same as #mtt */ border: 1px solid var(--color-menu-border); background: var(--color-bg); box-shadow: var(--color-popup-shadow); border-radius: 5px; opacity: 1; } #modal .modal-content { padding: 1.5rem; } #modal .modal-content input { margin-top: 1.5rem; width: 100%; } #modal input { padding: 3px; border: 1px solid var(--color-border-default); border-radius: 2px; } #modal .modal-bottom { border-top: 1px solid var(--color-border-default); padding: 0.8rem; } /* font for small screens */ @media only screen and (max-width: 600px), only screen and (max-height: 600px) { html { font-size: 16px; -webkit-text-size-adjust: 100%; /* Dont increase font-size in horizontal orientation on ios */ } textarea.form-input.inmax { font-size: 14px; } } /* ========== narrow screens =========*/ @media only screen and (max-width: 600px) { #mtt { padding: 15px 8px 0px; } #mtt.page-ajax .topblock { display: none; } /* hide topmost block if settings pages is opened */ #mtt.page-taskedit .topblock { display: none; } /* hide topmost block if edit/add task page is opened */ h2 { font-size:1rem; } .topblock h2 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* min-width of parent flex is required for this */ } .mtt-img-button { padding:4px; } .mtt-menu-button { padding:4px; } h3.page-title a.mtt-back-button { width: 2rem; } #page_ajax.mtt-page-settings h3.page-title { padding-left: 10px; border: none; } #page_ajax.mtt-page-settings h3.page-title a.mtt-back-button { width: 1.5rem; } .mtt-tabs-new-button { padding-left:0.4rem; padding-right:0.4rem; } /* make thiсker */ /* singletab */ li.mtt-tab { display:none; } li.mtt-tab.mtt-tab-selected { display:block; } #task { padding:5px; padding-right:18px; margin-left:-22px; } #task_placeholder span { padding:6px; } .searchbox-c { width:30%; max-width:190px; } #toolbar.mtt-intask .searchbox-c { display:none; } #toolbar.mtt-insearch .taskbox-c { display:none; } #toolbar.mtt-insearch .searchbox-c { width:100%; max-width:100%; } #search { padding:5px 20px; border-radius:15px; } #tagcloudSearch { margin-top: 0.5rem; } .task-date { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .task-note-actions { display:block; padding-top:8px; } .task-note-block { padding-left: 0.625rem; padding-right: 0.625rem; display: none; } .task-note-area textarea { width:95%; } .taskactionbtn { padding: 2px 0.5rem; mask-origin: content-box; -webkit-mask-origin: content-box; visibility: visible; } .duedate:before { content:'\279d\20'; /* Use another arrow, because Rightwards Arrow U+2192 is ugly in Noto font used in some Androids */ } #tasklist li.task-row .task-through { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } #tasklist li.task-row.task-expanded .task-note-block { display:none; } .task-toggle { display:none; } #tasklist .mtt-task-placeholder { line-height:1rem; padding-top:0.6rem; padding-bottom:0.6rem; } #tasklist .ui-sortable-helper { box-shadow:0px 0px 3px #555; opacity: 0.9; } #tasklist .ui-sortable-helper .task-left { visibility: hidden } #tasklist .ui-sortable-helper .task-actions > * { visibility: hidden } #page_taskedit { max-width:100%; border:none; position:static; padding:0; } #page_taskedit .form-table { width:100%; } #page_taskedit .form-row textarea { height: 150px; } #loading { padding:0px; padding-top:1px; padding-right:1px; height:16px; overflow:hidden; } #tagcloud { max-width:100%; margin:0px 5px 5px 5px; } .mtt-settings-table .in350 { min-width:50px; width:100%; } .mtt-notes-showhide { display:none; } .mtt-menu-container li { padding-top: 0.4rem; padding-bottom: 0.4rem; } #page_taskviewer .container { flex-direction: column; } #page_taskviewer .container .left { border-right: none; padding-right: 0; } #page_taskviewer .container .right { max-width: unset; padding-left: 0; } #page_taskviewer .container .right .property { padding: 0.8rem 0; } #page_taskviewer .container .right .form-bottom-buttons .mtt-back-button { display: block; } #page_taskviewer .note, #page_taskviewer .no-note { padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 2px solid var(--color-tasklist-row-expanded-border); } .mtt-settings-table .tr:not(.form-bottom-buttons) { display: block; background-color:var(--color-settings-row); border-radius: 10px; padding:10px 15px; margin-bottom: 5px; } .mtt-settings-table .group { background-color: var(--color-settings-row); } .mtt-settings-table .group .tr { border-radius: 0; padding: 1rem 1rem; margin: 0 0; margin-bottom: 5px; background-color: unset; } .mtt-settings-table .group .tr:not(:last-child) { border-bottom:1px solid var(--color-border-default); } .mtt-settings-table .group .th { padding: 0; } .mtt-settings-table .th { width: auto; padding-left: 0px; } .mtt-settings-table .td { padding-top: 4px; padding-left: 0px; } .mtt-settings-table .td.extensions { line-height: 2em; } .mtt-settings-table input, .mtt-settings-table select, .mtt-settings-table label { margin-top:5px; box-sizing: border-box; } .form-bottom-buttons > * { padding: 7px; border-radius: 14px; } } /* end of @media min 600px */ ================================================ FILE: src/content/theme/style_rtl.css ================================================ body { direction:rtl; } .topblock h2 { background-position: right; padding-left: 10px; padding-right: 30px; } #loading { margin-left:6px; margin-right:0px; } .bar-menu { text-align: left; } .bar-menu > * { margin-right: 10px; margin-left: unset; } .mtt-tab { margin-left:3px; margin-right:0px; border-top-right-radius:0px; border-top-left-radius:8px; } .mtt-tabs-new-button { border-top-right-radius:0px; border-top-left-radius:8px; } .mtt-tabs-select-button>span { transform:rotateY(180deg); } #task { padding-left:20px; padding-right:4px; } .mtt-taskbox-icon { left:2px; right:auto; transform:rotateY(180deg); } #newtask_adv { margin-left:0; margin-right:0.5rem; transform:rotateY(180deg); } .mtt-searchbox-icon.mtt-icon-search { right:4px; left:auto; } .mtt-searchbox-icon.mtt-icon-cancelsearch { left:4px; right:auto; } #tagcloudbtn { margin-left:0; margin-right:auto; } .task-toggle { margin-left:2px; margin-right:0; transform:rotate(180deg); } .task-middle { margin-left:0; margin-right:5px; } .task-actions { margin-left:0; margin-right:5px; } .task-prio, .task-title, .task-date, .task-tags { display:inline-block; } /* TODO: fix this */ .task-prio { margin-left:5px; margin-right:0; } .task-note-block { margin-left:0; margin-right:2px; padding-left:0; padding-right:19px; background-position:right 0px;} .task-date { margin-left:0; margin-right:4px; } .duedate { margin-left:0; margin-right:5px; } .duedate-arrow { display:none; } .duedate:before { content:'\20\2190\20'; } #tagcloud { box-shadow:-1px 2px 5px rgba(0,0,0,0.5); } h3.page-title a.mtt-back-button { transform:rotate(180deg); } .form-row-short { margin-left:12px; margin-right:0; } .alltags-cell { padding-left:0; padding-right:5px; } .mtt-menu-container { box-shadow:-1px 2px 5px rgba(0,0,0,0.5); } .mtt-menu-container .menu-icon { left:auto; right:6px; } li.mtt-menu-indicator .submenu-icon { left:6px; right:auto; transform:rotate(180deg); } ================================================ FILE: src/db/.htaccess ================================================ deny from all ================================================ FILE: src/docker-config.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ //$dontStartSession = 1; require_once('./init.php'); $listId = (int)_get('list'); $db = DBConnection::instance(); $listData = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=$listId"); if ( $listData && !is_logged() && !$listData['published'] ) { $extra = json_decode($listData['extra'] ?? '', true, 10, JSON_INVALID_UTF8_SUBSTITUTE); $feedKey = (string) ($extra['feedKey'] ?? ''); $inFeedKey = trim(_get('key')); if ($feedKey == '' || $feedKey != $inFeedKey) { die("Access denied."); } } if (!$listData) { die("No list found."); } $data = DBCore::default()->getTasksByListId($listId, '', (int)$listData['sorting']); if (_get('format') == 'ical') { printICal($listData, $data); } else { printCSV($listData, $data); } function printCSV(array $listData, array $data) { $s = "\xEF\xBB\xBF". "Completed;Priority;Task;Notes;Tags;Due;DateCreated;DateCompleted\n"; foreach($data as $r) { $s .= ($r['compl']?'1':'0'). ';'. $r['prio']. ';'. escape_csv($r['title'] ?? ''). ';'. escape_csv($r['note'] ?? ''). ';'. escape_csv($r['tags'] ?? ''). ';'. $r['duedate']. ';'. date('Y-m-d H:i:s O',$r['d_created']). ';'. ($r['d_completed'] ? date('Y-m-d H:i:s O',$r['d_completed']) :''). "\n"; } header('Content-type: text/csv; charset=utf-8'); header('Content-disposition: attachment; filename=list_'.(int)$listData['id'].'.csv'); print $s; } function escape_csv(string $v) { //escape formulas $nf = ''; $trimmed = ltrim($v); if (strlen($trimmed) > 0 && in_array(substr($trimmed, 0, 1), array('=', '+', '-', '@'))) { $nf = "'"; } return '"'. $nf. str_replace('"', '""', $v). '"'; } function printICal(array $listData, array $data) { $mttToIcalPrio = array("1" => 5, "2" => 1); $s = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nMETHOD:PUBLISH\r\nCALSCALE:GREGORIAN\r\nPRODID:-//myTinyTodo//iCalendar Export v1.4//EN\r\n". "X-WR-CALNAME:". $listData['name']. "\r\nX-MTT-TIMEZONE:".Config::get('timezone')."\r\n"; # to-do foreach($data as $r) { $a = array(); $a[] = "BEGIN:VTODO"; $a[] = "UID:". $r['uuid']; $a[] = "CREATED:". gmdate('Ymd\THis\Z', $r['d_created']); $a[] = "DTSTAMP:". gmdate('Ymd\THis\Z', $r['d_edited']); $a[] = "LAST-MODIFIED:". gmdate('Ymd\THis\Z', $r['d_edited']); $a[] = utf8chunks("SUMMARY:". $r['title']); if($r['duedate']) { $dda = explode('-', $r['duedate']); $a[] = "DUE;VALUE=DATE:".sprintf("%u%02u%02u", $dda[0], $dda[1], $dda[2]); } # Apple's iCal priorities: low-9, medium-5, high-1 if($r['prio'] > 0 && isset($mttToIcalPrio[$r['prio']])) $a[] = "PRIORITY:". $mttToIcalPrio[$r['prio']]; $a[] = "X-MTT-PRIORITY:". $r['prio']; $descr = array(); if($r['tags'] != '') $descr[] = Lang::instance()->get('tags'). ": ". str_replace(',', ', ', $r['tags']); if($r['note'] != '') $descr[] = Lang::instance()->get('note'). ": ". $r['note']; if($descr) $a[] = utf8chunks("DESCRIPTION:". str_replace("\n", '\\n', implode("\n",$descr))); if($r['compl']) { $a[] = "STATUS:COMPLETED"; #used in Sunbird $a[] = "COMPLETED:". gmdate('Ymd\THis\Z', $r['d_completed']); #$a[] = "PERCENT-COMPLETE:100"; #used in Sunbird } if($r['tags'] != '') $a[] = utf8chunks("X-MTT-TAGS:". $r['tags']); $a[] = "END:VTODO\r\n"; $s .= implode("\r\n", $a); } # events foreach($data as $r) { if(!$r['duedate'] || $r['compl']) continue; # skip tasks completed and without duedate $a = array(); $a[] = "BEGIN:VEVENT"; $a[] = "UID:_". $r['uuid']; # do not duplicate VTODO UID $a[] = "CREATED:". gmdate('Ymd\THis\Z', $r['d_created']); $a[] = "DTSTAMP:". gmdate('Ymd\THis\Z', $r['d_edited']); $a[] = "LAST-MODIFIED:". gmdate('Ymd\THis\Z', $r['d_edited']); $a[] = utf8chunks("SUMMARY:". $r['title']); if($r['prio'] > 0 && isset($mttToIcalPrio[$r['prio']])) $a[] = "PRIORITY:". $mttToIcalPrio[$r['prio']]; $dda = explode('-', $r['duedate']); $a[] = "DTSTART;VALUE=DATE:".sprintf("%u%02u%02u", $dda[0], $dda[1], $dda[2]); $a[] = "DTEND;VALUE=DATE:".date('Ymd', mktime(1,1,1,$dda[1],$dda[2],$dda[0]) + 86400); $descr = array(); if($r['tags'] != '') $descr[] = Lang::instance()->get('tags'). ": ". str_replace(',', ', ', $r['tags']); if($r['note'] != '') $descr[] = Lang::instance()->get('note'). ": ". $r['note']; if($descr) $a[] = utf8chunks("DESCRIPTION:". str_replace("\n", '\\n', implode("\n",$descr))); $a[] = "END:VEVENT\r\n"; $s .= implode("\r\n", $a); } $s .= "END:VCALENDAR\r\n"; header('Content-type: text/calendar; charset=utf-8'); header('Content-disposition: attachment; filename=list_'.(int)$listData['id'].'.ics'); print $s; } function utf8chunks($text, $chunklen=75, $delimiter="\r\n\t") { if($text == '') return ''; preg_match_all('/./u', $text, $m); $chars = $m[0]; $a = array(); $s = ''; $max = count($chars); for($i=0; $i<$max; $i++) { $ch = $chars[$i]; if(strlen($s) + strlen($ch) > $chunklen) { # line should be not more than $chunklen bytes $a[] = $s; $s = $ch; } else $s .= $ch; } if($s != '') $a[] = $s; return implode($delimiter, $a); } ================================================ FILE: src/ext/.htaccess ================================================ Order deny,allow Deny from all ================================================ FILE: src/ext/CustomCSS/.htaccess ================================================ deny from all ================================================ FILE: src/ext/CustomCSS/extension.json ================================================ { "bundleId": "CustomCSS", "name": "Custom CSS", "version": "1.0.1", "description": "Add you own css rules" } ================================================ FILE: src/ext/CustomCSS/lang/en.json ================================================ { "ext.CustomCSS.name": "Custom CSS", "customcss.h_css": "CSS", "customcss.d_css": "Write you own CSS rules", "customcss.not_writable": "CSS file is not writable, check permissions for custom.css", "customcss.saved": "Saved" } ================================================ FILE: src/ext/CustomCSS/lang/pl.json ================================================ { "ext.CustomCSS.name": "Własny styl CSS", "customcss.h_css": "CSS", "customcss.d_css": "Dodaj własne reguły CSS", "customcss.not_writable": "Plik stylu nie jest zapisywalny, sprawdź uprawnienia dla pliku custom.css", "customcss.saved": "Zmiany zostały zapisane" } ================================================ FILE: src/ext/CustomCSS/lang/ru.json ================================================ { "ext.CustomCSS.name": "Дополнительные стили", "customcss.h_css": "CSS", "customcss.d_css": "Добавляйте собственные стили CSS", "customcss.not_writable": "Ошибка записи в файл, проверьте права доступа к custom.css", "customcss.saved": "Сохранено" } ================================================ FILE: src/ext/CustomCSS/loader.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ if (!defined('MTTPATH')) { die("Unexpected usage."); } function mtt_ext_customcss_instance(): MTTExtension { return new CustomCssExtension(); } class CustomCssExtension extends MTTExtension implements MTTExtensionSettingsInterface { //the same as dir name const bundleId = 'CustomCSS'; // settings domain const domain = "ext.customcss.json"; const cssFilename = 'custom.css'; function init() { $prefs = self::preferences(); if (isset($prefs['css'])) { $href = htmlspecialchars( get_unsafe_mttinfo('theme_url'). self::cssFilename. '?v='. ($prefs['edited'] ?? 0) ); $cb = function() use ($href) { print "\n"; }; add_action('theme_head_end', $cb); } } function settingsPage(): string { $e = function($s) { return __($s, true); }; $prefs = self::preferences(); $css = htmlspecialchars($prefs['css'] ?? ''); return <<
    {$e('customcss.h_css')}
    {$e('customcss.d_css')}
    EOD; } function settingsPageType(): int { return 0; //default page } function saveSettings(array $params, ?string &$outMessage): bool { if (defined('MTT_DEMO')) { $outMessage = "Demo"; return true; } $css = $params['css'] ?? ''; $cssFilename = MTT_THEME_PATH. self::cssFilename; if (!file_exists($cssFilename)) { @touch($cssFilename); } if (!is_writable($cssFilename)) { $outMessage = __('customcss.not_writable'); return false; } @file_put_contents($cssFilename, $css); $outMessage = __('customcss.saved'); return true; } static function preferences(): array { $prefs['cssFilename'] = $cssFilename = MTT_THEME_PATH. self::cssFilename; if (file_exists($prefs['cssFilename'])) { $prefs['edited'] = filemtime($cssFilename) ?? 0; $prefs['css'] = @file_get_contents($cssFilename) ?? ''; } return $prefs; } } ================================================ FILE: src/ext/_examples/CustomSmartSyntax/.htaccess ================================================ deny from all ================================================ FILE: src/ext/_examples/CustomSmartSyntax/extension.json ================================================ { "bundleId": "CustomSmartSyntax", "name": "Custom Smart Syntax", "version": "1.0", "description": "Prototype to write you own smart syntax parser" } ================================================ FILE: src/ext/_examples/CustomSmartSyntax/loader.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ if (!defined('MTTPATH')) { die("Unexpected usage."); } function mtt_ext_customsmartsyntax_instance(): MTTExtension { return new CustomSmartSyntaxExtension(); } class CustomSmartSyntaxExtension extends MTTExtension implements MTTFilterInterface { //the same as dir name const bundleId = 'CustomSmartSyntax'; function init() { \MTTFilterCenter::addFilterForAction('parseSmartSyntax', $this); } // parseSmartSyntax function filter($title, &$out) { $a = [ 'prio' => 0, // int: -1 .. 2 'title' => $title, // string 'tags' => '', // string: "tag1, tag2, tag3,..." 'duedate' => null, // string: "Y-m-d" format ]; // This filter just overwrites results of default parser $out = $a; } } ================================================ FILE: src/ext/backup/.htaccess ================================================ deny from all ================================================ FILE: src/ext/backup/class.backup.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace BackupExtension; use Exception; class Backup { public $lastErrorString = null; public $filename; private $fh; private $level = 0; private $tagClosed = true; function __construct(?string $filename) { $this->filename = is_null($filename) ? MTTPATH. 'db/backup.xml' : $filename; } function isFileWritable() { if (!file_exists($this->filename)) { @touch($this->filename); } if (!is_writable($this->filename)) { return false; } return true; } function makeBackup() { if (!$this->isFileWritable()) { $this->lastErrorString = __('backup.not_writable'); return false; } $this->fh = fopen($this->filename, 'w'); if ($this->fh === false) { $ea = error_get_last(); $this->lastErrorString = $ea['message'] ?? "Failed to open file for writing"; return false; } $db = \DBConnection::instance(); fwrite($this->fh, "\n"); $this->writeOpeningTag('mttdb', [ 'version' => 1, 'appversion' => \mytinytodo\Version::VERSION, 'dbversion' => \mytinytodo\Version::DB_VERSION, 'dbtype' => $db::DBTYPE, 'created' => date(DATE_ATOM) ]); $this->level = 0; $this->writeTable($db->prefix.'lists', 'lists', 'list'); $this->writeTable($db->prefix.'todolist', 'tasks', 'task'); $this->writeTable($db->prefix.'tags', 'tags', 'tag'); $this->writeTable($db->prefix.'tag2task', 'tag2task', 'item'); $this->writeTable($db->prefix.'settings', 'settings', 'item'); $this->writeClosingTag('mttdb'); fwrite($this->fh, "\n"); if (!fclose($this->fh)) { $ea = error_get_last(); $this->lastErrorString = $ea['message'] ?? "Failed to close file"; return false; } return true; } function writeTable(string $table, string $group, string $itemName) { if (!preg_match("/^[\\w:]+$/", $table)) { throw new Exception("Malformed table name: $table"); } $db = \DBConnection::instance(); $props = null; $autoinc = $this->getTableAutoIncrement($table); if ($autoinc != '') { $props = ['auto_increment' => $autoinc]; } $this->writeOpeningTag($group, $props); $q = $db->dq("SELECT * FROM $table"); while ($r = $q->fetchAssoc()) { $this->writeItem($itemName, $r); } $this->writeClosingTag($group); } function writeItem(string $entity, $r) { $tagAttrs = null; if (isset($r['id'])) { $tagAttrs = ['id' => $r['id']]; unset($r['id']); } $this->writeOpeningTag($entity, $tagAttrs); foreach ($r as $field => $value) { $props = null; if (is_null($value)) { $props['isnull'] = 'yes'; } $this->writeOpeningTag($field, $props); $this->writeTagContent((string)$value); $this->writeClosingTag($field); } $this->writeClosingTag($entity); } function getTableAutoIncrement($table): string { $db = \DBConnection::instance(); if ($db::DBTYPE == \DBConnection::DBTYPE_MYSQL) { $r = $db->sqa("SHOW TABLE STATUS WHERE Name=?", [$table]); return (string)$r['Auto_increment'] ?? ''; } else if ($db::DBTYPE == \DBConnection::DBTYPE_SQLITE) { $seq = (int)$db->sq("SELECT seq FROM sqlite_sequence WHERE name=?", [$table]); if ($seq > 0) return (string)$seq; } else if ($db::DBTYPE == \DBConnection::DBTYPE_POSTGRES) { if ($db->tableFieldExists($table, 'id')) { $v = (int)$db->sq("SELECT last_value FROM ". $table. '_id_seq'); if ($v > 0) return (string)$v; } } return ''; } function writeOpeningTag(string $tag, ?array $attrs = null) { if (!preg_match("/^[\\w:]+$/", $tag)) { throw new Exception("Malformed tag: $tag"); } $data = "<$tag"; if ($attrs !== null) { $a = []; foreach ($attrs as $k => $v) { if (!preg_match("/^[\\w:-]+$/", $k)) { throw new Exception("Malformed attribute name: $k"); } $v = (string)$v; if (preg_match("/[\\r\\n]+/", $v)) { throw new Exception("Malformed attribute value: $v"); } $a[] = "$k=\"". htmlspecialchars($v). "\""; } if (count($a) > 0) { $data .= " ". implode(" ", $a); } } $data .= ">"; $this->write( ($this->tagClosed ? "" : "\n"). str_repeat(' ', $this->level) . $data ); $this->level += 1; $this->tagClosed = false; } function writeClosingTag(string $tag) { if (!preg_match("/^[\\w:]+$/", $tag)) { throw new Exception("Malformed tag: $tag"); } $this->level -= 1; if ($this->level < 0) $this->level = 0; $padding = ''; if ($this->tagClosed) { $padding = str_repeat(' ', $this->level); } $this->write( $padding . "\n" ); $this->tagClosed = true; } function writeTagContent(?string $content) { if ($content !== null) { $this->write( htmlspecialchars($content, ENT_XML1, 'UTF-8') ); //TODO: make xml compliant? } } function write(string $data) { if (false === @fwrite($this->fh, $data)) { $ea = error_get_last(); throw new Exception("Failed to write to file: ". ($ea['message'] ?? "unknown reason")); } } } ================================================ FILE: src/ext/backup/class.check.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace BackupExtension; use DBConnection; class Check { public $lastErrorString = null; public $report = ''; function check(): bool { $db = DBConnection::instance(); $msg = []; // Task without list $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}todolist WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)"); if ($count) { $msg[] = "Tasks without list: $count"; } // Tag without task (not a broblem) $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tags WHERE id NOT IN (SELECT tag_id FROM {$db->prefix}tag2task)"); if ($count) { $msg[] = "Tags without task: $count"; } // tag2task no list $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tag2task WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)"); if ($count) { $msg[] = "tag2task no list: $count"; } // tag2task no tag $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tag2task WHERE tag_id NOT IN (SELECT id FROM {$db->prefix}tags)"); if ($count) { $msg[] = "tag2task no tag: $count"; } // tag2task no task $count = $db->sq("SELECT COUNT(*) FROM {$db->prefix}tag2task WHERE task_id NOT IN (SELECT id FROM {$db->prefix}todolist)"); if ($count) { $msg[] = "tag2task no task: $count"; } $count = 0; $uniqTag = []; // lowerTag => [id, tag] $nonuniqTag = []; // id => [tag, lowerTag, uniqId, uniqTag, taskCount] $q = $db->dq("SELECT id,name,COUNT(task_id) c FROM {$db->prefix}tags t LEFT JOIN {$db->prefix}tag2task tt ON t.id=tt.tag_id GROUP BY id ORDER BY id"); while ($r = $q->fetchAssoc()) { $v = mb_strtolower((string)$r['name'], 'UTF-8'); if (!isset($uniqTag[$v])) { $uniqTag[$v] = [$r['id'], $r['name']]; } else { $count++; $nonuniqTag[$r['id']] = [$r['name'], $v, $uniqTag[$v][0], $uniqTag[$v][1], $r['c']]; } } if ($count > 0) { $msg[] = "Non-unique tags: $count"; foreach ($nonuniqTag as $id => $a) { $msg[] = " ID:{$id} Tag:{$a[0]} (tasks: {$a[4]}) same as ID:{$a[2]} Tag:{$a[3]}"; } } if (count($msg) == 0) { $msg[] = "OK"; } $this->report = implode("\n", $msg); return true; } function repair(): bool { $db = DBConnection::instance(); $db->ex("BEGIN"); // Task without list $count = (int)$db->sq("SELECT COUNT(*) FROM {$db->prefix}todolist WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)"); if ($count > 0) { // Move to new list $listID = \DBCore::default()->createListWithName("Restored tasks"); $db->ex("UPDATE {$db->prefix}todolist SET list_id=? WHERE list_id NOT IN (SELECT id FROM {$db->prefix}lists)", [$listID]); } //Tags $db->ex("DELETE FROM {$db->prefix}tags WHERE id NOT IN (SELECT tag_id FROM {$db->prefix}tag2task)"); $db->ex("DELETE FROM {$db->prefix}tag2task WHERE task_id NOT IN (SELECT id FROM {$db->prefix}todolist)"); $db->ex("DELETE FROM {$db->prefix}tag2task WHERE tag_id NOT IN (SELECT id FROM {$db->prefix}tags)"); //Non-unique tags replace with first unique $uniqTag = []; $replace = []; $q = $db->dq("SELECT id,name FROM {$db->prefix}tags t LEFT JOIN {$db->prefix}tag2task tt ON t.id=tt.tag_id GROUP BY id ORDER BY id"); while ($r = $q->fetchAssoc()) { $v = mb_strtolower((string)$r['name'], 'UTF-8'); if (!isset($uniqTag[$v])) { $uniqTag[$v] = $r['id']; } else { $replace[$r['id']] = $uniqTag[$v]; } } foreach ($replace as $id => $newId) { $db->ex("UPDATE {$db->prefix}tag2task SET tag_id=? WHERE tag_id=?", [$newId, $id]); } $db->ex("DELETE FROM {$db->prefix}tags WHERE id NOT IN (SELECT tag_id FROM {$db->prefix}tag2task)"); // TODO: tag2task no list ? $db->ex("COMMIT"); return true; } } ================================================ FILE: src/ext/backup/class.controller.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace BackupExtension; use BackupExtension; use BackupExtension\Backup; use BackupExtension\Download; use BackupExtension\Check; class Controller extends \ApiController { function postMakeBackup() { require_once('class.backup.php'); $filename = BackupExtension::backupFilePath(); $backup = new Backup($filename); if (!$backup->makeBackup()) { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $backup->lastErrorString ?? '', ]; } $this->response->data = [ 'total' => 1, 'ok' => true, 'msg' => __("backup.done"), 'alertTextOnLoad' => __("backup.done"), ]; } function postDownload() { require_once('class.download.php'); $filename = BackupExtension::backupFilePath(); $download = new Download($filename); if (!$download->checkFileAccess()) { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $download->lastErrorString ?? '', ]; return; } $this->response->data = [ 'total' => 1, 'redirect' => $download->downloadUrl() ]; } function getDownload() { require_once('class.download.php'); $filename = BackupExtension::backupFilePath(); $download = new Download($filename); $ott = (string)_get('t'); if (!$download->checkFileAccess($ott)) { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $download->lastErrorString ?? '', ]; return; } $download->printFile(); exit(); } function postRestore() { require_once('class.restore.php'); $restore = new Restore(); if (!$restore->isUploaded()) { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $restore->lastErrorString ?? '', ]; return; } if (!$restore->restore()) { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $restore->lastErrorString ?? '', ]; return; } $this->response->data = [ 'total' => 1, 'msg' => __("backup.done"), 'redirect' => get_mttinfo('url'), ]; } function postCheckInconsistency() { require_once('class.check.php'); $check = new Check(); if (!$check->check()) { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $check->lastErrorString ?? '', ]; return; } $this->response->data = [ 'total' => 1, 'ok' => true, 'msg' => __("backup.done"), ]; if ($check->report == 'OK') { $this->response->data['alertText'] = "OK"; } else { $this->response->data['html'] = "
    ". htmlspecialchars($check->report). "
    "; } } function postRepairInconsistency() { require_once('class.check.php'); $check = new Check(); if (!$check->repair()) { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $check->lastErrorString ?? '', ]; return; } $this->response->data = [ 'total' => 1, 'ok' => true, 'msg' => __("backup.done"), 'alertText' => __("backup.done"), ]; } } ================================================ FILE: src/ext/backup/class.download.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace BackupExtension; use BackupExtension; class Download { public $filename; public $lastErrorString = null; private $token = ''; function __construct(?string $filename) { $this->filename = is_null($filename) ? MTTPATH. 'db/backup.xml' : $filename; } function checkFileAccess(?string $tokenHash = null): bool { if (!file_exists($this->filename)) { $this->lastErrorString = "Backup file not found"; return false; } $this->token = access_token(); if ($this->token == '') { $this->lastErrorString = "No token provided"; return false; } if (!is_null($tokenHash)) { $a = explode(':', $tokenHash, 2); $rnd = $a[0] ?? ''; $hash = $a[1] ?? ''; if (!hash_equals(hash_hmac('sha256', $rnd, $this->token), $hash)) { $this->lastErrorString = "No temp token provided"; return false; } } return true; } function downloadUrl() { $rnd = randomString(); $hash = $rnd. ':'. hash_hmac('sha256', $rnd, $this->token); $url = BackupExtension::extApiActionUrl("download", "t=$hash"); return $url; } function printFile() { header('Content-type: application/xml; charset=utf-8'); header('Content-disposition: attachment; filename=backup.xml'); $fh = fopen($this->filename, "r") or die("Couldn't open file"); if ($fh) { while (!feof($fh)) { $buffer = fgets($fh, 4096); print($buffer); } fclose($fh); } exit(); } } ================================================ FILE: src/ext/backup/class.restore.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace BackupExtension; use XMLReader; use DBConnection; use Exception; class Restore { public $lastErrorString = null; private $filename; /** @var XMLReader */ private $reader; private $tableItem; function __construct() { // xml table => [ db table, xml item ] $this->tableItem = [ 'lists' => ['lists', 'list'], 'tasks' => ['todolist', 'task'], 'tags' => ['tags', 'tag'], 'tag2task' => ['tag2task', 'item'], 'settings' => ['settings', 'item'], ]; } function isUploaded(): bool { if (!isset($_FILES['file']) || !isset($_FILES['file']['name']) || !isset($_FILES['file']['tmp_name'])) { $this->lastErrorString = "Not uploaded"; return false; } $this->filename = $_FILES['file']['tmp_name']; if (!file_exists($this->filename) || !is_readable($this->filename)) { $this->lastErrorString = "Can't open file"; return false; } return true; } function restore(): bool { $this->reader = $reader = new XMLReader(); $reader->open($this->filename); // root element $reader->next('mttdb'); if ($reader->name != 'mttdb') { $this->lastErrorString = "Incorrect format: missing 'mttdb'."; return false; } if (!$this->moveNextElement()) { $this->lastErrorString = "Incorrect format: tables not found."; return false; } $this->beginRestore(); $tables = array_keys($this->tableItem); // Enumerate tables do { if ($reader->nodeType != XMLReader::ELEMENT) { error_log("Unexpected element '{$reader->name}' of type: {$reader->nodeType}"); break; } //error_log("Found table '{$reader->name}'"); $result = null; if (in_array($reader->name, $tables)) { $result = $this->readTable($this->tableItem[$reader->name][0], $this->tableItem[$reader->name][1]); } else { continue; // Unexpected table, just skip } if (is_null($result)) { return false; // Incorrect format, error is set, stop } } while ($this->moveNextElementSameLevel()); $this->endRestore(); $reader->close(); return true; } function moveNextElement(?string $el = null): ?bool { while ($this->reader->read()) { if ($this->reader->nodeType == XMLReader::ELEMENT) { if (!is_null($el) && $this->reader->name != $el) { return false; } return true; } else if ($this->reader->nodeType == XMLReader::END_ELEMENT) { return null; } } return null; } function moveNextElementSameLevel(?string $el = null) { return $this->reader->next() && ($this->reader->nodeType == XMLReader::ELEMENT || $this->moveNextElement($el)); } function readTable(string $table, string $itemName): ?int { $autoinc = $this->reader->getAttribute("auto_increment"); $maxId = 0; $count = 0; // find first item $found = $this->moveNextElement($itemName); if ($found === false) { $this->lastErrorString = "Incorrect item name {$this->reader->name}, expected '{$itemName}'"; return null; // Error } else if (is_null($found)) { return 0; // No items found } do { $count++; $id = $this->reader->getAttribute("id"); if (!is_null($id)) { $maxId = max($maxId, (int)$id); } // error_log("# $count: found $itemName with id $id"); $itemXml = $this->reader->readOuterXml(); $xml = simplexml_load_string($itemXml); //SimpleXMLElement if ($xml === false) { error_log("Incorrect format of $itemName"); continue; } if (!$this->insertToTable($table, $xml)) { return null; // Error } } while ($this->moveNextElementSameLevel($itemName)); // restore table last auto_increment (mysql) if (!is_null($autoinc)) { $autoinc = max((int)$autoinc, $maxId); } else { $autoinc = $maxId + 1; } if ($autoinc > 1) { $this->updateAutoinc($table, $autoinc); } return $count; } private function insertToTable(string $table, \SimpleXMLElement $xml): bool { if (!preg_match("/^[\\w]+$/", $table)) { throw new Exception("Malformed table name: $table"); } $fields = []; $values = []; $attrsXml = $xml->attributes(); if (isset($attrsXml['id']) && $attrsXml['id'] != '') { $fields[] = 'id'; $values[] = (string)$attrsXml['id']; } foreach ($xml->children() as $item) { $field = $item->getName(); $value = (string)$item; if (!preg_match("/^[\\w]+$/", $field)) { throw new Exception("Malformed field name: $field"); } $attrsXml = $item->attributes(); if (isset($attrsXml['isnull']) && $attrsXml['isnull'] == 'yes') { //$attrsXml['isnull']->__toString() $value = null; } $fields[] = $field; $values[] = $value; } $fieldsStr = implode(',', $fields); // id,name,title ... $subsStr = implode(',', array_fill(0, count($fields), '?')); // ?,?,? ... $db = DBConnection::instance(); try { $db->ex("INSERT INTO {$db->prefix}{$table} ($fieldsStr) VALUES ($subsStr)", $values); } catch (Exception $e) { error_log("Failed query: {$db->lastQuery}"); $this->lastErrorString = "Failed to add data to table '{$db->prefix}$table'. Database error (see query in error log): ". $e->getMessage(); return false; } return true; } private function updateAutoinc(string $table, int $autoinc) { $db = DBConnection::instance(); switch ($db::DBTYPE) { case DBConnection::DBTYPE_MYSQL: $db->ex("ALTER TABLE {$db->prefix}$table AUTO_INCREMENT = ". (int)$autoinc); break; case DBConnection::DBTYPE_POSTGRES: $db->ex("ALTER TABLE {$db->prefix}$table ALTER COLUMN id RESTART WITH ". (int)$autoinc); break; case DBConnection::DBTYPE_SQLITE: $db->ex("UPDATE sqlite_sequence SET seq=? WHERE name=?", [$autoinc, $db->prefix. $table]); break; default: break; } } private function beginRestore() { $db = DBConnection::instance(); $db->ex("BEGIN"); foreach ($this->tableItem as $a) { $table = $db->prefix. $a[0]; if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) { $db->ex("TRUNCATE TABLE $table RESTART IDENTITY"); } else { # - we do not use TRUNCATE on mysql due to autocommit # - sqlite has truncate optimizer while delete all to make it faster # - no need to reset auto_increment sequence before inserting lower ids $db->ex("DELETE FROM $table"); } } $db->ex("DELETE FROM {$db->prefix}sessions"); } private function endRestore() { $db = DBConnection::instance(); $db->ex("COMMIT"); // vacuum? } } ================================================ FILE: src/ext/backup/extension.json ================================================ { "bundleId": "backup", "name": "Backup", "version": "1.1", "description": "Backup" } ================================================ FILE: src/ext/backup/lang/en.json ================================================ { "ext.backup.name": "Backup", "backup.h_make": "Make backup", "backup.d_make": "Will create backup file in '%s' folder.", "backup.make": "Make", "backup.download": "Download", "backup.done": "Done", "backup.not_writable": "Backup file is not writable, check permissions for db/backup.xml", "backup.last_backup": "Last backup: %s", "backup.h_inconsistency": "Check for inconsistency", "backup.d_inconsistency": "If you want to restore the backup to another database version or type, it's better to fix inconsistency before making backup.", "backup.check": "Check", "backup.repair": "Repair", "backup.h_restore": "Restore from backup", "backup.d_restore": "Will delete all records in existing database before restoring.", "backup.restore": "Restore…" } ================================================ FILE: src/ext/backup/lang/ru.json ================================================ { "ext.backup.name": "Резервное копирование", "backup.h_make": "Сделать копию", "backup.d_make": "Сохранит файл в папке '%s'.", "backup.make": "Создать", "backup.download": "Скачать", "backup.done": "Выполнено", "backup.not_writable": "Ошибка записи в файл, проверьте права доступа к db/backup.xml", "backup.last_backup": "Резервная копия: %s", "backup.h_inconsistency": "Проверка базы данных", "backup.d_inconsistency": "Если планируете восстановление из резервной копии в другую базу данных, то лучше исправить возможные ошибки перед резервным копированием.", "backup.check": "Проверить", "backup.repair": "Исправить", "backup.h_restore": "Восстановить из резервной копии", "backup.d_restore": "Удалит все существующие записи во время восстановления.", "backup.restore": "Восстановить…" } ================================================ FILE: src/ext/backup/loader.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ if (!defined('MTTPATH')) { die("Unexpected usage."); } require_once('class.controller.php'); function mtt_ext_backup_instance(): MTTExtension { return new BackupExtension(); } use BackupExtension\Controller; class BackupExtension extends MTTExtension implements MTTExtensionSettingsInterface, MTTHttpApiExtender { //the same as dir name const bundleId = 'backup'; // settings domain const domain = "ext.backup.json"; function init() { } // MTTHttpApiExtender function extendHttpApi(): array { return array( '/makeBackup' => [ 'POST' => [ Controller::class , 'postMakeBackup' ], ], '/download' => [ 'POST' => [ Controller::class , 'postDownload' ], 'GET' => [ Controller::class , 'getDownload', true ], // doesn't check auth token ], '/restore' => [ 'POST' => [ Controller::class , 'postRestore' ], ], '/checkInconsistency' => [ 'POST' => [ Controller::class , 'postCheckInconsistency' ], ], '/repairInconsistency' => [ 'POST' => [ Controller::class , 'postRepairInconsistency' ], ], ); } function settingsPage(): string { $warning = ''; $e = function($s, $arg=null) { return __($s, true, $arg); }; $ext = htmlspecialchars(self::bundleId); $downloadDisabled = ''; $lastBackup = ''; $filename = MTTPATH. 'db/backup.xml'; if (file_exists($filename)) { $time = filemtime($filename); $lastBackup = htmlspecialchars( sprintf($e('backup.last_backup'), formatTime(Config::get('dateformat'). " H:i:s", $time)) ); } else { $downloadDisabled = 'disabled'; } return << function onBackupFileChange(el) { const fd = new FormData(); fd.append('file', el.files[0]); mytinytodo.extensionSettingsAction(el.dataset.extSettingsAction, el.dataset.ext, fd); }
    {$e('backup.h_make')}
    {$e('backup.d_make', 'db')}


    $lastBackup  
    {$e('backup.h_inconsistency')}
    {$e('backup.d_inconsistency')}
     
    {$e('backup.h_restore')}
    {$e('backup.d_restore')}
    EOD; } function settingsPageType(): int { return 1; //no form buttons } function saveSettings(array $params, ?string &$outMessage): bool { return false; } /* static function preferences(): array { return [ 'backupFilePath' => MTTPATH. 'db/backup.xml' ]; } */ static function backupFilePath() { //return self::preferences()['backupFilePath']; return MTTPATH. 'db/backup.xml'; } } ================================================ FILE: src/ext/index.html ================================================ ================================================ FILE: src/ext/notifications/.htaccess ================================================ deny from all ================================================ FILE: src/ext/notifications/class.controller.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace Notify; use NotificationsExtension; use Config; class Controller extends \ApiController { function postDeactivateAll() { $prefs = Config::requestDomain(NotificationsExtension::domain); if (isset($prefs['chats'])) { $prefs['chats'] = []; Config::saveDomain(NotificationsExtension::domain, $prefs); } $this->response->data = [ 'total' => 1, 'msg' => __("notifications.all_chats_deactivated") ]; } function postCheck() { $prefs = Config::requestDomain(NotificationsExtension::domain); if (!($prefs['validToken'] ?? false)) { $this->response->data = [ 'total' => 0, 'msg' => __("notifications.bot_not_configured") ]; return; } if (!isset($prefs['chats']) || !is_array($prefs['chats'])) { $prefs['chats'] = []; } $code = $prefs['code'] ?? null; $codeExpires = $prefs['codeExpires'] ?? 0; $token = $prefs['token'] ?? ''; $this->response->data = [ 'total' => 0, 'msg' => __("notifications.no_new_chats") ]; // Read messages since last check $maxId = $prefs['lastUpdateId'] ?? 0; $api = new TelegramApi($token); $api->logApiErrors = true; $updates = $api->getUpdates([ 'offset' => $maxId + 1, 'allowed_updates' => ['message'] ]); if (!is_array($updates) || count($updates) == 0) { return; } // Select last message in every chat $messages = array(); foreach ($updates as $update) { $message = $update['message'] ?? []; $chatId = (string)($message['chat']['id'] ?? 0); $prefs['lastUpdateId'] = max($maxId, $update['update_id'] ?? 0); $messages[$chatId] = $message; } $total = 0; foreach ($messages as $chatId => $message) { $chatId = (int) $chatId; $text = $message['text'] ?? ''; $msgId = (int) ($message['message_id'] ?? 0); if (in_array($chatId, $prefs['chats'])) { $api->sendMessage([ 'chat_id' => $chatId, 'text' => __("notifications.already_active") ]); } else if ($text === '/start') { $api->sendMessage([ 'chat_id' => $chatId, 'text' => __("notifications.please_send") ]); } else if ($code === null) { $api->sendMessage([ 'chat_id' => $chatId, 'text' => __("notifications.code_not_set") ]); } else if ($codeExpires < time()) { $api->sendMessage([ 'chat_id' => $chatId, 'text' => __("notifications.code_expired") ]); } else if ($text == $code) { $prefs['chats'][] = $chatId; $api->sendMessage([ 'chat_id' => $chatId, 'reply_to_message_id' => $msgId, 'text' => __("notifications.activated") ]); $total++; $this->response->data = [ 'total' => $total, 'msg' => __("notifications.activated") ]; } else { $api->sendMessage([ 'chat_id' => $chatId, 'reply_to_message_id' => $msgId, 'text' => __("notifications.code_wrong") ]); } } Config::saveDomain(NotificationsExtension::domain, $prefs); } } ================================================ FILE: src/ext/notifications/class.observer.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace Notify; use NotificationsExtension; use MTTNotification; use MTTNotificationCenter; use DBConnection; class NotificationObserver implements \MTTNotificationObserverInterface { private $prefs = null; private $delayedNotifications = []; public function notification(string $notification, $object) { if (!$this->prefs) { $this->init(); } if (count($this->prefs['chats']) == 0 && count($this->prefs['emails']) == 0) { return; // nobody to notify } $db = DBConnection::instance(); switch ($notification) { case MTTNotification::didFinishRequest: $this->processDelayed(); break; case MTTNotification::didCreateTask: case MTTNotification::didCreateList: // Get list name $list = $db->sqa( "SELECT name FROM {$db->prefix}lists WHERE id=?", array($object['listId'] ?? 0) ); $object['listName'] = htmlspecialchars($list['name'] ?? ''); $this->delayedNotifications[] = [ 'notification' => $notification, 'object' => $object ]; MTTNotificationCenter::addObserverForNotification(MTTNotification::didFinishRequest, $this); } } private function processDelayed() { //$db = DBConnection::instance(); $useCli = !function_exists('fastcgi_finish_request'); $sender = new Sender( $this->prefs, $useCli ); foreach ($this->delayedNotifications as $item) { $sender->notify($item); } } private function init() { $this->prefs = NotificationsExtension::preferences(); //$this->token = $this->prefs['token'] ?? ''; } } ================================================ FILE: src/ext/notifications/class.sender.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace Notify; use NotificationsExtension; use MTTNotification; class Sender { private $prefs; private $cli = false; function __construct(array $prefs, bool $useCli = false) { $this->prefs = $prefs; if ($useCli && function_exists('pcntl_fork') && function_exists('posix_setsid')) { $this->cli = true; } } function notify(array $item) { $notification = $item['notification'] ?? ''; $object = $item['object'] ?? null; switch ($notification) { case MTTNotification::didCreateTask: $this->notifyTaskCreated($object); break; case MTTNotification::didCreateList: $this->notifyListCreated($object); break; } } /* $task['title'], $task['tags'], $task['duedate'], $task['listName'] are already escaped */ private function notifyTaskCreated($task) { $link = get_mttinfo('url'). '?task='. $task['id']; //email if (count($this->prefs['emails']) > 0) { $aText = []; $aText[] = "New task in ". htmlspecialchars_decode($task['listName']). ":"; $aText[] = htmlspecialchars_decode($task['title']); $aText[] = ""; if ($task['duedate'] != '') { $aText[] = "Due: ". htmlspecialchars_decode($task['duedate']); } if ($task['tags'] != '') { $aText[] = "Tags: ". implode(", ", preg_split("/,\s*/", htmlspecialchars_decode($task['tags']), -1, PREG_SPLIT_NO_EMPTY)); } if ($aText[count($aText)-1] != '') { $aText[] = ""; } $aText[] = "Link: $link"; $text = implode("\r\n", $aText); $this->sendEmails( $text, "New task #". $task['id']); } // telegram if (count($this->prefs['chats']) > 0) { $text = "New task #". $task['id']. " in ". $task['listName'] .": ". $task['title']; if ($task['duedate'] != '') { $text .= "\nDue: ". $task['duedate']; } if ($task['tags'] != '') { $text .= "\nTags: ". implode(", ", preg_split("/,\s*/", $task['tags'], -1, PREG_SPLIT_NO_EMPTY)); } $this->sendTelegrams($text); } } /* $list['name'] is already escaped */ private function notifyListCreated($list) { $link = get_mttinfo('url'). '?list='. $list['id']; //email if (count($this->prefs['emails']) > 0) { $aText = []; $aText[] = "New list:"; $aText[] = htmlspecialchars_decode($list['name']); $aText[] = ""; $aText[] = "Link: $link"; $text = implode("\r\n", $aText); $this->sendEmails( $text, "New list"); } // telegram if (count($this->prefs['chats']) > 0) { $text = "New list: ". $list['name']. ""; $this->sendTelegrams($text); } } private function sendEmails(string $text, string $subject) { $fromAddr = ''; if (isset($this->prefs['mailfrom']) && $this->prefs['mailfrom'] != '') { $fromAddr = str_replace( ["\r", "\n", ":", "\"", "'", "?"], '', $this->prefs['mailfrom']); } else { $fromAddr = self::suggestedMailFrom(); } $from = "myTinyTodo <$fromAddr>"; $mttTitle = str_replace( ["\r","\n"], '', get_unsafe_mttinfo('title') ); $subject = "[$mttTitle] $subject"; if (!mb_check_encoding($subject, 'ASCII')) { $subject = mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n"); } $headers = [ 'From: '. $from ]; if (mb_check_encoding($text, 'ASCII')) { $headers[] = 'Content-Type: text/plain'; } else { $headers[] = 'Content-Type: text/plain; charset=UTF-8'; $headers[] = 'Content-Transfer-Encoding: 8bit'; } foreach ($this->prefs['emails'] as $email) { mail($email, $subject, $text, implode("\r\n", $headers)); } } private function sendTelegrams(string $text) { if ($this->cli) { $this->sendTelegramsInBackground($text); } else { $this->sendTelegramsWithApi($text); } } // public! function sendTelegramsWithApi(string $text) { if (!isset($this->prefs['token'])) { return; } $api = new TelegramApi($this->prefs['token']); $api->logApiErrors = true; $blockedChats = []; foreach ($this->prefs['chats'] as $chatId) { // try-catch? $result = $api->sendMessage([ 'chat_id' => $chatId, 'parse_mode' => 'HTML', //or MarkdownV2 'text' => $text, 'disable_web_page_preview' => true ]); if (!$result && $api->lastError && $api->lastError['error_code'] == 403) { // User has blocked the bot $blockedChats[] = $chatId; error_log("Bot is blocked in chat $chatId, chat will be deactivated"); } } //We can remove blocked chats from settings if (count($blockedChats) > 0) { $this->prefs['chats'] = array_diff($this->prefs['chats'], $blockedChats); \Config::saveDomain(NotificationsExtension::domain, $this->prefs); } } private function sendTelegramsInBackground(string $text) { $hash = password_hash($this->prefs['token'], PASSWORD_DEFAULT); $dir = __DIR__; $outfile = ''; # or '> /dev/null 2>&1'; //$outfile = '> /dev/null 2>&1'; // if (MTT_DEBUG) { // $outfile = "> $dir/../../db/cli-notify.log 2>&1"; // } $fh = popen("php -f $dir/cli-notify.php $outfile", 'w'); fwrite($fh, $hash."\n".$text); fclose($fh); } public static function suggestedMailFrom(): string { $host = parse_url(get_unsafe_mttinfo('url'), PHP_URL_HOST); $host = preg_replace('/^(www\.)/', '', $host); //$host = gethostname(); if (function_exists('posix_getpwuid') && false !== ($userinfo = posix_getpwuid(posix_getuid())) ) { return $userinfo['name']. '@'. $host; } return "mytinytodo@$host"; } } ================================================ FILE: src/ext/notifications/class.telegramapi.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace Notify; class TelegramApi { private $token = ''; /** @var ?array $lastError */ public $lastError = null; public $logApiErrors = false; public $throwExceptionOnApiError = false; function __construct(string $token) { $this->token = $token; } function getMe(): ?array { return $this->makeGetRequest('getMe'); } function getUpdates(?array $params = null): ?array { return $this->makePostRequest('getUpdates', $params ?? []); } function sendMessage(array $params): ?array { return $this->makePostRequest('sendMessage', $params); } private function makeGetRequest(string $method): ?array { $options = array( 'http' => array( 'ignore_errors' => true ) ); $context = stream_context_create($options); $this->lastError = null; $body = $err = null; set_error_handler(function ($errno, $message, $file, $line) { throw new \ErrorException($message, $errno, $errno, $file, $line); }); try { $body = @file_get_contents('https://api.telegram.org/bot'. $this->token .'/'. $method, false, $context); } catch (\Exception $e) { $err = boolval(ini_get('html_errors')) ? htmlspecialchars_decode($e->getMessage()) : $e->getMessage(); } restore_error_handler(); if ($body === false || null !== $err) { $msg = "Failed to make request to Telegram API ($method)". ($err ? ": $err" : ""); if ($this->logApiErrors) { error_log($msg); } throw new \Exception($msg); } $decodedBody = $this->decodeBody($body, $method); return $decodedBody['result'] ?? []; } private function makePostRequest(string $method, array $params): ?array { $json = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); $options = array( 'http' => array( 'header' => "Content-type: application/json\r\n", 'method' => 'POST', 'content' => $json, 'ignore_errors' => true ) ); $context = stream_context_create($options); $this->lastError = null; $body = $err = null; set_error_handler(function ($errno, $message, $file, $line) { throw new \ErrorException($message, $errno, $errno, $file, $line); }); try { $body = @file_get_contents('https://api.telegram.org/bot'. $this->token .'/'. $method, false, $context); } catch (\Exception $e) { $err = boolval(ini_get('html_errors')) ? htmlspecialchars_decode($e->getMessage()) : $e->getMessage(); } restore_error_handler(); if ($body === false || null !== $err) { $msg = "Failed to make request to Telegram API ($method)". ($err ? ": $err" : ""); if ($this->logApiErrors) { error_log($msg); } throw new \Exception($msg); } $decodedBody = $this->decodeBody($body, $method); return $decodedBody['result'] ?? []; } private function decodeBody(string $body, string $method = ''): array { $decodedBody = json_decode($body, true); if (!is_array($decodedBody)) { $decodedBody = []; } if (!isset($decodedBody['ok'])) { throw new \Exception("Telegram API ($method) Error"); } if ($decodedBody['ok'] === false) { $this->lastError = [ 'error_code' => $decodedBody['error_code'] ?? 0, 'description' => ($decodedBody['description'] ?? '') ]; if ($this->logApiErrors) { error_log("Telegram API ($method) Error ". $this->lastError['error_code']. "): ". $this->lastError['description']); } if ($this->throwExceptionOnApiError) { throw new \Exception("Telegram API ($method) Error ". $this->lastError['error_code']. ": ". $this->lastError['description']); } } return $decodedBody; } } ================================================ FILE: src/ext/notifications/cli-notify.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ set_time_limit(30); if (php_sapi_name() != 'cli') { error_log("Supports cli only"); exit(-1); } if (!function_exists('pcntl_fork')) { error_log("Required PHP module is not found: pcntl"); exit(-2); } if (!function_exists('posix_setsid')) { error_log("Required PHP module is not found: posix"); exit(-2); } $dontStartSession = 1; require(__DIR__.'/../../init.php'); $hash = fgets(STDIN); if ($hash === false) { error_log("No input"); exit(-3); } $hash = trim($hash); $text = stream_get_contents(STDIN); // Wi will fork a child to do a long work $pid = pcntl_fork(); if ($pid == -1) { error_log("Failed to fork a child"); exit(-1); } else if ($pid) { // parent will not wait for child's exit exit; } // Child is here, detach it if (posix_setsid() < 0) { error_log("posix_setsid() failed"); exit; } $prefs = NotificationsExtension::preferences(); if (!isset($prefs['token'])) { error_log("No telegram token"); exit(-4); } $token = $prefs['token'] ?? ''; if (!password_verify($prefs['token'], $hash)) { error_log("Not authorized"); exit(-5); } $sender = new Notify\Sender($prefs); $sender->sendTelegramsWithApi($text); ================================================ FILE: src/ext/notifications/extension.json ================================================ { "bundleId": "notifications", "name": "Notifications", "version": "1.2.1", "description": "Notify about new tasks and lists on e-mail or telegram" } ================================================ FILE: src/ext/notifications/lang/de.json ================================================ { "ext.notifications.name": "Benachrichtigungen", "notifications.urlconfigwarning": "Aktivieren Sie die PHP-Direktive 'allow_url_fopen', um Telegram-Benachrichtigungen zu verwenden.", "notifications.check": "Check", "notifications.bot_not_configured": "Bot ist nicht konfiguriert", "notifications.g_email": "E-Mail", "notifications.h_email": "E-Mail:", "notifications.d_email": "Mehrere Adressen mit Komma trennen.", "notifications.h_mailfrom": "Mail von:", "notifications.d_mailfrom": "Diese E-Mail als Absenderadresse verwenden.", "notifications.g_telegram": "Telegram", "notifications.h_token": "Bot-Token:", "notifications.d_token": "Telegram Bot API-Token von @BotFather", "notifications.h_active_chats": "Aktive Chats:", "notifications.d_active_chats": "Anzahl der Chats, bei denen der Bot Benachrichtigungen sendet.", "notifications.deactivate_all": "Alle deaktivieren", "notifications.h_new_chat": "Neuer Chat:", "notifications.d_new_chat": "Starte eine neue Unterhaltung mit dem Bot, sende diesen Code an den Chat und klicke hier auf "Überprüfen".", "notifications.saved": "Gesichert", "notifications.invalid_email": "Ungültige E-Mail Adresse", "notifications.no_bot_info": "Kann keine Bot-Informationen erhalten, Token scheint ungültig zu sein", "notifications.all_chats_deactivated": "Alle Chats deaktiviert", "notifications.no_new_chats": "Keine neuen Chats": "Keine neuen Chats", "notifications.already_active": "Bereits aktiv", "notifications.please_send": "Bitte senden Sie einen Code zur Aktivierung", "notifications.code_not_set": "Code ist nicht gesetzt", "notifications.code_expired": "Der Code ist abgelaufen", "notifications.code_wrong": "Falscher Code", "notifications.activated": "Aktiviert" } ================================================ FILE: src/ext/notifications/lang/en.json ================================================ { "ext.notifications.name": "Notifications", "notifications.urlconfigwarning": "Enable PHP 'allow_url_fopen' directive to use Telegram notifications.", "notifications.check": "Check", "notifications.bot_not_configured": "Bot is not configured", "notifications.g_email": "E-Mail", "notifications.h_email": "E-mail:", "notifications.d_email": "Separate multiple addresses with comma.", "notifications.h_mailfrom": "Mail from:", "notifications.d_mailfrom": "Use this e-mail as sender's address.", "notifications.g_telegram": "Telegram", "notifications.h_token": "Bot token:", "notifications.d_token": "Telegram Bot API token from @BotFather.", "notifications.h_active_chats": "Active chats:", "notifications.d_active_chats": "Number of chats where bot sends notifications.", "notifications.deactivate_all": "Deactivate all", "notifications.h_new_chat": "New chat:", "notifications.d_new_chat": "Start new conversation with the bot, send this code to the chat and click \"Check\" here.", "notifications.saved": "Saved", "notifications.invalid_email": "Invalid email address", "notifications.no_bot_info": "Can not get bot info, seems token is invalid", "notifications.all_chats_deactivated": "All chats deactivated", "notifications.no_new_chats": "No new chats", "notifications.already_active": "Already active", "notifications.please_send": "Please send a code to activate", "notifications.code_not_set": "Code is not set", "notifications.code_expired": "Code has expired", "notifications.code_wrong": "Wrong code", "notifications.activated": "Activated" } ================================================ FILE: src/ext/notifications/lang/ru.json ================================================ { "ext.notifications.name": "Уведомления", "notifications.urlconfigwarning": "Для использования Telegram требуется включить директиву 'allow_url_fopen' в настройках PHP .", "notifications.check": "Проверить", "notifications.bot_not_configured": "Бот не настроен", "notifications.g_email": "E-Mail", "notifications.h_email": "E-mail:", "notifications.d_email": "Разделите несколько адресов c помощью запятой.", "notifications.h_mailfrom": "Адрес отправителя:", "notifications.d_mailfrom": "Этот адрес будет указан как адрес отправителя в письмах с уведомлениями.", "notifications.g_telegram": "Telegram", "notifications.h_token": "Токен для бота:", "notifications.d_token": "Токен для телеграм-бота, полученный от @BotFather.", "notifications.h_active_chats": "Активные чаты:", "notifications.d_active_chats": "Количество чатов, куда бот присылает уведомления.", "notifications.deactivate_all": "Отключить все", "notifications.h_new_chat": "Новый чат:", "notifications.d_new_chat": "Начните чат с ботом, отправьте ему этот код и нажмите \"Проверить\".", "notifications.saved": "Сохранено", "notifications.invalid_email": "Некорректный адрес e-mail", "notifications.no_bot_info": "Ошибка в подключении бота; возможно, токен некорректный", "notifications.all_chats_deactivated": "All chats deactivated", "notifications.no_new_chats": "Нет новых чатов", "notifications.already_active": "Уже активирован", "notifications.please_send": "Пожалуйста, отправьте код для активации", "notifications.code_not_set": "Код не установлен", "notifications.code_expired": "Истек срок действия кода", "notifications.code_wrong": "Неправильный код", "notifications.activated": "Активирован" } ================================================ FILE: src/ext/notifications/loader.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ if (!defined('MTTPATH')) { die("Unexpected usage."); } if (!function_exists('mb_internal_encoding')) { throw new Exception("Required PHP module is not found: mbstring"); } if (strtoupper(mb_internal_encoding()) != 'UTF-8') { throw new Exception("mb_internal_encoding is not UTF-8"); } require_once('class.observer.php'); require_once('class.controller.php'); require_once('class.sender.php'); require_once('class.telegramapi.php'); // on PHP-FPM we can send telegrams on didFinishRequest without delay // folder is the bundleId of extension // name of function for extension loader has format "mtt_ext_${bundleId}_loader" function mtt_ext_notifications_instance(): MTTExtension { return new NotificationsExtension(); } use Notify\NotificationObserver; use Notify\Controller; use Notify\TelegramApi; class NotificationsExtension extends MTTExtension implements MTTHttpApiExtender, MTTExtensionSettingsInterface { //the same as dir name const bundleId = 'notifications'; // settings domain const domain = "ext.notifications.json"; function init() { // subscribe for notifications MTTNotificationCenter::addObserverForNotifications( [ MTTNotification::didCreateTask, MTTNotification::didCreateList ], new NotificationObserver() ); } // produces smth like like /ext/notifications/deactivate function extendHttpApi(): array { return array( '/deactivate' => [ 'POST' => [ Controller::class , 'postDeactivateAll' ], ], '/check' => [ 'POST' => [ Controller::class , 'postCheck' ], ] ); } function settingsPage(): string { $e = function($s) { return __($s, true); }; $ext = htmlspecialchars(self::bundleId); $prefs = self::preferences(); $emails = htmlspecialchars( implode(', ', $prefs['emails']) ); $mailfrom = htmlspecialchars($prefs['mailfrom'] ?? ''); $mailfromDefault = htmlspecialchars(Notify\Sender::suggestedMailFrom()); $numberOfChats = count($prefs['chats']); $token = $prefs['token'] ?? ''; if (defined('MTT_DEMO') && $token != '') { $token = ""; } $token = htmlspecialchars($token); $botname = $prefs['botname'] ?? ''; $botLink = ''; if ($botname != '') { $botname = htmlspecialchars($botname); $botLink = "@$botname"; $code = $prefs['code'] ?? null; $codeExpires = $prefs['codeExpires'] ?? 0; if ($code === null || $codeExpires < time()) { $prefs['code'] = $code = randomString(6, '0123456789'); $prefs['codeExpires'] = $codeExpires = time() + 60*15; // 15 min Config::saveDomain(self::domain, $prefs); } $newChat = "$botLink

    $code   {$e('notifications.check')}"; } else { $newChat = $e('notifications.bot_not_configured'); } $warning = ''; if (!boolval(ini_get('allow_url_fopen'))) { $warning = "
    ⚠️ {$e('notifications.urlconfigwarning')}
    "; } //$e = function($s) { return __($s, true); }; //$c = function($key) { return htmlspecialchars(Config::get($key)); }; return <<
    {$e('notifications.g_email')}
    {$e('notifications.h_email')}
    {$e('notifications.d_email')}
    {$e('notifications.h_mailfrom')}
    {$e('notifications.d_mailfrom')}
    {$e('notifications.g_telegram')}
    {$e('notifications.h_token')}
    {$e('notifications.d_token')}
    {$e('notifications.h_active_chats')}
    {$e('notifications.d_active_chats')}
    {$e('notifications.h_new_chat')}
    {$e('notifications.d_new_chat')}
    $newChat
    EOD; } function settingsPageType(): int { return 0; //default page } function saveSettings(array $params, ?string &$outMessage): bool { if (defined('MTT_DEMO')) { $outMessage = "Demo"; return true; } $token = $params['token'] ?? ''; $emails = $params['emails'] ?? ''; $mailfrom = $params['mailfrom'] ?? ''; if (!is_string($token) || !is_string($emails) || !is_string($mailfrom)) { throw new Exception("Invalid format"); } $prefs = Config::requestDomain(self::domain); if ($token !== ($prefs['token'] ?? '')) { $prefs['botname'] = ''; $prefs['chats'] = []; $prefs['validToken'] = false; } $prefs['token'] = $token; $prefs['code'] = null; $prefs['emails'] = []; $prefs['mailfrom'] = str_replace(["\r", "\n", ":", "\"", "'", "?"], '', trim($mailfrom)); // validate emails if ($emails != '') { $a = explode(',', $emails); foreach ($a as $email) { $email = trim($email); if (preg_match('/^[^\s\@\|]+@[^\s\@\|]+$/', $email)) { $prefs['emails'][] = $email; } else { $outMessage = __('notifications.invalid_email'); return false; } } } // validate token if ($token != '' && !$prefs['validToken']) { $api = new TelegramApi($token); $api->logApiErrors = true; $api->throwExceptionOnApiError = true; try { $result = $api->getMe(); if ($result && isset($result['username'])) { $prefs['botname'] = $result['username']; } $prefs['validToken'] = true; } catch (Exception $e) { $prefs['token'] = ''; $outMessage = __('notifications.no_bot_info'); if (MTT_DEBUG) { $outMessage .= " (". $e->getMessage(). ")"; } return false; } } Config::saveDomain(self::domain, $prefs); $outMessage = __('notifications.saved'); return true; } static function preferences(): array { $prefs = Config::requestDomain(self::domain); if (!isset($prefs['chats']) || !is_array($prefs['chats'])) { $prefs['chats'] = []; } if (!isset($prefs['emails']) || !is_array($prefs['emails'])) { $prefs['emails'] = []; } return $prefs; } } ================================================ FILE: src/ext/updater/.htaccess ================================================ deny from all ================================================ FILE: src/ext/updater/class.controller.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace UpdaterExtension; use \UpdaterExtension; use \Config; class Controller extends \ApiController { function postCheck() { $prefs = UpdaterExtension::preferences(); $updater = new Updater; $a = $updater->lastVersionInfo(); if ($a) { $prefs['lastCheck'] = time(); $prefs['version'] = $a['version'] ?? ''; $prefs['download'] = $a['download'] ?? ''; Config::saveDomain(UpdaterExtension::domain, $prefs); $this->response->data = [ 'total' => 1 ]; } else { $this->response->data = [ 'total' => 0, 'msg' => __("error"), 'details' => $updater->lastErrorString ?? '' ]; } } function postUpdate() { $prefs = UpdaterExtension::preferences(); $url = $prefs['download'] ?? ''; if ($url == '') { $this->response->data = [ 'total' => 0, 'msg' => __("updater.download_error") ]; return; } $updater = new Updater; $file = MTTPATH. 'update.tar.gz'; if (!$updater->download($url, $file)) { $this->response->data = [ 'total' => 0, 'msg' => __("updater.download_error"), 'details' => $updater->lastErrorString ?? '' ]; return; } if (!$updater->extractAndReplace($file)) { $this->response->data = [ 'total' => 0, 'msg' => __("updater.update_error"), 'details' => $updater->lastErrorString ?? '' ]; return; } @unlink($file); if (function_exists("opcache_reset")) { opcache_reset(); } // TODO: need to run post-update by new version // ... // remove /includes/lang/cns.json #renamed to zh-cn.jpon $prefs['version'] = ''; $prefs['download'] = ''; $prefs['lastCheck'] = 0; Config::saveDomain(UpdaterExtension::domain, $prefs); $this->response->data = [ 'total' => 1, 'msg' => __("updater.updated"), 'reload' => 'ext-settings' ]; } } ================================================ FILE: src/ext/updater/class.updater.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ namespace UpdaterExtension; class Updater { public $lastErrorString = null; public function requestJson(string $url): ?string { $options = array( 'http' => array( 'header' => "Content-type: application/json\r\nUser-Agent: mytinytodo\r\n" ) ); $context = stream_context_create($options); set_error_handler(function ($errno, $message, $file, $line) { throw new \ErrorException($message, $errno, $errno, $file, $line); }); $json = null; $this->lastErrorString = null; try { $json = @file_get_contents($url, false, $context); } catch (\Exception $e) { $this->lastErrorString = boolval(ini_get('html_errors')) ? htmlspecialchars_decode($e->getMessage()) : $e->getMessage(); } restore_error_handler(); if ($json === false) { return null; } return $json; } public function lastVersionInfo(): ?array { $json = $this->requestJson("https://api.github.com/repos/maxpozdeev/mytinytodo/releases"); if ($json === null || $json == '') { error_log("Failed to request releases info: ".$this->lastErrorString); return null; } $releases = json_decode($json, true) ?? []; $a = null; foreach ($releases as $rel) { // find only stable if ($rel['prerelease'] ?? false) { continue; } $ver = substr($rel['tag_name'] ?? 'v', 1); if ($ver == '') continue; if ($a && version_compare($a['__ver'], $ver)) { continue; // skip lower version } $rel['__ver'] = $ver; $a = $rel; } if (null === $a) { $this->lastErrorString = "No release to update to"; return null; } $ret = []; $ver = ''; if (isset($a['tag_name'])) { $ver = substr($a['tag_name'], 1); //remove first 'v' } if ($ver != '' && isset($a['assets']) && is_array($a['assets']) && count($a['assets']) > 0 && ($asset = $a['assets'][0]) && isset($asset['browser_download_url']) ) { $ret['version'] = $ver; $ret['download'] = $asset['browser_download_url']; } else { error_log("HTTP response contains unexpected content"); $this->lastErrorString = "HTTP response contains unexpected content"; } return $ret; } public function download(string $url, string $outfile): bool { $this->lastErrorString = null; $dir = dirname($outfile); if (!is_dir($dir) || !is_writable($dir)) { $this->lastErrorString = "myTinyTodo directory is not writable"; return false; } $f = @fopen($url, 'r'); if ($f === false) { $ea = error_get_last(); $this->lastErrorString = $ea['message'] ?? "Failed to open stream"; return false; } $bytes = @file_put_contents($outfile, $f, LOCK_EX); $ea = error_get_last(); fclose($f); if ($bytes === false) { $this->lastErrorString = $ea['message'] ?? "Can not save file"; return false; } return true; } public function extractAndReplace(string $filename): bool { $this->lastErrorString = null; $dir = MTTPATH; if (!is_dir($dir) || !is_writable($dir)) { $this->lastErrorString = "myTinyTodo directory is not writable"; return false; } $output = null; $retval = null; $command = "tar xzf ". escapeshellarg($filename). " --strip-components 1 -C ". escapeshellarg($dir). " 2>&1"; @exec($command, $output, $retval); if ($retval != 0) { $this->lastErrorString = "Failed to execute tar command ($retval): ". ($output ? implode("\n", $output) : "no output"); error_log($this->lastErrorString); return false; } // Extensions $dir = MTT_EXT; $filename = $dir . 'extensions.tar.gz'; if (file_exists($filename)) { if (!is_writable($dir)) { $this->lastErrorString = "Extensions directory is not writable"; return false; } $command = "tar xzf ". escapeshellarg($filename). " -C ". escapeshellarg($dir). " 2>&1"; @exec($command, $output, $retval); if ($retval != 0) { $this->lastErrorString = "Extensions: failed to execute tar command ($retval): ". ($output ? implode("\n", $output) : "no output"); error_log($this->lastErrorString); return false; } unlink($filename); } return true; } } ================================================ FILE: src/ext/updater/extension.json ================================================ { "bundleId": "updater", "name": "Updates", "version": "0.9.4", "description": "myTinyTodo self-updater" } ================================================ FILE: src/ext/updater/lang/de.json ================================================ { "ext.updater.name": "Updates", "updater.urlconfigwarning": "Aktivieren Sie die PHP-Anweisung 'allow_url_fopen', um Updates herunterladen zu können.", "updater.tarwarning": "Update nicht möglich, 'tar' Erweiterung nicht gefunden.", "updater.h_check_updates": "Checke Updates", "updater.check": "Check", "updater.no_updates": "Keine Updates", "updater.last_version": "Letzte verfügbare Version: %s", "updater.current_version": "Aktuelle Version", "updater.last_checked": "Letzter Check", "updater.new_version_available": "Neue Version verfügbar", "updater.update": "Update", "updater.updated": "Update komplett", "updater.download_error": "Download fehlgeschlagen", "updater.update_error": "Updatefehler" } ================================================ FILE: src/ext/updater/lang/en.json ================================================ { "ext.updater.name": "Updates", "updater.urlconfigwarning": "Enable PHP 'allow_url_fopen' directive to be able to download updates.", "updater.tarwarning": "Update is not possible, 'tar' utility is not found.", "updater.h_check_updates": "Check updates", "updater.check": "Check", "updater.no_updates": "No updates", "updater.last_version": "Last available version: %s", "updater.current_version": "Current version", "updater.last_checked": "Last checked", "updater.new_version_available": "New version is available", "updater.update": "Update", "updater.updated": "Update has completed", "updater.download_error": "Download failed", "updater.update_error": "Update error" } ================================================ FILE: src/ext/updater/lang/ru.json ================================================ { "ext.updater.name": "Обновления", "updater.urlconfigwarning": "Для получения обновлений требуется включить директиву 'allow_url_fopen' в настройках PHP .", "updater.tarwarning": "Исполняемый файл 'tar' не найден, обновление невозможно.", "updater.h_check_updates": "Проверка обновлений", "updater.check": "Проверить", "updater.no_updates": "Нет обновлений", "updater.last_version": "Актуальная версия: %s", "updater.current_version": "Текущая версия", "updater.last_checked": "Последняя проверка", "updater.new_version_available": "Доступно обновление", "updater.update": "Обновить", "updater.updated": "Обновление завершено", "updater.download_error": "Ошибка при загрузке", "updater.update_error": "Ошибка при обновлении" } ================================================ FILE: src/ext/updater/loader.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ if (!defined('MTTPATH')) { die("Unexpected usage."); } require_once('class.controller.php'); require_once('class.updater.php'); function mtt_ext_updater_instance(): MTTExtension { return new UpdaterExtension(); } use UpdaterExtension\Controller; use UpdaterExtension\Updater; class UpdaterExtension extends MTTExtension implements MTTExtensionSettingsInterface, MTTHttpApiExtender { //the same as dir name const bundleId = 'updater'; // settings domain const domain = "ext.updater.json"; function init() { } // MTTHttpApiExtender function extendHttpApi(): array { return array( '/check' => [ 'POST' => [ Controller::class , 'postCheck' ], ], '/update' => [ 'POST' => [ Controller::class , 'postUpdate' ], ] ); } function settingsPage(): string { $e = function($s, $arg=null) { return __($s, true, $arg); }; $ext = htmlspecialchars(self::bundleId); $prefs = self::preferences(); $lastCheck = $prefs['lastCheck'] ?? 0; $version = $prefs['version'] ?? ''; $updateStr = ''; $curVersion = htmlspecialchars(mytinytodo\Version::VERSION); $err = null; if (time() - $lastCheck > 86400*7) { $updater = new Updater; $a = $updater->lastVersionInfo(); if ($a) { $lastCheck = $prefs['lastCheck'] = time(); $version = $prefs['version'] = $a['version'] ?? ''; $prefs['download'] = $a['download'] ?? ''; Config::saveDomain(self::domain, $prefs); } else { $err = $updater->lastErrorString; } } $warning = ''; if ($version != '') { if ( version_compare($version, mytinytodo\Version::VERSION) > 0 ) { $updateStr = "
    {$e('updater.new_version_available')}: ". htmlspecialchars($version); # allow update to v1.7.x and 1.8.x only if ( in_array(substr($version, 0, 4), ["1.7.", "1.8."]) ) { $updateStr .= "

    \n {$e('updater.update')} "; } $retval = 0; $output = null; unset($output); @exec('tar --version', $output, $retval); if ($retval != 0) { $warning = "
    ⚠️ {$e('updater.tarwarning')}
    "; } } else { $updateStr = "
    {$e('updater.no_updates')}
    {$e('updater.last_version', $version)}"; } } $lastCheckStr = $err ? $e('updater.download_error') : ($lastCheck ? timestampToDatetime($lastCheck, true) : ""); if (!boolval(ini_get('allow_url_fopen'))) { $warning .= "
    ⚠️ {$e('updater.urlconfigwarning')}
    "; } return <<
    {$e('updater.h_check_updates')}
    {$e('updater.current_version')}: $curVersion
    {$e('updater.last_checked')}: $lastCheckStr  
    $updateStr
    EOD; } function settingsPageType(): int { return 1; // no form buttons } function saveSettings(array $params, ?string &$outMessage): bool { return true; } static function preferences(): array { $prefs = Config::requestDomain(self::domain); return $prefs; } } ================================================ FILE: src/feed.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ $dontStartSession = 1; require_once('./init.php'); require_once(MTTINC. 'markup.php'); $lang = Lang::instance(); $listId = (int)_get('list'); $db = DBConnection::instance(); $listData = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=$listId"); if ( $listData && need_auth() && !$listData['published'] ) { $extra = json_decode($listData['extra'] ?? '', true, 10, JSON_INVALID_UTF8_SUBSTITUTE); $feedKey = (string) ($extra['feedKey'] ?? ''); $inFeedKey = trim(_get('key')); if ($feedKey == '' || $feedKey != $inFeedKey) { die("Access denied!
    List is not published."); } } if (!$listData) { die("No list found."); } $data = array(); $feedType = _get('feed'); if($feedType == 'completed') { $listData['_feed_descr'] = $lang->get('feed_completed_tasks'); fillData( $data, $listId, 'd_completed', 'compl=1' ); } elseif($feedType == 'modified') { $listData['_feed_descr'] = $lang->get('feed_modified_tasks'); fillData( $data, $listId, 'd_edited', '' ); } elseif($feedType == 'current') { $listData['_feed_descr'] = $lang->get('feed_new_tasks'); fillData( $data, $listId, 'd_created', 'compl=0' ); } elseif($feedType == 'status') { $listData['_feed_descr'] = $lang->get('feed_tasks'); fillData( $data, $listId, 'd_created', '' ); fillData( $data, $listId, 'd_edited', 'compl=0 AND d_edited > d_created' ); fillData( $data, $listId, 'd_completed', 'compl=1' ); } else { $listData['_feed_descr'] = $lang->get('feed_new_tasks'); $feedType = 'tasks'; fillData( $data, $listId, 'd_created', '' ); } $listData['_feed_title'] = sprintf($lang->get('feed_title'), $listData['name']) . ' - '. $listData['_feed_descr']; $listData['_feed_link'] = get_mttinfo('mtt_url'). "feed.php?list=". (int)$listData['id'] . ($feedType != '' ? "&feed=". $feedType : ''); $listData['_feed_type'] = $feedType; htmlarray_ref($listData); printRss($data, $listData); function fillData(array &$data, int $listId, string $field, string $sqlWhere ) { $tasks = DBCore::default()->getTasksByListId($listId, $sqlWhere, "$field DESC", 100); $lang = Lang::instance(); foreach ($tasks as $r) { if ($r['prio'] > 0) { $r['prio'] = '+'.$r['prio']; } $a = array(); //for _descr $a[] = $lang->get('task'). ": ". $r['title']; if ($r['prio']) { $a[] = $lang->get('priority'). ": $r[prio]"; } if ($r['duedate'] != '') { $ad = explode('-', $r['duedate']); $a[] = $lang->get('due'). ": ".formatDate3(Config::get('dateformat'), (int)$ad[0], (int)$ad[1], (int)$ad[2], $lang); } if ($r['tags'] != '') { $a[] = $lang->get('tags'). ": ". str_replace(',', ', ', $r['tags']); } if ($r['compl']) { $a[] = $lang->get('taskdate_completed'). ": ". timestampToDatetime($r['d_completed']); } $r['title'] = htmlspecialchars( $r['title'] ); $r['note'] = noteMarkup($r['note'], true); $r['_descr'] = implode("
    ", htmlarray($a)). "

    ". $r['note']; $r['_title'] = "#". (int)$r['id']. ": ". $r['title']; $r['_d'] = gmdate('r', $r[$field]); $r['_field'] = $field; $data[] = $r; } } function printRss(array $data, array $listData) { $lang = Lang::instance(); $link = get_mttinfo('url'). "?list=". (int)$listData['id']; $buildDate = gmdate('r'); $s = "\n". "\n". "\n". "$listData[_feed_title]\n". "$link\n". "\n". "$listData[_feed_descr]\n". "$buildDate\n\n"; foreach($data as $v) { $guid = $listData['_feed_type']. '-'. $listData['id']. '-'. $v['id']. '-'. $v[$v['_field']]; $itemLink = $link. "&task=". (int)$v['id']; $status = ''; if ( $listData['_feed_type'] == 'status' ) { if ( $v['_field'] == 'd_created' ) { $status = $lang->get('feed_status_new'); } elseif ( $v['_field'] == 'd_edited' ) { $status = $lang->get('feed_status_updated'); } elseif ( $v['_field'] == 'd_completed' ) { $status = $lang->get('feed_status_completed'); } } if ( $status !='' ) $status = "[$status] "; $s .= "\t\n". "\t\t". $status. $v['title']. "\n". "\t\t". $itemLink. "\n". "\t\t". $v['_d']. "\n". "\t\t\n". "\t\t$guid\n". "\t\n\n"; } $s .= "\n"; header("Content-type: text/xml; charset=utf-8"); print $s; } ================================================ FILE: src/includes/.htaccess ================================================ deny from all ================================================ FILE: src/includes/api/AuthController.php ================================================ 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': $this->response->data = $this->login(); break; case 'logout': $this->response->data = $this->logout(); break; case 'session': $this->response->data = $this->createSession(); break; default: $this->response->data = ['total' => 0]; // error 400 ? } } private function login(): ?array { 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(): ?array { check_token(); updateSessionLogged(false); update_token(); session_regenerate_id(true); $t = array('logged' => 0); return $t; } private function createSession(): ?array { $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; } } ================================================ FILE: src/includes/api/ExtSettingsController.php ================================================ extInstance($ext); if (!$instance) { return; } $meta = MTTExtension::extMetaInfo($ext); if (!$meta || !isset($meta['name'])) { return; } $data = $instance->settingsPage(); $lang = Lang::instance(); $nameKey = 'ext.'. $ext. '.name'; if ($lang->hasKey($nameKey)) { $name = htmlspecialchars($lang->get($nameKey)); } else { $name = htmlspecialchars($meta['name']); } $escapedExt = htmlspecialchars($ext); $e = function($s) use($lang) { return htmlspecialchars($lang->get($s)); }; $formStart = ''; $formEnd = ''; $formButtons = ''; if ($instance->settingsPageType() == 0) { $formStart = "
    "; $formEnd = "
    "; $formButtons = << EOD; } $data = << $name $formStart
    $data $formButtons
    $formEnd EOD; $this->response->htmlContent($data); } /** * Save extension settings * @return void * @throws Exception */ function put(string $ext) { checkWriteAccess(); /** @var MTTExtension|MTTExtensionSettingsInterface $instance */ $instance = $this->extInstance($ext); if (!$instance) { return; } //$userError = ''; $saved = $instance->saveSettings($this->req->jsonBody ?? [], $userError); $a = [ 'saved' => (int)$saved ]; if ($userError) { $a['msg'] = $userError; } $this->response->data = $a; } private function extInstance(string $ext): ?MTTExtensionSettingsInterface { $instance = MTTExtensionLoader::extensionInstance($ext); if (!$instance) { $this->response->data = [ 'msg' => "Unknown extension" ]; $this->response->code = 404; return null; } if (! ($instance instanceof MTTExtensionSettingsInterface) ) { $this->response->data = [ 'msg' => "No settings page for extension" ]; $this->response->code = 500; return null; } return $instance; } } ================================================ FILE: src/includes/api/ListsController.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class ListsController extends ApiController { /** * Get all lists * @return void * @throws Exception */ function get() { $db = DBConnection::instance(); check_token(); $t = array(); $t['total'] = 0; $haveWriteAccess = haveWriteAccess(); if (!$haveWriteAccess) { $sqlWhere = 'WHERE published=1'; } else { $sqlWhere = ''; $t['list'][] = $this->prepareAllTasksList(); // show alltasks lists only for authorized user $t['total'] = 1; } $t['time'] = time(); $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, $haveWriteAccess); } $this->response->data = $t; } /** * Create new list and Actions with all lists * Code 201 on success * @return void * @throws Exception */ function post() { checkWriteAccess(); $action = $this->req->jsonBody['action'] ?? ''; switch ($action) { case 'order': $this->response->data = $this->changeListOrder(); break; //compatibility case 'new': default: $this->response->data = $this->createList(); } } /** * Actions with all lists * @return void * @throws Exception */ function put() { checkWriteAccess(); $action = $this->req->jsonBody['action'] ?? ''; switch ($action) { case 'order': $this->response->data = $this->changeListOrder(); break; default: $this->response->data = ['total' => 0]; // error 400 ? } } /* Single list */ /** * Get single list by Id * @param mixed $id * @return void * @throws Exception */ function getId($id) { checkReadAccess($id); $db = DBConnection::instance(); $r = $db->sqa( "SELECT * FROM {$db->prefix}lists WHERE id=?", array($id) ); if (!$r) { $this->response->data = null; return; } $t = $this->prepareList($r, haveWriteAccess()); $this->response->data = $t; } /** * Delete list by Id * @param mixed $id * @return void * @throws Exception */ function deleteId($id) { checkWriteAccess(); $this->response->data = $this->deleteList((int)$id); } /** * Edit some properties of List * Actions: rename, ... * @param mixed $id * @return void * @throws Exception */ function putId($id) { checkWriteAccess(); $id = (int)$id; $action = $this->req->jsonBody['action'] ?? ''; switch ($action) { case 'rename': $this->response->data = $this->renameList($id); break; case 'sort': $this->response->data = $this->sortList($id); break; case 'publish': $this->response->data = $this->publishList($id); break; case 'enableFeedKey': $this->response->data = $this->enableFeedKey($id); break; case 'showNotes': $this->response->data = $this->showNotes($id); break; case 'hide': $this->response->data = $this->hideList($id); break; case 'clearCompleted': $this->response->data = $this->clearCompleted($id); break; case 'delete': $this->response->data = $this->deleteList($id); break; //compatibility default: $this->response->data = ['total' => 0]; } } /* Private Functions */ private function prepareAllTasksList(): array { //default values $hidden = 1; $sort = 3; $showCompleted = 1; $opts = Config::requestDomain('alltasks.json'); if ( isset($opts['hidden']) ) $hidden = (int)$opts['hidden'] ? 1 : 0; if ( isset($opts['sort']) ) $sort = (int)$opts['sort']; if ( isset($opts['showCompleted']) ) $showCompleted = (int)$opts['showCompleted']; return array( 'id' => -1, 'name' => htmlarray(__('alltasks')), 'sort' => $sort, 'published' => 0, 'showCompl' => $showCompleted, 'showNotes' => 0, 'hidden' => $hidden, 'feedKey' => '', ); } private function getListRowById(int $id) { $r = DBCore::default()->getListById($id); if (!$r) { throw new Exception("Failed to fetch list data"); } return $this->prepareList($r, true); } private function prepareList($row, bool $haveWriteAccess): array { $taskview = (int)$row['taskview']; $feedKey = ''; if ($haveWriteAccess) { $extra = json_decode($row['extra'] ?? '', true, 10, JSON_INVALID_UTF8_SUBSTITUTE); if ($extra === false) { error_log("Failed to decodes JSON data of list extra listId=". (int)$row['id'] . ": " . json_last_error_msg()); $extra = []; } $feedKey = (string) ($extra['feedKey'] ?? ''); } 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, 'feedKey' => $feedKey, ); } private function createList(): ?array { $t = array(); $t['total'] = 0; $id = DBCore::default()->createListWithName($this->req->jsonBody['name'] ?? ''); if (!$id) { return $t; } $db = DBConnection::instance(); $t['total'] = 1; $r = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=$id"); $oo = $this->prepareList($r, true); MTTNotificationCenter::postNotification(MTTNotification::didCreateList, $oo); $t['list'][] = $oo; return $t; } private function renameList(int $id): ?array { $db = DBConnection::instance(); $t = array(); $t['total'] = 0; $name = trim($this->req->jsonBody['name'] ?? ''); if ($name == '') return $t; $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, true); return $t; } private function sortList(int $listId): ?array { $sort = (int)($this->req->jsonBody['sort'] ?? 0); self::setListSortingById($listId, $sort); return ['total'=>1]; } static function setListSortingById(int $listId, int $sort) { $db = DBConnection::instance(); if ($sort < 0 || ($sort > 5 && $sort < 100) || $sort > 105) { $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())); } } static function setListShowCompletedById(int $listId, bool $showCompleted) { $db = DBConnection::instance(); if ($listId == -1) { $opts = Config::requestDomain('alltasks.json'); $opts['showCompleted'] = (int)$showCompleted; Config::saveDomain('alltasks.json', $opts); } else { $bitwise = $showCompleted ? 'taskview | 1' : 'taskview & ~1'; $db->dq("UPDATE {$db->prefix}lists SET taskview=$bitwise WHERE id=?", [$listId]); } } private function publishList(int $listId): ?array { $db = DBConnection::instance(); $publish = (int)($this->req->jsonBody['publish'] ?? 0); $db->ex("UPDATE {$db->prefix}lists SET published=?,d_edited=? WHERE id=$listId", array($publish ? 1 : 0, time())); return ['total'=>1]; } private function enableFeedKey(int $listId): ?array { $db = DBConnection::instance(); $flag = (int)($this->req->jsonBody['enable'] ?? 0); $json = $db->sq("SELECT extra FROM {$db->prefix}lists WHERE id=$listId") ?? ''; $extra = strlen($json) > 0 ? json_decode($json, true, 10, JSON_INVALID_UTF8_SUBSTITUTE) : []; if ($extra === false) { error_log("Failed to decodes JSON data of list extra listId=$listId: " . json_last_error_msg()); $extra = []; } if ($flag == 0) { $extra['feedKey'] = ''; } else { $extra['feedKey'] = randomString(); } $json = json_encode($extra, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $db->ex("UPDATE {$db->prefix}lists SET extra=?,d_edited=? WHERE id=$listId", array($json, time())); return [ 'total' => 1, 'list' => [[ 'id' => $listId, 'feedKey' => $extra['feedKey'] ]] ]; } private function showNotes(int $listId): ?array { $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): ?array { $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): ?array { $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"); if (MTTNotificationCenter::hasObserversForNotification(MTTNotification::didDeleteCompletedInList)) { $list = $this->getListRowById($listId); MTTNotificationCenter::postNotification(MTTNotification::didDeleteCompletedInList, [ 'total' => $t['total'], 'list' => $list ]); } return $t; } private function changeListOrder(): ?array { $t = array(); $t['total'] = 0; $order = $this->req->jsonBody['order'] ?? []; if (!array_is_list($order)) { return $t; } $db = DBConnection::instance(); $a = array(); $setCase = ''; $max = count($order); for ($i = 0; $i < $max; $i++) { $id = (int)$order[$i]; $a[] = $id; $setCase .= "WHEN id=$id THEN $i\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; } private function deleteList(int $id) { $db = DBConnection::instance(); $t = array(); $t['total'] = 0; $id = (int)$id; $list = null; if (MTTNotificationCenter::hasObserversForNotification(MTTNotification::didDeleteList)) { $list = $this->getListRowById($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"); if ($t['total'] && MTTNotificationCenter::hasObserversForNotification(MTTNotification::didDeleteList)) { MTTNotificationCenter::postNotification(MTTNotification::didDeleteList, $list); } return $t; } } ================================================ FILE: src/includes/api/TagsController.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class TagsController extends ApiController { /** * Get tag cloud * @return void * @throws Exception */ function getCloud($listId) { $listId = (int)$listId; checkReadAccess($listId); $db = DBConnection::instance(); $sqlWhere = ($listId == -1) ? "" : "WHERE list_id = $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 $sqlWhere GROUP BY tag_id, name ORDER BY tags_count DESC"); $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 = count($at); if (!$count) { $this->response->data = $t; return; } $qmax = max($ac); $qmin = min($ac); if ($count >= 10) $grades = 10; else $grades = $count; $step = ($qmax - $qmin)/$grades; foreach ($at as $i => $tag) { $t['items'][] = array( 'tag' => htmlspecialchars($tag['name']), 'tagText' => (string)$tag['name'], 'id' => (int)$tag['id'], 'count' => $ac[$i], 'w' => $this->tagWeight($qmin, $ac[$i], $step) ); } $t['total'] = $count; $this->response->data = $t; } /** * @return void * @throws Exception */ function getSuggestions($listId) { $listId = (int)_get('list'); checkWriteAccess($listId); $db = DBConnection::instance(); $begin = trim(_get('q')); $limit = 8; $q = $db->dq("SELECT name, tag_id AS id FROM {$db->prefix}tags INNER JOIN {$db->prefix}tag2task ON id=tag_id WHERE list_id=$listId AND ". $db->like('name', '%s%%', $begin). " GROUP BY tag_id, name ORDER BY name LIMIT $limit"); $t = array(); while ($r = $q->fetchRow()) { $t[] = $r[0]; } $this->response->data = $t; } private function tagWeight(int $qmin, int $q, float $step): float { if ($step == 0) return 1.0; $v = ceil(($q - $qmin)/$step); if ($v == 0) return 0.0; else return $v - 1.0; } } ================================================ FILE: src/includes/api/TasksController.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ require_once(MTTINC. 'smartsyntax.php'); class TasksController extends ApiController { /** * Get tasks. * Filters are set with query parameters. * @return void * @throws Exception */ function get() { $listId = (int)_get('list'); checkReadAccess($listId); $db = DBConnection::instance(); $dbcore = DBCore::default(); $sqlWhere = $sqlWhereListId = $sqlHaving = ''; $userLists = []; if ($listId == -1) { $userLists = $this->getUserListsSimple(); $userListsIds = implode(',', array_keys($userLists)); $sqlWhereListId = "todo.list_id IN ($userListsIds) "; } else { $sqlWhereListId = "todo.list_id=". $listId; } if (_get('compl') == 0) { $sqlWhere .= ' AND compl=0'; } $tag = trim(_get('t')); if ($tag != '') { $at = explode(',', $tag); $tagIds = array(); # [ [id1,id2], [id3]... ] $tagExIds = array(); foreach ($at as $atv) { $atv = trim($atv); if ($atv == '') continue; // tasks without tags (ignore other tags included or excluded) if ($atv == '^') { $tagIds = []; $tagExIds = []; if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) $sqlHaving = "string_agg(tags.name, ',') IS NULL"; // catches if tag name is '' else $sqlHaving = "tags_ids IS NULL OR tags_ids = ''"; break; } // tasks with any tag else if ($atv == '^^') { if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) $sqlHaving = "string_agg(tags.name, ',') != ''"; else $sqlHaving = "tags_ids != ''"; } else if (substr($atv,0,1) == '^') { array_push($tagExIds, ...$dbcore->getTagIdsByName(substr($atv,1))); } else { $tagIds[] = $dbcore->getTagIdsByName($atv); } } // Include tags if (count($tagIds) > 0) { $tagAnd = []; foreach ($tagIds as $ids) { $tagAnd[] = "task_id IN (SELECT task_id FROM {$db->prefix}tag2task WHERE tag_id IN (". implode(',', $ids). "))"; } $sqlWhere .= "\n AND todo.id IN (". "SELECT DISTINCT task_id FROM {$db->prefix}tag2task WHERE ". implode(' AND ', $tagAnd). ")"; } // Exclude tags if (count($tagExIds) > 0) { $sqlWhere .= "\n AND todo.id NOT IN (SELECT DISTINCT task_id FROM {$db->prefix}tag2task ". "WHERE tag_id IN (". implode(',', $tagExIds). "))"; } } $s = trim(_get('s')); if ($s != '') { if (preg_match("|^#(\d+)$|", $s, $m)) { $sqlWhere .= " AND todo.id = ". (int)$m[1]; } else { $sqlWhere .= " AND (". $db->like("title", "%%%s%%", $s). " OR ". $db->like("note", "%%%s%%", $s). ")"; } } $sort = (int)_get('sort'); $sqlSort = "ORDER BY compl ASC, "; // sortings are same as in DBCore::getTasksByListId if ($sort == 0) $sqlSort .= "ow ASC"; // byHand elseif ($sort == 100) $sqlSort .= "ow DESC"; // byHand (reverse) elseif ($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) elseif ($sort == 5) $sqlSort .= "title ASC, prio DESC, ow ASC"; // byTitle elseif ($sort == 105) $sqlSort .= "title DESC, prio ASC, ow DESC"; // byTitle (reverse) else $sqlSort .= "ow ASC"; $t = array(); $t['total'] = 0; $t['list'] = array(); $t['time'] = time(); $groupConcat = ''; if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) { $groupConcat = "array_to_string(array_agg(tags.id), ',') AS tags_ids, string_agg(tags.name, ',') AS tags"; } else { $groupConcat = "GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags"; } if ($sqlHaving != '') $sqlHaving = "HAVING $sqlHaving"; $q = $db->dq(" SELECT todo.*, todo.duedate IS NULL AS ddn, $groupConcat FROM {$db->prefix}todolist AS todo LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id WHERE $sqlWhereListId $sqlWhere GROUP BY todo.id $sqlHaving $sqlSort "); while ($r = $q->fetchAssoc()) { $t['total']++; if ($listId == -1 && $r['list_id']) { $r['list_name'] = $userLists[ (string)$r['list_id'] ] ?? '((undefined))'; } $t['list'][] = $this->prepareTaskRow($r); } if (_get('setCompl') && haveWriteAccess($listId)) { ListsController::setListShowCompletedById($listId, !(_get('compl') == 0) ); } if (_get('saveSort') == 1 && haveWriteAccess($listId)) { ListsController::setListSortingById($listId, $sort); } $this->response->data = $t; } /** * Create new task * action: newSimple or newFull * @return void * @throws Exception */ function post() { $action = $this->req->jsonBody['action'] ?? ''; if ($action == 'order') { //compatibility checkWriteAccess(); $this->response->data = $this->changeTaskOrder(); } else { $listId = (int)($this->req->jsonBody['list'] ?? 0); checkWriteAccess($listId); if ($action == 'newFull') { $this->response->data = $this->fullNewTaskInList($listId); } else { $this->response->data = $this->newTaskInList($listId); } } } /** * Actions with multiple tasks * @return void * @throws Exception */ function put() { checkWriteAccess(); $action = $this->req->jsonBody['action'] ?? ''; switch ($action) { case 'order': $this->response->data = $this->changeTaskOrder(); break; default: $this->response->data = ['total' => 0]; // error 400 ? } } /** * Delete task by Id * @param mixed $id * @return void * @throws Exception */ function deleteId($id) { checkWriteAccess(); $this->response->data = $this->deleteTask((int)$id); } /** * Edit some properties of Task * @param mixed $id * @return void * @throws Exception */ function putId($id) { checkWriteAccess(); $id = (int)$id; if (!DBCore::default()->taskExists($id)) { $this->response->data = ['total' => 0]; return; } $action = $this->req->jsonBody['action'] ?? ''; switch ($action) { case 'edit': $this->response->data = $this->editTask($id); break; case 'complete': $this->response->data = $this->completeTask($id); break; case 'note': $this->response->data = $this->editNote($id); break; case 'move': $this->response->data = $this->moveTask($id); break; case 'priority': $this->response->data = $this->priorityTask($id); break; case 'delete': $this->response->data = $this->deleteTask($id); break; //compatibility default: $this->response->data = ['total' => 0]; } } /** * Parse task input string to components for representing in edit/add form * @return void * @throws Exception */ function postTitleParse() { checkWriteAccess(); $t = array( 'title' => trim( $this->req->jsonBody['title'] ?? '' ), 'prio' => 0, 'tags' => '', 'duedate' => '', ); if (Config::get('smartsyntax') != 0 && (false !== $a = parseSmartSyntax($t['title']))) { $t['title'] = (string) ($a['title'] ?? ''); $t['prio'] = (int) ($a['prio'] ?? 0); $t['tags'] = (string) ($a['tags'] ?? ''); if (isset($a['duedate']) && $a['duedate'] != '') { $dueA = $this->prepareDuedate($a['duedate']); $t['duedate'] = $dueA['formatted']; } } $this->response->data = $t; } function postNewCounter() { checkReadAccess(); $lists = $this->req->jsonBody['lists'] ?? []; if (!is_array($lists)) $lists = []; $userLists = []; // [string] if (!haveWriteAccess()) { $userLists = $this->getUserListsSimple(true); if ($userLists) { $sqlWhereList = "AND list_id IN (". implode(',', $userLists). ")"; // remove lists without access granted $lists = array_filter($lists, function($item) use ($userLists) { return in_array( (string)($item['listId'] ?? ''), $userLists ); }); } } $sqlWhereList = []; foreach ($lists as $item) { $later = (int) ($item['later'] ?? 0); $sqlWhereList[] = "(list_id = ". (int)$item['listId']. " AND compl=0 AND d_created > $later)"; } $db = DBConnection::instance(); $a = []; $time = time(); if ($sqlWhereList) { $sqlWhere = implode(' OR ', $sqlWhereList); $q = $db->dq("SELECT list_id, COUNT(id) c FROM {$db->prefix}todolist WHERE $sqlWhere GROUP BY list_id"); while ($r = $q->fetchAssoc()) { $a[] = [ 'listId' => (int)$r['list_id'], 'counter' => (int)$r['c'], ]; } } $b = []; $list = (int) ($this->req->jsonBody['list'] ?? 0); $later = (int) ($this->req->jsonBody['later'] ?? 0); if ($list > 0 && $later > 0 && (!$userLists || in_array((string)$list, $userLists))) { $q = $db->dq("SELECT id FROM {$db->prefix}todolist WHERE list_id = $list AND compl=0 AND d_created > $later"); while ($r = $q->fetchAssoc()) { $b[] = (int)$r['id']; } } $this->response->data = [ 'ok' => true, 'total' => count($b) + count($a), 'tasks' => $b, 'lists' => $a, 'time' => $time ]; } /* Private Functions */ private function newTaskInList(int $listId): ?array { $db = DBConnection::instance(); $t = array(); $t['total'] = 0; $title = trim($this->req->jsonBody['title'] ?? ''); $prio = 0; $tags = ''; $duedate = null; if (Config::get('smartsyntax') != 0) { $a = parseSmartSyntax($title); if ($a === false) { return $t; } $title = (string)$a['title']; $prio = (int)$a['prio']; $tags = (string)$a['tags']; if (isset($a['duedate']) && preg_match("|^\d+-\d+-\d+$|", $a['duedate'])) { $duedate = $a['duedate']; } } 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"); $date = time(); $db->ex("BEGIN"); $db->dq("INSERT INTO {$db->prefix}todolist (uuid,list_id,title,d_created,d_edited,ow,prio,duedate) VALUES (?,?,?,?,?,?,?,?)", array(generateUUID(), $listId, $title, $date, $date, $ow, $prio, $duedate) ); $id = (int) $db->lastInsertId(); if ($tags != '') { $aTags = $this->prepareTags($tags); if ($aTags) { $this->addTaskTags($id, $aTags['ids'], $listId); } } $db->ex("COMMIT"); $task = $this->getTaskRowById($id); MTTNotificationCenter::postNotification(MTTNotification::didCreateTask, $task); $t['list'][] = $task; $t['total'] = 1; return $t; } private function fullNewTaskInList(int $listId): ?array { $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 = MTTSmartSyntax::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"); $date = time(); $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, $date, $date, $ow, $prio, $note, $duedate) ); $id = (int) $db->lastInsertId(); if ($tags != '') { $aTags = $this->prepareTags($tags); if ($aTags) { $this->addTaskTags($id, $aTags['ids'], $listId); } } $db->ex("COMMIT"); $task = $this->getTaskRowById($id); MTTNotificationCenter::postNotification(MTTNotification::didCreateTask, $task); $t['list'][] = $task; $t['total'] = 1; return $t; } private function editTask(int $id): ?array { $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 = MTTSmartSyntax::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) { $this->addTaskTags($id, $aTags['ids'], $listId); } $db->dq("UPDATE {$db->prefix}todolist SET title=?,note=?,prio=?,duedate=?,d_edited=? WHERE id=$id", array($title, $note, $prio, $duedate, time()) ); $db->ex("COMMIT"); $task = $this->getTaskRowById($id, true); MTTNotificationCenter::postNotification(MTTNotification::didEditTask, ['task' => $task]); $t['list'][] = $task; $t['total'] = 1; return $t; } private function moveTask(int $id): ?array { $fromId = (int)($this->req->jsonBody['from'] ?? 0); $toId = (int)($this->req->jsonBody['to'] ?? 0); $listName = ''; $result = $this->doMoveTask($id, $toId, $listName); $task = null; if ($result && MTTNotificationCenter::hasObserversForNotification(MTTNotification::didEditTask)) { $task = $this->getTaskRowById($id); MTTNotificationCenter::postNotification(MTTNotification::didEditTask, [ 'property' => 'list', 'task' => $task ]); } $t = array('total' => $result ? 1 : 0); if ($fromId == -1 && $result) { if (!$task) { $r = DBCore::default()->getTaskById($id); $r['list_name'] = $listName; $task = $this->prepareTaskRow($r); } $t['list'][] = $task; } return $t; } private function doMoveTask(int $id, int $listId, &$listName): bool { $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 $l = $db->sqa("SELECT id,name FROM {$db->prefix}lists WHERE id=?", [$listId]); if (!$l) return false; $listName = $l['name']; $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): ?array { $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"); $date = time(); $dateCompleted = $compl ? $date : 0; $db->dq("UPDATE {$db->prefix}todolist SET compl=$compl,ow=$ow,d_completed=?,d_edited=? WHERE id=$id", array($dateCompleted, $date) ); $task = $this->getTaskRowById($id); MTTNotificationCenter::postNotification(MTTNotification::didCompleteTask, $task); $t = array(); $t['total'] = 1; $t['list'][] = $task; return $t; } private function editNote(int $id): ?array { $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()) ); if (MTTNotificationCenter::hasObserversForNotification(MTTNotification::didEditTask)) { $task = $this->getTaskRowById($id); MTTNotificationCenter::postNotification(MTTNotification::didEditTask, [ 'property' => 'note', 'task' => $task ]); } $t = array(); $t['total'] = 1; $t['list'][] = array('id'=>$id, 'note'=> noteMarkup($note), 'noteText'=>(string)$note); return $t; } private function priorityTask(int $id): ?array { $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()) ); if (MTTNotificationCenter::hasObserversForNotification(MTTNotification::didEditTask)) { $task = $this->getTaskRowById($id); MTTNotificationCenter::postNotification(MTTNotification::didEditTask, [ 'property' => 'priority', 'task' => $task ]); } $t = array(); $t['total'] = 1; $t['list'][] = array('id'=>$id, 'prio'=>$prio); return $t; } private function changeTaskOrder(): ?array { $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 deleteTask(int $id) { $id = (int)$id; $task = null; if (MTTNotificationCenter::hasObserversForNotification(MTTNotification::didDeleteTask)) { $task = $this->getTaskRowById($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"); if ($deleted && MTTNotificationCenter::hasObserversForNotification(MTTNotification::didDeleteTask)) { MTTNotificationCenter::postNotification(MTTNotification::didDeleteTask, $task); } $t = array(); $t['total'] = $deleted; $t['list'][] = array('id' => $id); return $t; } private function getUserListsSimple(bool $readOnly = false): array { $db = DBConnection::instance(); $sqlWhere = ''; if ($readOnly) { $sqlWhere = "WHERE published=1"; } $a = array(); $q = $db->dq("SELECT id,name FROM {$db->prefix}lists $sqlWhere ORDER BY id ASC"); while($r = $q->fetchRow()) { $a[ (string)$r[0] ] = (string)$r[1]; } return $a; } private function getTaskRowById(int $id, bool $getListName = false): ?array { $r = DBCore::default()->getTaskById($id); if (!$r) { throw new Exception("Failed to fetch task data"); } if ($getListName) { $list = DBCore::default()->getListById( (int)$r['list_id'] ); $r['list_name'] = (string) ($list['name'] ?? ''); } return $this->prepareTaskRow($r); } private function prepareTaskRow(array $r): array { $lang = Lang::instance(); $dueA = $this->prepareDuedate($r['duedate']); $dCreated = timestampToDatetime($r['d_created']); $isEdited = ($r['d_edited'] != $r['d_created']); $dEdited = $isEdited ? timestampToDatetime($r['d_edited']) : ''; $dCompleted = $r['d_completed'] ? timestampToDatetime($r['d_completed']) : ''; if (!Config::get('showtime')) { $dCreatedFull = timestampToDatetime($r['d_created'], true); $dEditedFull = $isEdited ? timestampToDatetime($r['d_edited'], true) : ''; $dCompletedFull = $r['d_completed'] ? timestampToDatetime($r['d_completed'], true) : ''; } else { $dCreatedFull = $dCreated; $dEditedFull = $dEdited; $dCompletedFull = $dCompleted; } return array( 'id' => $r['id'], 'title' => titleMarkup( $r['title'] ), 'titleText' => (string)$r['title'], 'listId' => $r['list_id'], 'listName' => htmlarray($r['list_name'] ?? ''), 'date' => htmlarray($dCreated), 'dateInt' => (int)$r['d_created'], 'dateFull' => htmlarray($dCreatedFull), 'dateInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_created'), $dCreated)), //TODO: move preparing of *inlineTitle to js 'dateEdited' => htmlarray($dEdited), 'dateEditedInt' => (int)$r['d_edited'], 'dateEditedFull' => htmlarray($dEditedFull), 'dateEditedInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_edited'), $dEdited)), 'isEdited' => (bool)$isEdited, 'dateCompleted' => htmlarray($dCompleted), 'dateCompletedFull' => htmlarray($dCompletedFull), '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' => htmlarray($dueA['formatted']), 'dueClass' => $dueA['class'], 'dueStr' => htmlarray($dueA['str']), 'dueInt' => $this->date2int($r['duedate']), 'dueTitle' => htmlarray(sprintf($lang->get('taskdate_inline_duedate'), $dueA['formattedlong'])), ); } private function prepareDuedate($duedate): array { $lang = Lang::instance(); $a = array( 'class'=>'', 'str'=>'', 'formatted'=>'', 'formattedlong'=>'', 'timestamp'=>0 ); if ($duedate == '') { return $a; } $ad = explode('-', $duedate); $y = (int)$ad[0]; $m = (int)$ad[1]; $d = (int)$ad[2]; $a['timestamp'] = mktime(0, 0, 0, $m, $d, $y); $oToday = new DateTimeImmutable(date("Y-m-d")); $oDue = new DateTimeImmutable($duedate); $oDiff = $oToday->diff($oDue); if ($oDiff === false) { return $a; } $thisYear = ((int)$oToday->format('Y') == $y); $days = $oDiff->days; if ($oDiff->invert) $days *= -1; $exact = Config::get('exactduedate') ? true : false; if ($days < -7 && !$thisYear) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformat2'), $y, $m, $d, $lang); } elseif ($days < -7) { $a['class'] = 'past'; $a['str'] = formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } elseif ($days < -1) { $a['class'] = 'past'; $a['str'] = !$exact ? sprintf($lang->get('daysago'), abs($days)) : formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } elseif ($days == -1) { $a['class'] = 'past'; $a['str'] = !$exact ? $lang->get('yesterday') : formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } elseif ($days == 0) { $a['class'] = 'today'; $a['str'] = !$exact ? $lang->get('today') : formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } elseif ($days == 1) { $a['class'] = 'today'; $a['str'] = !$exact ? $lang->get('tomorrow') : formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } elseif ($days <= 7) { $a['class'] = 'soon'; $a['str'] = !$exact ? sprintf($lang->get('indays'), $days) : formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } elseif ($thisYear) { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformatshort'), $y, $m, $d, $lang); } else { $a['class'] = 'future'; $a['str'] = formatDate3(Config::get('dateformat2'), $y, $m, $d, $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) : int { 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 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): array { $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): ?array { $tags = explode(',', $tagsStr); if (!$tags) return null; $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) ); } } } ================================================ FILE: src/includes/class.config.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class Config { /** @var bool */ public static $noDatabase = false; /** @var array[] */ private static $dbparams = array( # Database type: sqlite or mysql 'db.type' => array('default'=>'sqlite', 'type'=>'s'), # Specific database api 'db.driver' => array('default'=>'', 'type'=>'s'), # Mysql connection settings 'db.host' => array('default'=>'localhost', 'type'=>'s'), 'db.user' => array('default'=>'mtt', 'type'=>'s'), 'db.password' => array('default'=>'mtt', 'type'=>'s'), 'db.name' => array('default'=>'mytinytodo', 'type'=>'s'), # Prefix for table names 'db.prefix' => array('default'=>'', 'type'=>'s') ); /** @var array[] */ private static $convert = array( 'mysql.host' => 'db.host', 'mysql.user' => 'db.user', 'mysql.password' => 'db.password', 'mysql.db' => 'db.name', 'db' => 'db.type', 'prefix' => 'db.prefix' ); /** @var array[] */ public static $params = array( # These two parameters are used when mytinytodo index.php called not from installation directory # 'url' - URL where index.php is called from (ex.: http://site.com/todo.php) # 'mtt_url' - directory URL where mytinytodo is installed (with trailing slash) (ex.: http://site.com/lib/mytinytodo/) 'url' => array('default'=>'', 'type'=>'s'), 'mtt_url' => array('default'=>'', 'type'=>'s'), # Top title 'title' => array('default'=>'', 'type'=>'s'), # Language pack 'lang' => array('default'=>'en', 'type'=>'s'), # Password to protect your tasks from modification, # leave empty that everyone could read/write todolist 'password' => array('default'=>'', 'type'=>'s'), # Smart Syntax enabled flag 'smartsyntax' => array('default'=>1, 'type'=>'i'), # Default Time zone 'timezone' => array('default'=>'UTC', 'type'=>'s'), # To disable auto adding selected tag set value to 0 'autotag' => array('default'=>1, 'type'=>'i'), # duedate calendar format: 1 => y-m-d (default), 2 => m/d/y, 3 => d.m.y 'duedateformat' => array('default'=>1, 'type'=>'i'), # First day of week: 0-Sunday, 1-Monday, 2-Tuesday, .. 6-Saturday 'firstdayofweek' => array('default'=>1, 'type'=>'i', 'options'=>array(0,1,2,3,4,5,6)), # Date/time formats 'clock' => array('default'=>24, 'type'=>'i', 'options'=>array(12,24)), 'dateformat' => array('default'=>'j M Y', 'type'=>'s'), 'dateformat2' => array('default'=>'n/j/y', 'type'=>'s'), 'dateformatshort' => array('default'=>'j M', 'type'=>'s'), # Show task date in list 'showdate' => array('default'=>0, 'type'=>'i'), 'showtime' => array('default'=>0, 'type'=>'i'), 'showdateInline' => array('default'=>0, 'type'=>'i'), 'exactduedate' => array('default'=>0, 'type'=>'i'), # Use Markdown syntax for notes. Set to 'v1' to use old v1.6 syntax. 'markup' => array('default'=>'markdown', 'type'=>'s'), # Appearance: system default or always light 'appearance' => array('default'=>'system', 'type'=>'s', 'options'=>array('system','light','dark')), # New tasks counter 'newTaskCounter' => array('default' => 0, 'type'=>'i'), 'newTaskCounterIcon' => array('default' => 0, 'type'=>'i'), # Array of activated extensions 'extensions' => array('default'=>[], 'type'=>'a') ); /** @var mixed[] */ private static $config = array(); /** * * @param mixed[] $config * @return void */ public static function loadConfigV14(array $config) { foreach ($config as $key => $val) { if (isset(self::$convert[$key])) { $key = self::$convert[$key]; } elseif ($key == 'mysqli' && (int)$val != 0) { $key = 'db.driver'; $val = 'mysqli'; } elseif ($key == 'password' && $val != '') { $val = passwordHash($val); // in v1.7 password is hashed } // if (!isset(self::$dbparams[$key])) { // throw new Exception("Unknown key: $key"); // } self::$config[$key] = $val; } } /** * * @return void * @throws Exception */ public static function load() { if (self::$noDatabase) { return; } $j = self::requestDefaultDomain(); foreach ($j as $key=>$val) { // Ignore params for database config if ( !isset(self::$dbparams[$key]) ) { self::$config[$key] = $val; } } } /** * * @param string $key * @return mixed */ public static function get($key) { if (isset(self::$config[$key])) return self::$config[$key]; elseif (isset(self::$params[$key])) return self::$params[$key]['default']; elseif (isset(self::$dbparams[$key])) return self::$dbparams[$key]['default']; else return null; } /** * * @param string $key * @return string|null */ public static function getUrl($key) { $url = ''; if ( isset(self::$config[$key]) ) $url = self::$config[$key]; else if( isset(self::$params[$key]) ) $url = self::$params[$key]['default']; else return null; return str_replace( ["\r","\n"], '', $url ); } /** * * @param string $key * @param mixed $value * @return void * @throws Exception */ public static function set($key, $value) { if ($key == "db.prefix" && $value != "" && !preg_match("/^[a-zA-Z0-9_]+$/", $value)) { throw new Exception("Incorrect table prefix. Can contain only latin letters, digits and underscore character."); } self::$config[$key] = $value; } /** * * @return void * @throws Exception */ public static function save() { $j = array(); foreach (self::$params as $param => $v) { if ( !isset(self::$config[$param]) ) $val = $v['default']; elseif ( isset($v['options']) && !in_array(self::$config[$param], $v['options'])) $val = $v['default']; else $val = self::$config[$param]; if ($v['type'] == 'i') { $val = (int)$val; } else if ($v['type'] == 'a') { if (!is_array($val)) $val = []; } else { $val = strval($val); } $j[$param] = $val; } self::saveDomain('config.json', $j); } /** * * @param string $key * @return array * @throws Exception */ public static function requestDomain(string $key): array { $db = DBConnection::instance(); $json = $db->sq("SELECT param_value FROM {$db->prefix}settings WHERE param_key = ?", array($key)); if (!$json) return array(); $j = json_decode($json, true, 100, JSON_INVALID_UTF8_SUBSTITUTE); if ($j === null) { error_log("MTT Error: Failed to decode JSON object with settings. Code: ". (int)json_last_error()); return array(); } return $j; } /** * * @return array * @throws Exception */ public static function requestDefaultDomain(): array { return self::requestDomain('config.json'); } /** * * @param string $key * @param array $array * @return void * @throws Exception */ public static function saveDomain($key, $array) { $json = json_encode($array, JSON_PRETTY_PRINT /*| JSON_INVALID_UTF8_SUBSTITUTE*/); if ($json === false) { throw new Exception("Failed to create JSON object with settings. Code: ". (int)json_last_error()); } $db = DBConnection::instance(); $keyExists = $db->sq("SELECT COUNT(param_key) FROM {$db->prefix}settings WHERE param_key = ?", array($key) ); if ($keyExists) { $db->ex("UPDATE {$db->prefix}settings SET param_value = ? WHERE param_key = ?", array($json,$key) ); } else { $db->ex("INSERT INTO {$db->prefix}settings (param_key,param_value) VALUES (?,?)", array($key,$json) ); } } public static function defineDbConstants() { define("MTT_DB_TYPE", self::get('db.type')); define("MTT_DB_HOST", self::get('db.host')); define("MTT_DB_USER", self::get('db.user')); define("MTT_DB_PASSWORD", self::get('db.password')); define("MTT_DB_NAME", self::get('db.name')); define("MTT_DB_PREFIX", self::get('db.prefix')); if ( self::get('db.driver') != '' ) { define("MTT_DB_DRIVER", self::get('db.driver')); } } public static function dbConfigAsFileContents(): string { $a = array(); $a[] = " Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ // ---------------------------------------------------------------------------- // class DatabaseResult_Mysql extends DatabaseResult_Abstract { /** @var PDOStatement */ protected $q; /** @var int */ protected $affected; function __construct(PDO $dbh, string $query, bool $resultless = false) { // use with DELETE, INSERT, UPDATE if ($resultless) { $this->affected = (int) $dbh->exec($query); //throws PDOException } // SELECT else { $this->q = $dbh->query($query); //throws PDOException $this->affected = $this->q->rowCount(); } } function fetchRow(): ?array { $res = $this->q->fetch(PDO::FETCH_NUM); if ($res === false || !is_array($res)) { return null; } return $res; } function fetchAssoc(): ?array { $res = $this->q->fetch(PDO::FETCH_ASSOC); if ($res === false || !is_array($res)) { return null; } return $res; } function rowsAffected(): int { return $this->affected; } } // ---------------------------------------------------------------------------- // class Database_Mysql extends Database_Abstract { const DBTYPE = 'mysql'; /** @var PDO */ protected $dbh; /** @var int */ protected $affected = 0; protected $dbname; function __construct() { } function connect(array $params): void { $host = $params['host']; $user = $params['user']; $pass = $params['password']; $db = $params['db']; $options = array( PDO::MYSQL_ATTR_FOUND_ROWS => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ); $this->dbname = $db; $this->dbh = new PDO("mysql:host=$host;dbname=$db", $user, $pass, $options); } /* Returns single row of SELECT query as indexed array (FETCH_NUM). Returns single field value if resulting array has only one field. */ function sq(string $query, ?array $values = null) { $q = $this->_dq($query, $values); $res = $q->fetchRow(); if ($res === false || !is_array($res)) { return null; } if (sizeof($res) > 1) return $res; else return $res[0]; } /* Returns single row of SELECT query as dictionary array (FETCH_ASSOC). */ function sqa(string $query, ?array $values = null): ?array { $q = $this->_dq($query, $values); $res = $q->fetchAssoc(); if ($res === false || !is_array($res)){ return null; } return $res; } function dq(string $query, ?array $values = null) : DatabaseResult_Abstract { return $this->_dq($query, $values); } /* for resultless queries like INSERT,UPDATE,DELETE */ function ex(string $query, ?array $values = null): void { $this->_dq($query, $values, true); } private function _dq(string $query, ?array $values = null, bool $resultless = false) : DatabaseResult_Abstract { if (null !== $values && sizeof($values) > 0) { $m = explode('?', $query); if (sizeof($m) < sizeof($values)+1) { throw new Exception("params to set MORE than query params"); } if (sizeof($m) > sizeof($values)+1) { throw new Exception("params to set LESS than query params"); } $query = ""; for ($i=0; $iquote($values[$i]); } $query .= $m[$i]; } $this->setLastQuery($query); $dbr = new DatabaseResult_Mysql($this->dbh, $query, $resultless); $this->affected = $dbr->rowsAffected(); return $dbr; } function affected(): int { return $this->affected; } function quote($value): string { if (null === $value) { return 'null'; } return '\''. addslashes( (string) $value). '\''; } function quoteForLike(string $format, string $string): string { $string = str_replace(array('%','_'), array('\%','\_'), addslashes($string)); return '\''. sprintf($format, $string). '\''; } function like(string $column, string $format, string $string): string { $column = str_replace('`', '``', $column); return '`'. $column. '` LIKE '. $this->quoteForLike($format, $string); } function ciEquals(string $column, string $value): string { $column = str_replace('`', '``', $column); return 'LOWER(`'. $column. '`) = LOWER('. $this->quote($value). ')'; } function lastInsertId(?string $name = null): ?string { $ret = $this->dbh->lastInsertId(); if (false === $ret) { return null; } return (string) $ret; } function tableExists(string $table): bool { $r = $this->sq("SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", array($this->dbname, $table) ); if ($r === false || $r === null) return false; return true; } function tableFieldExists(string $table, string $field): bool { $table = str_replace('`', '\\`', addslashes($table)); $q = $this->dq("DESCRIBE `$table`"); while ($r = $q->fetchRow()) { if ($r[0] == $field) return true; } return false; } } ================================================ FILE: src/includes/class.db.mysqli.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ // ---------------------------------------------------------------------------- // class DatabaseResult_Mysqli extends DatabaseResult_Abstract { /** @var mysqli_result */ protected $q; function __construct(mysqli $dbh, string $query, bool $resultless = false) { $this->q = $dbh->query($query); //throws mysqli_sql_exception } function fetchRow(): ?array { $res = $this->q->fetch_row(); if ($res === null || $res === false || !is_array($res)) { return null; } return $res; } function fetchAssoc(): ?array { $res = $this->q->fetch_assoc(); if ($res === null || $res === false || !is_array($res)) { return null; } return $res; } } // ---------------------------------------------------------------------------- // class Database_Mysqli extends Database_Abstract { const DBTYPE = 'mysql'; /** @var mysqli */ protected $dbh; protected $dbname; function __construct() { // enable throwing exceptions mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); } function connect(array $params): void { $host = $params['host']; $user = $params['user']; $pass = $params['password']; $db = $params['db']; $this->dbname = $db; $this->dbh = new mysqli($host, $user, $pass, $db); //throws mysqli_sql_exception } function lastInsertId(?string $name = null): ?string { return (string) $this->dbh->insert_id; } function sq(string $query, ?array $values = null) { $q = $this->_dq($query, $values); $res = $q->fetchRow(); if ($res === false || !is_array($res)) { return null; } if (sizeof($res) > 1) return $res; else return $res[0]; } /* Returns single row of SELECT query as dictionary array (fetch_assoc()). */ function sqa(string $query, ?array $values = null): ?array { $q = $this->_dq($query, $values); $res = $q->fetchAssoc(); if ($res === false || !is_array($res)){ return null; } return $res; } function dq(string $query, ?array $values = null) : DatabaseResult_Abstract { return $this->_dq($query, $values); } /* for resultless queries like INSERT,UPDATE,DELETE */ function ex(string $query, ?array $values = null): void { $this->_dq($query, $values, true); } private function _dq(string $query, ?array $values = null, bool $resultless = false) : DatabaseResult_Abstract { if (null !== $values && sizeof($values) > 0) { $m = explode('?', $query); if (sizeof($m) < sizeof($values)+1) { throw new Exception("params to set MORE than query params"); } if (sizeof($m) > sizeof($values)+1) { throw new Exception("params to set LESS than query params"); } $query = ""; for ($i=0; $i < sizeof($m)-1; $i++) { $query .= $m[$i]. $this->quote($values[$i]); } $query .= $m[$i]; } $this->setLastQuery($query); return new DatabaseResult_Mysqli($this->dbh, $query, $resultless); } function affected(): int { return max( (int)$this->dbh->affected_rows, 0 ); } function quote($value): string { if (null === $value) { return 'null'; } return '\''. addslashes( (string) $value). '\''; } function quoteForLike(string $format, string $string): string { $string = str_replace(array('%','_'), array('\%','\_'), addslashes($string)); return '\''. sprintf($format, $string). '\''; } function like(string $column, string $format, string $string): string { $column = str_replace('`', '``', $column); return '`'. $column. '` LIKE '. $this->quoteForLike($format, $string); } function ciEquals(string $column, string $value): string { $column = str_replace('`', '``', $column); return 'LOWER(`'. $column. '`) = LOWER('. $this->quote($value). ')'; } function tableExists(string $table): bool { $r = $this->sq("SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", array($this->dbname, $table) ); if ($r === false || $r === null) return false; return true; } function tableFieldExists(string $table, string $field): bool { $table = str_replace('`', '\\`', addslashes($table)); $q = $this->dq("DESCRIBE `$table`"); while ($r = $q->fetchRow()) { if ($r[0] == $field) return true; } return false; } } ================================================ FILE: src/includes/class.db.postgres.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ // ---------------------------------------------------------------------------- // class DatabaseResult_Postgres extends DatabaseResult_Abstract { /** @var PDOStatement */ protected $q; /** @var int */ protected $affected; function __construct(PDO $dbh, string $query, bool $resultless = false) { // use with DELETE, INSERT, UPDATE if ($resultless) { $this->affected = (int) $dbh->exec($query); //throws PDOException } // SELECT else { $this->q = $dbh->query($query); //throws PDOException $this->affected = $this->q->rowCount(); } } function fetchRow(): ?array { $res = $this->q->fetch(PDO::FETCH_NUM); if ($res === false || !is_array($res)) { return null; } return $res; } function fetchAssoc(): ?array { $res = $this->q->fetch(PDO::FETCH_ASSOC); if ($res === false || !is_array($res)) { return null; } return $res; } function rowsAffected(): int { return $this->affected; } } // ---------------------------------------------------------------------------- // class Database_Postgres extends Database_Abstract { const DBTYPE = 'postgres'; /** @var PDO */ protected $dbh; /** @var int */ protected $affected = 0; protected $dbname; /** @var string const */ protected $schema = 'public'; function __construct() { } function connect(array $params): void { $host = $params['host']; $user = $params['user']; $pass = $params['password']; $db = $params['db']; $options = array( PDO::PGSQL_ATTR_DISABLE_PREPARES => 1, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ); $this->dbname = $db; $this->dbh = new PDO("pgsql:host=$host;dbname=$db", $user, $pass, $options); } /* Returns single row of SELECT query as indexed array (FETCH_NUM). Returns single field value if resulting array has only one field. */ function sq(string $query, ?array $values = null) { $q = $this->_dq($query, $values); $res = $q->fetchRow(); if ($res === false || !is_array($res)) { return null; } if (sizeof($res) > 1) return $res; else return $res[0]; } /* Returns single row of SELECT query as dictionary array (FETCH_ASSOC). */ function sqa(string $query, ?array $values = null): ?array { $q = $this->_dq($query, $values); $res = $q->fetchAssoc(); if ($res === false || !is_array($res)){ return null; } return $res; } function dq(string $query, ?array $values = null) : DatabaseResult_Abstract { return $this->_dq($query, $values); } /* for resultless queries like INSERT,UPDATE,DELETE */ function ex(string $query, ?array $values = null): void { $this->_dq($query, $values, true); } private function _dq(string $query, ?array $values = null, bool $resultless = false) : DatabaseResult_Abstract { if (null !== $values && sizeof($values) > 0) { $m = explode('?', $query); if (sizeof($m) < sizeof($values)+1) { throw new Exception("params to set MORE than query params"); } if (sizeof($m) > sizeof($values)+1) { throw new Exception("params to set LESS than query params"); } $query = ""; for ($i=0; $iquote($values[$i]); } $query .= $m[$i]; } $this->setLastQuery($query); $dbr = new DatabaseResult_Postgres($this->dbh, $query, $resultless); $this->affected = $dbr->rowsAffected(); return $dbr; } function affected(): int { return $this->affected; } function quote($value): string { if (null === $value) { return 'null'; } return $this->dbh->quote((string) $value); } function quoteForLike(string $format, string $string): string { $string = str_replace(array('\\','%','_'), array('\\\\','\%','\_'), $string); return $this->dbh->quote(sprintf($format, $string)). " ESCAPE '\'"; } function like(string $column, string $format, string $string): string { $column = str_replace('"', '""', $column); return '"'. $column. '" ILIKE '. $this->quoteForLike($format, $string); } function ciEquals(string $column, string $value): string { $column = str_replace('"', '""', $column); return 'LOWER("'. $column. '") = LOWER('. $this->quote($value). ')'; } function lastInsertId(?string $name = null): ?string { $ret = $this->dbh->lastInsertId(); if (false === $ret) { return null; } return (string) $ret; } function tableExists(string $table): bool { $r = $this->sq("SELECT 1 FROM information_schema.tables WHERE table_catalog = ? AND table_name = ?", array($this->dbname, $table) ); if ($r === false || $r === null) return false; return true; } function tableFieldExists(string $table, string $field): bool { $r = $this->sq("SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = ? AND table_schema = ?", array($table, $field, $this->schema) ); if ($r === false || $r === null) return false; return true; } } ================================================ FILE: src/includes/class.db.sqlite3.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class DatabaseResult_Sqlite3 extends DatabaseResult_Abstract { /** @var PDOStatement */ protected $q; /** @var int */ protected $affected; function __construct(PDO $dbh, string $query, bool $resultless = false) { // use with DELETE, INSERT, UPDATE if ($resultless) { $this->affected = (int) $dbh->exec($query); //throws PDOException } // SELECT else { $this->q = $dbh->query($query); //throws PDOException $this->affected = $this->q->rowCount(); } } function fetchRow(): ?array { $res = $this->q->fetch(PDO::FETCH_NUM); if ($res === false || !is_array($res)) { return null; } return $res; } function fetchAssoc(): ?array { $res = $this->q->fetch(PDO::FETCH_ASSOC); if ($res === false || !is_array($res)) { return null; } return $res; } function rowsAffected(): int { return $this->affected; } } class Database_Sqlite3 extends Database_Abstract { const DBTYPE = 'sqlite'; /** @var PDO|\Pdo\Sqlite */ protected $dbh; /** @var int */ protected $affected = 0; /** @var bool */ protected $useNormalizedUtf8 = true; function __construct(?array $params = null) { if (is_array($params)) { if (isset($params['useNormalizedUtf8'])) { $this->useNormalizedUtf8 = boolval($params['useNormalizedUtf8']); } } } function connect(array $params): void { $filename = $params['filename']; $options = array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ); if (PHP_VERSION_ID < 80500) { $this->dbh = new PDO("sqlite:$filename", null, null, $options); //throws PDOException # Deprecated since PHP 8.5 $this->dbh->sqliteCreateFunction('utf8_lower', [$this, 'utf8_lower'], 1); $this->dbh->sqliteCreateFunction('utf8_normalized_lower', [$this, 'utf8_normalized_lower'], 1); $this->dbh->sqliteCreateCollation('UTF8CI', [$this, 'collate_utf8ci']); $this->dbh->sqliteCreateCollation('UTF8CI_NORMALIZED', [$this, 'collate_utf8ci_normalized']); } else { $this->dbh = new \Pdo\Sqlite("sqlite:$filename", null, null, $options); //throws PDOException $this->dbh->createFunction('utf8_lower', [$this, 'utf8_lower'], 1); $this->dbh->createFunction('utf8_normalized_lower', [$this, 'utf8_normalized_lower'], 1); $this->dbh->createCollation('UTF8CI', [$this, 'collate_utf8ci']); $this->dbh->createCollation('UTF8CI_NORMALIZED', [$this, 'collate_utf8ci_normalized']); } } /* SELECT queries for single row */ function sq(string $query, ?array $values = null) { $q = $this->_dq($query, $values); $res = $q->fetchRow(); if ($res === false || !is_array($res)) { return null; } if (sizeof($res) > 1) return $res; else return $res[0]; } /* Returns single row of SELECT query as dictionary array (FETCH_ASSOC). */ function sqa(string $query, ?array $values = null): ?array { $q = $this->_dq($query, $values); $res = $q->fetchAssoc(); if ($res === false || !is_array($res)) { return null; } return $res; } /* SELECT queries for multiple rows */ function dq(string $query, ?array $values = null) : DatabaseResult_Abstract { return $this->_dq($query, $values); } /* for resultless queries like INSERT,UPDATE,DELETE */ function ex(string $query, ?array $values = null): void { $this->_dq($query, $values, true); } private function _dq(string $query, ?array $values = null, bool $resultless = false) : DatabaseResult_Abstract { if (null !== $values && sizeof($values) > 0) { $m = explode('?', $query); if (sizeof($m) < sizeof($values)+1) { throw new Exception("params to set MORE than query params"); } if (sizeof($m) > sizeof($values)+1) { throw new Exception("params to set LESS than query params"); } $query = ""; for ($i=0; $iquote($values[$i]); } $query .= $m[$i]; } $this->setLastQuery($query); $dbr = new DatabaseResult_Sqlite3($this->dbh, $query, $resultless); $this->affected = $dbr->rowsAffected(); return $dbr; } function affected(): int { return $this->affected; } function quote($value): string { if (null === $value) { return 'null'; } return $this->dbh->quote( (string) $value); } function quoteForLike(string $format, string $string): string { $string = str_replace(array('\\','%','_'), array('\\\\','\%','\_'), $string); return $this->dbh->quote(sprintf($format, $string)). " ESCAPE '\'"; } /** * Produce case-insensitive like */ function like(string $column, string $format, string $string): string { $column = str_replace('"', '""', $column); if ($this->useNormalizedUtf8) { return 'utf8_normalized_lower("'. $column. '") LIKE '. $this->quoteForLike($format, $this->utf8_normalized_lower($string)); } return 'utf8_lower("'. $column. '") LIKE '. $this->quoteForLike($format, $this->utf8_lower($string)); } function ciEquals(string $column, string $value): string { $column = str_replace('"', '""', $column); if ($this->useNormalizedUtf8) { return 'utf8_normalized_lower("'. $column. '") = '. $this->quote($this->utf8_normalized_lower($value)); } return 'utf8_lower("'. $column. '") = '. $this->quote($this->utf8_lower($value)); } function lastInsertId(?string $name = null): ?string { $ret = $this->dbh->lastInsertId(); if (false === $ret) { return null; } return (string) $ret; } function tableExists(string $table): bool { $exists = $this->sq("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", [$table]); if ($exists == "1") { return true; } $exists = $this->sq("SELECT 1 FROM sqlite_temp_master WHERE type='table' AND name=?", [$table]); if ($exists == "1") { return true; } return false; } function tableFieldExists(string $table, string $field): bool { $q = $this->dq("PRAGMA table_info(". $this->quote($table). ")"); while ($r = $q->fetchRow()) { if ($r[1] == $field) return true; } return false; } public function utf8_lower($value): string { if (is_null($value)) return ''; return mb_strtolower((string)$value, 'UTF-8'); } public function utf8_normalized_lower($value): string { if (is_null($value)) return ''; $value = self::normalizeValue((string) $value); return mb_strtolower($value, 'UTF-8'); } public function collate_utf8ci(string $str1, string $str2): int { return strcmp(mb_strtolower($str1, 'UTF-8'), mb_strtolower($str2, 'UTF-8')); } public function collate_utf8ci_normalized(string $str1, string $str2): int { $str1 = self::normalizeValue($str1); $str2 = self::normalizeValue($str2); return strcmp(mb_strtolower($str1, 'UTF-8'), mb_strtolower($str2, 'UTF-8')); } public static function normalizeValue(string $str): string { $str = Normalizer::normalize($str, Normalizer::FORM_KD); if (false === preg_match_all("/./u", $str, $m)) { $ea = error_get_last(); $error = ($ea && isset($ea['message'])) ? $ea['message'] : "preg_match_all() failed"; throw new Exception($error); } $chars = $m[0]; static $map = [ // https://en.wikipedia.org/wiki/Combining_character "\u{0300}" => '', "\u{0301}" => '', "\u{0302}" => '', "\u{0303}" => '', "\u{0304}" => '', "\u{0305}" => '', "\u{0306}" => '', "\u{0307}" => '', "\u{0308}" => '', "\u{0309}" => '', "\u{030a}" => '', "\u{030b}" => '', "\u{030c}" => '', "\u{030d}" => '', "\u{030e}" => '', "\u{030f}" => '', "\u{0310}" => '', "\u{0311}" => '', "\u{0312}" => '', "\u{0313}" => '', "\u{0314}" => '', "\u{0315}" => '', "\u{0316}" => '', "\u{0317}" => '', "\u{0318}" => '', "\u{0319}" => '', "\u{031a}" => '', "\u{031b}" => '', "\u{031c}" => '', "\u{031d}" => '', "\u{031e}" => '', "\u{031f}" => '', "\u{0320}" => '', "\u{0321}" => '', "\u{0322}" => '', "\u{0323}" => '', "\u{0324}" => '', "\u{0325}" => '', "\u{0326}" => '', "\u{0327}" => '', "\u{0328}" => '', "\u{0329}" => '', "\u{032a}" => '', "\u{032b}" => '', "\u{032c}" => '', "\u{032d}" => '', "\u{032e}" => '', "\u{032f}" => '', "\u{0330}" => '', "\u{0331}" => '', "\u{0332}" => '', "\u{0333}" => '', "\u{0334}" => '', "\u{0335}" => '', "\u{0336}" => '', "\u{0337}" => '', "\u{0338}" => '', "\u{0339}" => '', "\u{033a}" => '', "\u{033b}" => '', "\u{033c}" => '', "\u{033d}" => '', "\u{033e}" => '', "\u{033f}" => '', "\u{0340}" => '', "\u{0341}" => '', "\u{0342}" => '', "\u{0343}" => '', "\u{0344}" => '', "\u{0345}" => '', "\u{0346}" => '', "\u{0347}" => '', "\u{0348}" => '', "\u{0349}" => '', "\u{034a}" => '', "\u{034b}" => '', "\u{034c}" => '', "\u{034d}" => '', "\u{034e}" => '', "\u{034f}" => '', "\u{0350}" => '', "\u{0351}" => '', "\u{0352}" => '', "\u{0353}" => '', "\u{0354}" => '', "\u{0355}" => '', "\u{0356}" => '', "\u{0357}" => '', "\u{0358}" => '', "\u{0359}" => '', "\u{035a}" => '', "\u{035b}" => '', "\u{035c}" => '', "\u{035d}" => '', "\u{035e}" => '', "\u{035f}" => '', "\u{0360}" => '', "\u{0361}" => '', "\u{0362}" => '', "\u{0363}" => '', "\u{0364}" => '', "\u{0365}" => '', "\u{0366}" => '', "\u{0367}" => '', "\u{0368}" => '', "\u{0369}" => '', "\u{036a}" => '', "\u{036b}" => '', "\u{036c}" => '', "\u{036d}" => '', "\u{036e}" => '', "\u{036f}" => '', "\u{1ab0}" => '', "\u{1ab1}" => '', "\u{1ab2}" => '', "\u{1ab3}" => '', "\u{1ab4}" => '', "\u{1ab5}" => '', "\u{1ab6}" => '', "\u{1ab7}" => '', "\u{1ab8}" => '', "\u{1ab9}" => '', "\u{1aba}" => '', "\u{1abb}" => '', "\u{1abc}" => '', "\u{1abd}" => '', "\u{1abe}" => '', "\u{1abf}" => '', "\u{1ac0}" => '', "\u{1ac1}" => '', "\u{1ac2}" => '', "\u{1ac3}" => '', "\u{1ac4}" => '', "\u{1ac5}" => '', "\u{1ac6}" => '', "\u{1ac7}" => '', "\u{1ac8}" => '', "\u{1ac9}" => '', "\u{1aca}" => '', "\u{1acb}" => '', "\u{1acc}" => '', "\u{1acd}" => '', "\u{1ace}" => '', "\u{1acf}" => '', "\u{1ad0}" => '', "\u{1ad1}" => '', "\u{1ad2}" => '', "\u{1ad3}" => '', "\u{1ad4}" => '', "\u{1ad5}" => '', "\u{1ad6}" => '', "\u{1ad7}" => '', "\u{1ad8}" => '', "\u{1ad9}" => '', "\u{1ada}" => '', "\u{1adb}" => '', "\u{1adc}" => '', "\u{1add}" => '', "\u{1ade}" => '', "\u{1adf}" => '', "\u{1ae0}" => '', "\u{1ae1}" => '', "\u{1ae2}" => '', "\u{1ae3}" => '', "\u{1ae4}" => '', "\u{1ae5}" => '', "\u{1ae6}" => '', "\u{1ae7}" => '', "\u{1ae8}" => '', "\u{1ae9}" => '', "\u{1aea}" => '', "\u{1aeb}" => '', "\u{1aec}" => '', "\u{1aed}" => '', "\u{1aee}" => '', "\u{1aef}" => '', "\u{1af0}" => '', "\u{1af1}" => '', "\u{1af2}" => '', "\u{1af3}" => '', "\u{1af4}" => '', "\u{1af5}" => '', "\u{1af6}" => '', "\u{1af7}" => '', "\u{1af8}" => '', "\u{1af9}" => '', "\u{1afa}" => '', "\u{1afb}" => '', "\u{1afc}" => '', "\u{1afd}" => '', "\u{1afe}" => '', "\u{1aff}" => '', "\u{1dc0}" => '', "\u{1dc1}" => '', "\u{1dc2}" => '', "\u{1dc3}" => '', "\u{1dc4}" => '', "\u{1dc5}" => '', "\u{1dc6}" => '', "\u{1dc7}" => '', "\u{1dc8}" => '', "\u{1dc9}" => '', "\u{1dca}" => '', "\u{1dcb}" => '', "\u{1dcc}" => '', "\u{1dcd}" => '', "\u{1dce}" => '', "\u{1dcf}" => '', "\u{1dd0}" => '', "\u{1dd1}" => '', "\u{1dd2}" => '', "\u{1dd3}" => '', "\u{1dd4}" => '', "\u{1dd5}" => '', "\u{1dd6}" => '', "\u{1dd7}" => '', "\u{1dd8}" => '', "\u{1dd9}" => '', "\u{1dda}" => '', "\u{1ddb}" => '', "\u{1ddc}" => '', "\u{1ddd}" => '', "\u{1dde}" => '', "\u{1ddf}" => '', "\u{1de0}" => '', "\u{1de1}" => '', "\u{1de2}" => '', "\u{1de3}" => '', "\u{1de4}" => '', "\u{1de5}" => '', "\u{1de6}" => '', "\u{1de7}" => '', "\u{1de8}" => '', "\u{1de9}" => '', "\u{1dea}" => '', "\u{1deb}" => '', "\u{1dec}" => '', "\u{1ded}" => '', "\u{1dee}" => '', "\u{1def}" => '', "\u{1df0}" => '', "\u{1df1}" => '', "\u{1df2}" => '', "\u{1df3}" => '', "\u{1df4}" => '', "\u{1df5}" => '', "\u{1df6}" => '', "\u{1df7}" => '', "\u{1df8}" => '', "\u{1df9}" => '', "\u{1dfa}" => '', "\u{1dfb}" => '', "\u{1dfc}" => '', "\u{1dfd}" => '', "\u{1dfe}" => '', "\u{1dff}" => '', "\u{20d0}" => '', "\u{20d1}" => '', "\u{20d2}" => '', "\u{20d3}" => '', "\u{20d4}" => '', "\u{20d5}" => '', "\u{20d6}" => '', "\u{20d7}" => '', "\u{20d8}" => '', "\u{20d9}" => '', "\u{20da}" => '', "\u{20db}" => '', "\u{20dc}" => '', "\u{20dd}" => '', "\u{20de}" => '', "\u{20df}" => '', "\u{20e0}" => '', "\u{20e1}" => '', "\u{20e2}" => '', "\u{20e3}" => '', "\u{20e4}" => '', "\u{20e5}" => '', "\u{20e6}" => '', "\u{20e7}" => '', "\u{20e8}" => '', "\u{20e9}" => '', "\u{20ea}" => '', "\u{20eb}" => '', "\u{20ec}" => '', "\u{20ed}" => '', "\u{20ee}" => '', "\u{20ef}" => '', "\u{20f0}" => '', "\u{20f1}" => '', "\u{20f2}" => '', "\u{20f3}" => '', "\u{20f4}" => '', "\u{20f5}" => '', "\u{20f6}" => '', "\u{20f7}" => '', "\u{20f8}" => '', "\u{20f9}" => '', "\u{20fa}" => '', "\u{20fb}" => '', "\u{20fc}" => '', "\u{20fd}" => '', "\u{20fe}" => '', "\u{20ff}" => '', "\u{fe20}" => '', "\u{fe21}" => '', "\u{fe22}" => '', "\u{fe23}" => '', "\u{fe24}" => '', "\u{fe25}" => '', "\u{fe26}" => '', "\u{fe27}" => '', "\u{fe28}" => '', "\u{fe29}" => '', "\u{fe2a}" => '', "\u{fe2b}" => '', "\u{fe2c}" => '', "\u{fe2d}" => '', "\u{fe2e}" => '', "\u{fe2f}" => '', 'Æ' => 'AE', // "U+00c6" 'æ' => 'ae', // "U+00e6" 'Œ' => 'OE', // "U+0152" 'œ' => 'oe', // "U+0153" 'Ł' => 'L', 'ł' => 'L' //U+141 and U+142 ]; $len = count($chars); for ($i = 0; $i < $len; $i++) { $unichar = $chars[$i]; if (isset($map[$unichar])) { $chars[$i] = $map[$unichar]; } } return implode('', $chars); } } ================================================ FILE: src/includes/class.dbconnection.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class DBConnection { const DBTYPE_SQLITE = "sqlite"; const DBTYPE_MYSQL = "mysql"; const DBTYPE_POSTGRES = "postgres"; protected static $instance; public static function init(Database_Abstract $instance) : Database_Abstract { self::$instance = $instance; return $instance; } public static function instance() : Database_Abstract { if (!isset(self::$instance)) { throw new Exception("DBConnection is not initialized"); } return self::$instance; } public static function setTablePrefix($prefix) { $db = self::instance(); $db->setPrefix($prefix); } } abstract class Database_Abstract { const DBTYPE = ''; protected static $readonlyProps = ['prefix', 'lastQuery']; /** @var string */ protected $prefix = ''; /** @var string */ protected $lastQuery = ''; /** @var null|string */ protected $logQueryToFile = null; abstract function connect(array $params): void; abstract function sq(string $query, ?array $values = null); abstract function sqa(string $query, ?array $values = null): ?array; abstract function dq(string $query, ?array $values = null): DatabaseResult_Abstract; abstract function ex(string $query, ?array $values = null): void; abstract function affected(): int; abstract function quote($value): string; abstract function quoteForLike(string $format, string $string): string; abstract function like(string $column, string $format, string $string): string; abstract function ciEquals(string $column, string $value): string; abstract function lastInsertId(?string $name = null): ?string; abstract function tableExists(string $table): bool; abstract function tableFieldExists(string $table, string $field): bool; function __get(string $propName) { if ( in_array($propName, self::$readonlyProps) ) { return $this->{$propName}; } throw new Error("Attempt to read undefined property ". get_class($this). "::\$$propName"); } function setPrefix(string $prefix): void { if ($prefix != '' && !preg_match("/^[a-zA-Z0-9_]+$/", $prefix)) { throw new Exception("Incorrect table prefix"); } $this->prefix = $prefix; } function setLogQueryToFile(?string $path) { //any checks? $this->logQueryToFile = $path; } function setLastQuery(string $lastQuery) { $this->lastQuery = $lastQuery; if (MTT_DEBUG && $this->logQueryToFile !== null) { $f = fopen($this->logQueryToFile, "a"); if ($f) { fwrite($f, $this->lastQuery . "\n"); fclose($f); } } } } abstract class DatabaseResult_Abstract { abstract function fetchRow(): ?array; abstract function fetchAssoc(): ?array; } ================================================ FILE: src/includes/class.dbcore.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ // FIXME: experimental, subject to change class DBCore { /** @var Database_Abstract $db */ protected $db; /** @var DBCore $defaultdb */ protected static $defaultInstance; /** * * @param Database_Abstract $db Value of DBConnection::instance() or similar * @return void */ public function __construct(Database_Abstract $db) { $this->db = $db; } /** * * @return Database_Abstract * @throws Exception */ public function connection() { if (!isset($this->db)) { throw new Exception("DBConnection is not set"); } return $this->db; } /** * * @return DBCore * @throws Exception */ public static function default() : DBCore { if (!isset(self::$defaultInstance)) { throw new Exception("DBCore defaultInstance is not initialized"); } return self::$defaultInstance; } /** * * @param DBCore $instance * @return void */ public static function setDefaultInstance(DBCore $instance) { self::$defaultInstance = $instance; } /** * * @param int $id * @return int */ public function getListIdByTaskId(int $id): int { $db = $this->db; $listId = (int)$db->sq("SELECT list_id FROM {$db->prefix}todolist WHERE id=". (int)$id); return $listId; } public function getListById(int $id): ?array { $db = $this->db; $r = $db->sqa("SELECT * FROM {$db->prefix}lists WHERE id=?", [$id]); return $r; } public function taskExists(int $id): bool { $db = $this->db; $count = (int) $db->sq("SELECT COUNT(*) FROM {$db->prefix}todolist WHERE id = $id"); return ($count > 0) ? true : false; } public function getTaskById(int $id): ?array { $db = $this->db; $groupConcat = ''; if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) { $groupConcat = "array_to_string(array_agg(tags.id), ',') AS tags_ids, string_agg(tags.name, ',') AS tags"; } else { $groupConcat = "GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags"; } $r = $db->sqa(" SELECT todo.*, $groupConcat FROM {$db->prefix}todolist AS todo LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id WHERE todo.id = $id GROUP BY todo.id "); return $r; } /** * * @param int $listId * @param string $sqlWhere * @param int|string $sort * @param null|int $limit * @return array */ public function getTasksByListId(int $listId, string $sqlWhere, /* int|string */ $sort, ?int $limit = null): array { $db = $this->db; if ($sqlWhere != '') { $sqlWhere = "AND $sqlWhere"; } $sqlSort = ''; if (is_int($sort)) { $sqlSort = "ORDER BY compl ASC, "; if ($sort == 0) $sqlSort .= "ow ASC"; // byHand elseif ($sort == 100) $sqlSort .= "ow DESC"; // byHand (reverse) elseif ($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) elseif ($sort == 5) $sqlSort .= "title ASC, prio DESC, ow ASC"; // byTitle elseif ($sort == 105) $sqlSort .= "title DESC, prio ASC, ow DESC"; // byTitle (reverse) else $sqlSort .= "ow ASC"; } else if ($sort != '') { $sqlSort = "ORDER BY $sort"; } $sqlLimit = ''; if (!is_null($limit)) { $sqlLimit = "LIMIT $limit"; } if ($db::DBTYPE == DBConnection::DBTYPE_POSTGRES) { $groupConcat = "array_to_string(array_agg(tags.id), ',') AS tags_ids, string_agg(tags.name, ',') AS tags"; } else { $groupConcat = "GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags"; } $q = $db->dq(" SELECT todo.*, todo.duedate IS NULL AS ddn, $groupConcat FROM {$db->prefix}todolist AS todo LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id WHERE todo.list_id = $listId $sqlWhere GROUP BY todo.id $sqlSort $sqlLimit "); $data = array(); while ($r = $q->fetchAssoc()) { $data[] = $r; } return $data; } function createListWithName(string $name): ?int { $db = DBConnection::instance(); $name = trim($name); if ($name == '') { return null; } $ow = 1 + (int)$db->sq("SELECT MAX(ow) FROM {$db->prefix}lists"); $time = time(); $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(); return (int)$id; } /** * Finds all variations of tag by its "normalized" name. Return array of id. * @param string $name * @return int[] * @throws Exception */ function getTagIdsByName(string $name): array { $db = DBConnection::instance(); $q = $db->dq("SELECT id FROM {$db->prefix}tags WHERE ". $db->ciEquals('name', $name)); $a = []; while ($r = $q->fetchAssoc()) { $a[] = (int) $r['id']; } return $a; } } ================================================ FILE: src/includes/class.lang.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ /* myTinyTodo language class */ class Lang { protected static $instance; protected static $langDir = MTTINC . 'lang/'; protected $code = 'en'; protected $default = 'en'; protected $strings; public static function instance(): Lang { if (!isset(self::$instance)) { $c = __CLASS__; self::$instance = new $c; } return self::$instance; } public static function loadLangOrDie($code, $die = 1) { $lang = self::instance(); //check if json file exists if ( self::langExists($code) ) { $jsonString = file_get_contents( self::$langDir. "{$code}.json" ); $lang->loadJsonString($code, $jsonString); } else if ( $die == 0 ) { //notice? $lang->code = $lang->default; //sure? $lang->loadDefaultStrings(); } else if ( $die == 1 ) { die("Language file not found (". htmlspecialchars($code). ".json)"); } } public static function loadLang($code) { self::loadLangOrDie($code, 0); } public static function langExists($code) { return file_exists(self::$langDir. $code. '.json'); } function loadJsonString($code, $jsonString) { $this->code = $code; $json = json_decode($jsonString, true); if ($json === null) { error_log("Failed to decode translation JSON, language '$code': ". json_last_error_msg()); $json = array(); } //load default language if ( $code != $this->default ) { $this->loadDefaultStrings(); $this->strings = array_replace($this->strings, $json); } else { $this->strings = $json; } } function loadDefaultStrings() { if ( ! self::langExists($this->default) ) { die("Default language file not found (". htmlspecialchars($this->default). ".json)"); } $defStr = file_get_contents($this->langDir(). "{$this->default}.json"); $this->strings = json_decode($defStr, true); if ($this->strings === null) { die("Invalid JSON in default language file (". htmlspecialchars($this->default). ".json): ". json_last_error_msg()); } } function get($key) { if ( isset($this->strings[$key]) ) { return $this->strings[$key]; } return $key; } function hasKey(string $key): bool { return isset($this->strings[$key]); } function rtl() { if ( isset($this->strings['_header']['rtl']) ) { return intval($this->strings['_header']['rtl']); } return 0; } /* minimal number of translated strings to use in js front-end */ function jsStrings(bool $escape = true) { $a = array(); $a['daysMin'] = $this->get('days_min'); $a['daysLong'] = $this->get('days_long'); $a['monthsShort'] = $this->get('months_short'); $a['monthsLong'] = $this->get('months_calendar'); $this->fillWithValues($a, [ 'confirmDelete', 'confirmLeave', 'actionNoteSave', 'actionNoteCancel', 'error', 'denied', 'listNotFound', 'noPublicLists', 'noTags', 'withoutTags', 'withAnyTag', 'invalidpass', 'addList', 'addListDefault', 'renameList', 'deleteList', 'clearCompleted', 'settingsSaved', 'tags', 'tasks', 'f_past', 'f_today', 'f_soon', 'alltasks', 'set_header' ]); $a['_rtl'] = $this->rtl() ? 1 : 0; return ($escape ? htmlarray($a) : $a); } protected function fillWithValues(array &$a, array $keys) { foreach ( $keys as $key ) { $a[$key] = $this->get($key); } } function langDir() { return self::$langDir; } function langCode() { return $this->code; } public function getExtensionLang(string $ext): ?array { $langDir = MTT_EXT. $ext. '/lang/'; if (!is_dir($langDir)) { return null; } $def = []; if (file_exists($langDir. 'en.json')) { $defStr = file_get_contents($langDir. 'en.json'); $def = json_decode($defStr, true); if ($def === null) { error_log("Failed to decode translation JSON of extension '$ext', language 'en': ". json_last_error_msg()); $def = []; } } else { #error_log("Default translation file 'en.json' not found in extension '$ext'"); } $lang = []; $langFile = $langDir. $this->code. '.json'; if ($this->code != 'en' && file_exists($langFile)) { $langStr = file_get_contents($langFile); $lang = json_decode($langStr, true); if ($lang === null) { error_log("Failed to decode translation JSON of extension '$ext', language '{$this->code}': ". json_last_error_msg()); $lang = []; } } $lang = array_replace($def, $lang); return $lang; } public function loadExtensionLang(string $ext) { $lang = $this->getExtensionLang($ext); if (!$lang) { return; } if (isset($lang['_header'])) { unset($lang['_header']); } $this->strings = array_replace($this->strings, $lang); } } ================================================ FILE: src/includes/class.sessionhandler.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class MTTSessionHandler implements SessionHandlerInterface, SessionUpdateTimestampHandlerInterface { /** * @var Database_Abstract */ private $db; private $isEmptyData = false; /** * @param string $path * @param string $name * @return bool * @throws Exception */ public function open($path, $name): bool { $this->db = DBConnection::instance(); return true; } /** * @return bool */ public function close(): bool { return true; } /** * @param string $id * @return string * @throws Exception */ #[\ReturnTypeWillChange] public function read($id) { // read session data if not expired $time = time(); $r = $this->db->sq("SELECT data,last_access,expires FROM {$this->db->prefix}sessions WHERE id = ?", [$id]); if ( is_null($r) ) { // We return '' instead of false to avoid warning return ''; } if ( (int)$r[2] < $time) { // maybe regenerate id? $r[0] = ''; } // update last access time and set expires in 14 days // refresh every 8 hours if ( $r[1] + 28800 < $time ) { $expire = $time + 14 * 86400; $this->db->ex("UPDATE {$this->db->prefix}sessions SET last_access=?,expires=? WHERE id = ?", array($time, $expire, $id) ); } if ($r[0] === '') { $this->isEmptyData = true; } return $r[0]; } /** * @param string $id * @param string $data * @return bool * @throws Exception */ public function write($id, $data): bool { // Ignore empty sessions without changes if ($this->isEmptyData && $data === '') return true; $time = time(); $expire = $time + 14 * 86400; $exists = $this->db->sq("SELECT COUNT(*) FROM {$this->db->prefix}sessions WHERE id = ?", [$id]); if (!$exists) { // Create new session with 14 days lifetime $this->db->ex("INSERT INTO {$this->db->prefix}sessions (id,data,last_access,expires) VALUES (?,?,?,?)", array($id, $data, $time, $expire) ); } else { // Update existing session $this->db->ex("UPDATE {$this->db->prefix}sessions SET data = ?, last_access=?, expires=? WHERE id = ?", array($data, $time, $expire, $id) ); } return true; } /** * @param string $id * @return bool * @throws Exception */ public function destroy($id): bool { $this->db->ex("DELETE FROM {$this->db->prefix}sessions WHERE id = ?", [$id]); return true; } /** * @param int $max_lifetime * @return int|false */ #[\ReturnTypeWillChange] public function gc($max_lifetime) { // We ignore php runtime 'session.gc_maxlifetime' $expire = time(); $this->db->ex("DELETE FROM {$this->db->prefix}sessions WHERE expires < $expire"); return $this->db->affected(); } /** * SessionUpdateTimestampHandlerInterface::validateId * @param string $id * @return bool */ public function validateId($id): bool { $r = $this->db->sq("SELECT COUNT(*) FROM {$this->db->prefix}sessions WHERE id = ?", [$id]); if ($r) return true; return false; } /** * SessionUpdateTimestampHandlerInterface::updateTimestamp * @param string $id * @param string $data * @return bool */ public function updateTimestamp($id, $data): bool { // Warning if return false return true; } } ================================================ FILE: src/includes/classes.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class ApiRequest { public $path; public $method; public $contentType; public $jsonBody; function __construct() { if (defined('MTT_API_USE_PATH_INFO')) { $this->path = $_SERVER['PATH_INFO']; } else { $this->path = $_GET['_path'] ?? ''; } $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; } } class ApiResponse { public $data = null; public $contentType = 'application/json'; public $code = null; function content(string $contentType, string $content, int $code = 200) { $this->contentType = $contentType; $this->data = $content; $this->code = $code; return $this; } function htmlContent(string $content, int $code = 200): ApiResponse { return $this->content('text/html', $content, $code); } function cssContent(string $content, int $code = 200): ApiResponse { return $this->content('text/css', $content, $code); } function exit() { if (is_null($this->data) && is_null($this->code)) { http_response_code(404); } if (!is_null($this->code)) { http_response_code($this->code); } if ($this->contentType != 'application/json') { header('Content-type: '. $this->contentType); print $this->data; exit(); } jsonExit($this->data); } } abstract class ApiController { /** @var ApiRequest */ protected $req; /** @var ApiResponse */ protected $response; function __construct(ApiRequest $req, ApiResponse $response) { $this->req = $req; $this->response = $response; } } abstract class MTTExtension { const bundleId = ''; //abstract function init() { } public static function extMetaInfo(string $ext): ?array { $file = MTT_EXT. $ext. '/extension.json'; if ( file_exists($file) && false !== ($json = file_get_contents($file)) && ($meta = json_decode($json, true)) && is_array($meta) ) { // check mandatory keys if (!isset($meta['bundleId']) || !isset($meta['name']) || !isset($meta['version']) || !isset($meta['description'])) { return null; } if (!is_string($meta['bundleId']) || !is_string($meta['name']) || !is_string($meta['version']) || !is_string($meta['description'])) { return null; } return $meta; } error_log("$ext/extension.json is missing or invalid"); return null; } public static function extApiActionUrl(string $action, ?string $params = null) { $url = get_unsafe_mttinfo('api_url'). 'ext/'. static::bundleId. "/$action"; if (!is_null($params)) { if (false !== strpos($url, '?')) { $url .= '&'. $params; } else { $url .= '?'. $params; } } return $url; } public static function getFileVer(string $filename): string { return (string)get_filever('ext', $filename, static::bundleId); } public static function getFileUri(string $filename, bool $versioned = true): string { $version = ($versioned) ? '?v='. htmlspecialchars(static::getFileVer($filename)) : ''; return get_mttinfo('mtt_uri'). htmlspecialchars('ext/'. static::bundleId. '/'. $filename) . $version; } } interface MTTHttpApiExtender { function extendHttpApi(): array; } interface MTTExtensionSettingsInterface { function settingsPage(): string; function settingsPageType(): int; function saveSettings(array $array, ?string &$outMesssage): bool; } class MTTExtensionLoaderException extends Exception {} class MTTExtensionLoader { private static $exts = []; /** * * @throws Exception */ public static function loadExtension(string $ext): bool { if (isset(self::$exts[$ext])) { error_log("Extension '$ext' is already registered"); return false; } $loader = MTT_EXT. $ext. '/loader.php'; if (!file_exists($loader)) { error_log("Failed to init extension '$ext': no loader.php"); return false; } require_once(MTT_EXT. $ext. '/loader.php'); $extNormalized = str_replace('-', '_', $ext); $instanceFunc = 'mtt_ext_'. $extNormalized. '_instance'; if (!function_exists($instanceFunc)) { throw new MTTExtensionLoaderException("Failed to init extension '$ext': no '$instanceFunc' function"); } $instance = $instanceFunc(); if ( ! ($instance instanceof MTTExtension) ) { throw new MTTExtensionLoaderException("Failed to init extension '$ext': incompatible instance"); } $className = get_class($instance); if (!defined("$className::bundleId")) { throw new MTTExtensionLoaderException("Failed to load extension '$ext': missing required class constants (bundleId)"); } if ($instance::bundleId != $ext) { throw new MTTExtensionLoaderException("Failed to load extension '$ext': bundleId does not conforms to extension dir"); } Lang::instance()->loadExtensionLang($ext); $instance->init(); self::$exts[$ext] = $instance; return true; } /** * @return MTTExtension[] */ public static function loadedExtensions(): array { $a = []; foreach (self::$exts as $ext => $instance) { $a[] = $instance; } return $a; } /** * @return string[] */ public static function bundles(): array { $lang = Lang::instance(); $a = []; $files = array_diff(scandir(MTT_EXT) ?? [], ['.', '..']); foreach ($files as $ext) { if ( !is_dir(MTT_EXT. $ext) || !file_exists(MTT_EXT. $ext. '/loader.php') ) { continue; } $meta = MTTExtension::extMetaInfo($ext); if (!$meta) { continue; } if ( $lang->langCode() != 'en' && null !== ($translation = $lang->getExtensionLang($ext)) && null !== ($locName = $translation['ext.'.$ext.'.name'] ?? null) ) { $meta['name'] = $locName; } $a[$ext] = $meta; } return $a; } public static function extensionInstance(string $ext): ?MTTExtension { return self::$exts[$ext] ?? null; } public static function isLoaded(string $ext): bool { return isset(self::$exts[$ext]); } } ================================================ FILE: src/includes/common.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ function htmlarray($a, $exclude=null) { htmlarray_ref($a, $exclude); return $a; } function htmlarray_ref(&$a, $exclude=null) { if(!$a) return; if(!is_array($a)) { $a = htmlspecialchars($a); return; } reset($a); if($exclude && !is_array($exclude)) $exclude = array($exclude); foreach($a as $k=>$v) { if(is_array($v)) $a[$k] = htmlarray($v, $exclude); elseif(!$exclude) $a[$k] = htmlspecialchars($v ?? ''); elseif(!in_array($k, $exclude)) $a[$k] = htmlspecialchars($v ?? ''); } return; } function _post($param,$defvalue = '') { if(!isset($_POST[$param])) { return $defvalue; } else { return $_POST[$param]; } } function _get($param,$defvalue = '') { if(!isset($_GET[$param])) { return $defvalue; } else { return $_GET[$param]; } } function _server($param, $defvalue = '') { if ( !isset($_SERVER[$param]) ) { return $defvalue; } else { return $_SERVER[$param]; } } function formatDate3($format, $ay, $am, $ad, $lang) { # F - month long, M - month short # m - month 2-digit, n - month 1-digit # d - day 2-digit, j - day 1-digit $ml = $lang->get('months_long'); $ms = $lang->get('months_short'); $Y = $ay; $YC = 100 * floor($Y/100); //...1900,2000,2100... if ($YC == 2000) $y = $Y < $YC+10 ? '0'.($Y-$YC) : $Y-$YC; else $y = $Y; $n = $am; $m = $n < 10 ? '0'.$n : $n; $F = $ml[$am-1]; $M = $ms[$am-1]; $j = $ad; $d = $j < 10 ? '0'.$j : $j; return strtr($format, array('Y'=>$Y, 'y'=>$y, 'F'=>$F, 'M'=>$M, 'n'=>$n, 'm'=>$m, 'd'=>$d, 'j'=>$j)); } function daysInMonth(int $m, int $y = 0): int { if ($y == 0) $y = (int)date('Y'); $isLeap = (0 == $y % 4) && ((0 != $y % 100) || (0 == $y % 400)); $a = array(1=>31, ($isLeap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); if (isset($a[$m])) return $a[$m]; else return 0; } function getRequestUri() { // Do not use HTTP_X_REWRITE_URL due to CVE-2018-14773 // SCRIPT_NAME or PATH_INFO ? 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 { $uri = $_SERVER['ORIG_PATH_INFO']; //has no query if (!empty($_SERVER['QUERY_STRING'])) $uri .= '?'. $_SERVER['QUERY_STRING']; return $uri; } } function url_dir(string $url, bool $onlyPath = true) { if (false !== $p = strpos($url, '?')) { $url = substr($url, 0, $p); # to avoid parse errors on strange query strings } $path = parse_url($url, PHP_URL_PATH) ?? ''; if ($onlyPath) { $url = $path; } if ($path === '') { return $url . '/'; } if (substr($url, -1) === '/') { return $url; } if (false !== $p = strrpos($url, '/')) { return substr($url, 0, $p+1); } return '/'; } function removeNewLines($s) { return str_replace( ["\r","\n"], '', $s ); } /** * Generates UUID v4 * Implementation from https://github.com/symfony/polyfill-uuid */ function generateUUID(): string { $uuid = bin2hex(random_bytes(16)); return sprintf('%08s-%04s-4%03s-%04x-%012s', substr($uuid, 0, 8), substr($uuid, 8, 4), // $uuid[14] = 4 substr($uuid, 13, 3), hexdec(substr($uuid, 16, 4)) & 0x3fff | 0x8000, substr($uuid, 20, 12) ); } function passwordHash(string $p): string { if ($p == '') return ''; return 'sha256:'. hash('sha256', $p); } /** * Compares raw (not hashed) password with password hash. Return true if equals. * @param string $p Raw password * @param string $hash Password hash * @return bool */ function isPasswordEqualsToHash(string $p, string $hash): bool { if ($hash == '' && $p == '') return true; if ($hash == '' || $p == '') return false; if ( false !== $pos = strpos($hash, ':') ) { $algo = substr($hash, 0, $pos); if ($algo != 'sha256') throw new Exception("Unsupported algo of password hash"); if ( hash_equals($hash, passwordHash($p)) ) return true; } return false; } function idSignature(string $id, string $key, string $salt): string { $secret = $key.$salt; return hash_hmac('sha256', $id, $secret); } function isValidSignature(string $signature, string $id, string $key, string $salt): bool { if ( hash_equals($signature, idSignature($id, $key, $salt)) ) return true; return false; } function randomString(int $len = 16, string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') : string { $a = []; $max = strlen($chars) - 1; for ($i = 0; $i < $len; $i++) { $a[]= $chars[random_int(0, $max)]; } return implode('', $a); } if (!function_exists('array_is_list')) { /** * Checks whether a given array is a list * @param array $array * @return bool */ // https://www.php.net/manual/en/function.array-is-list.php#127044 function array_is_list(array $array): bool { $i = -1; foreach ($array as $k => $v) { ++$i; if ($k !== $i) { return false; } } return true; } } ================================================ FILE: src/includes/filters.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class MTTFilterCenter { private static $filters = []; public static function addFilterCallbackForAction(string $action, callable $callback): bool { if (!isset(self::$filters[$action])) { self::$filters[$action] = []; } if (!in_array($callback, self::$filters[$action])) { // do not duplicate same callback self::$filters[$action][] = $callback; return true; } return false; } public static function addFilterForAction(string $action, MTTFilterInterface $filter): bool { if (!isset(self::$filters[$action])) { self::$filters[$action] = []; } if (!in_array($filter, self::$filters[$action])) { // do not duplicate same filter self::$filters[$action][] = $filter; return true; } return false; } public static function hasFiltersForAction(string $action): bool { if (isset(self::$filters[$action]) && count(self::$filters[$action]) > 0) { return true; } return false; } public static function filter(string $action, $in, &$out): bool { if (!isset(self::$filters[$action]) || count(self::$filters[$action]) == 0) { return false; } foreach (self::$filters[$action] as $filter) { if ($filter instanceof MTTFilterInterface) { $filter->filter($in, $out); } else { $filter($in, $out); } } return true; } } interface MTTFilterInterface { function filter($in, &$out); } function add_filter(string $action, MTTFilterInterface $filter) { MTTFilterCenter::addFilterForAction($action, $filter); } function add_filter_callback(string $action, callable $callback) { MTTFilterCenter::addFilterCallbackForAction($action, $callback); } function do_filter(string $action, $in, &$out): bool { return MTTFilterCenter::filter($action, $in, $out); } ================================================ FILE: src/includes/index.html ================================================ ================================================ FILE: src/includes/lang/_percent.php ================================================ #!/usr/bin/env php $translated) { $rows[] = [$lang, "$translated/$totalKeys", round(100 * $translated/$totalKeys)."%"]; } #calc column width $width = [0,0,0]; foreach ($rows as $row) { $width[0] = max($width[0], strlen($row[0])); $width[1] = max($width[1], strlen($row[1])); $width[2] = max($width[2], strlen($row[2])); } # print table print "# myTinyTodo Translations\n\n"; foreach ($rows as $i => $row) { if ($i == 0) { print("| ". str_pad($row[0], $width[0], " ", STR_PAD_BOTH). " | ". str_pad($row[1], $width[1], " ", STR_PAD_BOTH). " | ". str_pad($row[2], $width[2], " ", STR_PAD_BOTH). " |\n"); print("|:". str_repeat("-", $width[0]). "-|-". str_repeat("-", $width[1]). ":|-". str_repeat("-", $width[2]). ":|\n"); } else { print("| ". str_pad($row[0], $width[0], " ", STR_PAD_RIGHT). " | ". str_pad($row[1], $width[1], " ", STR_PAD_LEFT). " | ". str_pad($row[2], $width[2], " ", STR_PAD_LEFT). " |\n"); } } function checkLang(array $src, string $file) : int { $lang = json_decode(file_get_contents($file), true) ?? []; unset($lang['_header']); $translated = checkArray($file, $src, $lang); return $translated; } function checkArray(string $file, array $src, ?array $a) : int { $translated = 0; foreach ($src as $k => $v) { if (!isset($a[$k])) { if (defined('P_VERBOSE')) { fwrite(STDERR, "$file: key `$k` is not defined\n"); } continue; } if (!is_array($v)) { ++$translated; } else if (is_array($a[$k])) { ++$translated; $translated += checkArray($file, $v, $a[$k]); } else if (defined('P_VERBOSE')) { fwrite(STDERR, "$file: key `$k` is not array\n"); } } return $translated; } ================================================ FILE: src/includes/lang/ar.json ================================================ { "_header": { "ver": "v1.4.2", "date": "2011-04-04", "language": "Arabic", "original_name": "عربي", "author": "Majid Al-Dharrab", "author_email": "majid@aldharrab.com", "rtl": 1 }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "مهمة جديدة", "htab_search": "بحث", "btn_add": "أضِف", "btn_search": "ابحث", "advanced_add": "متقدم", "searching": "يبحث عن", "tasks": "المهمات", "taskdate_inline_created": "أنشئت في %s", "taskdate_inline_completed": "اكتملت في %s", "taskdate_inline_duedate": "تنتهي في %s", "taskdate_created": "أنشئت", "taskdate_completed": "اكتملت", "edit_task": "حرِّر المهمة", "add_task": "مهمة جديدة", "priority": "الأولوية", "task": "المهمة", "note": "الملاحظة", "tags": "الوسوم", "save": "احفظ", "cancel": "ألغِ", "password": "كلمة السر", "btn_login": "لُج", "a_login": "لُج", "a_logout": "اخرج", "public_tasks": "المهمات العامة", "tagcloud": "الوسوم", "tagfilter_cancel": "ألغِ المرشح", "sortByHand": "رتِّب يدويًا", "sortByPriority": "رتِّب حسب الأولوية", "sortByDueDate": "رتِّب حسب تاريخ الانتهاء", "sortByDateCreated": "رتِّب حسب تاريخ الإنشاء", "sortByDateModified": "رتِّب حسب تاريخ التعديل", "due": "تنتهي", "daysago": "منذ %d أيام", "indays": "خلال %d أيام", "months_short": [ "ينا", "فبر", "مار", "أبر", "ماي", "يون", "يول", "أغس", "سبت", "أكت", "نوف", "ديس" ], "months_long": [ "يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر" ], "days_min": [ "أحد", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت" ], "days_long": [ "الأحد", "الإثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت" ], "today": "اليوم", "yesterday": "أمس", "tomorrow": "غدًا", "f_past": "متأخر", "f_today": "اليوم وغدًا", "f_soon": "قريبًا", "action_edit": "حرِّر", "action_note": "حرِّر الملاحظة", "action_delete": "احذف", "action_priority": "الأولوية", "action_move": "انقل إلى", "notes": "الملاحظات:", "notes_show": "أظهر", "notes_hide": "أخفِ", "list_new": "قائمة جديدة", "list_rename": "غيِّر اسم القائمة", "list_delete": "احذف القائمة", "list_publish": "انشر القائمة", "list_showcompleted": "أظهر المهمات المكتملة", "list_clearcompleted": "امحُ المهمات المكتملة", "list_select": "اختر القائمة", "list_export": "صدِّر", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "تلقيم آر‌إس‌إس", "alltags": "كل الوسوم:", "alltags_show": "أظهر الكل", "alltags_hide": "أخفِ الكل", "a_settings": "الإعدادات", "rss_feed": "تلقيم آر‌إس‌إس", "feed_title": "%s", "feed_completed_tasks": "المهمات المكتملة", "feed_modified_tasks": "المهمات المعدلة", "feed_new_tasks": "المهمات الجديدة", "alltasks": "كل المهمات", "set_header": "الإعدادات", "set_title": "العنوان", "set_title_descr": "(حدِّد ما إذا أردت تغيير العنوان المبدئي)", "set_language": "اللغة", "set_protection": "الحماية بكلمة سر", "set_enabled": "مفعلة", "set_disabled": "معطلة", "set_newpass": "كلمة السر الجديدة", "set_newpass_descr": "(اتركها فارغة إذا لم تكن ترغب بتغيير كلمة السر الحالية)", "set_smartsyntax": "الصياغة الذكية", "set_smartsyntax_descr": "(/الأولوية/ المهمة /الوسوم/)", "set_timezone": "المنطقة الزمنية", "set_autotag": "الوسوم الآلية", "set_autotag_descr": "(يضيف الوسوم الموجودة في مرشّح الوسوم إلى المهمات الجديدة)", "set_sessions": "طريقة التعامل مع الجلسات", "set_sessions_php": "بي‌إتش‌بي", "set_sessions_files": "ملفات", "set_firstdayofweek": "أول أيام الأسبوع", "set_custom": "مخصصة", "set_date": "صيغة التواريخ", "set_date2": "الصيغة القصيرة للتواريخ", "set_shortdate": "الصيغة القصيرة لتواريخ السنة الجارية", "set_clock": "صيغة الوقت", "set_12hour": "12 ساعة", "set_24hour": "24 ساعة", "set_submit": "أرسل التغييرات", "set_cancel": "ألغِ", "set_showdate": "تواريخ المهمات تظهر في القائمة", "confirmDelete": "هل أنت متأكد أنك تود حذف المهمة؟", "confirmLeave": "قد تود بيانات لم تُحفظ. هل تود المغادرة حقًا؟", "actionNoteSave": "احفظ", "actionNoteCancel": "ألغِ", "error": "حدث خطأ ما (انقر لمشاهدة التفاصيل)", "denied": "لم يُسمح بالنفاذ", "invalidpass": "كلمة السر خاطئة", "addList": "أنشئ قائمة جديدة", "addListDefault": "Todo", "renameList": "غيِّر اسم القائمة", "deleteList": "ستُحذف القائمة الحالية وكل ما بها من مهمات.\nهل أنت متأكد أنك تود فعل ذلك؟", "clearCompleted": "ستُحذف كل المهمات المكتملة في القائمة.\nهل أنت متأكد أنك تود فعل ذلك؟", "settingsSaved": "حُفظت الإعدادات. يعيد التحميل..." } ================================================ FILE: src/includes/lang/bg.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-02-24", "language": "Bulgarian", "original_name": "Български", "author": "Vladimir Komarov", "author_url": "http://www.myastrodata.com" }, "My Tiny Todolist": "Моят малък To-do списък", "htab_newtask": "Нова задача", "htab_search": "Търсене", "btn_add": "Доабвяне", "btn_search": "Търсене", "advanced_add": "Разширено", "searching": "Търсене за", "tasks": "Задачи", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Дата на създаване", "taskdate_completed": "Дата на приключване", "edit_task": "Редактирай задача", "add_task": "Нова задача", "priority": "Приоритет", "task": "Задача", "note": "Бележка", "tags": "Етикети", "save": "Записване", "cancel": "Отказ", "password": "Парола", "btn_login": "Вход", "a_login": "Вход", "a_logout": "Изход", "public_tasks": "Публични задачи", "tagcloud": "Tags", "tagfilter_cancel": "Отказ от филтър", "sortByHand": "Ръчно сортиране", "sortByPriority": "Сортиране по приоритет", "sortByDueDate": "Сортиране по дата на падеж", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Падеж", "daysago": "преди %d ден(а)", "indays": "в %d ден(а)", "months_short": [ "Яну", "Фев", "Мар", "Апр", "Май", "Юни", "Юли", "Авг", "Сеп", "Окт", "Нов", "Дек" ], "months_long": [ "Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември" ], "days_min": [ "Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" ], "days_long": [ "Неделя", "Понеделник", "Вторник", "Сряда", "Четвъртък", "Петък", "Събота" ], "today": "днес", "yesterday": "вчера", "tomorrow": "утре", "f_past": "Просрочен падеж", "f_today": "Днес и утре", "f_soon": "Скоро", "action_edit": "Редакция", "action_note": "Редактиране на бележката", "action_delete": "Изтриване", "action_priority": "Приоритет", "action_move": "Преместване в", "notes": "Бележки:", "notes_show": "Покажи", "notes_hide": "Скрий", "list_new": "Нов списък", "list_rename": "Преименуване на списък", "list_delete": "Изтриване на списък", "list_publish": "Публикуване на списък", "list_showcompleted": "Покажи приключените задачи", "list_clearcompleted": "Изчисти приключените задачи", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Всички етикети:", "alltags_show": "Покажи всички", "alltags_hide": "Скрий всички", "a_settings": "Настройки", "rss_feed": "RSS емисия", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Настройки", "set_title": "Заглави", "set_title_descr": "(уточнете, ако искате да смените подразбиращото се заглавие)", "set_language": "Език", "set_protection": "Защита с парола", "set_enabled": "Разрешено", "set_disabled": "Забранено", "set_newpass": "Нова парола", "set_newpass_descr": "(оставете полето празно, ако не искате да сменяте паролата)", "set_smartsyntax": "Кратък синтаксис", "set_smartsyntax_descr": "(/приоритет/ задача /етикети/)", "set_timezone": "Time zone", "set_autotag": "Autotagging", "set_autotag_descr": "(автоматично добавяне на етикет към текущия етикет-филтър за новодобавената задача)", "set_sessions": "Механизъм за съхраняване на сесии", "set_sessions_php": "PHP", "set_sessions_files": "Files", "set_firstdayofweek": "Първи ден на седмицата", "set_custom": "Custom", "set_date": "Формат на датата", "set_date2": "Short Date format", "set_shortdate": "Съкратен формат на датата", "set_clock": "Формат за часовете", "set_12hour": "12 часов", "set_24hour": "24 часов", "set_submit": "Запис на промените", "set_cancel": "Отказ", "set_showdate": "Показвай датата на задачите в списъка", "confirmDelete": "Сигурни ли сте, че искате да изтриете тази задача?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "Запис", "actionNoteCancel": "Отказ", "error": "Възникна проблем (кликнете за повече информация)", "denied": "Достъп забранен", "invalidpass": "Грешна парола!", "addList": "Създаване на нов списък", "addListDefault": "Todo", "renameList": "Преименуване на списък", "deleteList": "Ще изтриете всички задачи в този списък!\nСигурни ли сте?", "clearCompleted": "Ще изтриете всички изпълнени задачи в този списък!\nСигурни ли сте?", "settingsSaved": "Настройките са запазени! Презареждане..." } ================================================ FILE: src/includes/lang/ca.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-05-04", "language": "Catalan", "original_name": "Català", "author": "Ariel vb", "author_url": "http://www.arielvb.com" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Nova tasca", "htab_search": "Cercar", "btn_add": "Afegir", "btn_search": "Cercar", "advanced_add": "Avançat", "searching": "Cercant", "tasks": "Tasques", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Data de creació", "taskdate_completed": "Data de realització", "edit_task": "Editar Tasca", "add_task": "Nova Tasca", "priority": "Prioritat", "task": "Tasca", "note": "Nota", "tags": "Etiquetes", "save": "Desar", "cancel": "Cancel·lar", "password": "Contrasenya", "btn_login": "Iniciar sessió", "a_login": "Iniciar sessió", "a_logout": "Tancar sessió", "public_tasks": "Tasques públiques", "tagcloud": "Tags", "tagfilter_cancel": "cancel·lar filtre", "sortByHand": "Ordenar a mà", "sortByPriority": "Ordenar per prioritat", "sortByDueDate": "Ordenar per data de venciment", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Venciment", "daysago": "fa %d dies", "indays": "en %d dies", "months_short": [ "Gen", "Feb", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Oct", "Nov", "Dec" ], "months_long": [ "Gener", "Febrer", "Març", "Abril", "Maig", "Juny", "Juliol", "Agost", "Setembre", "Octubre", "Novembre", "Decembre" ], "days_min": [ "Dmg.", "Dl.", "Dm.", "Dmc.", "Dj.", "Dv.", "Ds." ], "days_long": [ "Diumenge", "Dilluns", "Dimarts", "Dimecres", "Dijous", "Divendres", "Dissabte" ], "today": "avui", "yesterday": "ahir", "tomorrow": "demà", "f_past": "Endarrerides", "f_today": "Avui i demà", "f_soon": "Aviat", "action_edit": "Editar", "action_note": "Editar Nota", "action_delete": "Esborrar", "action_priority": "Prioritat", "action_move": "Mou a", "notes": "Notes:", "notes_show": "Mostrar", "notes_hide": "Ocultar", "list_new": "Nova llista", "list_rename": "Reanomenar llista", "list_delete": "Esborrar llista", "list_publish": "Publicar llista", "list_showcompleted": "Mostrar tasques realitzades", "list_clearcompleted": "Esborrar tasques titolarealitzades", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Totes les etiquetes:", "alltags_show": "Mostrar totes", "alltags_hide": "Ocultar totes", "a_settings": "Configuració", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Configuració", "set_title": "Títol", "set_title_descr": "(omplir si es vol canviar el títol per defecte)", "set_language": "Idioma", "set_protection": "Protecció per contrasenya", "set_enabled": "Activar", "set_disabled": "Desactivar", "set_newpass": "Nova contrasenya", "set_newpass_descr": "(deixar en blanc si no es vol canviar la contrasenya actual)", "set_smartsyntax": "Sintaxi intel·ligent", "set_smartsyntax_descr": "(/prioritat/ tasca /etiquetes/)", "set_timezone": "Time zone", "set_autotag": "Auto etiquetar", "set_autotag_descr": "(afegir automàticament etiquetes del filtre actual a les noves tasques)", "set_sessions": "Mecanisme de gestió de sessions", "set_sessions_php": "PHP", "set_sessions_files": "Fitxers", "set_firstdayofweek": "Primer dia de la setmana", "set_custom": "Custom", "set_date": "Format de data", "set_date2": "Short Date format", "set_shortdate": "Format curt de data", "set_clock": "Format de l'hora", "set_12hour": "12 hores", "set_24hour": "24 hores", "set_submit": "Desar canvis", "set_cancel": "Cancel·lar", "set_showdate": "Mostrar data de la tasca a la llista", "confirmDelete": "Estàs segur d'esborrar la tasca?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "desar", "actionNoteCancel": "cancel·lar", "error": "Hi ha hagut un error (clica per més detalls)", "denied": "Accés denegat", "invalidpass": "Contrasenya incorrecta", "addList": "Crear nova llista", "addListDefault": "Todo", "renameList": "Reanomenar llista", "deleteList": "Això esborrarà la llista actual i totes les seves tasques.\nEstàs segur?", "clearCompleted": "Això esborrarà totes les tasques realitzades de la llista.\nEstàs segur?", "settingsSaved": "Configuració desada. Recarregant..." } ================================================ FILE: src/includes/lang/cz.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-04-09", "language": "Czech", "original_name": "Čeština", "author": "Adam Heinrich", "author_url": "http://www.adamh.cz" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Nový úkol", "htab_search": "Hledat", "btn_add": "Nový", "btn_search": "Hledat", "advanced_add": "Rozšířené", "searching": "Vyhledávání", "tasks": "Úkoly", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Datum vytvoření", "taskdate_completed": "Datum splnění", "edit_task": "Upravit úkol", "add_task": "Nový úkol", "priority": "Priorita", "task": "Úkol", "note": "Poznámka", "tags": "Tagy", "save": "Uložit", "cancel": "Zrušit", "password": "Heslo", "btn_login": "Login", "a_login": "Přihlásit", "a_logout": "Odhlásit", "public_tasks": "Veřejné úkoly", "tagcloud": "Tags", "tagfilter_cancel": "zrušit filtry", "sortByHand": "Třídit ručně", "sortByPriority": "Třídit podle priority", "sortByDueDate": "Třídit podle termínu", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Termín", "daysago": "před %d dny", "indays": "ve %d dnech", "months_short": [ "Led", "Úno", "Bře", "Dub", "Kvě", "Če6", "Če7", "Srp", "Zář", "Říj", "Lis", "Pro" ], "months_long": [ "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec" ], "days_min": [ "Ne", "Po", "Út", "St", "Čt", "Pá", "So" ], "days_long": [ "Neděle", "Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota" ], "today": "dnes", "yesterday": "včera", "tomorrow": "zítra", "f_past": "Overdue", "f_today": "Dnes a zítra", "f_soon": "Brzy", "action_edit": "Upravit", "action_note": "Upravit poznámku", "action_delete": "Smazat", "action_priority": "Priorita", "action_move": "Přesunout do", "notes": "Poznámky:", "notes_show": "Zobrazit", "notes_hide": "Skrýt", "list_new": "Nový seznam", "list_rename": "Přejmenovat seznam", "list_delete": "Smazat seznam", "list_publish": "Zveřejnit seznam", "list_showcompleted": "Zobrazit splněné úkoly", "list_clearcompleted": "Smazat splněné úkoly", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Všechny tagy:", "alltags_show": "Zobrazit vše", "alltags_hide": "Skrýt vše", "a_settings": "Nastavení", "rss_feed": "RSS kanál", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Nastavení", "set_title": "Titulek", "set_title_descr": "(zadejte, pokud chcete změnit výchozí titulek)", "set_language": "Jazyk", "set_protection": "Zaheslováno", "set_enabled": "Zapnuto", "set_disabled": "Vypnuto", "set_newpass": "Nové heslo", "set_newpass_descr": "(nevyplňujte, pokud nechcete měnit stávající heslo)", "set_smartsyntax": "\"Smart\" syntaxe", "set_smartsyntax_descr": "(Zápis: \"/priorita/ test úkolu /tagy/\")", "set_timezone": "Time zone", "set_autotag": "Automatické tagování", "set_autotag_descr": "(automaticky přiřadí k tagům text z filtru)", "set_sessions": "Správa sessions", "set_sessions_php": "PHP", "set_sessions_files": "Soubory", "set_firstdayofweek": "První den v týdnu", "set_custom": "Custom", "set_date": "Formát data", "set_date2": "Short Date format", "set_shortdate": "Zkrácený formát data", "set_clock": "Formát času", "set_12hour": "12 hodinový", "set_24hour": "24 hodinový", "set_submit": "Uložit změny", "set_cancel": "Zrušit", "set_showdate": "Zobrazit v seznamu datum vytvoření úkolu", "confirmDelete": "Opravdu chcete smazat úkol?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "uložit", "actionNoteCancel": "zrušit", "error": "Objevil se problém (klikněte pro více informací)", "denied": "Přístup odepřen", "invalidpass": "Špatné heslo", "addList": "Vytvořit nový seznam", "addListDefault": "Todo", "renameList": "Přejmenovat seznam", "deleteList": "Tímto smažete seznam a všechny úkoly v něm.\nChcete pokračovat?", "clearCompleted": "Tímto smažete všechny splněné úkoly.\nChcete pokračovat?", "settingsSaved": "Nastavení uloženo. Načítám..." } ================================================ FILE: src/includes/lang/da.json ================================================ { "_header": { "ver": "v1.3.5", "date": "2010-05-22", "language": "Danish", "original_name": "Dansk", "author": "Per Jensen", "author_url": "http://www.plads9000.dk" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Ny opgave", "htab_search": "Søg", "btn_add": "Tilføj", "btn_search": "Søg", "advanced_add": "Udvidet", "searching": "Søger efter", "tasks": "Opgaver", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Dato for oprettelse", "taskdate_completed": "Dato for færdiggørelse", "edit_task": "Rediger opgave", "add_task": "Ny opgave", "priority": "Prioritet", "task": "Opgave", "note": "Note", "tags": "Tags", "save": "Gem", "cancel": "Annuller", "password": "Password", "btn_login": "Login", "a_login": "Login", "a_logout": "Logout", "public_tasks": "Offentlige opgaver", "tagcloud": "Tags", "tagfilter_cancel": "Annuller filter", "sortByHand": "Sorter manuelt", "sortByPriority": "Sorter efter prioritet", "sortByDueDate": "Sorter efter forfaldsdato", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Forfald", "daysago": "%d dage siden", "indays": "om %d dage", "months_short": [ "Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec" ], "months_long": [ "Januar", "Februar", "Marts", "April", "Maj", "Juni", "Juli", "August", "September", "Oktober", "November", "December" ], "days_min": [ "Sø", "Ma", "Ti", "On", "To", "Fr", "Lø" ], "days_long": [ "Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag" ], "today": "I dag", "yesterday": "I går", "tomorrow": "I morgen", "f_past": "Forfalden", "f_today": "I dag og i morgen", "f_soon": "Snart", "action_edit": "Rediger", "action_note": "Rediger note", "action_delete": "Slet", "action_priority": "Prioritet", "action_move": "Flyt til", "notes": "Noter:", "notes_show": "Vis", "notes_hide": "Gem", "list_new": "Ny liste", "list_rename": "Omdøb liste", "list_delete": "Slet liste", "list_publish": "Udgiv liste", "list_showcompleted": "Vis fuldførte opgaver", "list_clearcompleted": "Slet fuldførte opgaver", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Alle tags:", "alltags_show": "Vis alle", "alltags_hide": "Gem alle", "a_settings": "Indstillinger", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Indstillinger", "set_title": "Titel", "set_title_descr": "(angiv hvis du ønsker at ændre standard titel)", "set_language": "Sprog", "set_protection": "Password beskyttelse", "set_enabled": "Aktiveret", "set_disabled": "Deaktiveret", "set_newpass": "Nyt password", "set_newpass_descr": "(lad være tomt hvis aktuelt password ikke skal ændres)", "set_smartsyntax": "Smart syntaks", "set_smartsyntax_descr": "(/ prioritet / opgave / tags /)", "set_timezone": "Time zone", "set_autotag": "Automatisk tagging", "set_autotag_descr": "(Tilføjer automatisk tags til nyoprettede opgaver)", "set_sessions": "Sessions håndtering", "set_sessions_php": "PHP", "set_sessions_files": "Filer", "set_firstdayofweek": "Første ugedag", "set_custom": "Custom", "set_date": "Dato format", "set_date2": "Short Date format", "set_shortdate": "Kort datoformat", "set_clock": "Tidsformat", "set_12hour": "12-timer", "set_24hour": "24-timer", "set_submit": "Gem ændringer", "set_cancel": "Annuller", "set_showdate": "Vis opgavedato i listen", "confirmDelete": "Er du sikker på at du vil slette denne opgave?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "gem", "actionNoteCancel": "annuller", "error": "Der opstod en fejl (klik for detaljer)", "denied": "Adgang nægtet", "invalidpass": "Forkert password", "addList": "Opret ny liste", "addListDefault": "Todo", "renameList": "Omdøb liste", "deleteList": "Dette vil slette den aktuelle liste og alle opgaver i den.\nEr du sikker?", "clearCompleted": "Dette vil slette alle afsluttede opgaver i listen.\nEr du sikker?", "settingsSaved": "Indstillinger gemt. Genindlæser..." } ================================================ FILE: src/includes/lang/de.json ================================================ { "_header": { "language": "German", "original_name": "Deutsch", "author": "Franky, Tobias Quathamer, Denny Korsukéwitz", "date": "2024-04-03", "ver": "v1.8.1" }, "My Tiny Todolist": "Meine winzige Todoliste", "powered_by": "Powered by", "htab_newtask": "Neue Aufgabe", "htab_search": "Suche", "btn_add": "Hinzufügen", "btn_search": "Suche", "advanced_add": "Erweitert", "searching": "Suche nach", "tasks": "Aufgaben", "taskdate_inline_created": "Erstellt am %s", "taskdate_inline_edited": "Bearbeitet am %s", "taskdate_inline_completed": "Abgeschlossen am %s", "taskdate_inline_duedate": "Fällig %s", "taskdate_created": "Erstellt", "taskdate_edited": "Zuletzt bearbeitet", "taskdate_completed": "Abgeschlossen", "edit_task": "Aufgabe bearbeiten", "add_task": "Neue Aufgabe", "priority": "Priorität", "task": "Aufgabe", "note": "Notiz", "tags": "Schlagwörter", "list": "Liste", "no_note": "Keine Notizen", "save": "Speichern", "cancel": "Abbrechen", "close": "Schließen", "password": "Passwort", "btn_login": "Anmelden", "a_login": "Anmelden", "a_logout": "Abmelden", "public_tasks": "Öffentliche Aufgabe", "tagcloud": "Schlagwörter", "tagfilter_cancel": "Filter aufheben", "filterTags": "Filter-Tags", "showTagsFromAllLists": "Anzeige der Schlagwörter aller Listen", "sortByHand": "Manuell sortieren", "sortByTitle": "Nach Titel sortieren", "sortByPriority": "Nach Priorität sortieren", "sortByDueDate": "Nach Fälligkeitsdatum sortieren", "sortByDateCreated": "Nach Erstelldatum sortieren", "sortByDateModified": "Nach Änderungsdatum sortieren", "due": "Fällig", "daysago": "vor %d Tagen", "indays": "in %d Tagen", "months_short": [ "Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez" ], "months_long": [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" ], "months_calendar": [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" ], "days_min": [ "So", "Mo", "Di", "Mi", "Do", "Fr", "Sa" ], "days_long": [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ], "today": "heute", "yesterday": "gestern", "tomorrow": "morgen", "f_past": "Überfällig", "f_today": "Heute und morgen", "f_soon": "Bald", "action_edit": "Bearbeiten", "action_note": "Notiz bearbeiten", "action_delete": "Löschen", "action_priority": "Priorität", "action_move": "Verschieben nach", "action_ok": "OK", "action_cancel": "Abbrechen", "notes": "Notizen:", "notes_show": "Anzeigen", "notes_hide": "Verbergen", "list_new": "Neue Liste", "list_rename": "Liste umbenennen", "list_delete": "Liste löschen", "list_showcompleted": "Abgeschlossene Aufgaben anzeigen", "list_clearcompleted": "Abgeschlossene Aufgaben löschen", "list_select": "Liste auswählen", "list_share": "Teilen", "list_publish": "Liste veröffentlichen", "list_enable_feedkey": "Feed-Key aktivieren", "list_show_feedkey": "Feed-Key anzeigen", "list_rssfeed": "RSS-Feed", "list_export_to_csv": "Als CSV exportieren", "list_export_to_ical": "Als iCalendar exportieren", "list_hide": "Liste ausblenden", "alltags": "Alle Schlagwörter:", "alltags_show": "Alle anzeigen", "alltags_hide": "Alle verbergen", "a_settings": "Einstellungen", "rss_feed": "RSS-Feed", "feed_title": "%s", "feed_completed_tasks": "Abgeschlossene Aufgaben", "feed_modified_tasks": "Geänderte Aufgaben", "feed_new_tasks": "Neue Aufgaben", "feed_tasks": "Aufgaben", "feed_status_new": "Neu", "feed_status_updated": "Aktualisiert", "feed_status_completed": "Abgeschlossen", "alltasks": "Alle Aufgaben", "set_header": "Einstellungen", "set_title": "Titel", "set_title_descr": "(angeben, um Standardtitel zu ändern)", "set_language": "Sprache", "set_protection": "Passwortschutz", "set_enabled": "Aktiviert", "set_disabled": "Deaktiviert", "set_newpass": "Neues Passwort", "set_newpass_descr": "(leer lassen, um aktuelles Passwort nicht zu ändern)", "set_smartsyntax": "Smartsyntax", "set_smartsyntax3_descr": "Beispiel: +1 Titel der Aufgabe #schlagwort1 #schlagwort2 @duedate", "set_timezone": "Zeitzone", "set_autotag": "Automatische Schlagwörter", "set_autotag_descr": "(fügt Schlagwort des aktuellen Filters automatisch der neu erstellten Aufgabe hinzu)", "set_markdown": "Markdown", "set_markdown_descr": "Unterstützt Markdown in den Notizen. Deaktiviere diese Option, wenn Du die alten Formatierungen weiter nutzen möchtest.", "set_firstdayofweek": "Erster Tag der Woche", "set_custom": "Benutzerdefiniert", "set_date": "Datumsformat", "set_date2": "Kurzes Datumsformat", "set_shortdate": "Kurzes Datumsformat (aktuelles Jahr)", "set_clock": "Zeitformat", "set_12hour": "12 Stunden", "set_24hour": "24 Stunden", "set_submit": "Änderungen speichern", "set_cancel": "Abbrechen", "set_showdate": "Aufgabendatum in Liste anzeigen", "set_showtime": "Zeit anzeigen", "set_showdate_inline": "Datum inline anzeigen", "set_exactduedate": "Fälligkeitsdatum immer als Datum anzeigen", "set_appearance": "Erscheinungsbild", "set_appearance_system": "Vom System übernehmen", "set_appearance_light": "Helles Thema", "set_appearance_dark": "Dunkles Thema", "set_newtaskcounter_h": "Zähler für neue Aufgaben", "set_newtaskcounter": "Überprüfe auf neue Aufgaben", "set_newtaskcountericon": "Zähler im Favicon anzeigen", "set_extensions": "Erweiterungen", "set_activate": "Aktivieren", "set_deactivate": "Deaktivieren", "confirmDelete": "Willst Du die Aufgabe wirklich löschen?", "confirmLeave": "Einige Daten wurden noch nicht gespeichert. Willst Du die Seite wirklich verlassen?", "actionNoteSave": "Speichern", "actionNoteCancel": "Abbrechen", "error": "Fehler aufgetreten (für Details klicken)", "denied": "Zugriff verweigert", "listNotFound": "Liste nicht gefunden", "noPublicLists": "Keine öffentlichen Aufgaben", "noTags": "keine Tags", "withoutTags": "keine Tags", "withAnyTag": "beliebiger Tag", "invalidpass": "Falsches Passwort", "addList": "Neue Liste anlegen", "addListDefault": "Todo", "renameList": "Liste umbenennen", "deleteList": "Die Liste wird mit allen Aufgaben gelöscht.\nBist Du sicher?", "clearCompleted": "Alle abgeschlossenen Aufgaben dieser Liste werden gelöscht.\nBist Du sicher?", "settingsSaved": "Einstellungen gespeichert. Aktualisierung …" } ================================================ FILE: src/includes/lang/el.json ================================================ { "_header": { "ver": "v1.3.6", "date": "2010-08-29", "language": "Greek", "original_name": "Ελληνικά", "author": "Κορναράκης Νίκος", "author_url": "http://www.kornarakis.gr" }, "My Tiny Todolist": "Η λίστα μου", "htab_newtask": "Νέα υποχρέωση", "htab_search": "Αναζήτηση", "btn_add": "Προσθήκη", "btn_search": "Αναζήτηση", "advanced_add": "Για προχωρημένους", "searching": "Αναζήτηση για", "tasks": "Υποχρεώσεις", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Ημερομηνία δημιουργίας", "taskdate_completed": "Ημερομηνία ολοκλήρωσης", "edit_task": "Επεξεργασία υποχρέωσης", "add_task": "Νέα υποχρέωση", "priority": "Προτεραιότητα", "task": "Υποχρέωση", "note": "Σημείωση", "tags": "Λέξεις κλειδιά", "save": "Αποθήκευση", "cancel": "Άκυρο", "password": "Κωδικός", "btn_login": "Σύνδεση", "a_login": "Σύνδεση", "a_logout": "Αποσύνδεση", "public_tasks": "Δημόσιες υποχρεώσεις", "tagcloud": "Tags", "tagfilter_cancel": "ακύρωση φίλτρου", "sortByHand": "Ταξινόμηση με το χέρι", "sortByPriority": "Ταξινόμηση ανα προτεραιότητα", "sortByDueDate": "Ταξινόμηση ανά ημερομηνία", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Πρέπει", "daysago": "πριν %d μέρες", "indays": "σε %d μέρες", "months_short": [ "Ιαν", "Φεβ", "Mαρ", "Απρ", "Μαι", "Ιουν", "Ιουλ", "Aύγ", "Σεπτ", "Οκτ", "Νοέμ", "Δεκ" ], "months_long": [ "Ιανουάριος", "Φεβρουάριος", "Mάρτιος", "Aπρίλιος", "Mάιος", "Ιούνιος", "Ιούλιος", "Aύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμριος" ], "days_min": [ "Κυ", "Δε", "Τρ", "Τε", "Πε", "Πα", "Σα" ], "days_long": [ "Κυριακή", "Δευτέρα", "Tρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο" ], "today": "σήμερα", "yesterday": "χτες", "tomorrow": "αύριο", "f_past": "Εκπρόσθεσμο", "f_today": "Σήμερα και αύριο", "f_soon": "Σύντομα", "action_edit": "Επεξεργασία", "action_note": "επεξεργασία σημείωσης", "action_delete": "Διαγραφή", "action_priority": "Προτεραιότητα", "action_move": "Μετακίνηση στο", "notes": "Σημειώσεις:", "notes_show": "Εμφάνιση", "notes_hide": "Απόκρυψη", "list_new": "Nέα λίστα", "list_rename": "Μετονομασία λίστας", "list_delete": "Διαγραφή λίστας", "list_publish": "Δημοσίευση λίστας", "list_showcompleted": "Εμφάνιση ολοκληρωμένων υποχρεώσεων", "list_clearcompleted": "Διαγραφή ολοκληρωμένων υποχρεώσεων", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Όλες οι λέξεις κλειδιά:", "alltags_show": "Εμφάνιση όλων", "alltags_hide": "Απόκρυψη όλων", "a_settings": "Ρυθμίσεις", "rss_feed": "RSS Τροφοδοσία", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Ρυθμίσεις", "set_title": "Tίτλος", "set_title_descr": "(συμπληρώστε το αν θέλετε να αλλάξετε τον τίτλο)", "set_language": "Γλώσσας", "set_protection": "Προστασία με κωδικό", "set_enabled": "Ενεργοποιημένο", "set_disabled": "Απενεργοποιημένο", "set_newpass": "Νέος κωδικός", "set_newpass_descr": "(αφήστε το κενό αν δεν αλλάξετε τον κωδικό)", "set_smartsyntax": "Έξυπνο συντακτικό", "set_smartsyntax_descr": "(/προτεραιότητα/ υποχρέωση /λέξεις κλειδιά/)", "set_timezone": "Time zone", "set_autotag": "Αυτόματος καθορισμός λέξεων κλειδιών", "set_autotag_descr": "(αυτόματη προσθήκη λέξεων κλειδιών του φίλτρου στις νέες υποχρεώσεις)", "set_sessions": "Τρόπος διαχείρησης συνεδρίας", "set_sessions_php": "PHP", "set_sessions_files": "Αρχείο", "set_firstdayofweek": "Πρώτη μέρα της εβδομάδας", "set_custom": "Custom", "set_date": "Τύπος ημερομηνίας", "set_date2": "Short Date format", "set_shortdate": "Εμφάνιση τύπου ημερομηνίας", "set_clock": "Τύπος ώρας", "set_12hour": "12ωρο", "set_24hour": "24ωρο", "set_submit": "Αποθήκευση αλλαγών", "set_cancel": "Άκυρο", "set_showdate": "Εμφάνιση ημερομηνίας στην λίστα", "confirmDelete": "Θέλετε σίγουρα να σβήσετε αυτήν την υποχρέωση?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "αποθήκευση", "actionNoteCancel": "άκυρο", "error": "Σφάλμα (κάντε κλικ για πληροφορίες)", "denied": "Απαγορεύετε η πρόσβαση", "invalidpass": "Λάθος κωδικός", "addList": "Δημιουργία νέας λίστας", "addListDefault": "Todo", "renameList": "Μετονομασία λίστας", "deleteList": "Θα διαγραφεί η λίστα μαζί με όλες τις υποχρεώσεις?\nΣίγουρα;", "clearCompleted": "Θα διαγραφούν όλες οι υποχρεώσεις στην λίστα.\nΣίγουρα;", "settingsSaved": "Οι ρυθμίσεις αποθηκεύτηκαν. Επαναφόρτωση..." } ================================================ FILE: src/includes/lang/en-rtl.json ================================================ { "_header": { "ver": "v1.6", "date": "2020-09-04", "language": "English (for RTL test)", "original_name": "English RTL", "author": "Max Pozdeev", "author_url": "http://www.mytinytodo.net", "rtl": 1 } } ================================================ FILE: src/includes/lang/en.json ================================================ { "_header": { "ver": "v1.8.2", "date": "2025-02-16", "language": "English", "original_name": "English", "authors": [ "Max Pozdeev (https://www.mytinytodo.net)" ] }, "My Tiny Todolist": "My Tiny Todolist", "powered_by": "Powered by", "htab_newtask": "New task", "htab_search": "Search", "btn_add": "Add", "btn_search": "Search", "advanced_add": "Advanced", "searching": "Searching for", "tasks": "Tasks", "taskdate_inline_created": "created on %s", "taskdate_inline_edited": "edited on %s", "taskdate_inline_completed": "completed on %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Created", "taskdate_edited": "Last edited", "taskdate_completed": "Completed", "edit_task": "Edit Task", "add_task": "New Task", "priority": "Priority", "task": "Task", "note": "Note", "tags": "Tags", "list": "List", "no_note": "No notes", "save": "Save", "cancel": "Cancel", "close": "Close", "password": "Password", "btn_login": "Login", "a_login": "Login", "a_logout": "Logout", "public_tasks": "Public Tasks", "tagcloud": "Tags", "tagfilter_cancel": "cancel filter", "filterTags": "Filter tags", "showTagsFromAllLists": "Show tags from all lists", "sortByHand": "Sort by hand", "sortByTitle": "Sort by title", "sortByPriority": "Sort by priority", "sortByDueDate": "Sort by due date", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Due", "daysago": "%d days ago", "indays": "in %d days", "months_short": [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ], "months_long": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "months_calendar": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "days_min": [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ], "days_long": [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], "today": "today", "yesterday": "yesterday", "tomorrow": "tomorrow", "f_past": "Overdue", "f_today": "Today and tomorrow", "f_soon": "Soon", "action_edit": "Edit", "action_note": "Edit Note", "action_delete": "Delete", "action_priority": "Priority", "action_move": "Move to", "action_ok": "OK", "action_cancel": "Cancel", "notes": "Notes:", "notes_show": "Show", "notes_hide": "Hide", "list_new": "New list", "list_rename": "Rename list", "list_delete": "Delete list", "list_showcompleted": "Show completed tasks", "list_clearcompleted": "Clear completed tasks", "list_select": "Select list", "list_share": "Share", "list_publish": "Publish list", "list_enable_feedkey": "Enable feed key", "list_show_feedkey": "Show feed key", "list_rssfeed": "RSS Feed", "list_export_to_csv": "Export to CSV", "list_export_to_ical": "Export to iCalendar", "list_hide": "Hide list", "alltags": "All tags:", "alltags_show": "Show all", "alltags_hide": "Hide all", "a_settings": "Settings", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "feed_tasks": "Tasks", "feed_status_new": "New", "feed_status_updated": "Updated", "feed_status_completed": "Completed", "alltasks": "All tasks", "set_header": "Settings", "set_title": "Title", "set_title_descr": "Specify if you want to change default title.", "set_language": "Language", "set_protection": "Password protection", "set_enabled": "Enabled", "set_disabled": "Disabled", "set_newpass": "New password", "set_newpass_descr": "Leave blank if won't change current password.", "set_smartsyntax": "Smart syntax", "set_smartsyntax3_descr": "Example: +1 Task title #tag1 #tag2 @duedate", "set_timezone": "Time zone", "set_autotag": "Autotagging", "set_autotag_descr": "Automatically adds tag of current tag filter to newly created task.", "set_markdown": "Markdown", "set_markdown_descr": "Adds support of Markdown in notes, disable if you want to use old markup.", "set_firstdayofweek": "First day of week", "set_custom": "Custom", "set_date": "Date format", "set_date2": "Short Date format", "set_shortdate": "Short Date (current year)", "set_clock": "Clock format", "set_12hour": "12-hour", "set_24hour": "24-hour", "set_submit": "Submit changes", "set_cancel": "Cancel", "set_showdate": "Show task date in list", "set_showtime": "Show time", "set_showdate_inline": "Show date inline", "set_exactduedate": "Always show due date as a date", "set_appearance": "Appearance", "set_appearance_system": "Same as system", "set_appearance_light": "Light", "set_appearance_dark": "Dark", "set_newtaskcounter_h": "New tasks counter", "set_newtaskcounter": "Check for new tasks", "set_newtaskcountericon": "Show counter in favicon", "set_extensions": "Extensions", "set_activate": "Activate", "set_deactivate": "Deactivate", "confirmDelete": "Are you sure you want to delete the task?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "save", "actionNoteCancel": "cancel", "error": "Some error occurred (click for details)", "denied": "Access denied", "listNotFound": "List not found", "noPublicLists": "No public tasks", "noTags": "No tags", "withoutTags": "No tags", "withAnyTag": "Any tag", "invalidpass": "Wrong password", "addList": "Create new list", "addListDefault": "Todo", "renameList": "Rename list", "deleteList": "This will delete current list with all tasks in it.\nAre you sure?", "clearCompleted": "This will delete all completed tasks in the list.\nAre you sure?", "settingsSaved": "Settings saved. Reloading..." } ================================================ FILE: src/includes/lang/es-mx.json ================================================ { "_header": { "ver": "v1.3.6", "date": "2010-08-03", "language": "Spanish (Mexico)", "original_name": "Español (de México)", "author": "eNK-Psy", "author_url": "http://www.buscachilpo.com.mx" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Nueva tarea", "htab_search": "Búsqueda", "btn_add": "Añadir", "btn_search": "Búsqueda", "advanced_add": "Avanzado", "searching": "En busca de", "tasks": "Tareas", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Fecha de creación", "taskdate_completed": "Fecha de la terminación", "edit_task": "Editar Tarea", "add_task": "Nueva tarea", "priority": "Prioridad", "task": "Tarea", "note": "Nota", "tags": "Etiquetas", "save": "Guardar", "cancel": "Cancelar", "password": "Contraseña", "btn_login": "Autentificarse", "a_login": "Autentificarse", "a_logout": "Cerrar sesión", "public_tasks": "funciones públicas", "tagcloud": "Tags", "tagfilter_cancel": "cancelar filtro", "sortByHand": "Ordenar a mano", "sortByPriority": "Ordenar por prioridad", "sortByDueDate": "Ordenar por fecha de vencimiento", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Vencimiento", "daysago": "Hace %d días", "indays": "en %d días", "months_short": [ "Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dec" ], "months_long": [ "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre" ], "days_min": [ "Dom", "Lun", "Mar", "Mie", "Jue", "Vie", "Sab" ], "days_long": [ "Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado" ], "today": "hoy", "yesterday": "ayer", "tomorrow": "mañana", "f_past": "Atrasado", "f_today": "Hoy y mañana", "f_soon": "Pronto", "action_edit": "Editar", "action_note": "Editar Nota", "action_delete": "Borrar", "action_priority": "Prioridad", "action_move": "Mover a", "notes": "Notas:", "notes_show": "Mostrar", "notes_hide": "Ocultar", "list_new": "Nueva lista", "list_rename": "Renombrar lista", "list_delete": "Borrar lista", "list_publish": "Publicar lista", "list_showcompleted": "Mostrar tareas completadas", "list_clearcompleted": "Borrar las tareas completadas", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Todas las etiquetas:", "alltags_show": "Mostrar todo", "alltags_hide": "Ocultar todo", "a_settings": "Configuración", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Configuración", "set_title": "Título", "set_title_descr": "(especifica si desea cambiar el título por defecto)", "set_language": "Idioma", "set_protection": "Contraseña de protección", "set_enabled": "Habilitado", "set_disabled": "Desabilitado", "set_newpass": "Nueva contraseña", "set_newpass_descr": "(deja en blanco si no quiere cambiar la contraseña actual)", "set_smartsyntax": "Sintaxis inteligente", "set_smartsyntax_descr": "(/prioridad/ tarea /etiqueta/)", "set_timezone": "Time zone", "set_autotag": "Autoetiquetado", "set_autotag_descr": "(agrega automáticamente la etiqueta de filtro de etiqueta actual a la tarea de nueva creación)", "set_sessions": "Mecanismo de manejo de sesión", "set_sessions_php": "PHP", "set_sessions_files": "Archivos", "set_firstdayofweek": "Primer día de la semana", "set_custom": "Custom", "set_date": "Formato de fecha", "set_date2": "Short Date format", "set_shortdate": "Ordenar formato de fecha", "set_clock": "Formato de reloj", "set_12hour": "12-horas", "set_24hour": "24-horas", "set_submit": "Enviar cambios", "set_cancel": "Cancelar", "set_showdate": "Mostrar fecha de tarea en lista", "confirmDelete": "¿Está seguro que desea eliminar la tarea?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "guardar", "actionNoteCancel": "cancelar", "error": "Algun error a ocurrido (haga clic para más detalles)", "denied": "Acceso denegado", "invalidpass": "Contraseña incorrecta", "addList": "Crear una nueva lista", "addListDefault": "Todo", "renameList": "Renombrar lista", "deleteList": "Esto eliminará la lista actual con todas las tareas en ella.\n¿Está seguro?", "clearCompleted": "Esto eliminará todas las tareas completadas en la lista.\n¿Está seguro?", "settingsSaved": "Configuración guardada. Abriendo..." } ================================================ FILE: src/includes/lang/es.json ================================================ { "_header": { "ver": "v1.4.0", "date": "2011-01-27", "language": "Spanish", "original_name": "Español", "author": "Antonio Garcia Marin", "author_email": "antoniogarciamarin@gmail.com" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Tarea nueva", "htab_search": "Buscar", "btn_add": "Añadir", "btn_search": "Buscar", "advanced_add": "Avanzado", "searching": "Buscando", "tasks": "Tareas", "taskdate_inline_created": "Creada el %s", "taskdate_inline_completed": "Completada el %s", "taskdate_inline_duedate": "Vencimiento el %s", "taskdate_created": "Fecha de creación", "taskdate_completed": "Fecha de término", "edit_task": "Editar tarea", "add_task": "Añadir tarea", "priority": "Prioridad", "task": "Tarea", "note": "Nota", "tags": "Etiquetas (separadas por comas)", "save": "Guardar", "cancel": "Cancelar", "password": "Contraseña", "btn_login": "Conectar", "a_login": "Conectar", "a_logout": "Desconectar", "public_tasks": "Tareas Públicas", "tagcloud": "Tags", "tagfilter_cancel": "cancelar filtrado", "sortByHand": "Ordenar a mano", "sortByPriority": "Ordenar por prioridad", "sortByDueDate": "Ordenar por fecha de vencimiento", "sortByDateCreated": "Ordenar por fecha creacion", "sortByDateModified": "Ordenar por fecha modificacion", "due": "Vencimiento", "daysago": "hace %d días", "indays": "en %d días", "months_short": [ "Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Set", "Oct", "Nov", "Dic" ], "months_long": [ "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Setiembre", "Octubre", "Noviembre", "Diciembre" ], "days_min": [ "Do", "Lu", "ma", "Mi", "Ju", "Vi", "Sa" ], "days_long": [ "Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado" ], "today": "hoy", "yesterday": "ayer", "tomorrow": "mañana", "f_past": "Atrasado", "f_today": "Hoy y mañana", "f_soon": "pronto", "action_edit": "Editar", "action_note": "Editar Nota", "action_delete": "Borrar", "action_priority": "Prioridad", "action_move": "Mover a", "notes": "Notas:", "notes_show": "Mostrar", "notes_hide": "Ocultar", "list_new": "Lista nueva", "list_rename": "Renombrar lista", "list_delete": "Borrar lista", "list_publish": "Publicar lista", "list_showcompleted": "Mostrar tareas completadas", "list_clearcompleted": "Borrar tareas completadas", "list_select": "Seleccionar lista", "list_export": "Exportar", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Todas las etiquetas:", "alltags_show": "Mostrar todas", "alltags_hide": "Ocultar todas", "a_settings": "Configuración", "rss_feed": "Fuente RSS", "feed_title": "%s", "feed_completed_tasks": "Tareas completadas", "feed_modified_tasks": "Tareas modificadas", "feed_new_tasks": "Nuevas tareas", "alltasks": "Todas las tareas", "set_header": "Configuración", "set_title": "Título", "set_title_descr": "(especifica si quieres cambiar el título por defecto)", "set_language": "Idioma", "set_protection": "Protección con contraseña", "set_enabled": "Activado", "set_disabled": "Desactivado", "set_newpass": "Contraseña nueva", "set_newpass_descr": "(deja en blanco si no has cambiado la contraseña actual)", "set_smartsyntax": "Sintaxis inteligente", "set_smartsyntax_descr": "(/prioridad/ tarea /etiquetas/)", "set_timezone": "Zona horaria", "set_autotag": "Auto etiquetado", "set_autotag_descr": "(añade una etiqueta automáticamente desde el filtro de etiquetas actual, a la última tarea creada)", "set_sessions": "Manejo de sesiones", "set_sessions_php": "PHP", "set_sessions_files": "Archivos", "set_firstdayofweek": "Primer día de la semana", "set_custom": "Personalizado", "set_date": "Formato de fecha", "set_date2": "Formato de fecha corto", "set_shortdate": "Formato de Fecha corta", "set_clock": "Formato de reloj", "set_12hour": "12-horas", "set_24hour": "24-horas", "set_submit": "Enviar cambios", "set_cancel": "Cancelar", "set_showdate": "Mostrar fecha de la tarea en la lista", "confirmDelete": "¿Estás seguro de borrar la tarea?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "guardar", "actionNoteCancel": "cancelar", "error": "Ocurrió un error (click para ver detalles)", "denied": "Acceso denegado", "invalidpass": "Contraseña incorrecta", "addList": "Crear lista", "addListDefault": "Todo", "renameList": "Renombrar lista", "deleteList": "Esto eliminará la lista actual, así como las tareas que contenga. \n¿Estás seguro?", "clearCompleted": "Esto eliminará todas las tareas completadas en la lista.\n?Estás seguro?", "settingsSaved": "Configuración guardada. Recargando..." } ================================================ FILE: src/includes/lang/et.json ================================================ { "_header": { "ver": "v1.8.1", "date": "2025-03-03", "language": "Estonian", "original_name": "Eesti", "authors": [ "Rivo Zängov" ] }, "My Tiny Todolist": "Minu väike tööde nimekiri", "powered_by": "Kasutatud tarkvara", "htab_newtask": "Uus ülesanne", "htab_search": "Otsi", "btn_add": "Lisa", "btn_search": "Otsi", "advanced_add": "Lisavalikud", "searching": "Otsitakse", "tasks": "Ülesanded", "taskdate_inline_created": "loodud %s", "taskdate_inline_edited": "muudetud %s", "taskdate_inline_completed": "lõpetatud %s", "taskdate_inline_duedate": "Tähtaeg %s", "taskdate_created": "Loodud", "taskdate_edited": "Viimati muudetud", "taskdate_completed": "Lõpetatud", "edit_task": "Muuda ülesannet", "add_task": "Uus ülesanne", "priority": "Tähtsus", "task": "Ülesanne", "note": "Märkus", "tags": "Sildid", "list": "Nimekiri", "no_note": "Märkmeid pole", "save": "Salvesta", "cancel": "Loobu", "close": "Sulge", "password": "Parool", "btn_login": "Logi sisse", "a_login": "Logi sisse", "a_logout": "Logi välja", "public_tasks": "Avalikud ülesanded", "tagcloud": "Sildid", "tagfilter_cancel": "tühista filter", "showTagsFromAllLists": "Näita silte kõigist nimekirjadest", "sortByHand": "Sorteeri käsitsi", "sortByTitle": "Sorteeri nime järgi", "sortByPriority": "Sorteeri tähtsuse järgi", "sortByDueDate": "Sorteeri tähtaja järgi", "sortByDateCreated": "Sorteeri loomise kuupäeva järgi", "sortByDateModified": "Sorteeri muutmise kuupäeva järgi", "due": "Tähtaeg", "daysago": "%d päeva tagasi", "indays": "%d päeva pärast", "months_short": [ "jaan", "veebr", "mär", "apr", "mai", "juun", "juul", "aug", "sept", "okt", "nov", "dets" ], "months_long": [ "jaanuar", "veebruar", "märts", "aprll", "mai", "juuni", "juuli", "august", "september", "oktoober", "november", "detsember" ], "months_calendar": [ "jaanuar", "veebruar", "märts", "aprill", "mai", "juuni", "juuli", "august", "september", "oktoober", "november", "detsember" ], "days_min": [ "P", "E", "T", "K", "N", "R", "L" ], "days_long": [ "Pühapäev", "Esmaspäev", "Teisipäev", "Kolmapäev", "Neljapäev", "Reede", "Laupäev" ], "today": "täna", "yesterday": "eile", "tomorrow": "homme", "f_past": "Tähtaja ületanud", "f_today": "Täna ja homme", "f_soon": "Varsti", "action_edit": "Muuda", "action_note": "Muuda märkust", "action_delete": "Kustuta", "action_priority": "Tähtsus", "action_move": "Liiguta", "action_ok": "OK", "action_cancel": "Loobu", "notes": "Märkmed:", "notes_show": "Näita", "notes_hide": "Peida", "list_new": "Uus nimekiri", "list_rename": "Nimeta nimekiri ümber", "list_delete": "Kustuta nimekiri", "list_showcompleted": "Näita lõpetatud ülesandeid", "list_clearcompleted": "Eemalda lõpetatud ülesanded", "list_select": "Vali nimekiri", "list_share": "Jaga", "list_publish": "Avalda nimekiri", "list_enable_feedkey": "Luba uudisvoogude võti", "list_show_feedkey": "Näita uudisvoogude võtit", "list_rssfeed": "RSS uudisvood", "list_export_to_csv": "Ekspordi CSV-na", "list_export_to_ical": "Ekspordi iCalendrisse", "list_hide": "Peida nimekiri", "alltags": "Kõik sildid:", "alltags_show": "Näita kõiki", "alltags_hide": "Peida kõik", "a_settings": "Seaded", "rss_feed": "RSS uudisvood", "feed_title": "%s", "feed_completed_tasks": "Lõpetatud ülesanded", "feed_modified_tasks": "Muudetud ülesanded", "feed_new_tasks": "Uued ülesanded", "feed_tasks": "Ülesanded", "feed_status_new": "Uus", "feed_status_updated": "Uuendatud", "feed_status_completed": "Lõpetatud", "alltasks": "Kõik ülesanded", "set_header": "Seaded", "set_title": "Pealkiri", "set_title_descr": "Määra, kas sa soovid muuta vaikimisi pealkirja.", "set_language": "Keel", "set_protection": "Parooliga kaitsmine", "set_enabled": "Sisse lülitatud", "set_disabled": "Välja lülitatud", "set_newpass": "Uus parool", "set_newpass_descr": "Jäta tühjaks, kui sa ei soovi praegust parooli muuta.", "set_smartsyntax": "Nutikas süntaks", "set_smartsyntax3_descr": "Näiteks: +1 Ülesande pealkiri #tag1 #tag2 @duedate", "set_timezone": "Ajavöönd", "set_autotag": "Automaatne sildistamine", "set_autotag_descr": "Lisab äsja loodud ülesande sildid automaatselt uuele ülesandele.", "set_markdown": "Markdown", "set_markdown_descr": "Lisab märkmetesse Markdown toe. Lülita see välja, kui soovid kasutada vana Markdowni", "set_firstdayofweek": "Nädala esimene päev", "set_custom": "Kohandatud", "set_date": "Kuupäeva vorming", "set_date2": "Lühikese kuupäeva vorming", "set_shortdate": "Lühike kuupäev (praegune aasta)", "set_clock": "Kellaaja vorming", "set_12hour": "12-tunnine", "set_24hour": "24-tunnine", "set_submit": "Salvesta muudatused", "set_cancel": "Loobu", "set_showdate": "Näita nimekirjas ülesande kuupäeva", "set_showtime": "Näita kellaaega", "set_showdate_inline": "Näita kuupeäva teksti sees", "set_exactduedate": "Näita kuupäevana alati tähtaega", "set_appearance": "Välimus", "set_appearance_system": "Sama, mis süsteemil", "set_appearance_light": "Hele", "set_appearance_dark": "Tume", "set_newtaskcounter_h": "Uute ülesannete loendur", "set_newtaskcounter": "Kontrolli uuesi ülesandeid", "set_extensions": "Lisaprogrammid", "set_activate": "Aktiveeri", "set_deactivate": "Deaktiveeri", "confirmDelete": "Oled sa kindel, et soovid selle ülesande kustutada?", "confirmLeave": "Siin on salvestamata infot. Kas sa tõesti soovid lahkuda?", "actionNoteSave": "salvesta", "actionNoteCancel": "loobu", "error": "Tekkis mingi tõrge (lisainfo saamiseks kliki)", "denied": "Ligipääs on keelatud", "listNotFound": "Nimekirja ei leitud", "noPublicLists": "Avalikke ülesandeid pole", "noTags": "Silte pole", "invalidpass": "Vale parool", "addList": "Loo uus nimekiri", "addListDefault": "Tööde nimekiri", "renameList": "Nimeta nimekiri ümber", "deleteList": "See kustutab praeguse nimekirja koos selles olevate ülesannetega.\\nOled sa kindel?", "clearCompleted": "See kustutab kõik lõpetatud ülesanded selles nimekirjas.\\nOled sa kindel?", "settingsSaved": "Seaded on salvestatud. Uuesti laadimine..." } ================================================ FILE: src/includes/lang/fa.json ================================================ { "_header": { "language": "Persian", "original_name": "پارسی", "author": "saeb khanzadeh", "author_url": "http://skhanzadeh.ir/fa/mytinytodo", "date": "2023-02-09", "ver": "v1.7", "rtl":1 }, "My Tiny Todolist": "فهرست انجام کوچک من", "powered_by": "نیرو گرفته از ", "htab_newtask": "کار تازه", "htab_search": "جستجو", "btn_add": "افزودن", "btn_search": "جستجو", "advanced_add": "پیشرفته", "searching": "کنکاش برای ", "tasks": "وظیفه ها", "taskdate_inline_created": "ساخته شده در %s", "taskdate_inline_edited": "ویرایش شده در %s", "taskdate_inline_completed": "پایان یافته در %s", "taskdate_inline_duedate": "سررسید در %s", "taskdate_created": "ساخته شد", "taskdate_edited": "ویرایش پیشین", "taskdate_completed": "پایان یافت", "edit_task": "ویرایش کار", "add_task": "کار تازه", "priority": "ارزش", "task": "کار", "note": "یادداشت", "tags": "برچسب", "save": "ذخیره", "cancel": "بازگردانده", "password": "گذرواژه", "btn_login": "وارد شدن", "a_login": "ورود", "a_logout": "بیرون رفتن", "public_tasks": "کار همگانی ", "tagcloud": "برچسب ها", "tagfilter_cancel": "بازگرداندن صافی", "showTagsFromAllLists": "نمایش برچسب ها از همه فهرست", "sortByHand": "آرایش دادن دستی", "sortByPriority": "آرایش ارزشی", "sortByDueDate": "آرایش زمان سررسید", "sortByDateCreated": "آرایش زمان ساخته شده", "sortByDateModified": "آرایش زمان ویرایش شده", "due": "سررسید", "daysago": " %d روز پیش", "indays": "پر %d روز", "months_short": [ "ژانویه", "فوریه", "مارس", "آپریل", "می", "ژوئن", "ژولای", "آگوست", "سپتامبر", "اکتبر", "نوامبر", "دسامبر" ], "months_long": [ "ژانویه", "فوریه", "مارس", "آپریل", "می", "ژوئن", "ژولای", "آگوست", "سپتامبر", "اکتبر", "نوامبر", "دسامبر" ], "months_calendar": [ "ژانویه", "فوریه", "مارس", "آپریل", "می", "ژوئن", "ژولای", "آگوست", "سپتامبر", "اکتبر", "نوامبر", "دسامبر" ], "days_min": [ "۱ش", "۲ش", "۳ش", "۴ش", "۵ش", "آد", "شن" ], "days_long": [ "یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنج شنبه", "آدینه", "شنبه" ], "today": "امروز", "yesterday": "دیروز", "tomorrow": "فردا", "f_past": "سررسیده", "f_today": "امروز و فردا", "f_soon": "بزودی", "action_edit": "ویرایش", "action_note": "ویرایش یادداشت", "action_delete": "پاک کردن", "action_priority": "ارزش", "action_move": "جابجا به", "action_ok": "باشه", "action_cancel": "روگرداندن", "notes": "یادداشت:", "notes_show": "نمایش", "notes_hide": "پنهان", "list_new": "فهرست تازه", "list_rename": "بازنامگذاری فهرست", "list_delete": "پاک کردن فهرست", "list_showcompleted": "نمایش کار های پایان یافته", "list_clearcompleted": "تمیز کردن از کارهای انجام شده", "list_select": "گزینش فهرست", "list_share": "همرسانی", "list_publish": "پخش فهرست", "list_enable_feedkey": "فعال سازی کلید خبرخوان", "list_show_feedkey": "نمایش کلید خبرخوان", "list_rssfeed": "خبرخوان RSS", "list_export_to_csv": "برون ریزی به CSV", "list_export_to_ical": "برون ریزی به iCalendar", "list_hide": "پنهان سازی فهرست", "alltags": "همه برچسب ها:", "alltags_show": "نمایش همه", "alltags_hide": "پنهان کردن همه", "a_settings": "گزینش ها", "rss_feed": "خبرخوان RSS", "feed_title": "%s", "feed_completed_tasks": "کار انجام شده", "feed_modified_tasks": "کار ویرایش شده", "feed_new_tasks": "کار تازه", "feed_tasks": "کارها", "feed_status_new": "تازه", "feed_status_updated": "بروزرسانی", "feed_status_completed": "انجام شده", "alltasks": "همه کار ها", "set_header": "گزینش ها", "set_title": "تیتر", "set_title_descr": "روشن کن اگر شما تیتر پیشفرض را می خواهید عوض کنید", "set_language": "زبان", "set_protection": "گذرواژه نگهبانی", "set_enabled": "فعال شده", "set_disabled": "غیرفعال شده", "set_newpass": "گذرواژه تازه", "set_newpass_descr": "خالی بگذار اگر شما نمی خواهید گذرواژه را عوض کنید", "set_smartsyntax": "سینتکس هوشمند", "set_smartsyntax2_descr": "برای نمونه: +1 تیتر کار #برچسب۱ #برچسب۲", "set_timezone": "نقطه زمانی", "set_autotag": "برچسب سازی خودکار", "set_autotag_descr": "به طور خودکار برچسب صافی برچسب کنونی را به کار تازه ایجاد شده افزون.", "set_markdown": "مارک داون", "set_markdown_descr": "افزودن پشتیبانی از مارک داون در یادداشت ها، غیرفعال کن اگر شما می خواهید از ویژگی مارک آپ گذشته بهره ببرید", "set_firstdayofweek": "روز نخست هفته", "set_custom": "سفارشی سازی", "set_date": "ساختار زمان", "set_date2": "ساختار زمان کوتاه", "set_shortdate": "زمان کوتاه (امسال)", "set_clock": "ساختار ساعت", "set_12hour": "۱۲ ساعت", "set_24hour": "۲۴ ساعت", "set_submit": "پذیرش ویرایش ها", "set_cancel": "روگرداندن", "set_showdate": "نمایش زمان کار در فهرست", "set_showtime": "نمایش زمان", "set_appearance": "نما", "set_appearance_system": "مانند سیستم", "set_appearance_light": "پوسته روزشن", "set_extensions": "افزونه ها", "set_activate": "بکار انداختن", "set_deactivate": "ازکارانداختن", "confirmDelete": "آیا شما از اینکه میخواهید این کار را پاک کنید مطمئن هستید؟", "confirmLeave": "داده های ذخیره نشده هست. آیا روی بیرون رفتن پافشاری دارید؟", "actionNoteSave": "ذخیره", "actionNoteCancel": "روگرداندن", "error": "خطایی رخ داده (برای نمایش جزییات کلیک کنید)", "denied": "دسترسی رد شد", "listNotFound": "فهرست یافت نشد", "noPublicLists": "هیچ کار همگانی ای نیست", "invalidpass": "گذرواژه نادرست", "addList": "ساخت فهرست تازه", "addListDefault": "انجام کار", "renameList": "بازنامگذاری فهرست", "deleteList": "فهرست کنونی با همه کار های درونش پاک خواهدد شد. مطمئنی؟", "clearCompleted": "همه کارهای انجام شده در این فهرست پاک خواهد شد. مطمئنی؟", "settingsSaved": "گزینش ها ذخیره شده. در حال بارگذاری دوباره ...." } ================================================ FILE: src/includes/lang/fr.json ================================================ { "_header": { "ver": "v1.8.1", "date": "2024-12-18", "language": "French", "original_name": "Français", "authors": [ "v1.2 - Mickael Fradin (http://blog.kewix.fr)", "v1.2.7 - Didier Corbière", "v1.3b3 - Philippe ALEXANDRE", "v1.3.2 - Olivier Gaillot (http://www.t1bis.com)", "v1.3.3 - Alexis Degrugillier", "v1.3.4 - Pierre Lemay", "v1.4.0 - liryk (http://liryk.lautre.net)", "v1.6.8 - @plabuse", "AlainR - https://www.artisan-du-web.ch/" ] }, "My Tiny Todolist": "My Tiny Todolist", "powered_by": "Powered by", "htab_newtask": "Nouvelle tâche", "htab_search": "Recherche", "btn_add": "Ajouter", "btn_search": "Rechercher", "advanced_add": "Avancé", "searching": "Recherche de", "tasks": "Tâches", "taskdate_inline_created": "créée le %s", "taskdate_inline_edited": "édité le %s", "taskdate_inline_completed": "achevée le %s", "taskdate_inline_duedate": "Échéance %s", "taskdate_created": "Créée", "taskdate_edited": "Dernière édition", "taskdate_completed": "Achevée", "edit_task": "Éditer la tâche", "add_task": "Nouvelle tâche", "priority": "Priorité", "task": "Tâche", "note": "Note", "tags": "Mots-clefs", "list": "Liste", "no_note": "Aucune note", "save": "Sauvegarder", "cancel": "Annuler", "close": "Ferme", "password": "Mot de passe", "btn_login": "Connexion", "a_login": "Connexion", "a_logout": "Déconnexion", "public_tasks": "Tâches publiques", "tagcloud": "Tags", "tagfilter_cancel": "Annuler le filtre", "showTagsFromAllLists": "Afficher les mots-clefs de toutes les listes", "sortByHand": "Trier manuellement", "sortByTitle": "Trier par titre", "sortByPriority": "Trier par priorité", "sortByDueDate": "Trier par date d’échéance", "sortByDateCreated": "Trier par date de création", "sortByDateModified": "Trier par date de modification", "due": "Échéance", "daysago": "il y a %d jours", "indays": "dans %d jours", "months_short": [ "Jan", "Fév", "Mar", "Avr", "Mai", "Juin", "Juil", "Août", "Sep", "Oct", "Nov", "Déc" ], "months_long": [ "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" ], "months_calendar": [ "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" ], "days_min": [ "Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam" ], "days_long": [ "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi" ], "today": "aujourd’hui", "yesterday": "hier", "tomorrow": "demain", "f_past": "En retard", "f_today": "Aujourd’hui et demain", "f_soon": "Bientôt", "action_edit": "Éditer", "action_note": "Éditer la note", "action_delete": "Supprimer", "action_priority": "Priorité", "action_move": "Envoyer vers", "action_ok": "OK", "action_cancel": "Annuler", "notes": "Notes:", "notes_show": "Afficher", "notes_hide": "Masquer", "list_new": "Nouvelle liste", "list_rename": "Renommer la liste", "list_delete": "Supprimer la liste", "list_showcompleted": "Montrer les tâches achevées", "list_clearcompleted": "Effacer les tâches achevées", "list_select": "Sélectionner la liste", "list_share": "Partager", "list_publish": "Publier la liste", "list_enable_feedkey": "Activer la clé de flux", "list_show_feedkey": "Afficher la clé de flux", "list_rssfeed": "Flux RSS", "list_hide": "Cacher la liste", "alltags": "Tous les mots-clefs:", "alltags_show": "Tout afficher", "alltags_hide": "Tout masquer", "a_settings": "Configuration", "rss_feed": "Flux RSS", "feed_title": "%s", "feed_completed_tasks": "Tâches achevées", "feed_modified_tasks": "Tâches modifiées", "feed_new_tasks": "Nouvelles tâches", "feed_tasks": "Tâches", "feed_status_new": "Nouveau", "feed_status_updated": "Mise à jour", "feed_status_completed": "Terminé", "alltasks": "Toutes les tâches", "set_header": "Configuration", "set_title": "Titre", "set_title_descr": "(Spécifiez si vous souhaitez changer le titre par défaut)", "set_language": "Langue", "set_protection": "Protection par mot de passe", "set_enabled": "Activé", "set_disabled": "Désactivé", "set_newpass": "Nouveau mot de passe", "set_newpass_descr": "(laissez blanc pour ne pas modifier le mot de passe actuel)", "set_smartsyntax": "Syntaxe rapide", "set_timezone": "Fuseaux horaires", "set_autotag": "Mots-clefs automatiques", "set_autotag_descr": "(ajoute automatiquement les mots-clefs aux nouvelles tâches parmis ceux que vous avez déjà définis)", "set_markdown": "Markdown", "set_markdown_descr": "Ajoute la prise en charge de Markdown dans les notes, désactiver si vous voulez utiliser un vieux balisage.", "set_firstdayofweek": "Premier jour de la semaine", "set_custom": "Personnalisé", "set_date": "Format de date", "set_date2": "Format de date court", "set_shortdate": "Date courte (année actuelle)", "set_clock": "Format de l’heure", "set_12hour": "12 heures", "set_24hour": "24 heures", "set_submit": "Sauvegarder la configuration", "set_cancel": "Annuler", "set_showdate": "Afficher la date dans la liste", "set_showtime": "Afficher l'heure", "set_showdate_inline": "AfficheShow date inline", "set_exactduedate": "Toujours afficher la date d'échéance comme une date", "set_appearance": "Apparence", "set_appearance_system": "Identique au système", "set_appearance_light": "Clair", "set_appearance_dark": "Foncé", "set_newtaskcounter_h": "Compteur nouvelles tâches", "set_newtaskcounter": "Vérifier les nouvelles tâches", "set_extensions": "Extensions", "set_activate": "Activer", "set_deactivate": "Désactiver", "confirmDelete": "Êtes-vous sûr de vouloir supprimer la tâche ?", "confirmLeave": "Il peut y avoir des données non enregistrées. Voulez-vous vraiment quitter ?", "actionNoteSave": "sauvegarder", "actionNoteCancel": "annuler", "error": "Il y a eu des erreurs (cliquez pour plus de détails)", "denied": "Accès refusé", "listNotFound": "Liste introuvable", "noPublicLists": "Aucune tâche publique", "noTags": "Aucun mot-clef", "invalidpass": "Mauvais mot de passe", "addList": "Créer une nouvelle liste", "addListDefault": "Todo", "renameList": "Renommer la liste", "deleteList": "Cela supprimera la liste actuelle avec toutes les tâches qu’elle contient.\nÊtes-vous sûr ?", "clearCompleted": "Cela supprimera toutes les tâches achevées de la liste.\nÊtes-vous sûr ?", "settingsSaved": "Réglages sauvegardés. Chargement..." } ================================================ FILE: src/includes/lang/he.json ================================================ { "_header": { "ver": "v1.3.6", "date": "2010-08-01", "language": "Hebrew", "original_name": "עברית", "author": "Ohad Raz", "author_url": "http://www.Bainternet.info", "rtl": 1 }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "משימה חדשה", "htab_search": "חיפוש", "btn_add": "הוספה", "btn_search": "לחפש", "advanced_add": "הוספה מתקדמת", "searching": "מחפש ", "tasks": "משימות", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "תאריך היצירה", "taskdate_completed": "תאריך סיום", "edit_task": "ערוך משימה", "add_task": "הוסף משימה", "priority": "עדיפות", "task": "משימה", "note": "פרטים", "tags": "ליבלים", "save": "שמור", "cancel": "בטל", "password": "סיסמה", "btn_login": "התחבר", "a_login": "התחבר", "a_logout": "התנתק", "public_tasks": "משימות פתוחות לציבור", "tagcloud": "Tags", "tagfilter_cancel": "הסר מסנן", "sortByHand": "סדר לפי רשימה", "sortByPriority": "סדר לפי עדיפות", "sortByDueDate": "סדר לפי תאריך סיום", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "יש לסיים עד", "daysago": "% d ימים לפני", "indays": "עודב% d ימים", "months_short": [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "July", "Aug", "Sep", "Oct", "Nov", "Dec" ], "months_long": [ "ינואר", "פברואר", "מרץ", "אפריל", "מאי", "יוני", "יולי", "אוגוסט", "ספטמבר", "אוקטובר", "נובמבר", "דצמבר" ], "days_min": [ "א", "ב", "ג", "ד", "ה", "ו", "ש" ], "days_long": [ "ראשון", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת" ], "today": "היום", "yesterday": "אתמול", "tomorrow": "מחר", "f_past": "מאוחר", "f_today": "היום ומחר", "f_soon": "בקרוב", "action_edit": "ערוך", "action_note": "ערוך פתק", "action_delete": "מחק", "action_priority": "עדיפות", "action_move": "הזז", "notes": "פתקים:", "notes_show": "הצג", "notes_hide": "הסתר", "list_new": "רשימה חדשה", "list_rename": "שנה שם", "list_delete": "מחק רשימה", "list_publish": "פרסם רשימה", "list_showcompleted": "הצג משימות שהסתימו", "list_clearcompleted": "הסר משימות שהסתימו", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "הכל:", "alltags_show": "הצג הכל", "alltags_hide": "הסתר", "a_settings": "הגדרות", "rss_feed": "פיד RSS", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "הגדרות", "set_title": "שם", "set_title_descr": "(ציין אם אתה רוצה לשנות את הכותרת כברירת מחדל)", "set_language": "שפה", "set_protection": "סגור בסיסמה", "set_enabled": "כן", "set_disabled": "לא", "set_newpass": "סיסמה חדשה", "set_newpass_descr": "(השאר ריק אם אתה לא לשנות את הסיסמה הנוכחית)", "set_smartsyntax": "תחביר מתקדם", "set_smartsyntax_descr": "(/ עדיפות / משימה / תגיות /)", "set_timezone": "Time zone", "set_autotag": "תיוג אוטומטי", "set_autotag_descr": "(הוספת מסנן התג אוטומטית של תוויות הנוכחי, המשימה האחרונה נוצר)", "set_sessions": "ניהול הפעלות", "set_sessions_php": "PHP", "set_sessions_files": "קבצים", "set_firstdayofweek": "יום ראשון של השבוע", "set_custom": "Custom", "set_date": "תאריך", "set_date2": "Short Date format", "set_shortdate": "תאריך מקוצר", "set_clock": "שעון", "set_12hour": "12-שעות", "set_24hour": "24-שעות", "set_submit": "שמור שינויים", "set_cancel": "בטל", "set_showdate": "הצג את התאריך של הפעילות ברשימה", "confirmDelete": "האם אתה בטוח למחוק את המשימה?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "אשר", "actionNoteCancel": "בטל", "error": "אירעה שגיאה (לחץ כדי להציג פרטים)", "denied": "הגישה נדחתה", "invalidpass": "סיסמה שגויה", "addList": "יצירת רשימה", "addListDefault": "Todo", "renameList": "שינוי שם הרשימה", "deleteList": "זה יבטל את הרשימה הנוכחית, ואת המשימות שהיא מכילה. \nהאם אתה בטוח?", "clearCompleted": "פעולה זו תמחק את כל המשימות שהושלמו ברשימה. \nהאם אתה בטוח?", "settingsSaved": "הגדרות שנשמרו. טוען ..." } ================================================ FILE: src/includes/lang/hu.json ================================================ { "_header": { "ver": "v1.3.2", "date": "2010-01-20", "language": "Hungarian", "original_name": "Magyar", "author": "Jozsef Kollar", "author_url": "http://www.bassline.hu" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Új Feladat", "htab_search": "Keresés", "btn_add": "Hozzáad", "btn_search": "Keresés", "advanced_add": "Részletes", "searching": "A következő keresése", "tasks": "Feladatok", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Létrehozás dátuma", "taskdate_completed": "Befejezés dátuma", "edit_task": "Feladat szerkesztése", "add_task": "Új Feladat", "priority": "Prioritás", "task": "Feladat", "note": "Megjegyzés", "tags": "Cimkék", "save": "Mentés", "cancel": "Mégse", "password": "Jelszó", "btn_login": "Belépés", "a_login": "Belépés", "a_logout": "Kilépés", "public_tasks": "Közös Feladatok", "tagcloud": "Tags", "tagfilter_cancel": "szűrő kikapcsolása", "sortByHand": "Rendezés kézzel", "sortByPriority": "Rendezés prioritás szerint", "sortByDueDate": "Rendezés dátum szerint", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Esedékes", "daysago": "%d nappal ezelött", "indays": "%d nap múlva", "months_short": [ "Jan", "Feb", "Már", "Ápr", "Máj", "Jún", "Júl", "Aug", "Szep", "Okt", "Nov", "Dec" ], "months_long": [ "Január", "Február", "Március", "Április", "Május", "Június", "Július", "Augusztus", "Szeptember", "Október", "November", "December" ], "days_min": [ "V", "H", "K", "Sz", "Cs", "P", "Sz" ], "days_long": [ "Vasárnap", "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat" ], "today": "Ma", "yesterday": "Tegnap", "tomorrow": "Holnap", "f_past": "Lejárt", "f_today": "Ma és holnap", "f_soon": "Hamarosan", "action_edit": "Szerkesztés", "action_note": "Megjegyzés szerkesztése", "action_delete": "Törlés", "action_priority": "Prioritás", "action_move": "Mozgatás ide", "notes": "Megjegyzés:", "notes_show": "Mutat", "notes_hide": "Elrejt", "list_new": "Új lista", "list_rename": "Lista átnevezése", "list_delete": "Lista törlése", "list_publish": "Lista közzététele", "list_showcompleted": "Show completed tasks", "list_clearcompleted": "Clear completed tasks", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Összes Cimke:", "alltags_show": "Összes mutatása", "alltags_hide": "Összes elrejtése", "a_settings": "Beállítások", "rss_feed": "RSS", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Beállítások", "set_title": "Cím", "set_title_descr": "(add meg ha megszeretnéd változtatni az alapértelmezett címet)", "set_language": "Nyelv", "set_protection": "Védelem jelszóval", "set_enabled": "Bekapcsolva", "set_disabled": "Kikapcsolva", "set_newpass": "Új jelszó", "set_newpass_descr": "(hagyd üresen ha nem szeretnéd megváltoztatni a jelenlegi jelszót)", "set_smartsyntax": "Gyors sorrend", "set_smartsyntax_descr": "(/prioritás/ feladatok / cimkék/)", "set_timezone": "Time zone", "set_autotag": "Autómatikus cimkézés", "set_autotag_descr": "(automatikusan az aktuális cimke hozzárendelése)", "set_sessions": "a feladat kezelés módja", "set_sessions_php": "PHP", "set_sessions_files": "Files", "set_firstdayofweek": "A hét első napja", "set_custom": "Custom", "set_date": "Dátum formátum", "set_date2": "Short Date format", "set_shortdate": "Rövid dátum formátum", "set_clock": "Óra formátum", "set_12hour": "12-óra", "set_24hour": "24-óra", "set_submit": "Változások mentése", "set_cancel": "Mégse", "set_showdate": "Show task date in list", "confirmDelete": "Biztosan törölni akarod a feladatot?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "Mentés", "actionNoteCancel": "Mégse", "error": "Hiba lépett fel (kattints a részletekért)", "denied": "Hozzáférés megtagadva", "invalidpass": "Hibás jelszó", "addList": "Új Lista", "addListDefault": "Todo", "renameList": "Lista átnevezése", "deleteList": "Az aktuális Lista törlése az összes Feladattal.\nBiztos benne?", "clearCompleted": "This will delete all completed tasks in the list.\nAre you sure?", "settingsSaved": "Beállítások mentve. Újratöltés..." } ================================================ FILE: src/includes/lang/it.json ================================================ { "_header": { "ver": "v1.3.2", "date": "2010-01-24", "language": "Italian", "original_name": "Italiano", "author": "Giuseppe Dessì" }, "My Tiny Todolist": "Tasks", "htab_newtask": "Nuovo Task", "htab_search": "Cerca", "btn_add": "Aggiungi", "btn_search": "Cerca", "advanced_add": "Avanzato", "searching": "Cercando", "tasks": "Tasks", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Data di creazione", "taskdate_completed": "Data di scadenza", "edit_task": "Modifica Task", "add_task": "Nuovo Task", "priority": "Priorità", "task": "Task", "note": "Nota", "tags": "Tags", "save": "Salva", "cancel": "Annulla", "password": "Password", "btn_login": "Login", "a_login": "Login", "a_logout": "Logout", "public_tasks": "Tasks Pubblici", "tagcloud": "Tags", "tagfilter_cancel": "annulla filtro", "sortByHand": "Ordina", "sortByPriority": "Ordina per priorità", "sortByDueDate": "Ordina per scadenza", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Scadenza", "daysago": "%d giorni fa", "indays": "entro %d giorni", "months_short": [ "Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic" ], "months_long": [ "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre" ], "days_min": [ "Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa" ], "days_long": [ "Domenica", "Lunedì", "martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato" ], "today": "oggi", "yesterday": "ieri", "tomorrow": "domani", "f_past": "Scaduto", "f_today": "Oggi e domani", "f_soon": "Prossimamente", "action_edit": "Modifica", "action_note": "Modifica Nota", "action_delete": "Elimina", "action_priority": "Priorità", "action_move": "sposta in", "notes": "Note:", "notes_show": "Mostra", "notes_hide": "Nascondi", "list_new": "Nuova lista", "list_rename": "Rinomina lista", "list_delete": "Elimina lista", "list_publish": "Pubblica lista", "list_showcompleted": "Show completed tasks", "list_clearcompleted": "Clear completed tasks", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Tutti i tags:", "alltags_show": "Visualizza tutti", "alltags_hide": "Nacondi tutti", "a_settings": "Impostazioni", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Impostazioni", "set_title": "Titolo", "set_title_descr": "(specifica se vuoi cambiare il titolo predefinito)", "set_language": "Linguaggio", "set_protection": "Protezione con password", "set_enabled": "Attivo", "set_disabled": "Disattivo", "set_newpass": "Nuova password", "set_newpass_descr": "(non compilare se non vuoi cambiare password)", "set_smartsyntax": "Smart syntax", "set_smartsyntax_descr": "(/priority/ task /tags/)", "set_timezone": "Time zone", "set_autotag": "Tagging automatico", "set_autotag_descr": "(aggiunge automaticamente i tag per i nuovi compiti)", "set_sessions": "Meccanismo di gestione sessione", "set_sessions_php": "PHP", "set_sessions_files": "Files", "set_firstdayofweek": "Primo giorno della settimana", "set_custom": "Custom", "set_date": "Formato data", "set_date2": "Short Date format", "set_shortdate": "Formato data abbreviata", "set_clock": "Formato orario", "set_12hour": "12-hour", "set_24hour": "24-hour", "set_submit": "applica le modifiche", "set_cancel": "Annulla", "set_showdate": "Show task date in list", "confirmDelete": "Sei sicuro di voler cancellare il task?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "salva", "actionNoteCancel": "annulla", "error": "Ci sono errori (clicca per dettagli)", "denied": "Accesso non consentito", "invalidpass": "Password errata", "addList": "Crea una nuova lista", "addListDefault": "Todo", "renameList": "Rinomina lista list", "deleteList": "Questo eliminerà la lista corrente e tutti i task inclusi.\nSei sicuro?", "clearCompleted": "This will delete all completed tasks in the list.\nAre you sure?", "settingsSaved": "Impostazioni salvate. Ricaricando..." } ================================================ FILE: src/includes/lang/ja.json ================================================ { "_header": { "ver": "v1.3.6", "date": "2010-12-17", "language": "Japanese", "original_name": "日本語", "author": "Calltella", "author_url": "http://calltella.com/" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "新規タスク", "htab_search": "検索", "btn_add": "追加", "btn_search": "検索", "advanced_add": "詳細追加", "searching": "検索中-", "tasks": "タスク", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "作成時間", "taskdate_completed": "完了時間", "edit_task": "タスク編集", "add_task": "新規タスク", "priority": "優先度", "task": "タスク", "note": "詳細", "tags": "タグ", "save": "保存", "cancel": "キャンセル", "password": "パスワード", "btn_login": "ログイン", "a_login": "ログイン", "a_logout": "ログアウト", "public_tasks": "公開タスク", "tagcloud": "Tags", "tagfilter_cancel": "キャンセル", "sortByHand": "手動で並び替え", "sortByPriority": "優先度で並び替え", "sortByDueDate": "日付で並び替え", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "期限", "daysago": "%d 日経過", "indays": "あと %d 日", "months_short": [ "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月" ], "months_long": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "days_min": [ "日", "月", "火", "水", "木", "金", "土" ], "days_long": [ "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日" ], "today": "今日", "yesterday": "昨日", "tomorrow": "明日", "f_past": "期限切れ", "f_today": "今日と明日", "f_soon": "もうすぐ", "action_edit": "編集", "action_note": "ノート編集", "action_delete": "削除", "action_priority": "優先度", "action_move": "移動先", "notes": "詳細:", "notes_show": "表示", "notes_hide": "非表示", "list_new": "新規リスト", "list_rename": "リスト名変更", "list_delete": "リスト削除", "list_publish": "公開リスト", "list_showcompleted": "完了済みタスクを表示", "list_clearcompleted": "完了済みタスクをクリア", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "全てのタグ:", "alltags_show": "全表示", "alltags_hide": "全非表示", "a_settings": "設定", "rss_feed": "RSSフィード", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "設定", "set_title": "タイトル", "set_title_descr": "(指定の無い場合はデフォルトのタイトルを使用します。)", "set_language": "言語", "set_protection": "パスワード保護", "set_enabled": "有効", "set_disabled": "無効", "set_newpass": "新規パスワード", "set_newpass_descr": "(空白の場合はパスワード変更されません。)", "set_smartsyntax": "Smart syntax", "set_smartsyntax_descr": "(/priority/ task /tags/)", "set_timezone": "Time zone", "set_autotag": "自動タグ設定", "set_autotag_descr": "(タスクフィルターしている場合は自動的にタグを挿入します。)", "set_sessions": "セッション処理", "set_sessions_php": "PHP", "set_sessions_files": "Files", "set_firstdayofweek": "週の始まり", "set_custom": "Custom", "set_date": "日付フォーマット", "set_date2": "Short Date format", "set_shortdate": "短縮日付フォーマット", "set_clock": "時刻フォーマット", "set_12hour": "12時間表示", "set_24hour": "24時間表示", "set_submit": "設定変更", "set_cancel": "キャンセル", "set_showdate": "タスクに日付を表示", "confirmDelete": "タスクを削除してもよろしいですか?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "保存", "actionNoteCancel": "キャンセル", "error": "エラーが発生しました。 (クリックで詳細)", "denied": "アクセスが拒否されました。", "invalidpass": "パスワードが違います。", "addList": "新規リスト作成", "addListDefault": "Todo", "renameList": "リスト名変更", "deleteList": "全てのタスクと現在のリストを削除します。\nよろしいですか?", "clearCompleted": "完了した全てのリストを削除します。\nよろしいですか?", "settingsSaved": "設定保存中..." } ================================================ FILE: src/includes/lang/lt.json ================================================ { "_header": { "ver": "v1.3.5", "date": "2010-06-05", "language": "Lithuanian", "original_name": "Lietuvių", "author": "Linas Pašviestis", "author_email": "linas.pasviestis@gmail.com" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Nauja užduotis", "htab_search": "Ieškoti", "btn_add": "Pridėti", "btn_search": "Ieškoti", "advanced_add": "Išplėstinis", "searching": "Ieškoma", "tasks": "Užduotys", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Sukurtas", "taskdate_completed": "Pabaigtas", "edit_task": "Užduoties redagavimas", "add_task": "Nauja užduotis", "priority": "Prioritetas:", "task": "Pavadinimas", "note": "Aprašymas", "tags": "Žymenys", "save": "Išsaugoti", "cancel": "Atšaukti", "password": "Slaptažodis", "btn_login": "Prisijungti", "a_login": "Prisijungti", "a_logout": "Atsijungti", "public_tasks": "Viešos užduotys", "tagcloud": "Tags", "tagfilter_cancel": "filtro atšaukimas", "sortByHand": "Rankinis rikiavimas", "sortByPriority": "Rikiavimas pagal prioritetą", "sortByDueDate": "Rikiavimas pagal datą", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Iki:", "daysago": "vėluoja %d d.", "indays": "liko %d d.", "months_short": [ "Sau", "Vas", "Kov", "Bal", "Geg", "Bir", "Lie", "Rugp", "Rugs", "Spa", "Lap", "Gruo" ], "months_long": [ "Sausis", "Vasaris", "Kovas", "Balandis", "Gegužė", "Birželis", "Liepa", "Rugpjūtis", "Rugsėjis", "Spalis", "Lapkritis", "Gruodis" ], "days_min": [ "Se", "Pi", "An", "Tr", "Ke", "Pe", "Še" ], "days_long": [ "Sekmadienis", "Pirmadienis", "Antradienis", "Trečiadienis", "Ketvirtadienis", "Penktadienis", "Šeštadienis" ], "today": "šiandien", "yesterday": "vakar", "tomorrow": "rytoj", "f_past": "Vėluojančios", "f_today": "Šiandien ir rytoj", "f_soon": "Greitai", "action_edit": "Redaguoti", "action_note": "Aprašymo redagavimas", "action_delete": "Ištrinti", "action_priority": "Prioritetas", "action_move": "Perkelti į", "notes": "Aprašymai:", "notes_show": "Rodyti", "notes_hide": "Paslėpti", "list_new": "Naujas sąrašas", "list_rename": "Pervadinti sąrašą", "list_delete": "Ištrinti sąrašą", "list_publish": "Paviešinti sąrašą", "list_showcompleted": "Rodyti užbaigtas užduotis", "list_clearcompleted": "Išvalyti užbaigtas užduotis", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Visi žymenys:", "alltags_show": "Rodyti visus", "alltags_hide": "Paslėpti visus", "a_settings": "Nustatymai", "rss_feed": "RSS Pateikimas", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Nustatymai", "set_title": "Pavadinimas", "set_title_descr": "(bus pakeistas standartinis pavadinimas)", "set_language": "Kalba", "set_protection": "Slaptažodžio apsauga", "set_enabled": "Įjungtas", "set_disabled": "Išjungtas", "set_newpass": "Naujas slaptažodis", "set_newpass_descr": "(palikite tuščią, jeigu neketinate keisti slaptažodžio)", "set_smartsyntax": "Protinga sintaksė", "set_smartsyntax_descr": "(/priority/ užduotis /tags/)", "set_timezone": "Time zone", "set_autotag": "Automatinis žymėjimas", "set_autotag_descr": "(automatiškas žymenų generavimas naujai sukurtoms užduotims)", "set_sessions": "Sesijos valdymo metodas", "set_sessions_php": "PHP", "set_sessions_files": "Failai", "set_firstdayofweek": "Pirma savaitės diena", "set_custom": "Custom", "set_date": "Datos formatas", "set_date2": "Short Date format", "set_shortdate": "Trumpos datos formatas", "set_clock": "Laikrodžio formatas", "set_12hour": "12 valandų", "set_24hour": "24 valandos", "set_submit": "Išsaugoti pakeitimus", "set_cancel": "Atšaukti", "set_showdate": "Rodyti datą šalia užduoties", "confirmDelete": "Ar tikrai norite ištrinti užduotį?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "išsaugoti", "actionNoteCancel": "atšaukti", "error": "Atsirado keletas klaidų (spauskite čia norėdami sužinoti detaliau)", "denied": "Neturite reikiamo leidimo", "invalidpass": "Neteisingas slaptažodis", "addList": "Sukurti naują sąrašą", "addListDefault": "Todo", "renameList": "Pakeisti sąrašo vardą", "deleteList": "Ketinama ištrinti sąrašą ir visas jam priklausančias užduotis.\nAr esate tikras?", "clearCompleted": "Ketinama ištrinti visas šiame sąraše įvygdytas užduotis.\nAr esate tikras?", "settingsSaved": "Vyksta nustatymų išsaugojimas. Palaukite..." } ================================================ FILE: src/includes/lang/mk.json ================================================ { "_header": { "ver": "v1.3.3", "date": "2010-02-11", "language": "Macedonian", "original_name": "Македонски", "author": "nGen Solutions", "author_url": "http://ngen.mk" }, "My Tiny Todolist": "Листа на Задачи", "htab_newtask": "Нова задача", "htab_search": "Пребарај", "btn_add": "Додади", "btn_search": "Барај", "advanced_add": "Напредно", "searching": "Пребарувам за", "tasks": "Задачи", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Креирана", "taskdate_completed": "Завршена", "edit_task": "Измени задача", "add_task": "Нова Задача", "priority": "Приоритет", "task": "Задача", "note": "Забелешка", "tags": "Ознаки", "save": "Сочувај", "cancel": "Откажи", "password": "Лозинка", "btn_login": "Најави се", "a_login": "Најава", "a_logout": "Одјави се", "public_tasks": "Јавни задачи", "tagcloud": "Tags", "tagfilter_cancel": "откажи филтер", "sortByHand": "Подреди рачно", "sortByPriority": "Подреди по приоритет", "sortByDueDate": "Подреди по рок", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Рок", "daysago": "пред %d денови", "indays": "за %d денови", "months_short": [ "Јан", "Фев", "Мар", "Апр", "Мај", "Јун", "Јул", "Авг", "Сеп", "Окт", "Нов", "Дек" ], "months_long": [ "Јануари", "Февруари", "Март", "Април", "Мај", "Јуни", "Јули", "Август", "Септември", "Октомври", "Ноември", "Декември" ], "days_min": [ "Не", "По", "Вт", "Ср", "Че", "Пе", "Са" ], "days_long": [ "Недела", "Понеделник", "Вторник", "Среда", "Четврток", "Петок", "Сабота" ], "today": "денес", "yesterday": "вчера", "tomorrow": "утре", "f_past": "Задоцнети", "f_today": "денес и утре", "f_soon": "наскоро", "action_edit": "Измени", "action_note": "Измени забелешка", "action_delete": "Избриши", "action_priority": "Приоритет", "action_move": "Премести во", "notes": "Забелешки:", "notes_show": "Прикажи", "notes_hide": "Сокриј", "list_new": "Нова листа", "list_rename": "Преименувај листа", "list_delete": "Избриши листа", "list_publish": "Објави листа", "list_showcompleted": "Покажи завршени задачи", "list_clearcompleted": "Clear completed tasks", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Сите ознаки:", "alltags_show": "Прикажи ги сите", "alltags_hide": "Сокриј ги сите", "a_settings": "Подесувања", "rss_feed": "RSS достава", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Подесувања", "set_title": "Наслов", "set_title_descr": "(промени го името на програмата)", "set_language": "Јазик", "set_protection": "Заштита со лозинка", "set_enabled": "Вклучи", "set_disabled": "Исклучи", "set_newpass": "Нова лозинка", "set_newpass_descr": "(празно нема да ја смени лозинката)", "set_smartsyntax": "Паметен приказ", "set_smartsyntax_descr": "(/приоритет/ задача /ознаки/)", "set_timezone": "Time zone", "set_autotag": "Автоматски ознаки", "set_autotag_descr": "(автоматски ја додава филтрираната ознака на новокреираната задача)", "set_sessions": "Механизам за справување со сесијата", "set_sessions_php": "PHP", "set_sessions_files": "Со Фајлови", "set_firstdayofweek": "Прв ден од неделата", "set_custom": "Custom", "set_date": "Приказ на дата", "set_date2": "Short Date format", "set_shortdate": "Краток формат", "set_clock": "Приказ на време", "set_12hour": "12-часа", "set_24hour": "24-часа", "set_submit": "Зачувај ги промените", "set_cancel": "Откажи", "set_showdate": "Show task date in list", "confirmDelete": "Дали си сигурен?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "зачувај", "actionNoteCancel": "откажи", "error": "Се појави грешка... (подетално)", "denied": "Забранет пристап", "invalidpass": "Неточна лозинка", "addList": "Направи нова листа", "addListDefault": "Todo", "renameList": "Преименувај листа", "deleteList": "Со ова ке ја избишете листата и сите задачи во неа.\nПродолжи?", "clearCompleted": "This will delete all completed tasks in the list.\nAre you sure?", "settingsSaved": "Промените зачувани. Вчитувам..." } ================================================ FILE: src/includes/lang/nl.json ================================================ { "_header": { "ver": "v1.8.2", "date": "2025-05-15", "language": "Dutch", "original_name": "Nederlands", "authors": [ "J.C.Barnhoorn" ] }, "My Tiny Todolist": "Mijn aantekeningen", "powered_by": "Bijgehouden door", "htab_newtask": "Nieuwe aantekening", "htab_search": "Zoeken", "btn_add": "Toevoegen", "btn_search": "Zoeken", "advanced_add": "Gevorderd", "searching": "Zoeken naar", "tasks": "Aantekening", "taskdate_inline_created": "gemaakt op %s", "taskdate_inline_edited": "bewerkt op %s", "taskdate_inline_completed": "klaar op %s", "taskdate_inline_duedate": "afgerond op %s", "taskdate_created": "Gemaakt", "taskdate_edited": "Laatst gewijzigd", "taskdate_completed": "Klaar", "edit_task": "Bewerk aantekening", "add_task": "Nieuwe aantekening", "priority": "Prioriteit", "task": "Aantekening", "note": "Notitie", "tags": "Labels", "save": "Opslaan", "cancel": "Annuleren", "password": "Wachtwoord", "btn_login": "Aanmelden", "a_login": "Aanmelden", "a_logout": "Afmelden", "public_tasks": "Openbaar", "tagcloud": "Labels", "tagfilter_cancel": "annuleer filter", "showTagsFromAllLists": "Toon labels van alle lijsten", "sortByHand": "Sorteer handmatig", "sortByPriority": "Sorteer op prioriteit", "sortByDueDate": "Sorteer op opleveringsdatum", "sortByDateCreated": "Sorteer op aanmaakdatum", "sortByDateModified": "Sorteer op wijzigingsdatum", "due": "Afgerond", "daysago": "%d dagen geleden", "indays": "binnen %d dagen", "months_short": [ "jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec" ], "months_long": [ "januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december" ], "months_calendar": [ "januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december" ], "days_min": [ "zo", "ma", "di", "wo", "do", "vr", "za" ], "days_long": [ "zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag" ], "today": "vandaag", "yesterday": "gisteren", "tomorrow": "morgen", "f_past": "Verleden", "f_today": "Vandaag en morgen", "f_soon": "Binnenkort", "action_edit": "Bewerk", "action_note": "Bewerk aantekening", "action_delete": "Verwijder", "action_priority": "Prioriteit", "action_move": "Veplaats naar", "action_ok": "OK", "action_cancel": "Annuleren", "notes": "Aantekeningen:", "notes_show": "Tonen", "notes_hide": "Verbergen", "list_new": "Nieuwe lijst", "list_rename": "Hernoem lijst", "list_delete": "Verwijder lijst", "list_showcompleted": "Toon afgeronde aantekeningen", "list_clearcompleted": "Verwijder afgeronde aantekeningen", "list_select": "Selecteer lijst", "list_share": "Delen", "list_publish": "Publiceer lijst", "list_enable_feedkey": "Inschakelen feed key", "list_show_feedkey": "Toon feed key", "list_rssfeed": "RSS Feed", "list_export_to_csv": "Exporteer naar CSV", "list_export_to_ical": "Exporteer naar iCalendar", "list_hide": "Verberg lijst", "alltags": "Alle labels:", "alltags_show": "Toon alles", "alltags_hide": "Verberg alles", "a_settings": "Instellingen", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Afgeronde aantekeningen", "feed_modified_tasks": "Gewijzigde aantekeningen", "feed_new_tasks": "Nieuwe aantekeningen", "feed_tasks": "Aantekeningen", "feed_status_new": "Nieuw", "feed_status_updated": "Aangepast", "feed_status_completed": "Klaar", "alltasks": "Alle aantekeningen", "set_header": "Instellen", "set_title": "Titel", "set_title_descr": "Invullen als je de standaard titel wil wijzigen.", "set_language": "Taal", "set_protection": "Wachtwoord", "set_enabled": "Ingeschakeld", "set_disabled": "Uitgeschakeld", "set_newpass": "Nieuw wachtwoord", "set_newpass_descr": "Leeglaten als het oude wachtwoord moet blijven.", "set_smartsyntax": "Slimme syntax", "set_smartsyntax3_descr": "Voorbeeld: +1 Aantekening titel #label1 #label2 @einddatum ", "set_timezone": "Tijdzone", "set_autotag": "Autolabels", "set_autotag_descr": "Automatisch labels toevoegen bij nieuwe aantekening.", "set_markdown": "Markdown", "set_markdown_descr": "Voeg Markdown in aantekeningen toe, uitschakelen en je gebruikt het oude markup.", "set_firstdayofweek": "Eerste dag van de week", "set_custom": "Aangepast", "set_date": "Datum indeling", "set_date2": "Korte datum indeling", "set_shortdate": "Korte datum (huidig jaar)", "set_clock": "Klok opmaak", "set_12hour": "12-uur", "set_24hour": "24-uur", "set_submit": "Wijzigingen toepassen", "set_cancel": "Annuleren", "set_showdate": "Laat datum aantekening zien in de lijst", "set_showtime": "Laat tijd zien", "set_showdate_inline": "Toon datum inline", "set_exactduedate": "Altijd einddatum tonen als datum", "set_appearance": "Opmaak", "set_appearance_system": "Gelijk aan systeem", "set_appearance_light": "Licht thema", "set_appearance_dark": "Donker", "set_newtaskcounter_h": "Nieuwe aantekeningen teller", "set_newtaskcounter": "Controleer op nieuwe aantekeningen", "set_newtaskcountericon": "Toon aantal in favicon", "set_extensions": "Uitbreidingen", "set_activate": "Activeren", "set_deactivate": "Deactiveren", "confirmDelete": "Wil je deze aantekening verwijderen?", "confirmLeave": "E zijn gegevens niet opgeslagen. Wil je echt afsluiten?", "actionNoteSave": "opslaan", "actionNoteCancel": "annuleren", "error": "Er heeft zich een fout voorgedaan (klik voor details)", "denied": "Toegang geweigerd", "listNotFound": "Lijst niet gevonden", "noPublicLists": "Niets openbaar", "noTags": "Geen labels", "withoutTags": "Geen labels", "withAnyTag": "Met labels", "invalidpass": "Verkeerd wachtwoord", "addList": "Maak een nieuwe lijst", "addListDefault": "Aantekening", "renameList": "Hernoem lijst", "deleteList": "Dit zorgt ervoor dat huidige lijst wordt verwijderd.\nWeet je het zeker?", "clearCompleted": "Dit verwijderd alle afgeronde aantekeningen in de huidige lijst.\nWeet je het zeker?", "settingsSaved": "Instellingen opgeslagen. Herladen..." } ================================================ FILE: src/includes/lang/no.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-10-30", "language": "Norwegian", "original_name": "Norsk", "author": "Simen Aas Henriksen", "author_url": "http://www.sweb.no" }, "My Tiny Todolist": "Min lille ToDo liste", "htab_newtask": "Ny oppgave", "htab_search": "Søk", "btn_add": "Legg til", "btn_search": "Søk", "advanced_add": "Avansert", "searching": "Søker etter", "tasks": "Oppgaver", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Opprettelsesdato", "taskdate_completed": "Dato ferdig", "edit_task": "Rediger oppgave", "add_task": "Ny oppgave", "priority": "Prioritet", "task": "Oppgave", "note": "Notat", "tags": "Tags", "save": "Lagre", "cancel": "Avbryt", "password": "Passord", "btn_login": "Logg inn", "a_login": "Logg inn", "a_logout": "Logg ut", "public_tasks": "Offentlige oppgaver", "tagcloud": "Tags", "tagfilter_cancel": "avbryt filter", "sortByHand": "Sorter manuelt", "sortByPriority": "Sorter etter prioritering", "sortByDueDate": "Sorter etter dato", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "forventes ferdig", "daysago": "%d dager siden", "indays": "om %d dager", "months_short": [ "Jan", "Feb", "Mar", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Des" ], "months_long": [ "Januar", "Februar", "Mars", "April", "Mai", "Juni", "July", "August", "September", "Oktober", "November", "Desember" ], "days_min": [ "Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør" ], "days_long": [ "Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag" ], "today": "idag", "yesterday": "igår", "tomorrow": "imorgen", "f_past": "Forfalt", "f_today": "Idag og imorgen", "f_soon": "Snart", "action_edit": "Rediger", "action_note": "Rediger notat", "action_delete": "Slett", "action_priority": "Prioritet", "action_move": "Flytt til", "notes": "Notater:", "notes_show": "Vis", "notes_hide": "Gjem", "list_new": "Ny liste", "list_rename": "Bytt navn på liste", "list_delete": "Slett liste", "list_publish": "Offentliggjør list", "list_showcompleted": "Vis ferdigstilte oppgaver", "list_clearcompleted": "Slett ferdigstilte oppgaver", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Alle tags:", "alltags_show": "Vise alle", "alltags_hide": "Gjem alle", "a_settings": "Innstillinger", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Innstillinger", "set_title": "Tittel", "set_title_descr": "(endre om du vil redigere originaltittelen)", "set_language": "Språk", "set_protection": "Passordsbeskyttelse", "set_enabled": "Aktivert", "set_disabled": "Deaktivert", "set_newpass": "Nytt passord", "set_newpass_descr": "(la stå om du ikke vil endre passord)", "set_smartsyntax": "Smart syntax", "set_smartsyntax_descr": "(/prioritet/oppgaver/tags/)", "set_timezone": "Time zone", "set_autotag": "Autotagging", "set_autotag_descr": "(Legger automatisk til tags fra nåværende tag filter på nyopprettede oppgaver)", "set_sessions": "Session handling mechanism", "set_sessions_php": "PHP", "set_sessions_files": "Filer", "set_firstdayofweek": "Første dagen i uken", "set_custom": "Custom", "set_date": "Dato format", "set_date2": "Short Date format", "set_shortdate": "'Kort' Dato format", "set_clock": "Klokkeformat", "set_12hour": "12-timer", "set_24hour": "24-timer", "set_submit": "Godta endringer", "set_cancel": "Avbryt", "set_showdate": "Vis oppgavedato i listen", "confirmDelete": "Er du sikker på at du vil slette denne oppgaven?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "lagre", "actionNoteCancel": "avbryt", "error": "En feil har oppstått (klikk for detaljer)", "denied": "Tilgang nektet!", "invalidpass": "Feil passord", "addList": "La ny liste", "addListDefault": "Todo", "renameList": "Bytt navn på liste", "deleteList": "Du sletter nå hele listen, og da innholdet i listen.\nEr du sikker på at du vil gjøre dette?", "clearCompleted": "Du sletter nå alle oppgaver som er markert som ferdig, fra listen.\nEr du sikker på at du vil gjøre dette?", "settingsSaved": "Innstillinger er lagret. Oppdaterer..." } ================================================ FILE: src/includes/lang/pl.json ================================================ { "_header": { "ver": "v1.7.3", "date": "2022-12-06", "language": "Polish", "original_name": "Polski", "author": "Marcin R Martin", "author_url": "https://polski.eu.org" }, "My Tiny Todolist": "Moja lista zadań", "powered_by": "Zasilane przez", "htab_newtask": "Nowe zadanie", "htab_search": "Szukaj", "btn_add": "Dodaj", "btn_search": "Szukaj", "advanced_add": "Zaawansowane", "searching": "Szukaj", "tasks": "Zadania", "taskdate_inline_created": "Utworzone %s", "taskdate_inline_edited": "Edytowane %s", "taskdate_inline_completed": "Zakończone %s", "taskdate_inline_duedate": "Termin %s", "taskdate_created": "Data utworzenia", "taskdate_edited": "Ostatnio edytowane", "taskdate_completed": "Data ukończenia", "edit_task": "Edytuj zadanie", "add_task": "Nowe zadanie", "priority": "Priorytet", "task": "Zadanie", "note": "Notatka", "tags": "Tagi", "save": "Zapisz", "cancel": "Cofnij", "password": "Hasło", "btn_login": "Zaloguj się", "a_login": "Logowanie", "a_logout": "Wyloguj się", "public_tasks": "Publiczne", "tagcloud": "Tagi", "tagfilter_cancel": "Usuń filtr", "showTagsFromAllLists": "Pokaż tagi z wszystkich list", "sortByHand": "Sortuj ręcznie", "sortByTitle": "Sort wg tytułu", "sortByPriority": "Sortuj wg priorytetu", "sortByDueDate": "Sortuj wg terminu", "sortByDateCreated": "Sortuj wg daty utworzenia", "sortByDateModified": "Sortuj wg daty modyfikacji", "due": "do", "daysago": "%d dni temu", "indays": "w ciągu %d dni", "months_short": [ "Sty", "Lut", "Mar", "Kwi", "Maj", "Cze", "Lip", "Sie", "Wrz", "Paź", "Lis", "Gru" ], "months_long": [ "Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpien", "Wrzesien", "Październik", "Listopad", "Grudzień" ], "days_min": [ "Ni", "Po", "Wt", "Śr", "Cz", "Pt", "So" ], "days_long": [ "Niedziela", "Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota" ], "today": "dziś", "yesterday": "wczoraj", "tomorrow": "jutro", "f_past": "opóźnione", "f_today": "Dziś i jutro", "f_soon": "Wkrótce", "action_edit": "Edytuj", "action_note": "Edytuj notatkę", "action_delete": "Usuń", "action_priority": "Priorytet", "action_move": "Przenieś do", "action_ok": "OK", "action_cancel": "Anuluj", "notes": "Notatki:", "notes_show": "Pokaż", "notes_hide": "Ukryj", "list_new": "Nowa lista", "list_rename": "Zmień nazwę listy", "list_delete": "Usuń listę", "list_showcompleted": "Pokaż ukończone zadania", "list_clearcompleted": "Usuń ukończone zadania", "list_select": "Wybierz listę", "list_share": "Udostępnij", "list_publish": "Opublikuj listę", "list_enable_feedkey": "Włącz klucz kanału", "list_show_feedkey": "Pokaż klucz kanału", "list_rssfeed": "Kanał RSS", "list_export_to_csv": "Eksportuj do CSV", "list_export_to_ical": "Eksportuj do iCalendar", "list_hide": "Ukryj listę", "alltags": "Wszystkie tagi:", "alltags_show": "Pokaż wszystkie", "alltags_hide": "Ukryj wszystkie", "a_settings": "Ustawienia", "rss_feed": "Subskrybcja RSS", "feed_title": "%s", "feed_completed_tasks": "Ukończone zadania", "feed_modified_tasks": "Zmodyfikowane zadania", "feed_new_tasks": "Nowe zadania", "feed_tasks": "Zadania", "feed_status_new": "Nowe", "feed_status_updated": "Zaktualizowane", "feed_status_completed": "Zakończone", "alltasks": "Wszystkie zadania", "set_header": "Ustawienia", "set_title": "Tytuł", "set_title_descr": "(możesz zmienić domyślny tytuł na własny)", "set_language": "Język", "set_protection": "Ochrona hasłem", "set_enabled": "Włączona", "set_disabled": "Wyłączona", "set_newpass": "Nowe hasło", "set_newpass_descr": "(zostaw puste jeśli nie chcesz zmieniać hasła)", "set_smartsyntax": "Inteligentna składnia", "set_smartsyntax2_descr": "Przykład: +1 Tytuł zadania #tag1 #tag2", "set_timezone": "Strefa czasowa", "set_autotag": "Automatyczne tagowanie", "set_autotag_descr": "(Automatycznie dodaj bieżący tag z filtra do nowego zadania)", "set_markdown": "Markdown", "set_markdown_descr": "Dodaje obsługę Markdown w notatkach, wyłącz to, jeśli chcesz używać starego znacznika.", "set_firstdayofweek": "Pierwszy dzień tygodnia", "set_custom": "Własne", "set_date": "Format daty", "set_date2": "Skrócony format daty", "set_shortdate": "Skrócony format (bieżący rok)", "set_clock": "Format czasu", "set_12hour": "12-godzinny", "set_24hour": "24-godzinny", "set_submit": "Zapisz zamiany", "set_cancel": "Cofnij", "set_showdate": "Pokazuj datę zadania na liście", "set_showtime": "Pokaż czas", "set_appearance": "Wygląd", "set_appearance_system": "Taki jak system", "set_appearance_light": "Lekki motyw", "set_extensions": "Rozszerzenia", "set_activate": "Aktywuj", "set_deactivate": "Wyłącz", "confirmDelete": "Czy na pewno chcesz usunąć zadanie?", "confirmLeave": "Mogą istnieć niezapisane dane. Czy naprawdę chcesz wyjść?", "actionNoteSave": "Zapisz", "actionNoteCancel": "Cofnij", "error": "Wystąpiły błędy (kliknij po szczegóły)", "denied": "Dostęp zabroniony", "listNotFound": "Nie znaleziono listy", "noPublicLists": "Brak publicznych zadań", "invalidpass": "Nieprawidłowe hasło", "addList": "Dodaj nową listę", "addListDefault": "Nowa lista", "renameList": "Zmień nazwę listy", "deleteList": "Usuwasz listę oraz wszyskie zawarte w niej zadania.\nCzy na pewno?", "clearCompleted": "Usuwasz wszystkie zadania na tej liście.\nCzy na pewno?", "settingsSaved": "Ustawienia zostały zapisane, wczytuję ponownie..." } ================================================ FILE: src/includes/lang/pt-br.json ================================================ { "_header": { "language": "Portuguese (Brazilian)", "original_name": "Português (do Brasil)", "author": "Vitor Micillo Junior (initial), Rafael da Silva Carrasco (2023)", "date": "2023-03-12", "ver": "v1.7" }, "My Tiny Todolist": "Minha lista de tarefas", "powered_by": "Powered by", "htab_newtask": "Nova tarefa", "htab_search": "Pesquisar", "btn_add": "Adicionar", "btn_search": "Pesquisar", "advanced_add": "Avançado", "searching": "Pesquisar por", "tasks": "Tarefas", "taskdate_inline_created": "criada em %s", "taskdate_inline_edited": "modificada em %s", "taskdate_inline_completed": "concluída em %s", "taskdate_inline_duedate": "Vence em %s", "taskdate_created": "Data da criação", "taskdate_edited": "Última modificação", "taskdate_completed": "Data da conclusão", "edit_task": "Editar Tarefa", "add_task": "Nova Tarefa", "priority": "Prioridade", "task": "Tarefa", "note": "Nota", "tags": "Tags", "save": "Salvar", "cancel": "Cancelar", "password": "Senha", "btn_login": "Acessar", "a_login": "Acessar", "a_logout": "Sair", "public_tasks": "Tarefa Pública", "tagcloud": "Tags", "tagfilter_cancel": "cancelar filtro", "showTagsFromAllLists": "Mostrar tags de todas as listas", "sortByHand": "Ordenar manualmente", "sortByPriority": "Ordenar por prioridade", "sortByDueDate": "Ordenar por data de vencimento", "sortByDateCreated": "Ordenar por data de criação", "sortByDateModified": "Ordenar por data de modificação", "due": "Prazo", "daysago": "%d dias atrás", "indays": "em %d dias", "months_short": [ "Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez" ], "months_long": [ "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" ], "months_calendar": [ "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" ], "days_min": [ "Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab" ], "days_long": [ "Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sabado" ], "today": "hoje", "yesterday": "ontem", "tomorrow": "amanhã", "f_past": "Vencidas", "f_today": "Hoje e amanhã", "f_soon": "Em breve", "action_edit": "Editar", "action_note": "Editar Nota", "action_delete": "Remover", "action_priority": "Prioridade", "action_move": "Mover para", "action_ok": "OK", "action_cancel": "Cancelar", "notes": "Notas:", "notes_show": "Mostrar", "notes_hide": "Esconder", "list_new": "Nova lista", "list_rename": "Renomear lista", "list_delete": "Remover lista", "list_showcompleted": "Mostrar tarefas completas", "list_clearcompleted": "Limpar tarefas completas", "list_select": "Selectionar lista", "list_share": "Compartilhar", "list_publish": "Publicar lista", "list_enable_feedkey": "Ativar chave do feed", "list_show_feedkey": "Exibir chave do feed", "list_rssfeed": "RSS Feed", "list_export_to_csv": "Exports para CSV", "list_export_to_ical": "Exportar para iCalendar", "list_hide": "Ocultar lista", "alltags": "Todas as tags", "alltags_show": "Mostrar todas", "alltags_hide": "Esconder todas", "a_settings": "Configurações", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Tarefas concluídas", "feed_modified_tasks": "Tarefas modificadas", "feed_new_tasks": "Novas tarefas", "feed_tasks": "Tarefas", "feed_status_new": "Novas", "feed_status_updated": "Atualizadas", "feed_status_completed": "Concluídas", "alltasks": "Todas as tarefas", "set_header": "Configurações", "set_title": "Título", "set_title_descr": "Especifique se você deseja alterar o título padrão.", "set_language": "Idíoma", "set_protection": "Proteção por Senha", "set_enabled": "Habilitar", "set_disabled": "Desabilitar", "set_newpass": "Nova senha", "set_newpass_descr": "Deixe em branco se não vai alterar a senha atual.", "set_smartsyntax": "Smart syntax", "set_smartsyntax2_descr": "Exemplo: +1 Título da tarefa #tag1 #tag2", "set_timezone": "Fuso horário", "set_autotag": "Autotagging", "set_autotag_descr": "Automaticamente adiciona tag do filtro de tags atual em novas tarefas.", "set_markdown": "Markdown", "set_markdown_descr": "Adiciona suporte a Markdown nas notas, desabilite caso queira utilizar o formato de marcação antigo.", "set_firstdayofweek": "Primeiro dia da semana", "set_custom": "Personalizado", "set_date": "Formato de data", "set_date2": "Formato de data curta", "set_shortdate": "Formato de data abreviada", "set_clock": "Formato do relógio", "set_12hour": "12 horas", "set_24hour": "24 horas", "set_submit": "Salvar", "set_cancel": "Cancelar", "set_showdate": "Mostrar a data na lista de tarefas", "set_showtime": "Mostrar hora", "set_appearance": "Aparência", "set_appearance_system": "O mesmo que o sistema", "set_appearance_light": "Tema claro", "set_extensions": "Extensões", "set_activate": "Ativar", "set_deactivate": "Desativar", "confirmDelete": "Remover esta tarefa?", "confirmLeave": "Podem haver dados não salvos. Você realmente deseja sair?", "actionNoteSave": "salvar", "actionNoteCancel": "cancelar", "error": "Erro (ver detalhes)", "denied": "Acesso negado", "listNotFound": "Lista não encontrafa", "noPublicLists": "Sem tarefas públicas", "invalidpass": "Senha errada", "addList": "Criar nova lista", "addListDefault": "Tarefas", "renameList": "Renomear lista", "deleteList": "Remover toda lista de tarefas.\nTem certeza?", "clearCompleted": "Essa ação removerá todas as tarefas concluídas da lista.\nTem certeza?", "settingsSaved": "Configurações salvas. Recarregando..." } ================================================ FILE: src/includes/lang/pt-pt.json ================================================ { "_header": { "ver": "v1.3.6", "date": "2010-07-29", "language": "Portuguese (Portugal)", "original_name": "Português (Europeu)", "author": "Sérgio Martins", "author_email": "eurospem@live.com.pt" }, "My Tiny Todolist": "Minha lista de tarefas", "htab_newtask": "Nova tarefa", "htab_search": "Pesquisar", "btn_add": "Adicionar", "btn_search": "Pesquisar", "advanced_add": "Avançado", "searching": "Pesquisar por", "tasks": "Tarefas", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Data da criação", "taskdate_completed": "Data da conclusão", "edit_task": "Editar Tarefa", "add_task": "Nova Tarefa", "priority": "Prioridade", "task": "Tarefa", "note": "Nota", "tags": "Etiquetas", "save": "Guardar", "cancel": "Cancelar", "password": "Senha", "btn_login": "Login", "a_login": "Login", "a_logout": "Sair", "public_tasks": "Tarefa Pública", "tagcloud": "Tags", "tagfilter_cancel": "cancelar filtro", "sortByHand": "Ordenar manualmente", "sortByPriority": "Ordenar por prioridade", "sortByDueDate": "Ordenar por data de vencimento", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Prazo", "daysago": "%d dias atrás", "indays": "em %d dias", "months_short": [ "Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez" ], "months_long": [ "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro" ], "days_min": [ "Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab" ], "days_long": [ "Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado" ], "today": "hoje", "yesterday": "ontem", "tomorrow": "amanhã", "f_past": "Vencidas", "f_today": "Hoje e Amanhã", "f_soon": "Brevemente", "action_edit": "Editar", "action_note": "Editar Nota", "action_delete": "Apagar", "action_priority": "Prioridade", "action_move": "Mover para", "notes": "Notas:", "notes_show": "Mostrar", "notes_hide": "Esconder", "list_new": "Nova lista", "list_rename": "Renomear lista", "list_delete": "Apagar lista", "list_publish": "Publicar lista", "list_showcompleted": "Mostrar tarefas completas", "list_clearcompleted": "Limpar tarefas completas", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Todos os títulos:", "alltags_show": "Mostrar todos", "alltags_hide": "Esconder todas", "a_settings": "Configurações", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Configurações", "set_title": "Título", "set_title_descr": "(especifique se deseja alterar o título padrão)", "set_language": "Idíoma", "set_protection": "Proteção por Senha", "set_enabled": "Habilitar", "set_disabled": "Desabilitar", "set_newpass": "Nova senha", "set_newpass_descr": "(deixe em branco se não vai alterar a senha atual)", "set_smartsyntax": "Smart syntax", "set_smartsyntax_descr": "(/prioridade/ tarefa /etiquetas/)", "set_timezone": "Time zone", "set_autotag": "Etiquetas Auto", "set_autotag_descr": "(adiciona automáticamente as etiquetas filtradas ás novas tarefas)", "set_sessions": "Mecanismo de manipulação de sessões", "set_sessions_php": "PHP", "set_sessions_files": "Arquivos", "set_firstdayofweek": "Primeiro dia da semana", "set_custom": "Custom", "set_date": "Formato da data", "set_date2": "Short Date format", "set_shortdate": "Formato de data abreviada", "set_clock": "Formato do relógio", "set_12hour": "12-horas", "set_24hour": "24-horas", "set_submit": "Guardar", "set_cancel": "Cancelar", "set_showdate": "Mostrar a data na lista de tarefas", "confirmDelete": "Apagar esta tarefa?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "guardar", "actionNoteCancel": "cancelar", "error": "Erro (ver detalhes)", "denied": "Acesso negado", "invalidpass": "Senha errada", "addList": "Criar nova lista", "addListDefault": "Todo", "renameList": "Renomear lista", "deleteList": "Apagar a lista de tarefas toda.\nTem certeza?", "clearCompleted": "Apagar todas as tarefas completas na lista.\nTem certeza?", "settingsSaved": "Configurações guardadas. Recarregando..." } ================================================ FILE: src/includes/lang/readme.md ================================================ # myTinyTodo Translations | Locale | Lines | % Done | |:-------|--------:|-------:| | ar | 147/202 | 73% | | bg | 147/202 | 73% | | ca | 147/202 | 73% | | cz | 147/202 | 73% | | da | 147/202 | 73% | | de | 198/202 | 98% | | el | 147/202 | 73% | | en | 202/202 | 100% | | es | 147/202 | 73% | | es-mx | 147/202 | 73% | | et | 198/202 | 98% | | fa | 187/202 | 93% | | fr | 195/202 | 97% | | he | 147/202 | 73% | | hu | 147/202 | 73% | | it | 147/202 | 73% | | ja | 147/202 | 73% | | lt | 147/202 | 73% | | mk | 147/202 | 73% | | nl | 197/202 | 98% | | no | 147/202 | 73% | | pl | 175/202 | 87% | | pt-br | 187/202 | 93% | | pt-pt | 147/202 | 73% | | ro | 147/202 | 73% | | ru | 202/202 | 100% | | sk | 147/202 | 73% | | sl | 147/202 | 73% | | sr | 147/202 | 73% | | sv | 147/202 | 73% | | th | 147/202 | 73% | | tr | 147/202 | 73% | | uk | 147/202 | 73% | | vi | 147/202 | 73% | | zh-cn | 196/202 | 97% | | zh-tw | 147/202 | 73% | ================================================ FILE: src/includes/lang/ro.json ================================================ { "_header": { "ver": "v1.3.6", "date": "2010-09-08", "language": "Romanian", "original_name": "română", "author": "Alin-Andrei Chican", "author_url": "http://www.chican.ro" }, "My Tiny Todolist": "Lista mea mică de sarcini", "htab_newtask": "Sarcină nouă", "htab_search": "Căutare", "btn_add": "Adaugă", "btn_search": "Caută", "advanced_add": "Avansat", "searching": "Căutare după", "tasks": "Sarcini", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Data creării", "taskdate_completed": "Data efectuării", "edit_task": "Modifică sarcină", "add_task": "Sarcină nouă", "priority": "Prioritate", "task": "Sarcină", "note": "Notiță", "tags": "Etichete", "save": "Salvează", "cancel": "Anulează", "password": "Parolă", "btn_login": "Autentifică-te", "a_login": "Autentificare", "a_logout": "Deautentificare", "public_tasks": "Sarcini publice", "tagcloud": "Tags", "tagfilter_cancel": "anulează filtru", "sortByHand": "Sortează manual", "sortByPriority": "Sortează după prioritate", "sortByDueDate": "Sortează după data limită", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Data limită", "daysago": "acum %d zile", "indays": "în %d zile", "months_short": [ "ian", "feb", "mar", "apr", "mai", "iun", "iul", "aug", "sep", "oct", "nov", "dec" ], "months_long": [ "ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie" ], "days_min": [ "du", "lu", "ma", "mi", "jo", "vi", "sa" ], "days_long": [ "duminică", "luni", "marți", "miercuri", "joi", "vineri", "sâmbătă" ], "today": "astăzi", "yesterday": "ieri", "tomorrow": "mâine", "f_past": "Întarziat", "f_today": "Astăzi și mâine", "f_soon": "În curand", "action_edit": "Modificare", "action_note": "Modifică notiță", "action_delete": "Șterge", "action_priority": "Prioritate", "action_move": "Mută la", "notes": "Notițe:", "notes_show": "Afișează", "notes_hide": "Ascunde", "list_new": "Listă nouă", "list_rename": "Redenumește listă", "list_delete": "Șterge listă", "list_publish": "Publică listă", "list_showcompleted": "Arată sarcinile efectuate", "list_clearcompleted": "Șterge sarcinile efectuate", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Toate etichetele:", "alltags_show": "Arată tot", "alltags_hide": "Ascunde tot", "a_settings": "Setări", "rss_feed": "Feed RSS", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Setări", "set_title": "Titlu", "set_title_descr": "(specifică dacă dorești să schimbi titlul standard)", "set_language": "Limbă", "set_protection": "Protecție cu parolă", "set_enabled": "Activat", "set_disabled": "Dezactivat", "set_newpass": "Parolă nouă", "set_newpass_descr": "(lasă necompletat dacă nu schimbi parola curentă)", "set_smartsyntax": "Sintaxă deșteaptă", "set_smartsyntax_descr": "(/prioritate/ sarcină /etichete/)", "set_timezone": "Time zone", "set_autotag": "Etichetare automată", "set_autotag_descr": "(adaugarea automată a etichetei filtrului de etichete curent la sarcina nou creată)", "set_sessions": "Metoda de manipulare a sesiunilor", "set_sessions_php": "PHP", "set_sessions_files": "Fișiere", "set_firstdayofweek": "Prima zi a săptămânii", "set_custom": "Custom", "set_date": "Formatul datei", "set_date2": "Short Date format", "set_shortdate": "Formatul scurt al datei", "set_clock": "Formatul orei", "set_12hour": "12-ore", "set_24hour": "24-ore", "set_submit": "Salvează modificările", "set_cancel": "Anulează", "set_showdate": "Arată data sarcinii în listă", "confirmDelete": "Ești sigur că vrei să ștergi sarcina?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "salvează", "actionNoteCancel": "anulează", "error": "A avut loc o eroare (click pentru detalii)", "denied": "Acces interzis", "invalidpass": "Parolă greșită", "addList": "Crează o nouă listă", "addListDefault": "Todo", "renameList": "Redenumește listă", "deleteList": "Această acțiune va șterge lista curentă cu toate sarcinile conținute.\nEști sigur?", "clearCompleted": "Această acțiune va șterge toate sarcinile efectuate din listă.\nEști sigur?", "settingsSaved": "Setări salvate. Împrospătare..." } ================================================ FILE: src/includes/lang/ru.json ================================================ { "_header": { "ver": "v1.8.2", "date": "2025-02-16", "language": "Russian", "original_name": "Русский", "authors": [ "Max Pozdeev (https://www.mytinytodo.net)" ] }, "My Tiny Todolist": "My Tiny Todolist", "powered_by": "Работает на", "htab_newtask": "Новая задача", "htab_search": "Поиск", "btn_add": "Добавить", "btn_search": "Искать", "advanced_add": "Расширенная форма", "searching": "Поиск", "tasks": "Задачи", "taskdate_inline_created": "добавлена %s", "taskdate_inline_edited": "отредактирована %s", "taskdate_inline_completed": "завершена %s", "taskdate_inline_duedate": "В срок %s", "taskdate_created": "Дата создания", "taskdate_edited": "Последнее изменение", "taskdate_completed": "Дата завершения", "edit_task": "Редактирование задачи", "add_task": "Новая задача", "priority": "Приоритет", "task": "Задача", "note": "Заметка", "tags": "Теги", "list": "Список", "no_note": "Нет заметки", "save": "Сохранить", "cancel": "Отмена", "close": "Закрыть", "password": "Пароль", "btn_login": "Войти", "a_login": "Вход", "a_logout": "Выйти", "public_tasks": "Опубликованные задачи", "tagcloud": "Теги", "tagfilter_cancel": "отменить фильтр по тегу", "filterTags": "Фильтр тегов", "showTagsFromAllLists": "Показать теги из всех списков", "sortByHand": "Сортировка вручную", "sortByTitle": "Сортировка по алфавиту", "sortByPriority": "Сортировка по приоритету", "sortByDueDate": "Сортировка по сроку", "sortByDateCreated": "Сортировка по дате добавления", "sortByDateModified": "Сортировка по дате изменения", "due": "Срок", "daysago": "%d дн. назад", "indays": "через %d дн.", "months_short": [ "Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек" ], "months_long": [ "января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря" ], "months_calendar": [ "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь" ], "days_min": [ "Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" ], "days_long": [ "Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота" ], "today": "сегодня", "yesterday": "вчера", "tomorrow": "завтра", "f_past": "Просроченные", "f_today": "Сегодня и завтра", "f_soon": "Скоро", "action_edit": "Редактировать", "action_note": "Заметка", "action_delete": "Удалить", "action_priority": "Приоритет", "action_move": "Переместить в", "action_ok": "ОК", "action_cancel": "Отмена", "notes": "Заметки:", "notes_show": "Показать", "notes_hide": "Скрыть", "list_new": "Новый список", "list_rename": "Переименовать список", "list_delete": "Удалить список", "list_showcompleted": "Показать завершенные задачи", "list_clearcompleted": "Удалить завершенные задачи", "list_select": "Выбрать список", "list_share": "Поделиться", "list_publish": "Опубликовать список", "list_enable_feedkey": "Включить доступ по ключу", "list_show_feedkey": "Показать ключ", "list_rssfeed": "RSS-лента", "list_export_to_csv": "Экспортировать в CSV", "list_export_to_ical": "Экспортировать в iCalendar", "list_hide": "Скрыть список", "alltags": "Все теги:", "alltags_show": "Показать все", "alltags_hide": "Скрыть все", "a_settings": "Настройки", "rss_feed": "RSS-лента", "feed_title": "%s", "feed_completed_tasks": "Завершенные задачи", "feed_modified_tasks": "Изменившиеся задачи", "feed_new_tasks": "Новые задачи", "feed_status_new": "Задача создана", "feed_status_updated": "Задача обновлена", "feed_status_completed": "Задача завершена", "feed_tasks": "Задачи", "alltasks": "Все задачи", "set_header": "Настройки", "set_title": "Заголовок страницы", "set_title_descr": "Если поле не заполнено, будет использован заголовок по-умолчанию.", "set_language": "Язык (Language)", "set_protection": "Парольная защита", "set_enabled": "Включено", "set_disabled": "Выключено", "set_newpass": "Новый пароль", "set_newpass_descr": "Не заполняйте поле если не хотите менять текущий пароль.", "set_smartsyntax": "Smart syntax", "set_smartsyntax3_descr": "Пример синтаксиса: +1 Задача #Тэг1 #Тэг2 @Срок", "set_timezone": "Часовой пояс", "set_autotag": "Автотеги", "set_autotag_descr": "Автодобавление текущего тега из фильтра в новую задачу.", "set_markdown": "Markdown", "set_markdown_descr": "Возможность использовать Markdown в заметках, выключите если хотите использовать старую разметку.", "set_firstdayofweek": "Первый день недели", "set_custom": "другой", "set_date": "Формат даты", "set_date2": "Формат короткой даты", "set_shortdate": "Короткая дата (в текущем году)", "set_clock": "Формат часов", "set_12hour": "12-часовой", "set_24hour": "24-часовой", "set_submit": "Сохранить изменения", "set_cancel": "Отмена", "set_showdate": "Показывать дату создания задачи", "set_showtime": "Показывать время", "set_showdate_inline": "В той же строке", "set_exactduedate": "Показывать срок исполнения всегда датой", "set_appearance": "Тема оформления", "set_appearance_system": "Как в системе", "set_appearance_light": "Светлая", "set_appearance_dark": "Тёмная", "set_newtaskcounter_h": "Счетчик новых задач", "set_newtaskcounter": "Проверять наличие новых задач", "set_newtaskcountericon": "Показывать счетчик в favicon", "set_extensions": "Дополнения", "set_activate": "Активировать", "set_deactivate": "Деактивировать", "confirmDelete": "Вы действительно хотите удалить задачу?", "confirmLeave": "На странице могут быть несохраненные данные. Вы действительно хотите закрыть страницу?", "actionNoteSave": "сохранить", "actionNoteCancel": "отмена", "error": "Ошибка", "denied": "Доступ запрещен", "listNotFound": "Список не найден", "noPublicLists": "Нет опубликованных списков", "noTags": "Нет тегов", "withoutTags": "Без тегов", "withAnyTag": "Любой тег", "invalidpass": "Неверный пароль", "addList": "Новый список", "addListDefault": "Todo", "renameList": "Переименовать список", "deleteList": "Вы действительно хотите удалить этот список со всеми задачами?", "clearCompleted": "Удалить все выполненные задачи из списка?", "settingsSaved": "Настройки сохранены. Перезагрузка..." } ================================================ FILE: src/includes/lang/sk.json ================================================ { "_header": { "ver": "v1.3.6", "date": "2010-12-16", "language": "Slovak", "original_name": "Slovenčina", "author": "Ľubomír Molent", "author_url": "" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Nová úloha", "htab_search": "Hľadať", "btn_add": "Pridať", "btn_search": "Hľadať", "advanced_add": "Rozšírené", "searching": "Vyhľadávanie", "tasks": "Úlohy", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Dátum vytvorenia", "taskdate_completed": "Dátum splnenia", "edit_task": "Upraviť úlohu", "add_task": "Nová úloha", "priority": "Priorita", "task": "Úloha", "note": "Poznámka", "tags": "Tagy", "save": "Uložit", "cancel": "Zrušit", "password": "Heslo", "btn_login": "Login", "a_login": "Prihlásiť", "a_logout": "Odhlásiť", "public_tasks": "Verejné úlohy", "tagcloud": "Tags", "tagfilter_cancel": "zrušit filtre", "sortByHand": "Triediť ručne", "sortByPriority": "Triediť podľa priority", "sortByDueDate": "Triediť podľa termínu", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Termín", "daysago": "pred %d dňami", "indays": "o %d dní", "months_short": [ "Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec" ], "months_long": [ "Január", "Február", "Marec", "Apríl", "Máj", "Jún", "Júl", "August", "September", "Október", "November", "December" ], "days_min": [ "Ne", "Po", "Ut", "St", "Št", "Pi", "So" ], "days_long": [ "Nedľa", "Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota" ], "today": "dnes", "yesterday": "včera", "tomorrow": "zajtra", "f_past": "Overdue", "f_today": "Dnes a zajtra", "f_soon": "Čoskoro", "action_edit": "Upraviť", "action_note": "Upraviť poznámku", "action_delete": "Zmazať", "action_priority": "Priorita", "action_move": "Presunúť do", "notes": "Poznámky:", "notes_show": "Zobraziť", "notes_hide": "Skryť", "list_new": "Nový zoznam", "list_rename": "Premenovať zoznam", "list_delete": "Zmazať zoznam", "list_publish": "Zverejniť zoznam", "list_showcompleted": "Zobraziť splnené úlohy", "list_clearcompleted": "Zmazať splnené úlohy", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Všetky tagy:", "alltags_show": "Zobraziť všetko", "alltags_hide": "Skryť všetko", "a_settings": "Nastavenie", "rss_feed": "RSS kanál", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Nastavenie", "set_title": "Titulok", "set_title_descr": "(zadajte, pokiaľ chcete zmeniť východzí titulok)", "set_language": "Jazyk", "set_protection": "Zaheslovanie", "set_enabled": "Zapnuté", "set_disabled": "Vypnuté", "set_newpass": "Nové heslo", "set_newpass_descr": "(nevypĺňajte, pokiaľ nechcete meniť nastavené heslo)", "set_smartsyntax": "\"Smart\" syntax", "set_smartsyntax_descr": "(Zápis: \"/priorita/ test úlohy /tagy/\")", "set_timezone": "Time zone", "set_autotag": "Automatické tagovanie", "set_autotag_descr": "(automaticky priradí k tagom text z filtra)", "set_sessions": "Správa sessions", "set_sessions_php": "PHP", "set_sessions_files": "Súbory", "set_firstdayofweek": "Prvný deň v týždni", "set_custom": "Custom", "set_date": "Formát dátumu", "set_date2": "Short Date format", "set_shortdate": "Zkrátený formát dátumu", "set_clock": "Formát času", "set_12hour": "12 hodinový", "set_24hour": "24 hodinový", "set_submit": "Uložiť zmeny", "set_cancel": "Zrušit", "set_showdate": "Zobrazit v zozname dátum vytvorenia úlohy", "confirmDelete": "Naozaj chcete vymazať úlohu?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "uložit", "actionNoteCancel": "zrušit", "error": "Vyskytol sa problém (kliknite pre viac informácií)", "denied": "Prístup zamietnutý", "invalidpass": "Nesprávne heslo", "addList": "Vytvoriť nový zoznam", "addListDefault": "Todo", "renameList": "Premenovat zoznam", "deleteList": "Týmto vymažete zoznam a všetky úlohy v ňom.\nChcete pokračovat?", "clearCompleted": "Týmto vymažete všetky splnené úlohy.\nChcete pokračovat?", "settingsSaved": "Nastavenie uložené. Načítavam..." } ================================================ FILE: src/includes/lang/sl.json ================================================ { "_header": { "ver": "v1.3.2", "date": "2010-01-08", "language": "Slovenian", "original_name": "slovensko", "author": "Janez Troha" }, "My Tiny Todolist": "Moj Seznam Nalog", "htab_newtask": "Naloga", "htab_search": "Iskalnik", "btn_add": "Dodaj", "btn_search": "Išči", "advanced_add": "Napredno", "searching": "Searching for", "tasks": "Naloge", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Datum nastanka", "taskdate_completed": "Datum zaključka", "edit_task": "Uredi nalogo", "add_task": "Nova Naloga", "priority": "Pomembnost", "task": "Naloga", "note": "Beležka", "tags": "Oznake", "save": "Shrani", "cancel": "Prekliči", "password": "Geslo", "btn_login": "Prijava", "a_login": "Prijava", "a_logout": "Odjava", "public_tasks": "Javne Naloge", "tagcloud": "Tags", "tagfilter_cancel": "ročno sortiraj", "sortByHand": "Sortiraj ročno", "sortByPriority": "Sortiraj po pomembnosti", "sortByDueDate": "Sortiraj po zapadlosti", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "zapadlost", "daysago": "pred %d", "indays": "čez %d dni", "months_short": [ "Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Avg", "Sep", "Okt", "Nov", "Dec" ], "months_long": [ "Januar", "Februar", "Marc", "April", "Maj", "Junij", "Julij", "Avgust", "September", "Oktober", "November", "December" ], "days_min": [ "Ned", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob" ], "days_long": [ "Nedelja", "Ponedeljek", "Torek", "Sreda", "Četrtek", "Petek", "Sobota" ], "today": "danes", "yesterday": "včeraj", "tomorrow": "jutri", "f_past": "čez rok", "f_today": "Danes in jutri", "f_soon": "Kmalu", "action_edit": "Uredi", "action_note": "Uredi Beležko", "action_delete": "Odstrani", "action_priority": "Pomembnost", "action_move": "Premakni v", "notes": "Beležke:", "notes_show": "Skrij", "notes_hide": "Prikaži", "list_new": "Nov seznam", "list_rename": "Preimenjuj seznam", "list_delete": "Odstrani seznam", "list_publish": "Objavi seznam", "list_showcompleted": "Show completed tasks", "list_clearcompleted": "Clear completed tasks", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Vse oznake:", "alltags_show": "Prikaži vse", "alltags_hide": "Skrij vse", "a_settings": "Nastavitve", "rss_feed": "RSS Vir", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Nastavitve", "set_title": "Naslov", "set_title_descr": "(uredi če želiš spremeniti privzeti naslov)", "set_language": "Jezik vmesnika", "set_protection": "Zaščiteno z geslom", "set_enabled": "Vključeno", "set_disabled": "Izključeno", "set_newpass": "Geslo", "set_newpass_descr": "(pusti polje prazno, če ne želiš spremeniti gesla)", "set_smartsyntax": "Pametne vnos", "set_smartsyntax_descr": "(/pomembnost/ naloga /oznake/)", "set_timezone": "Time zone", "set_autotag": "Samodejne oznake", "set_autotag_descr": "(samodejno doda oznako trenutno izbranega filtra oznake)", "set_sessions": "Shranjevanje seje", "set_sessions_php": "PHP", "set_sessions_files": "Datoteka", "set_firstdayofweek": "Prvi dan v tednu", "set_custom": "Custom", "set_date": "Format datuma", "set_date2": "Short Date format", "set_shortdate": "Kratki format datuma", "set_clock": "Format prikaza ure", "set_12hour": "12-urni", "set_24hour": "24-urni", "set_submit": "Shrani spremembe", "set_cancel": "Prekliči", "set_showdate": "Show task date in list", "confirmDelete": "Ali ste prepričani da želite izbisati izbrano nalogo?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "shrani", "actionNoteCancel": "prekliči", "error": "Napaka (klikni za podrobnosti)", "denied": "Dostop zavrnjen", "invalidpass": "Napačno geslo", "addList": "Ustvari nov seznam", "addListDefault": "Todo", "renameList": "Preimenjuj seznam", "deleteList": "Ta ukaz bo izbrisal tudi vse naloge, v tem seznamu.\nAli ste prepričani?", "clearCompleted": "This will delete all completed tasks in the list.\nAre you sure?", "settingsSaved": "Nastavitve shranjene. Posodabljam..." } ================================================ FILE: src/includes/lang/sr.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-02-21", "language": "Serbian", "original_name": "Српски", "author": "Goran Trajkovic", "author_url": "http://www.crelativ.com" }, "My Tiny Todolist": "РОКОВНИК", "htab_newtask": "Нови задатак", "htab_search": "Претрага", "btn_add": "Упиши", "btn_search": "Тражи", "advanced_add": "Датаљан упис задатка", "searching": "Претраживање у току...", "tasks": "Текући", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Дан и час креирања", "taskdate_completed": "Дан и час завршетка", "edit_task": "Измена задатка", "add_task": "Нови задатак", "priority": "Приоритет", "task": "Наслов", "note": "Опис", "tags": "Филтер по категоријама", "save": " Упиши ", "cancel": "Одустани", "password": "Лозинка", "btn_login": "Пријави се", "a_login": "Пријављивање", "a_logout": "Одјави се", "public_tasks": "Јавни задаци", "tagcloud": "Tags", "tagfilter_cancel": "искључи филтер", "sortByHand": "Ручно уређивање задатака", "sortByPriority": "Уређивање по приоритету", "sortByDueDate": "Уређивање по датуму обављања", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Датум обављања", "daysago": "пре %d дана", "indays": "за %d дана", "months_short": [ "Jан", "Феб", "Maр", "Aпр", "Maј", "Jун", "Jул", "Aвг", "Сеп", "Oкт", "Нов", "Дец" ], "months_long": [ "Јануар", "Фебруар", "Март", "Април", "Maј", "Jун", "Jул", "Aвгуст", "Септембар", "Oктобар", "Новембар", "Децембар" ], "days_min": [ "Нед", "Пон", "Уто", "Сре", "Чет", "Пет", "Суб" ], "days_long": [ "Недеља", "Понедељак", "Уторак", "Среда", "Четвртак", "Петак", "Субота" ], "today": "данас", "yesterday": "јуче", "tomorrow": "сутра", "f_past": "Пробивени", "f_today": "Данас и сутра", "f_soon": "Ускоро", "action_edit": "Измена задатка", "action_note": "Промена описа", "action_delete": "Брисање", "action_priority": "Промена приоритета", "action_move": "Премештање у категорију", "notes": "Опис задатка:", "notes_show": "прикажи", "notes_hide": "сакриј", "list_new": "Додавање нове листе", "list_rename": "Преименовање текуће листе", "list_delete": "Брисање текуће листе", "list_publish": "Постављање текуће листе за јавну", "list_showcompleted": "Приказ завршених задатака", "list_clearcompleted": "Брисање завршених задатака", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Све категорије:", "alltags_show": "Прикажи категорије", "alltags_hide": "Сакриј категорије", "a_settings": "Подешавања", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Подешавања", "set_title": "Наслов", "set_title_descr": "унесите уколико желите да промените подразумевани наслов", "set_language": "Језик", "set_protection": "Уптреба лозинке код приступа", "set_enabled": "Да", "set_disabled": "Не", "set_newpass": "Нова лозинка", "set_newpass_descr": "оставите поље празно ако не желите да промените лозинку", "set_smartsyntax": "Smart синтакса", "set_smartsyntax_descr": "(/priority/ task /tags/)", "set_timezone": "Time zone", "set_autotag": "Аутоматско задавање тагова", "set_autotag_descr": "код уноса новог задатка аутоматски задаје тага користећи вредност из текућег филтра", "set_sessions": "Механизам за чување сесија", "set_sessions_php": "PHP", "set_sessions_files": "Фајл систем", "set_firstdayofweek": "Први дан у недељи", "set_custom": "Custom", "set_date": "Формат за приказ датума задатка", "set_date2": "Short Date format", "set_shortdate": "Кратки формат датума", "set_clock": "Формат времена часовника", "set_12hour": "12-часовни", "set_24hour": "24-часовни", "set_submit": " Упиши ", "set_cancel": "Одустани", "set_showdate": "Прикажи датум задатка у листи", "confirmDelete": "Да ли сте сигурни?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "сними", "actionNoteCancel": "одустани", "error": "Грешке у раду програма (кликните да бисте видели детаље)", "denied": "Немогућ приступ апликацији", "invalidpass": "Неисправна лозинка", "addList": "Направи нову листу", "addListDefault": "Todo", "renameList": "Унесите нови назив листе", "deleteList": "Брисање текуће листе са свим припадајућим задацима\nДа ли сте сигурни?", "clearCompleted": "Брисање свих завршених задатака у листи\nДа ли сте сигурни?", "settingsSaved": "Промене у подешавањима су сачуване. Поновно учитавање..." } ================================================ FILE: src/includes/lang/sv.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-05-25", "language": "Swedish", "original_name": "Svenska", "author": "Martin Danielsson" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Ny uppgift", "htab_search": "Sök", "btn_add": "Lägg till", "btn_search": "Sök", "advanced_add": "Avancerat", "searching": "Söker efter", "tasks": "Uppgifter", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Skapad", "taskdate_completed": "Avslutad", "edit_task": "Ändra uppgift", "add_task": "Ny uppgift", "priority": "Prioritet", "task": "Uppgift", "note": "Notering", "tags": "Taggar", "save": "Spara", "cancel": "Avbryt", "password": "Lösenord", "btn_login": "Logga in", "a_login": "Logga in", "a_logout": "Logga ut", "public_tasks": "Allmänna uppgifter", "tagcloud": "Tags", "tagfilter_cancel": "ta bort filter", "sortByHand": "Sortera för hand", "sortByPriority": "Sortera efter prioritet", "sortByDueDate": "Sortera efter deadline", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Deadline", "daysago": "%d dagar sen", "indays": "om %d dagar", "months_short": [ "Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec" ], "months_long": [ "Januari", "Februari", "Mars", "April", "Maj", "Juni", "July", "Augusti", "September", "Oktober", "November", "December" ], "days_min": [ "Sö", "Må", "Ti", "On", "To", "Fr", "Lö" ], "days_long": [ "Söndag", "Månday", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag" ], "today": "idag", "yesterday": "igår", "tomorrow": "imorgon", "f_past": "Försenad", "f_today": "Idag och imorgon", "f_soon": "Snart", "action_edit": "Ändra", "action_note": "Ändra notering", "action_delete": "Ta bort", "action_priority": "Prioritet", "action_move": "Flytta till", "notes": "Noteringar:", "notes_show": "Visa", "notes_hide": "Dölj", "list_new": "Ny lista", "list_rename": "Döp om lista", "list_delete": "Ta bort lista", "list_publish": "Publicera lista", "list_showcompleted": "Visa avslutade uppgifter", "list_clearcompleted": "Töm avslutade uppgifter", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Alla taggar:", "alltags_show": "Visa alla", "alltags_hide": "Dölj alla", "a_settings": "Inställningar", "rss_feed": "RSS-flöde", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Inställningar", "set_title": "Titel", "set_title_descr": "(specifiera om du vill ändra standardtitel)", "set_language": "Språk", "set_protection": "Lösenordsskydd", "set_enabled": "På", "set_disabled": "Av", "set_newpass": "Nytt lösenord", "set_newpass_descr": "(lämna blank för att inte byta lösenord)", "set_smartsyntax": "Smart syntax", "set_smartsyntax_descr": "(/prioritet/ uppgift /taggar/)", "set_timezone": "Time zone", "set_autotag": "Autotaggning", "set_autotag_descr": "(lägger automatiskt till taggar för det aktuella filtret när man skapar nya uppgifter)", "set_sessions": "Hantering av sessioner", "set_sessions_php": "PHP", "set_sessions_files": "Filer", "set_firstdayofweek": "Vilken veckodag börjar veckan på", "set_custom": "Custom", "set_date": "Datumformat", "set_date2": "Short Date format", "set_shortdate": "Kort datumformat", "set_clock": "Tidsformat", "set_12hour": "12-timmars", "set_24hour": "24-timmars", "set_submit": "Spara ändringar", "set_cancel": "Avbryt", "set_showdate": "Visa datum i listan", "confirmDelete": "Vill du verkligen radera den här uppgiften?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "spara", "actionNoteCancel": "avbryt", "error": "Ett fel har uppstått (Tryck för mer info)", "denied": "Tillträde nekas", "invalidpass": "Fel lösenord", "addList": "Skapa ny lista", "addListDefault": "Todo", "renameList": "Döp om lista", "deleteList": "Det här tar bort listan och alla uppgifter.\nVill du fortsätta?", "clearCompleted": "Det här tar bort alla avslutade uppgifter.\nFortsätt?", "settingsSaved": "Inställningar sparade. Laddar om..." } ================================================ FILE: src/includes/lang/th.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-11-15", "language": "Thai", "original_name": "ไทย", "author": "Maxasus123", "author_url": "http://www.bob.in.th" }, "My Tiny Todolist": "Todolist ของฉัน", "htab_newtask": "งานใหม่", "htab_search": "ค้นหา", "btn_add": "เพิ่ม", "btn_search": "ค้นหา", "advanced_add": "ขั้นสูง", "searching": "การค้นหา", "tasks": "งาน", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "วันที่สร้าง", "taskdate_completed": "วันที่เสร็จสิ้น", "edit_task": "แก้ไขงาน", "add_task": "เพิ่มงานใหม่", "priority": "ลำดับความสำคัญ", "task": "งาน", "note": "Note", "tags": "แท็ก", "save": "บันทึก", "cancel": "ยกเลิก", "password": "รหัสผ่าน", "btn_login": "เข้าสู่ระบบ", "a_login": "เข้าสู่ระบบ", "a_logout": "ออกจากระบบ", "public_tasks": "Public Tasks", "tagcloud": "Tags", "tagfilter_cancel": "ยกเลิกการกรอง", "sortByHand": "เรียงด้วยมือ", "sortByPriority": "เรียงตามลำดับความสำคัญ", "sortByDueDate": "เรียงตามวันที่กำหนด", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "ครบกำหนา", "daysago": "%d วันที่ผ่านมา", "indays": "ใน %d วัน", "months_short": [ "ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค." ], "months_long": [ "มกราคม", "กุมภาพันธ์", "มีนาคม", "เมษายน", "พฤษภาคม", "มิถุนายน", "กรกฎาคม", "สิงหาคม", "กันยายน", "ตุลาคม", "พฤศจิกายน", "ธันวาคม" ], "days_min": [ "อา.", "จ.", "อ.", "พ.", "พฤ.", "ศ.", "ส." ], "days_long": [ "อาทิตย์", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์" ], "today": "วันนี้", "yesterday": "เมื่อวาน", "tomorrow": "พรุ่งนี้", "f_past": "เกินกำหนด", "f_today": "วันนี้และวันพรุ่งนี้", "f_soon": "ในไม่ช้า", "action_edit": "แก้ไข", "action_note": "แก้ไข Note", "action_delete": "ลบ", "action_priority": "ลำดับความสำคัญ", "action_move": "ย้ายไป", "notes": "Notes:", "notes_show": "โชว์", "notes_hide": "ซ่อน", "list_new": "รายการใหม่", "list_rename": "เปลี่ยนชื่อรายการ", "list_delete": "ลบรายการ", "list_publish": "เผยแพร่รายการ", "list_showcompleted": "แสดงงานที่เสร็จแล้ว", "list_clearcompleted": "Clear งานที่เสร็จแล้ว", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "แท็กทั้งหมด:", "alltags_show": "โชว์ ทั้งหมด", "alltags_hide": "ซ่อนทั้งหม", "a_settings": "การตั้งค่า", "rss_feed": "RSS Feed", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "การตั้งค่า", "set_title": "ชื่อเรื่อง", "set_title_descr": "(ระบุหากคุณต้องการเปลี่ยนชื่อเรื่องเริ่มต้น)", "set_language": "ภาษา", "set_protection": "รหัสป้องกัน", "set_enabled": "เปิดใช้งาน", "set_disabled": "ปิดใช้งาน", "set_newpass": "รหัสผ่านใหม่", "set_newpass_descr": "(เว้นว่างไว้หากจะไม่มีการเปลี่ยนแปลงรหัสผ่านปัจจุบัน)", "set_smartsyntax": "Smart syntax", "set_smartsyntax_descr": "(/ลำดับความสำคัญ/ งาน /แท็ก/)", "set_timezone": "Time zone", "set_autotag": "Autotagging", "set_autotag_descr": "(โดยอัตโนมัติเพิ่มแท็กแท็กของตัวกรองปัจจุบันกับงานที่สร้างขึ้นใหม่)", "set_sessions": "Session handling mechanism", "set_sessions_php": "PHP", "set_sessions_files": "ไฟล์", "set_firstdayofweek": "วันแรกของสัปดาห์", "set_custom": "Custom", "set_date": "รูปแบบวันที่", "set_date2": "Short Date format", "set_shortdate": "วันที่แบบย่อ", "set_clock": "รูปแบบเวลา", "set_12hour": "12 ชั่วโมง", "set_24hour": "24 ชั่วโมง", "set_submit": "บันทึก", "set_cancel": "ยกเลิก", "set_showdate": "วันที่งานแสดงในรายการ", "confirmDelete": "คุณแน่ใจหรือว่าต้องการลบงาน?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "บันทึก", "actionNoteCancel": "ยกเลิก", "error": "ข้อผิดพลาดบางอย่างเกิดขึ้น (คลิกเพื่อดูรายละเอียด)", "denied": "ปฏิเสธการเข้าใช้", "invalidpass": "รหัสผ่านผิด", "addList": "การสร้างรายการใหม่", "addListDefault": "Todo", "renameList": "เปลี่ยนชื่อรายการใหม่", "deleteList": "นี้จะลบรายการปัจจุบันกับงานทั้งหมดในนั้น. \nคุณแน่ใจหรือไม่?", "clearCompleted": "นี้จะลบรายการที่ทำเสร็จทั้งหมดในรายการ\nคุณแน่ใจหรือไม่?", "settingsSaved": "บันทึกการตั้งค่า โหลด ..." } ================================================ FILE: src/includes/lang/tr.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-03-11", "language": "Turkish", "original_name": "Türkçe", "author": "Feyyaz Esatoğlu, Emrullah Hocaoğlu" }, "My Tiny Todolist": "My Tiny Todolist", "htab_newtask": "Yeni Görev", "htab_search": "Ara", "btn_add": "Ekle", "btn_search": "Ara", "advanced_add": "İleri seviye", "searching": "Aranıyor", "tasks": "Görevler", "taskdate_inline_created": "%s oluşturuldu", "taskdate_inline_completed": "%s tamamlandı", "taskdate_inline_duedate": "%s vadesi dolacak", "taskdate_created": "Oluşturulma tarihi", "taskdate_completed": "Tamamlanma tarihi", "edit_task": "Görevi düzenle", "add_task": "Yeni görev", "priority": "Öncelik", "task": "Görev", "note": "Not", "tags": "Etiketler", "save": "Kaydet", "cancel": "İptal", "password": "Şifre", "btn_login": "Giriş", "a_login": "Giriş", "a_logout": "Çıkış", "public_tasks": "Genel(Kamuya açık) Görevler", "tagcloud": "Etiketler", "tagfilter_cancel": "filtre iptal", "sortByHand": "Elle sırala", "sortByPriority": "Önceliğe göre sırala", "sortByDueDate": "Vadesi gelmiş olanlara göre sırala", "sortByDateCreated": "Oluşturulma tarihine göre sırala", "sortByDateModified": "Değiştirilme tarihine göre sırala", "due": "Vadesi gelmiş", "daysago": "%d gün önce", "indays": "%d gün içinde", "months_short": [ "Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara" ], "months_long": [ "Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık" ], "days_min": [ "Pz", "Pt", "Sa", "Ça", "Pe", "Cu", "Ct" ], "days_long": [ "Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi" ], "today": "bugün", "yesterday": "Dün", "tomorrow": "Yarın", "f_past": "Vadesi geçmiş", "f_today": "Bugün ve Yarın", "f_soon": "Yakın zamanda", "action_edit": "Düzenle", "action_note": "Not düzenle", "action_delete": "Sil", "action_priority": "Öncelik", "action_move": "Hareket ettir", "notes": "Notlar:", "notes_show": "Göster", "notes_hide": "Gizle", "list_new": "Yeni Liste", "list_rename": "Listeyi yeniden adlandır", "list_delete": "Listeyi sil", "list_publish": "Listeyi yayımla", "list_showcompleted": "Biten görevleri göster", "list_clearcompleted": "Biten görevleri temizle", "list_select": "Liste seç", "list_export": "Dışa Aktar", "list_export_csv": "CSV dosyasına aktar", "list_export_ical": "iCalendar dosyasına aktar", "list_rssfeed": "RSS Beslemesi", "alltags": "Tüm Etiketler:", "alltags_show": "Tümünü göster", "alltags_hide": "Tümünü gizle", "a_settings": "Ayarlar", "rss_feed": "RSS Beslemesi", "feed_title": "%s", "feed_completed_tasks": "Tamamlanmış görevler", "feed_modified_tasks": "Değiştirilmiş görevler", "feed_new_tasks": "Yeni Görev", "alltasks": "Tüm görevler", "set_header": "Ayarlar", "set_title": "Etiket", "set_title_descr": "(eğer varsayılan etiketi değiştirecekseniz belirtiniz.)", "set_language": "Dil", "set_protection": "Şifre koruma", "set_enabled": "Erişilebilir", "set_disabled": "Engelli", "set_newpass": "Yeni şifre", "set_newpass_descr": "(geçerli şifreyi değiştirmek istemiyorsanız boş bırakınız)", "set_smartsyntax": "Akıllı sözdizimi", "set_smartsyntax_descr": "(/öncelik/ görev /etiketler/)", "set_timezone": "Saat dilimi", "set_autotag": "Otomatik etiketleme", "set_autotag_descr": "(Yeni oluşturulmuş görevler için geçerli etiket filtresine otomatik tag ekler)", "set_sessions": "Oturuma müdahale mekanizması", "set_sessions_php": "PHP", "set_sessions_files": "Dosyalar", "set_firstdayofweek": "Haftanın ilk günü", "set_custom": "Kişiselleştirilmiş", "set_date": "Tarih formatı", "set_date2": "Kısa tarih formatı", "set_shortdate": "Kısa tarih formatı", "set_clock": "Saat formatı", "set_12hour": "12-saat", "set_24hour": "24-saat", "set_submit": "Değişiklikleri Onayla", "set_cancel": "İptal", "set_showdate": "Listedeki görev tarihini göster", "confirmDelete": "Görevi silmek istediğinizden emin misiniz?", "confirmLeave": "Kaydedilmemiş veri olabilir. Ayrılmak istediğine emin misin?", "actionNoteSave": "Kaydet", "actionNoteCancel": "İptal", "error": "Bazı problemler oluştu (detaylar için tıklayın)", "denied": "İzin Yok", "invalidpass": "Yanlış Şifre", "addList": "Yeni Liste Oluştur", "addListDefault": "Yeni Liste", "renameList": "Listeyi Yeniden Adlandır", "deleteList": "Geçerli liste ve içindeki tüm görevler silinecek.\nEmin misiniz?", "clearCompleted": "Listedeki tamamlanmış tüm görevler silinecek.\nEmin misiniz?", "settingsSaved": "Ayarlar kaydedildi.Yükleniyor..." } ================================================ FILE: src/includes/lang/uk.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-03-17", "language": "Ukrainian", "original_name": "Українська", "author": "Sergii Iavorskyi" }, "My Tiny Todolist": "Мої завдання", "htab_newtask": "Нове завдання", "htab_search": "Пошук", "btn_add": "Додати", "btn_search": "Шукати", "advanced_add": "Розширена форма", "searching": "Пошук", "tasks": "Усі завдання", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Дата створення", "taskdate_completed": "Дата завершення", "edit_task": "Редагування завдання", "add_task": "Нове завдання", "priority": "Пріорітет", "task": "Завдання", "note": "Нотатки", "tags": "Теги", "save": "Зберегти", "cancel": "Відмінити", "password": "Пароль", "btn_login": "Увійти", "a_login": "Вхід", "a_logout": "Вийти", "public_tasks": "Опубліковані завдання", "tagcloud": "Tags", "tagfilter_cancel": "відмінити фільтрування по тегами", "sortByHand": "Сортування вручну", "sortByPriority": "Сортування за пріорітетом", "sortByDueDate": "Сортування за датою", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Завершити до", "daysago": "%d дн. тому", "indays": "через %d дн.", "months_short": [ "Січ", "Лют", "Бер", "Квіт", "Трав", "Чер", "Лип", "Сер", "Вер", "Жов", "Лис", "Груд" ], "months_long": [ "Січень", "Лютий", "Березень", "Квітень", "Травень", "Червень", "Липень", "Серпень", "Вересень", "Жовтень", "Листопад", "Грудень" ], "days_min": [ "Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" ], "days_long": [ "Неділя", "Понеділок", "Вівторок", "Середа", "Четвер", "П'ятница", "Субота" ], "today": "сьогодні", "yesterday": "вчора", "tomorrow": "завтра", "f_past": "Просрочені", "f_today": "Сьогодні і завтра", "f_soon": "Скоро", "action_edit": "Редагувати", "action_note": "Нотатки", "action_delete": "Видалити", "action_priority": "Пріорітет", "action_move": "Перемістити в", "notes": "Нотатки:", "notes_show": "Відобразити", "notes_hide": "Сховати", "list_new": "Новий список", "list_rename": "Переіменувати список", "list_delete": "Видалити список", "list_publish": "Опублікувати список", "list_showcompleted": "Показати завершені завдання", "list_clearcompleted": "Видалити завершені задання", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Всі теги:", "alltags_show": "Вибрати теги", "alltags_hide": "Сховати теги", "a_settings": "Налаштуваня", "rss_feed": "RSS-стрічка", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Налаштування", "set_title": "Заголовок сторінки", "set_title_descr": "(якщо поле на заповнено, то буде використаний заголовок по замовчуванню)", "set_language": "Мова (Language)", "set_protection": "Захист паролем", "set_enabled": "Увімкнено", "set_disabled": "Вимкнено", "set_newpass": "Новий пароль", "set_newpass_descr": "(не заповнюйте поле якщо не хочете змінювати теперішній пароль)", "set_smartsyntax": "Розширений синтаксис", "set_smartsyntax_descr": "(можливість використовувати запис вигляду /пріорітет/завдання/теги/)", "set_timezone": "Time zone", "set_autotag": "Автоматичне присвоєння тегів", "set_autotag_descr": "(автоматичне додавання тегу з фільтру у нові завдання)", "set_sessions": "Збереження сесій", "set_sessions_php": "засобами PHP", "set_sessions_files": "в файлах", "set_firstdayofweek": "Перший день тижня", "set_custom": "Custom", "set_date": "Формат дати", "set_date2": "Short Date format", "set_shortdate": "Скорочений формат дати", "set_clock": "Формат часу", "set_12hour": "12-годинний", "set_24hour": "24-годинний", "set_submit": "Зберегти зміни", "set_cancel": "Відмінити", "set_showdate": "Відображати дату завдання", "confirmDelete": "Чи дійсно Ви хочете видалити це завдання?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "зберегти", "actionNoteCancel": "відмінити", "error": "Помилка", "denied": "Доступ заборонено", "invalidpass": "Невірний пароль", "addList": "Новий список", "addListDefault": "Todo", "renameList": "Переіменувати список", "deleteList": "Чи дійсно Ви хочете видалити список разом з усіма завданнями?", "clearCompleted": "Видалити зі списку всі завершені завдання?", "settingsSaved": "Налаштування збережені. Перезавантаження…" } ================================================ FILE: src/includes/lang/vi.json ================================================ { "_header": { "ver": "v1.3.4", "date": "2010-05-05", "language": "Vietnamese", "original_name": "Tiếng Việt", "author": "AloneRoad", "author_url": "http://aoi.vn" }, "My Tiny Todolist": "Danh sách các công việc cần làm", "htab_newtask": "Công việc", "htab_search": "Tìm kiếm", "btn_add": "Thêm", "btn_search": "Tìm", "advanced_add": "Nâng cao", "searching": "Kết quả tìm kiếm cho từ khóa:", "tasks": "Công việc", "taskdate_inline_created": "created at %s", "taskdate_inline_completed": "Completed at %s", "taskdate_inline_duedate": "Due %s", "taskdate_created": "Ngày tạo", "taskdate_completed": "Ngày hoàn thành", "edit_task": "Chỉnh sửa", "add_task": "Thêm việc mới", "priority": "Độ ưu tiên", "task": "Công việc", "note": "Ghi chú", "tags": "Từ khóa phân loại", "save": "Lưu", "cancel": "Hủy", "password": "Mật khẩu", "btn_login": "Đăng nhập", "a_login": "Đăng nhập", "a_logout": "Đăng xuất", "public_tasks": "Công việc chung", "tagcloud": "Tags", "tagfilter_cancel": "Hủy bộ lọc", "sortByHand": "Xếp thủ công", "sortByPriority": "Xếp theo mức độ ưu tiên", "sortByDueDate": "Xếp theo ngày", "sortByDateCreated": "Sort by date created", "sortByDateModified": "Sort by date modified", "due": "Hạn", "daysago": "%d ngày trước", "indays": "trong %d ngày nữa", "months_short": [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ], "months_long": [ "Tháng 1", "Tháng 2", "Tháng 3", "Tháng 4", "Tháng 5", "Tháng 6", "Tháng 7", "Tháng 8", "Tháng 9", "Tháng 10", "Tháng 11", "Tháng 12" ], "days_min": [ "CN", "T.2", "T.3", "T.4", "T.5", "T.6", "T.7" ], "days_long": [ "Chủ Nhật", "Thứ Hai", "Thứ Ba", "Thứ Tư", "Thứ Năm", "Thứ Sáu", "Thứ Bảy" ], "today": "hôm nay", "yesterday": "hôm qua", "tomorrow": "ngày mai", "f_past": "Quá hạn", "f_today": "Hôm nay và ngày mai", "f_soon": "Cần làm ngay", "action_edit": "Chỉnh sửa", "action_note": "Sửa ghi chú", "action_delete": "Xóa", "action_priority": "Mức độ ưu tiên", "action_move": "Chuyển sang", "notes": "Ghi chú:", "notes_show": "Hiện", "notes_hide": "Ẩn", "list_new": "Danh sách mới", "list_rename": "Đổi tên", "list_delete": "Xóa danh sách", "list_publish": "Công khai danh sách", "list_showcompleted": "Hiển thị các công việc đã làm xong", "list_clearcompleted": "Xóa các công việc đã làm xong", "list_select": "Select list", "list_export": "Export", "list_export_csv": "CSV", "list_export_ical": "iCalendar", "list_rssfeed": "RSS Feed", "alltags": "Toàn bộ các từ khóa dùng để phân loại:", "alltags_show": "Hiển thị toàn bộ", "alltags_hide": "Ẩn toàn bộ", "a_settings": "Thiết lập", "rss_feed": "RSS", "feed_title": "%s", "feed_completed_tasks": "Completed tasks", "feed_modified_tasks": "Modified tasks", "feed_new_tasks": "New tasks", "alltasks": "All tasks", "set_header": "Thiết lập", "set_title": "Tiêu đề", "set_title_descr": "(Thay đổi tiêu đề mặc định ở đây)", "set_language": "Ngôn ngữ", "set_protection": "Sử dụng mật khẩu", "set_enabled": "Kích hoạt", "set_disabled": "Vô hiệu hóa", "set_newpass": "Mật khẩu mới", "set_newpass_descr": "(để trống nếu bạn không muốn đổi mật khẩu đang dùng)", "set_smartsyntax": "Cú pháp thông minh", "set_smartsyntax_descr": "(/độ ưu tiên/công việc/phân loại/)", "set_timezone": "Time zone", "set_autotag": "Tự động phân loại", "set_autotag_descr": "(tự động thêm từ khóa của bộ lọc hiện tại vào danh sách từ khóa phân loại khi tạo một công việc mới)", "set_sessions": "Cơ chế điều khiển phiên làm việc", "set_sessions_php": "PHP", "set_sessions_files": "Files", "set_firstdayofweek": "Ngày bắt đầu của tuần", "set_custom": "Custom", "set_date": "Ngày", "set_date2": "Short Date format", "set_shortdate": "Ngắn gọn", "set_clock": "Đồng hồ", "set_12hour": "Dạng 12 giờ", "set_24hour": "Dạng 24 giờ", "set_submit": "Lưu các thay đổi", "set_cancel": "Hủy bỏ", "set_showdate": "Hiện ngày tháng trong danh sách công việc", "confirmDelete": "Bạn muốn xóa công việc này?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "Lưu", "actionNoteCancel": "Hủy", "error": "Có lỗi đã xảy ra (nhấn vào đây để xem chi tiết)", "denied": "Từ chối truy cập", "invalidpass": "Sai mật khẩu", "addList": "Tạo danh sách mới", "addListDefault": "Todo", "renameList": "Đổi tên", "deleteList": "This will delete current list with all tasks in it.\nAre you sure?", "clearCompleted": "This will delete all completed tasks in the list.\nAre you sure?", "settingsSaved": "Các thiết lập đã lưu thành công..." } ================================================ FILE: src/includes/lang/zh-cn.json ================================================ { "_header": { "ver": "v1.8.2", "date": "2025-07-23", "language": "Chinese Simplified", "original_name": "中文 (简体)", "authors": [ "v1.3 - heraldboy ", "v1.6 - wangyouworld : http://ramble.3vshej.cn", "v1.7 - xhemj : @xhemj", "v1.8.2 wangyouworld : https://blog.3vshej.cn" ] }, "My Tiny Todolist": "我的待办事项列表", "powered_by": "来自", "htab_newtask": "新建任务", "htab_search": "搜索", "btn_add": "添加", "btn_search": "搜索", "advanced_add": "高级", "searching": "正在搜索", "tasks": "任务", "taskdate_inline_created": "创建于 %s", "taskdate_inline_edited": "编辑于 %s", "taskdate_inline_completed": "完成于 %s", "taskdate_inline_duedate": "截止日期为 %s", "taskdate_created": "创建于", "taskdate_edited": "最后编辑于", "taskdate_completed": "完成于", "edit_task": "编辑任务", "add_task": "新建任务", "priority": "优先级", "task": "任务", "note": "备注", "tags": "标签", "list": "清单", "no_note": "无备注", "save": "保存", "cancel": "取消", "close": "关闭", "password": "密码", "btn_login": "登录", "a_login": "登录", "a_logout": "退出", "public_tasks": "公开任务", "tagcloud": "标签", "tagfilter_cancel": "取消筛选", "filterTags": "筛选标签", "showTagsFromAllLists": "显示所有清单的标签", "sortByHand": "手动排序", "sortByTitle": "按标题排序", "sortByPriority": "按优先级排序", "sortByDueDate": "按截止日期排序", "sortByDateCreated": "按创建日期排序", "sortByDateModified": "按修改日期排序", "due": "截止日期", "daysago": "%d 天前", "indays": "%d 天后", "months_short": [ "1 月", "2 月", "3 月", "4 月", "5 月", "6 月", "7 月", "8 月", "9 月", "10 月", "11 月", "12 月" ], "months_long": [ "1 月", "2 月", "3 月", "4 月", "5 月", "6 月", "7 月", "8 月", "9 月", "10 月", "11 月", "12 月" ], "months_calendar": [ "1 月", "2 月", "3 月", "4 月", "5 月", "6 月", "7 月", "8 月", "9 月", "10 月", "11 月", "12 月" ], "days_min": [ "日", "一", "二", "三", "四", "五", "六" ], "days_long": [ "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" ], "today": "今天", "yesterday": "昨天", "tomorrow": "明天", "f_past": "已过期", "f_today": "今天和明天", "f_soon": "即将到期", "action_edit": "编辑", "action_note": "编辑备注", "action_delete": "删除", "action_priority": "优先级", "action_move": "移动到", "action_ok": "确定", "action_cancel": "取消", "notes": "备注:", "notes_show": "显示", "notes_hide": "隐藏", "list_new": "新建清单", "list_rename": "重命名清单", "list_delete": "删除清单", "list_showcompleted": "显示已完成任务", "list_clearcompleted": "清空已完成任务", "list_select": "选择清单", "list_share": "分享", "list_publish": "发布清单", "list_enable_feedkey": "启用订阅密钥", "list_show_feedkey": "显示订阅密钥", "list_rssfeed": "RSS 订阅", "list_export_to_csv": "导出为 CSV", "list_export_to_ical": "导出为 iCalendar", "list_hide": "隐藏清单", "alltags": "所有标签:", "alltags_show": "显示全部", "alltags_hide": "隐藏全部", "a_settings": "设置", "rss_feed": "RSS 订阅", "feed_title": "%s", "feed_completed_tasks": "已完成任务", "feed_modified_tasks": "已修改任务", "feed_new_tasks": "新任务", "feed_tasks": "任务", "feed_status_new": "新建", "feed_status_updated": "已更新", "feed_status_completed": "已完成", "alltasks": "所有任务", "set_header": "设置", "set_title": "标题", "set_title_descr": "指定默认标题。", "set_language": "语言", "set_protection": "密码保护", "set_enabled": "启用", "set_disabled": "禁用", "set_newpass": "新密码", "set_newpass_descr": "不修改密码请留空", "set_smartsyntax": "智能语法", "set_smartsyntax3_descr": "示例: +1 任务名 #1 标签 #2 @duedate", "set_timezone": "时区", "set_autotag": "自动标记", "set_autotag_descr": "自动将当前标签过滤添加到新创建的任务中。", "set_markdown": "Markdown", "set_markdown_descr": "添加支持 Markdown 的功能在备注中;禁用,则使用旧的标记方式。", "set_firstdayofweek": "一周的第一天是", "set_custom": "自定义", "set_date": "日期格式", "set_date2": "短日期格式", "set_shortdate": "短日期(当年)", "set_clock": "时间格式", "set_12hour": "12 小时制", "set_24hour": "24 小时制", "set_submit": "提交更改", "set_cancel": "取消", "set_showdate": "在列表中显示任务日期", "set_showtime": "显示时间", "set_showdate_inline": "内联显示日期", "set_exactduedate": "始终将截止日期显示为具体日期", "set_appearance": "外观", "set_appearance_system": "跟随系统", "set_appearance_light": "浅色", "set_appearance_dark": "深色", "set_newtaskcounter_h": "新任务计数器", "set_newtaskcounter": "检查新任务", "set_newtaskcountericon": "在网站图标显示计数器", "set_extensions": "扩展功能", "set_activate": "激活", "set_deactivate": "停用", "confirmDelete": "确认删除此任务?", "confirmLeave": "可能存在未保存的数据,确定要离开吗?", "actionNoteSave": "保存", "actionNoteCancel": "取消", "error": "发生错误(点击查看详情)", "denied": "访问被拒绝", "listNotFound": "清单未找到", "noPublicLists": "无公开任务", "noTags": "无标签", "withoutTags": "无标签", "withAnyTag": "任意标签", "invalidpass": "密码错误", "addList": "创建新清单", "addListDefault": "待办事项", "renameList": "重命名清单", "deleteList": "将删除当前清单及其所有任务\n确定继续?", "clearCompleted": "将清空本清单所有已完成任务\n确定继续?", "settingsSaved": "设置已保存,正在重新加载..." } ================================================ FILE: src/includes/lang/zh-tw.json ================================================ { "_header": { "ver": "v1.8.2", "date": "2025-07-23", "language": "Chinese Traditional", "original_name": "中文(繁體)", "authors": [ "v1.3.6 DonaldIsFreak : http://donaldknuth.blogspot.com/", "v1.8.2 wangyouworld : https://blog.3vshej.cn" ] }, "My Tiny Todolist": "我的待辦事項清單", "powered_by": "來自", "htab_newtask": "新增任務", "htab_search": "搜尋", "btn_add": "新增", "btn_search": "搜尋", "advanced_add": "進階", "searching": "正在搜尋", "tasks": "任務", "taskdate_inline_created": "建立於 %s", "taskdate_inline_edited": "編輯於 %s", "taskdate_inline_completed": "完成於 %s", "taskdate_inline_duedate": "截止日期 %s", "taskdate_created": "建立時間", "taskdate_edited": "最後編輯", "taskdate_completed": "完成時間", "edit_task": "編輯任務", "add_task": "新增任務", "priority": "優先級", "task": "任務", "note": "備註", "tags": "標籤", "list": "清單", "no_note": "無備註", "save": "儲存", "cancel": "取消", "close": "關閉", "password": "密碼", "btn_login": "登入", "a_login": "登入", "a_logout": "登出", "public_tasks": "公開任務", "tagcloud": "標籤雲", "tagfilter_cancel": "取消篩選", "filterTags": "篩選標籤", "showTagsFromAllLists": "顯示所有清單的標籤", "sortByHand": "手動排序", "sortByTitle": "按標題排序", "sortByPriority": "按優先級排序", "sortByDueDate": "按截止日期排序", "sortByDateCreated": "按建立日期排序", "sortByDateModified": "按修改日期排序", "due": "截止", "daysago": "%d 天前", "indays": "%d 天後", "months_short": [ "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月" ], "months_long": [ "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" ], "months_calendar": [ "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月" ], "days_min": [ "日", "一", "二", "三", "四", "五", "六" ], "days_long": [ "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" ], "today": "今天", "yesterday": "昨天", "tomorrow": "明天", "f_past": "已逾期", "f_today": "今天和明天", "f_soon": "即將到期", "action_edit": "編輯", "action_note": "編輯備註", "action_delete": "刪除", "action_priority": "優先級", "action_move": "移動到", "action_ok": "確定", "action_cancel": "取消", "notes": "備註:", "notes_show": "顯示", "notes_hide": "隱藏", "list_new": "新增清單", "list_rename": "重新命名清單", "list_delete": "刪除清單", "list_showcompleted": "顯示已完成任務", "list_clearcompleted": "清除已完成任務", "list_select": "選擇清單", "list_share": "分享", "list_publish": "發佈清單", "list_enable_feedkey": "啟用訂閱金鑰", "list_show_feedkey": "顯示訂閱金鑰", "list_rssfeed": "RSS訂閱", "list_export_to_csv": "匯出為CSV", "list_export_to_ical": "匯出為iCalendar", "list_hide": "隱藏清單", "alltags": "所有標籤:", "alltags_show": "顯示全部", "alltags_hide": "隱藏全部", "a_settings": "設定", "rss_feed": "RSS訂閱", "feed_title": "%s", "feed_completed_tasks": "已完成任務", "feed_modified_tasks": "已修改任務", "feed_new_tasks": "新任務", "feed_tasks": "任務", "feed_status_new": "新建", "feed_status_updated": "已更新", "feed_status_completed": "已完成", "alltasks": "所有任務", "set_header": "設定", "set_title": "標題", "set_title_descr": "如需修改預設標題請填寫此項", "set_language": "語言", "set_protection": "密碼保護", "set_enabled": "啟用", "set_disabled": "禁用", "set_newpass": "新密碼", "set_newpass_descr": "若不修改密碼請留空", "set_smartsyntax": "智能語法", "set_smartsyntax3_descr": "範例:+1 任務標題 #標籤1 #標籤2 @截止日期", "set_timezone": "時區", "set_autotag": "自動標記", "set_autotag_descr": "自動為新建任務添加當前標籤篩選器的標籤", "set_markdown": "Markdown", "set_markdown_descr": "在備註中支援Markdown語法,如需使用舊版標記請禁用", "set_firstdayofweek": "週起始日", "set_custom": "自訂", "set_date": "日期格式", "set_date2": "短日期格式", "set_shortdate": "短日期(當年)", "set_clock": "時鐘格式", "set_12hour": "12小時制", "set_24hour": "24小時制", "set_submit": "提交變更", "set_cancel": "取消", "set_showdate": "在清單中顯示任務日期", "set_showtime": "顯示時間", "set_showdate_inline": "內聯顯示日期", "set_exactduedate": "始終顯示具體截止日期", "set_appearance": "外觀", "set_appearance_system": "跟隨系統", "set_appearance_light": "淺色", "set_appearance_dark": "深色", "set_newtaskcounter_h": "新任務計數器", "set_newtaskcounter": "檢查新任務", "set_newtaskcountericon": "在網站圖示顯示計數器", "set_extensions": "擴充功能", "set_activate": "啟用", "set_deactivate": "停用", "confirmDelete": "確認刪除該任務?", "confirmLeave": "存在未儲存資料,確定要離開嗎?", "actionNoteSave": "儲存", "actionNoteCancel": "取消", "error": "發生錯誤(點擊查看詳情)", "denied": "存取被拒絕", "listNotFound": "清單未找到", "noPublicLists": "無公開任務", "noTags": "無標籤", "withoutTags": "無標籤", "withAnyTag": "任意標籤", "invalidpass": "密碼錯誤", "addList": "建立新清單", "addListDefault": "待辦事項", "renameList": "重新命名清單", "deleteList": "將刪除當前清單及其所有任務\n確定繼續?", "clearCompleted": "將清空清單中所有已完成任務\n確定繼續?", "settingsSaved": "設定已儲存,正在重新載入..." } ================================================ FILE: src/includes/markup.commonmark.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ require_once(MTTINC. 'vendor/autoload.php'); use League\CommonMark\MarkdownConverter; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use League\CommonMark\Extension\Mention\MentionExtension; use League\CommonMark\Extension\Mention\Mention; class MTTCommonmarkWrapper implements MTTMarkdownInterface { protected $toExternal; /** @var MarkdownConverter */ protected $converter; function __construct() { $this->toExternal = false; $environment = new Environment([ 'html_input' => 'escape', 'allow_unsafe_links' => false, 'mentions' => [ 'task_id' => [ 'prefix' => '#', 'pattern' => '\d+', 'generator' => function ($mention) { if (!($mention instanceof Mention)) { return null; } $mention->setUrl(\sprintf(get_mttinfo('url'). "?task=%d", $mention->getIdentifier())); if (!$this->toExternal) { $mention->data->append('attributes/class', 'mtt-link-to-task'); $mention->data->append('attributes/data-target-id', $mention->getIdentifier()); } return $mention; }, ] ], ]); $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new GithubFlavoredMarkdownExtension()); $environment->addExtension(new MentionExtension()); $this->converter = new MarkdownConverter($environment); } public function convert(string $s, bool $toExternal = false) { $this->toExternal = $toExternal; return (string) $this->converter->convert($s); } } ================================================ FILE: src/includes/markup.parsedown.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ # We do not use composer autoloader because only one class is declared in Parsedown. require_once(MTTINC. 'vendor/erusev/parsedown/Parsedown.php'); class MTTParsedownWrapper implements MTTMarkdownInterface { /** @var MTTParsedown */ protected $converter; function __construct() { $this->converter = new MTTParsedown(); $this->converter->setSafeMode(true); //$this->converter->setBreaksEnabled(true); } public function convert(string $s, bool $toExternal = false) { $this->converter->setToExternal($toExternal); return $this->converter->text($s); } } class MTTParsedown extends Parsedown { protected $toExternal; function __construct() { $this->toExternal = false; $this->InlineTypes['#'][]= 'TaskId'; $this->inlineMarkerList .= '#'; } public function setToExternal(bool $v) { $this->toExternal = $v; } protected function inlineTaskId($excerpt) { if (preg_match('/^#(\d+)/', $excerpt['text'], $matches)) { $attrs = array( 'href' => get_mttinfo('url'). '?task='. $matches[1], 'target' => '_blank', ); if (!$this->toExternal) { $attrs['class'] = 'mtt-link-to-task'; $attrs['data-target-id'] = $matches[1]; } return array( // How many characters to advance the Parsedown's // cursor after being done processing this tag. 'extent' => strlen($matches[0]), 'element' => array( 'name' => 'a', 'text' => '#'. $matches[1], 'attributes' => $attrs, ), ); } } protected function inlineLink($Excerpt) { $a = parent::inlineLink($Excerpt); if (is_array($a) && isset($a['element']['attributes']['href'])) { $a['element']['attributes']['target'] = '_blank'; } return $a; } protected function inlineUrl($Excerpt) { $a = parent::inlineUrl($Excerpt); if (is_array($a) && isset($a['element']['attributes']['href'])) { $a['element']['attributes']['target'] = '_blank'; } return $a; } } ================================================ FILE: src/includes/markup.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ require_once(MTTINC. 'markup.parsedown.php'); //require_once(MTTINC. 'markup.commonmark.php'); interface MTTMarkdownInterface { public function convert(string $s, bool $toExternal = false); } final class MTTMarkdown { /** @var MTTMarkdownInterface */ private static $instance; /** @var string */ private static $instanceClass = MTTParsedownWrapper::class; //private static $instanceClass = MTTCommonmarkWrapper::class; /** * * @return MTTMarkdownInterface */ public static function instance() : MTTMarkdownInterface { if (isset(self::$instance)) return self::$instance; // if (do_filter('markdownConverterClass', self::$instanceClass, $newClass) && $newClass) { // self::setInstanceClass($newClass); // } self::$instance = new self::$instanceClass(); return self::$instance; } public static function setInstanceClass(string $class) { if (!is_a($class, MTTMarkdownInterface::class, true)) { throw new Exception("Class '$class' is not a MTTMarkdownInterface"); } self::$instanceClass = $class; self::$instance = null; } } interface MTTTitleMarkupInterface { public function convert(string $title): string; } class MTTTitleMarkupConverter implements MTTTitleMarkupInterface { public function convert(string $title): string { //escape all unsafe $title = htmlspecialchars($title, ENT_QUOTES); // make links from text starting with 'www.' $title = preg_replace( "/(^|\s|>)(www\.([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/iu" , '$1$2$4' , $title ); // make link from text starting with protocol like 'http://' $title = preg_replace( "/(^|\s|>)([a-z]+:\/\/([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/iu" , '$1$2$4' , $title ); return $title; } } final class MTTTitleMarkup { /** @var MTTTitleMarkupInterface */ private static $instance; /** @var string */ private static $instanceClass = MTTTitleMarkupConverter::class; public static function instance() : MTTTitleMarkupInterface { if (isset(self::$instance)) return self::$instance; self::$instance = new self::$instanceClass(); return self::$instance; } public static function setInstanceClass(string $class) { if (!is_a($class, MTTTitleMarkupInterface::class, true)) { throw new Exception("Class '$class' is not a MTTTitleMarkupInterface"); } self::$instanceClass = $class; self::$instance = null; } } function noteMarkup($note, $toExternal = false) { if ($note === null) { $note = ''; } if (Config::get('markup') == 'v1') { return mttMarkup_v1($note); } return markdownToHtml($note, $toExternal); } function markdownToHtml($s, $toExternal = false) { return MTTMarkdown::instance()->convert($s, $toExternal); } // Convert note's raw text to html with allowed elements (b,i,u,s and raw urls) function mttMarkup_v1($s) { //hide allowed elements from escaping $c1 = chr(1); $c2 = chr(2); $s = preg_replace("~([\s\S]*?)~i", "{$c1}b{$c2}\$1{$c1}/b{$c2}", $s); $s = preg_replace("~([\s\S]*?)~i", "{$c1}i{$c2}\$1{$c1}/i{$c2}", $s); $s = preg_replace("~([\s\S]*?)~i", "{$c1}u{$c2}\$1{$c1}/u{$c2}", $s); $s = preg_replace("~([\s\S]*?)~i", "{$c1}s{$c2}\$1{$c1}/s{$c2}", $s); $s = htmlspecialchars($s, ENT_QUOTES); //escape all elements, except above $s = str_replace( [$c1, $c2], ['<','>'], $s ); //unhide $s = nl2br($s); // make links from text starting with 'www.' $s = preg_replace( "/(^|\s|>)(www\.([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/iu" , '$1$2$4' , $s ); // make link from text starting with protocol like 'http://' $s = preg_replace( "/(^|\s|>)([a-z]+:\/\/([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/iu" , '$1$2$4' , $s ); return $s; } // Convert raw title to html with allowed urls function titleMarkup($title) { return MTTTitleMarkup::instance()->convert($title); } ================================================ FILE: src/includes/notifications.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class MTTNotificationCenter { /** * @var array */ private static $observers = []; /** * @param string $notification * @param MTTNotificationObserverInterface $observer * @return void */ public static function addObserverForNotification(string $notification, MTTNotificationObserverInterface $observer) { if (!isset(self::$observers[$notification])) { self::$observers[$notification] = []; } if (!in_array($observer, self::$observers[$notification])) { // do not duplicate same observer self::$observers[$notification][] = $observer; } } /** * @param string[] $notifications * @param MTTNotificationObserverInterface $observer * @return void */ public static function addObserverForNotifications(array $notifications, MTTNotificationObserverInterface $observer) { foreach ($notifications as $notification) { self::addObserverForNotification($notification, $observer); } } /** * * @param string $notification * @param callable $callback * @return void */ public static function addCallbackForNotification(string $notification, callable $callback) { if (!isset(self::$observers[$notification])) { self::$observers[$notification] = []; } self::$observers[$notification][] = $callback; } /** * * @param string $notification * @return bool */ public static function hasObserversForNotification(string $notification): bool { if (isset(self::$observers[$notification]) && count(self::$observers[$notification]) > 0) { return true; } return false; } public static function postNotification(string $notification, $object) { if (!isset(self::$observers[$notification])) { return; // No observers for this notification } foreach (self::$observers[$notification] as $observer) { if ($observer instanceof MTTNotificationObserverInterface) { $observer->notification($notification, $object); } else { $observer($object); } } } /** * Run this near exit() * @return void */ public static function postDidFinishRequestNotification() { if ( ! isset(self::$observers[MTTNotification::didFinishRequest]) ) { return; // No observers for didFinishRequest } if (function_exists('fastcgi_finish_request')) { if (session_status() == PHP_SESSION_ACTIVE) { session_write_close(); // Close active session } fastcgi_finish_request(); } self::postNotification(MTTNotification::didFinishRequest, null); } } interface MTTNotificationObserverInterface { function notification(string $notification, $object); } // Enum abstract class MTTNotification { const didFinishRequest = 'didFinishRequest'; const didCreateTask = 'didCreateTask'; const didEditTask = 'didEditTask'; const didDeleteTask = 'didDeleteTask'; const didCompleteTask = 'didCompleteTask'; const didCreateList = 'didCreateList'; const didDeleteList = 'didDeleteList'; const didDeleteCompletedInList = 'didDeleteCompletedInList'; } function add_action(string $notification, callable $callback) { MTTNotificationCenter::addCallbackForNotification($notification, $callback); } function do_action(string $notification, $object = null) { MTTNotificationCenter::postNotification($notification, $object); } ================================================ FILE: src/includes/smartsyntax.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ class MTTSmartSyntax implements MTTSmartSyntaxInterface { protected $tagPrefix = '#'; protected $duedatePrefix = '@!'; protected $weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; //3-letter not present in lang /** @var MTTSmartSyntaxInterface */ protected static $instance; public static function instance(): MTTSmartSyntaxInterface { if (!isset(static::$instance)) { static::$instance = new static(); } return static::$instance; } public function parse(string $title): array { $a = [ 'prio' => 0, 'title' => $title, 'tags' => '', 'duedate' => null, ]; // priority 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]); } // duedate if ( preg_match("|(.+)[{$this->duedatePrefix}]{1}(\S+)$|", $a['title'], $m) ) { $rest = $m[1]; $duepre = $m[2]; $duedate = $this->findDuedate($duepre); if ($duedate) { $a['duedate'] = $duedate; $a['title'] = $rest; } } // tags $tags = []; $a['title'] = trim( preg_replace_callback( "/(?:^|\s+)[{$this->tagPrefix}]{1}([^{$this->tagPrefix}\s]+)/", function ($matches) use (&$tags) { $tags[] = $matches[1]; return ''; }, $a['title'] ) ); if (count($tags) > 0) { $a['tags'] = implode( ',' , $tags ); } return $a; } private function findDuedate(string $s): ?string { if (preg_match("|^(\d+)([dwmy]{1})$|",$s, $m)) { // 5d,2w... $count = (int)$m[1]; $period = $m[2]; if ($period == 'd' || $period == 'w') { // days, weeks if ($period == 'w') $count *= 7; return date("Y-m-d", time() + 86400*$count); } else if ($period == 'm' || $period == 'y') { //months,years if ($period == 'y') $count *= 12; $a = explode(',', date('Y,m,d')); $y = (int)$a[0]; $m = (int)$a[1] + $count; $d = (int)$a[2]; if ($m > 12) { $yy = (int)floor($m/12); $y += $yy; $m = $m - $yy*12; } $d = min($d, daysInMonth($m, $y)); return "$y-$m-$d"; } } if (null !== $duedate = $this->parseDuedate($s)) { return $duedate; } $lang = Lang::instance(); //TODO: add 3-letter short? $needle = mb_strtolower($s); $wd = null; foreach ($lang->get('days_min') as $idx => $weekday) { if ($needle === mb_strtolower($weekday)) { $wd = $idx; break; } } if (null === $wd) { foreach ($this->weekdays as $idx => $weekday) { if ($needle === $weekday) { $wd = $idx; break; } } } if (null === $wd) { foreach ($lang->get('days_long') as $idx => $weekday) { if ($needle === mb_strtolower($weekday)) { $wd = $idx; break; } } } if (null !== $wd) { $curWD = (int)date('w'); $daysAdd = 0; if ($wd <= $curWD) { //next week $daysAdd = 7 - ($curWD - $wd); } else { //current week $daysAdd = $wd - $curWD; } $oDue = new DateTime(); $oDue->add( new DateInterval("P{$daysAdd}D") ); return $oDue->format('Y-m-d'); } return null; } /** * Try to parse input string as a duedate and return in format "Y-m-d". * Return null if fail. * @param string $s * @return null|string */ public static function parseDuedate(string $s): ?string { $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"; } } interface MTTSmartSyntaxInterface { public function parse(string $title): array; } function parseSmartSyntax(string $title): ?array { $a = MTTSmartSyntax::instance()->parse($title); do_filter('parseSmartSyntax', $title, $a); return $a; } ================================================ FILE: src/includes/theme.php ================================================ <?php mttinfo('title'); ?> rtl()) echo 'dir="rtl"'; ?>>

    ================================================ FILE: src/includes/version.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ $checkDbExists = true; require_once('./init.php'); //Parse query string if ( isset($_SERVER['QUERY_STRING']) && $_SERVER['QUERY_STRING'] != '' ) { parseRoute($_SERVER['QUERY_STRING']); } $lang = Lang::instance(); if ($lang->rtl()) { Config::set('rtl', 1); } if (!is_int(Config::get('firstdayofweek')) || Config::get('firstdayofweek')<0 || Config::get('firstdayofweek')>6) { Config::set('firstdayofweek', 1); } if ( access_token() == '' ) { update_token(); } if (MTT_THEME != 'theme') { // custom theme require(MTT_THEME_PATH. 'index.php'); } else { require(MTTINC. 'theme.php'); } MTTNotificationCenter::postDidFinishRequestNotification(); // end function parseRoute($queryString) { parse_str($queryString, $q); if (isset($q['list'])) { $hash = ($q['list'] == 'alltasks') ? ['alltasks'] : ['list', (int)$q['list']]; unset($q['list']); redirectWithHashRoute($hash, $q); } else if (isset($q['task'])) { $listId = (int)DBCore::default()->getListIdByTaskId((int)$q['task']); if ($listId > 0) { $h = [ 'list', $listId, 'search', '#'. (int)$q['task']]; redirectWithHashRoute($h); } // TODO: not found } } function redirectWithHashRoute(array $hash, array $q = []) { $url = get_unsafe_mttinfo('url'); $query = http_build_query($q); if ($query != '') $url .= "?$query"; if (count($hash) > 0) { $encodedHash = implode("/", array_map("rawurlencode", $hash)); $url .= "#$encodedHash"; } header("Location: ". $url); exit; } function js_options() { // Here we can use URIs instead of full URLs. $homeUrl = htmlspecialchars(Config::getUrl('url')); if ($homeUrl == '') { $homeUrl = get_mttinfo('mtt_uri'); } $a = array( "token" => htmlspecialchars(access_token()), "title" => get_unsafe_mttinfo('title'), "lang" => Lang::instance()->jsStrings(), "mttUrl" => get_mttinfo('mtt_uri'), "homeUrl" => $homeUrl, "apiUrl" => get_mttinfo('api_url'), "needAuth" => need_auth() ? true : false, "isLogged" => is_logged() ? true : false, "showdate" => Config::get('showdate') ? true : false, "showtime" => Config::get('showtime') ? true : false, "showdateInline" => Config::get('showdateInline') ? true : false, "duedatepickerformat" => htmlspecialchars(Config::get('dateformat2')), "firstdayofweek" => (int) Config::get('firstdayofweek'), "calendarIcon" => get_mttinfo('theme_url'). 'images/calendar.svg', "autotag" => Config::get('autotag') ? true : false, "markdown" => Config::get('markup') == 'v1' ? false : true, "newTaskCounter" => Config::get('newTaskCounter') ? true : false, "newTaskCounterIcon" => Config::get('newTaskCounterIcon') ? true : false, ); $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE; if (MTT_DEBUG) { $flags |= JSON_PRETTY_PRINT; } $json = json_encode($a, $flags); if ($json === false) { error_log("MTT Error: Failed to encode array of options to JSON. Code: ". (int)json_last_error()); echo "{}"; } else { echo $json; } } ================================================ FILE: src/init.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ if (version_compare(PHP_VERSION, '7.2.0') < 0) { die("PHP 7.2 or above is required"); } if(!defined('MTTPATH')) define('MTTPATH', dirname(__FILE__) .'/'); if(!defined('MTTINC')) define('MTTINC', MTTPATH. 'includes/'); if(!defined('MTT_CONTENT_PATH')) define('MTT_CONTENT_PATH', MTTPATH. 'content/'); requireConfig(); if (!defined('MTT_THEME')) { define('MTT_THEME', 'theme'); } define('MTT_THEME_PATH', MTT_CONTENT_PATH. MTT_THEME. '/'); if (getenv('MTT_ENABLE_DEBUG') == 'YES' || (defined('MTT_DEBUG') && MTT_DEBUG) ) { if (!defined('MTT_DEBUG')) define('MTT_DEBUG', true); error_reporting(E_ALL); ini_set('display_errors', '1'); ini_set('log_errors', '1'); } else { //ini_set('display_errors', '0'); //ini_set('log_errors', '1'); if (!defined('MTT_DEBUG')) define('MTT_DEBUG', false); } require_once(MTTINC. 'common.php'); require_once(MTTINC. 'classes.php'); require_once(MTTINC. 'version.php'); require_once(MTTINC. 'class.dbconnection.php'); require_once(MTTINC. 'class.dbcore.php'); require_once(MTTINC. 'class.config.php'); require_once(MTTINC. 'notifications.php'); require_once(MTTINC. 'filters.php'); require_once(MTTINC. 'markup.php'); configureDbConnection(); Config::load(); date_default_timezone_set(Config::get('timezone')); //User can override language setting by cookies or query $forceLang = ''; if (isset($_COOKIE['lang'])) $forceLang = (string) $_COOKIE['lang']; //else if (isset($_GET['lang'])) $forceLang = (string) $_GET['lang']; if ( $forceLang !== '' && preg_match("/^[a-z-]+$/i", $forceLang) ) { Config::set('lang', $forceLang); //TODO: special for demo, do not change config } require_once(MTTINC. 'class.lang.php'); Lang::loadLang( Config::get('lang') ); $_mttinfo = array(); if (need_auth() && !isset($dontStartSession) && !Config::$noDatabase) { setup_and_start_session(); } set_nocache_headers(); if (!defined('MTT_DISABLE_EXT')) { define('MTT_EXT', MTTPATH . 'ext/'); loadExtensions(); } function requireConfig() { $exists = file_exists(MTTPATH. 'config.php'); $defined = false; if ($exists) { require_once(MTTPATH. 'config.php'); $defined = defined('MTT_DB_TYPE'); } # It seems not installed if (!$defined) { die("Not installed. Run setup.php first."); } } function configureDbConnection() { # MySQL Database Connection if (MTT_DB_TYPE == 'mysql') { if (defined('MTT_DB_DRIVER') && MTT_DB_DRIVER == 'mysqli') { require_once(MTTINC. 'class.db.mysqli.php'); $db = new Database_Mysqli(); } else { require_once(MTTINC. 'class.db.mysql.php'); $db = new Database_Mysql(); } DBConnection::init($db); try { $db->connect([ 'host' => MTT_DB_HOST, 'user' => MTT_DB_USER, 'password' => MTT_DB_PASSWORD, 'db' => MTT_DB_NAME, ]); } catch(Exception $e) { logAndDie("Failed to connect to mysql database: ". $e->getMessage()); } $db->dq("SET NAMES utf8mb4"); } # PostgreSQL Database else if (MTT_DB_TYPE == 'postgres') { require_once(MTTINC. 'class.db.postgres.php'); $db = DBConnection::init(new Database_Postgres()); try { $db->connect([ 'host' => MTT_DB_HOST, 'user' => MTT_DB_USER, 'password' => MTT_DB_PASSWORD, 'db' => MTT_DB_NAME, ]); } catch(Exception $e) { $errlog = "Failed to connect to PostgreSQL database: ". $e->getMessage(); if (MTT_DEBUG) { logAndDie($errlog); } else { logAndDie("Failed to connect to database", $errlog); } } $db->dq("SET NAMES 'utf8'"); } # SQLite3 Database elseif (MTT_DB_TYPE == 'sqlite') { require_once(MTTINC. 'vendor/autoload.php'); require_once(MTTINC. 'class.db.sqlite3.php'); $db = DBConnection::init(new Database_Sqlite3()); $db->connect([ 'filename' => MTTPATH. 'db/todolist.db' ]); } else { die("Incorrect database connection config"); } DBConnection::setTablePrefix(MTT_DB_PREFIX); DBCore::setDefaultInstance(new DBCore($db)); # Check tables created global $checkDbExists; if (!Config::$noDatabase && isset($checkDbExists) && $checkDbExists) { $exists = $db->tableExists($db->prefix.'settings'); if (!$exists) { die("Need to create or update the database. Run setup.php first."); } } } function need_auth(): bool { return (Config::get('password') != '') ? true : false; } function is_logged(): bool { if ( !need_auth() ) return true; if ( !isset($_SESSION['logged']) || !isset($_SESSION['sign']) ) return false; if ( !(int)$_SESSION['logged'] ) return false; return isValidSignature($_SESSION['sign'], session_id(), Config::get('password'), defined('MTT_SALT') ? MTT_SALT : ''); } function is_readonly(): bool { if ( !is_logged() ) return true; return false; } function updateSessionLogged(bool $logged) { if ($logged) { $_SESSION['logged'] = 1; $_SESSION['sign'] = idSignature(session_id(), Config::get('password'), defined('MTT_SALT') ? MTT_SALT : ''); } else { unset($_SESSION['logged']); unset($_SESSION['sign']); } } function access_token(): string { if ( need_auth() ) { if (!isset($_SESSION)) return ''; return $_SESSION['token'] ?? ''; } else { if (!isset($_COOKIE)) return ''; return $_COOKIE['mtt-token'] ?? ''; } } /** * Check if HTTP request have required MTT-Token header with value * the same as stored in session (if password set) or mtt-token cookie (if no password). * Prohibits further execution if no tokens are found. * @return void */ 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! No token provided."); } } function update_token(): string { $token = generateUUID(); if ( need_auth() ) { $_SESSION['token'] = $token; } else { if (PHP_VERSION_ID < 70300) { setcookie('mtt-token', $token, 0, url_dir(get_unsafe_mttinfo('mtt_url')). '; samesite=lax', '', false, true ); } else { /** @disregard P1006 available in php 7.3 */ setcookie('mtt-token', $token, [ 'path' => url_dir(get_unsafe_mttinfo('mtt_url')), 'httponly' => true, 'samesite' => 'lax' ]); } $_COOKIE['mtt-token'] = $token; } return $token; } function setup_and_start_session() { require_once(MTTINC. 'class.sessionhandler.php'); session_set_save_handler(new MTTSessionHandler()); ini_set('session.use_cookies', true); ini_set('session.use_only_cookies', true); /* After any request we may have 14 days of inactivity (i.e. not requesting session data), then we have to re-login (look at MTTSessionHandler). Activity without re-login lasts for max 60 days, the cookie lifetime, then cookie dies and we have to re-login having new session id. */ $lifetime = 5184000; # 60 days session cookie lifetime $path = url_dir(Config::get('url')=='' ? getRequestUri() : Config::getUrl('url')); if (PHP_VERSION_ID < 70300) { # this is a known samesite flag workaround, was fixed in 7.3 session_set_cookie_params($lifetime, $path. '; samesite=lax', null, null, true); } else { /** @disregard P1006 available in php 7.3 */ session_set_cookie_params([ 'lifetime' => $lifetime, 'path' => $path, 'httponly' => true, 'samesite' => 'lax' ]); } session_name('mtt-session'); session_start(); } function timestampToDatetime($timestamp, $forceTime = false) : string { $format = Config::get('dateformat'); if ($forceTime || Config::get('showtime')) { $format .= ' '. (Config::get('clock') == 12 ? 'g:i A' : 'H:i'); } return formatTime($format, $timestamp); } function formatTime($format, $timestamp=0) : string { $lang = Lang::instance(); if($timestamp == 0) $timestamp = time(); $newformat = strtr($format, array('F'=>'%1', 'M'=>'%2')); $adate = explode(',', date('n,'.$newformat, $timestamp), 2); $s = $adate[1]; if($newformat != $format) { $am = (int)$adate[0]; $ml = $lang->get('months_long'); $ms = $lang->get('months_short'); $F = $ml[$am-1]; $M = $ms[$am-1]; $s = strtr($s, array('%1'=>$F, '%2'=>$M)); } return $s; } function _e(string $s) { echo __($s, true); } function __(string $s, bool $escape = false, ?string $arg = null) { $v = Lang::instance()->get($s); if (null !== $arg) { $v = sprintf($v, $arg); } return $escape ? htmlspecialchars($v) : $v; } function mttinfo($v) { echo get_mttinfo($v); } function get_mttinfo($v) { return htmlspecialchars( get_unsafe_mttinfo($v) ); } /* * Returned values from get_unsafe_mttinfo() can be unsafe for html. * But '\r' and '\n' in URLs taken from config are removed. */ function get_unsafe_mttinfo($v) { global $_mttinfo; if (isset($_mttinfo[$v])) { return $_mttinfo[$v]; } switch($v) { case 'theme_url': $_mttinfo['theme_url'] = get_unsafe_mttinfo('mtt_uri'). 'content/'. MTT_THEME. '/'; return $_mttinfo['theme_url']; case 'content_url': $_mttinfo['content_url'] = get_unsafe_mttinfo('mtt_uri'). 'content/'; return $_mttinfo['content_url']; case 'url': /* full url to homepage: directory with root index.php or custom index file in the root. */ /* ex: http://my.site/mytinytodo/ or https://my.site/mytinytodo/home_for_2nd_theme.php */ /* Should not contain a query string. Have to be set in config if custom port is used or wrong detection. */ $_mttinfo['url'] = Config::getUrl('url'); if ($_mttinfo['url'] == '') { $is_https = is_https(); $_mttinfo['url'] = ($is_https ? 'https://' : 'http://'). $_SERVER['HTTP_HOST']. url_dir(getRequestUri()); } return $_mttinfo['url']; case 'mtt_url': /* 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'), false ); } return $_mttinfo['mtt_url']; case 'mtt_uri': $_mttinfo['mtt_uri'] = Config::getUrl('mtt_url'); // need to have a trailing slash if ($_mttinfo['mtt_uri'] == '') { if ( '' != $url = Config::getUrl('url') ) { $_mttinfo['mtt_uri'] = url_dir($url); } else { $_mttinfo['mtt_uri'] = url_dir(getRequestUri()); } } return $_mttinfo['mtt_uri']; 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'] == '') { if (defined('MTT_API_USE_PATH_INFO')) { $_mttinfo['api_url'] = get_unsafe_mttinfo('mtt_uri'). 'api/'; } else { $_mttinfo['api_url'] = get_unsafe_mttinfo('mtt_uri'). 'api.php?_path=/'; } } return $_mttinfo['api_url']; case 'title': $_mttinfo['title'] = (Config::get('title') != '') ? Config::get('title') : __('My Tiny Todolist'); return $_mttinfo['title']; case 'version': $_mttinfo['version'] = mytinytodo\Version::VERSION; return $_mttinfo['version']; case 'appearance': $_mttinfo['appearance'] = Config::get('appearance'); return $_mttinfo['appearance']; } } function reset_mttinfo($key) { global $_mttinfo; unset( $_mttinfo[$key] ); } function is_https(): bool { if (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') { return true; } if (defined('MTT_USE_HTTPS') && MTT_USE_HTTPS) { return true; } // This HTTP header can be overriden by user agent! if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') { return true; } return false; } function set_nocache_headers() { // little more info at https://www.php.net/manual/en/function.session-cache-limiter.php header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); header('Expires: Wed, 29 Apr 2009 10:00:00 GMT'); header('Pragma: no-cache'); // for old HTTP/1.0 intermediate caches } function jsonExit($data) { header('Content-type: application/json; charset=utf-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); MTTNotificationCenter::postDidFinishRequestNotification(); exit; } function logAndDie($userText, $errText = null) { $errText === null ? error_log($userText) : error_log($errText); if (ini_get('display_errors')) { echo htmlspecialchars($userText); } else { echo "Error! See details in error log."; } exit(1); } function loadExtensions() { $a = Config::get('extensions') ?: null; if (!$a || !is_array($a)) { return; } if (!array_is_list($a)) { $a = array_values($a); } foreach ($a as $ext) { if (is_string($ext)) { try { MTTExtensionLoader::loadExtension($ext); } catch (MTTExtensionLoaderException $e) { error_log($e->getMessage()); } catch (Exception $e) { if (MTT_DEBUG) throw $e; else error_log("Error while loading extension '$ext': ". $e->getMessage()); } } } } function get_filever(string $dir, string $filename, ?string $ext = null) { if (!MTT_DEBUG) { return get_mttinfo('version'); } $prefix = get_mttinfo('version'). '-'. time(); $path = null; if ($dir == 'content') { $path = MTTPATH. 'content/'; } else if ($dir == 'theme') { $path = MTTPATH. 'content/'. MTT_THEME. '/'; } else if ($dir == 'ext') { $path = MTT_EXT. $ext. '/'; } else { return $prefix. '-unknown'; } $fullPath = $path. $filename; if (!file_exists($fullPath)) { return $prefix. '-not-found'; } $mtime = filemtime($fullPath); if ($mtime === false) { return $prefix. '-no-access'; } return $mtime; } function filever(string $dir, string $filename) { print get_filever($dir, $filename); } ================================================ FILE: src/mtt-edit-settings.php ================================================ \n". " mtt-edit-settings.php write \n". " mtt-edit-settings.php password \n" ); } $dontStartSession = true; require_once(__DIR__ . '/init.php'); $cmd = $argv[1]; $param = $argv[2]; $value = $argc > 3 ? $argv[3] : null; switch ($cmd) { case 'read': cmd_read($param, $value); break; case 'write': cmd_write($param, $value); break; case 'password': cmd_password($param); break; default: die("Unknown command: $cmd\n"); } function cmd_read($param) { print Config::get($param) . "\n"; } function cmd_write($param, $value) { if ($value === null) { die("Can not write '$param': value is not specified\n"); } print ("Set '$param' to '$value'\n"); Config::set($param, $value); Config::save(); print ("Done!\n"); } function cmd_password($value) { $value = passwordHash($value); cmd_write('password', $value); } ================================================ FILE: src/mtt-emergency.php ================================================ "); } function exitmsg(?string $text = '') { echo "

    Password Reset

    \n"; echo $text; echo "


    For security reasons delete this file after usage!"; exit; } ================================================ FILE: src/settings.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ require_once('./init.php'); $lang = Lang::instance(); if ( !is_logged() ) { die("Access denied!
    Disable password protection or Log in."); } if(isset($_POST['save'])) { check_token(); $t = array(); $langs = getLangs(); Config::$params['lang']['options'] = array_keys($langs); Config::set('lang', _post('lang')); // in Demo mode we can set only language by cookies if (defined('MTT_DEMO')) { setcookie('lang', Config::get('lang'), 0, url_dir(Config::get('url')=='' ? getRequestUri() : Config::getUrl('url'))); $t['saved'] = 1; jsonExit($t); } if (isset($_POST['password']) && $_POST['password'] != '') Config::set('password', passwordHash($_POST['password'])) ; elseif (!_post('allowpassword')) Config::set('password', ''); Config::set('smartsyntax', (int)_post('smartsyntax')); // Do not set invalid timezone try { $tz = trim(_post('timezone')); $testTZ = new DateTimeZone($tz); //will throw Exception on invalid timezone Config::set('timezone', $tz); } catch (Exception $e) { } Config::set('autotag', (int)_post('autotag')); Config::set('markup', (int)_post('markdown') == 0 ? 'v1' : 'markdown'); Config::set('firstdayofweek', (int)_post('firstdayofweek')); Config::set('clock', (int)_post('clock')); Config::set('dateformat', removeNewLines(_post('dateformat')) ); Config::set('dateformat2', removeNewLines(_post('dateformat2')) ); Config::set('dateformatshort', removeNewLines(_post('dateformatshort')) ); Config::set('title', removeNewLines(trim(_post('title'))) ); Config::set('showdate', (int)_post('showdate')); Config::set('showtime', (int)_post('showtime')); Config::set('showdateInline', (int)_post('showdateInline')); Config::set('exactduedate', (int)_post('exactduedate')); Config::set('appearance', removeNewLines(trim(_post('appearance'))) ); Config::set('newTaskCounter', (int)_post('newTaskCounter')); Config::set('newTaskCounterIcon', (int)_post('newTaskCounterIcon')); Config::save(); $t['saved'] = 1; jsonExit($t); } else if (isset($_POST['activate'])) { check_token(); $t = array('saved'=>0); // in Demo mode we do nothing if (defined('MTT_DEMO')) { $t['saved'] = 1; jsonExit($t); } $activate = (int)_post('activate'); $ext = _post('ext'); $extBundles = MTTExtensionLoader::bundles(); $exts = array_keys($extBundles); $a = Config::get('extensions'); if (!is_array($a)) $a = []; if (in_array($ext, $exts)) { if ($activate) { try { MTTExtensionLoader::loadExtension($ext); $a[] = $ext; } catch (Exception $e) { http_response_code(500); logAndDie($e->getMessage()); } } else $a = array_values(array_diff($a, [$ext])); Config::set('extensions', $a); Config::save(); } else if (!$activate && in_array($ext, $a)) { $a = array_values(array_diff($a, [$ext])); Config::set('extensions', $a); Config::save(); } $t['saved'] = 1; jsonExit($t); } function _c($key) { return Config::get($key); } function getLangs() { $langDir = Lang::instance()->langDir(); if ( ! $h = opendir($langDir) ) { return false; } $a = array(); while ( false !== ($file = readdir($h)) ) { if ( preg_match('/(.+)\.json$/', $file, $m) ) { $jsonText = file_get_contents($langDir. $file); if (false === $jsonText) { die("false "); continue; } $a[$m[1]] = $m[1]; $j = json_decode($jsonText, true); if ( isset($j['_header']['language']) && isset($j['_header']['original_name']) ) { $a[$m[1]]= [ 'name' => $j['_header']['original_name'], 'title' => $j['_header']['language'] ]; } } } closedir($h); uasort($a, 'cmpLangs'); return $a; } function cmpLangs($a, $b) : int { //return strcmp( mb_strtoupper($a['name']), mb_strtoupper($b['name']) ); return strcasecmp($a['title'], $b['title']); } function selectOptions($a, $value, $default=null) { if(!$a) return ''; $s = ''; if($default !== null && !isset($a[$value])) $value = $default; foreach($a as $k=>$v) { $s .= ''; } return $s; } /** * @param array $a array of id=>array(name, optional title) * @param mixed $key Key of OPTION to be selected * @param mixed $default Default key if $key is not present in $a */ function selectOptionsA($a, $key, $default=null) { if(!$a) return ''; $s = ''; if ($default !== null && !isset($a[$key])) $key = $default; else if ($default === null && !isset($a[$key])) { $s .= ''; } foreach($a as $k=>$v) { if (!is_array($v)) { $v = array('name' => $k); } $s .= ''; } return $s; } function timezoneIdentifiers() { $zones = DateTimeZone::listIdentifiers(); $a = array(); foreach($zones as $v) { $a[$v] = $v; } return $a; } function listExtensions() { $extBundles = MTTExtensionLoader::bundles(); $activatedExts = Config::get('extensions'); if (!is_array($activatedExts)) $activatedExts = []; $a = []; foreach ($extBundles as $ext => $meta) { $out = htmlspecialchars($meta['name']. ' v'. $meta['version']). ' '; if (in_array($ext, $activatedExts)) { $out .= "". __('set_deactivate', true). ''; $instance = MTTExtensionLoader::extensionInstance($ext); if ($instance instanceof MTTExtensionSettingsInterface) { $out .= " ". __('a_settings', true). ""; } $activatedExts = array_diff($activatedExts, [$ext]); } else { $out .= "". __('set_activate', true). ''; } $a[] = $out; } // removed and not deactivated foreach ($activatedExts as $ext) { $out = "$ext <not found> "."". __('set_deactivate', true). ''; $a[] = $out; } print( implode("
    \n", $a) ); } header('Content-type:text/html; charset=utf-8'); ?>

    "; $j = json_encode($j, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE); ?>
    config.json
    :
    :
    :


    :
    />
    :

    :

    :

    :
    :
    :
    :
    :
    :
    :



    :

    :


    :

    :
    ================================================ FILE: src/setup.php ================================================ Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ // Can be used to upgrade database from myTinyTodo v1.4 or later $lastVer = '1.8'; if (version_compare(PHP_VERSION, '7.2.0') < 0) { die("PHP 7.2 or above is required"); } if (getenv('MTT_ENABLE_DEBUG') == 'YES') { set_exception_handler('debugExceptionHandler'); define('MTT_DEBUG', true); } else { set_exception_handler('myExceptionHandler'); define('MTT_DEBUG', false); #ini_set('zend.exception_ignore_args', 1); // php 7.4+ } if (!defined('MTTPATH')) define('MTTPATH', dirname(__FILE__) .'/'); if (!defined('MTTINC')) define('MTTINC', MTTPATH. 'includes/'); require_once(MTTINC. 'common.php'); require_once(MTTINC. 'class.dbconnection.php'); require_once(MTTINC. 'class.config.php'); require_once(MTTINC. 'version.php'); $db = null; $ver = ''; $error = ''; $passwordAskedV14 = false; $csrfToken = setupToken(); if ($csrfToken == '' || strlen($csrfToken) != 36) { $csrfToken = setSetupToken(); } $csrfToken = htmlspecialchars($csrfToken); $configExists = file_exists(MTTPATH. 'config.php'); $oldConfigExists = file_exists(MTTPATH. 'db/config.php'); $mttVersion = htmlspecialchars(mytinytodo\Version::VERSION); echo "myTinyTodo $mttVersion Setup"; echo "myTinyTodo $mttVersion Setup

    "; if (!$configExists && $oldConfigExists) { // First we need to migrate database config of db v1.4 require_once(MTTPATH. 'db/config.php'); askPasswordV14($config, $csrfToken); $passwordAskedV14 = true; Config::loadConfigV14($config); tryToSaveDBConfig(); $configExists = true; } if ($configExists) { // No need to migrate database config require_once(MTTPATH. 'config.php'); $db = testConnect($error); if (!$db) { exitMessage( "Database connection config file seems to be incorrect. You can remove config.php or edit it manually and then reload setup.

    ". "Error: ". htmlspecialchars($error) ); } // Config file v1.7 already exists and set up correctly $dbtype = MTT_DB_TYPE; // Determine current installed db version $ver = databaseVersion($db); if (MTT_DEBUG) { error_log("Database version detected: $ver"); } if ($ver == '') { // Clean install // Don't load settings from database in init.php Config::$noDatabase = true; } else if (version_compare($ver, '1.4') < 0) { // Very old or previously failed while install exitMessage(htmlspecialchars("Can not update. Unsupported database version ($ver).")); } else if ($ver == '1.4') { // Need to upgrade. Do not ask for old password Config::$noDatabase = true; //don't load settings from db require_once(MTTPATH. 'db/config.php'); if ( !$passwordAskedV14 ) { askPasswordV14($config, $csrfToken); $passwordAskedV14 = true; } Config::loadConfigV14($config); unset($config); DBConnection::init($db); } require_once('./init.php'); if ( !Config::$noDatabase && !is_logged() ) { die("Access denied!
    Disable password protection or Log in."); } } if ($ver == '') { $install = trim(_post('install')); if ($install == '' && $db !== null) { # We already have settings file and need to create tables. exitMessage("
    Click next to create tables in ". htmlspecialchars(databaseTypeName($db)). " database.

    "); } elseif ($install == '') { # Specify database type and connection settings to save. exitMessage("
    Select database type to use:







    "); } elseif ($install == 'config') { checkSetupToken(); # Save configuration $dbtype = $_POST['db_type'] ?? ''; if ($dbtype != 'mysql' && $dbtype != 'postgres' && $dbtype != 'sqlite') { exitMessage("Unknown database type $dbtype"); } Config::set('db.type', $dbtype); if ($dbtype == 'mysql' || $dbtype == 'postgres') { Config::set('db.host', _post('db_host')); Config::set('db.name', _post('db_name')); Config::set('db.user', _post('db_user')); Config::set('db.password', _post('db_password')); Config::set('db.prefix', trim(_post('db_prefix'))); } Config::defineDbConstants(); $db = testConnect($error); if (!$db) { exitMessage("Database connection error: ". htmlspecialchars($error)); } if (defined('MTT_DB_DRIVER')) { Config::set('db.driver', MTT_DB_DRIVER); } tryToSaveDBConfig(); exitMessage("
    This will create myTinyTodo database

    "); } elseif ($install == 'create') { checkSetupToken(); # install database createAllTables($db, $dbtype); # throws # create default list $db->ex( "INSERT INTO {$db->prefix}lists (uuid,name,d_created,taskview) VALUES (?,?,?,?)", array(generateUUID(), 'Todo', time(), 1) ); Config::save(); } else { exitMessage("Unknown action"); } } elseif ($ver == $lastVer) { exitMessage("Installed version does not require database update."); } else { if (!in_array($ver, array('1.4','1.7'))) { exitMessage(htmlspecialchars("Can not update. Unsupported database version ($ver).")); } if (!isset($_POST['update'])) { exitMessage(htmlspecialchars("Update database v$ver to v$lastVer"). "

    "); } # update process checkSetupToken(); if ($ver == '1.4') { update_14_17($db, $dbtype); update_17_18($db, $dbtype); } elseif ($ver == '1.7') { update_17_18($db, $dbtype); } } echo "Done

    Attention! Delete this file for security reasons.

    Go to homepage."; printFooter(); function setupToken() { return $_COOKIE['mtt-s-token'] ?? ''; } function setSetupToken() : string { $token = randomString(36); if (PHP_VERSION_ID < 70300) { setcookie('mtt-s-token', $token, 0, url_dir(getRequestUri()). '; samesite=lax', '', false, true ) ; } else { /** @disregard P1006 available in php 7.3 */ setcookie('mtt-s-token', $token, [ 'path' => url_dir(getRequestUri()), 'httponly' => true, 'samesite' => 'lax' ]); } $_COOKIE['mtt-s-token'] = $token; return $token; } function checkSetupToken() { $token = $_POST['stoken'] ?? ''; if ( $token == '' || $token != setupToken() ) { die("Access denied! No token provided."); } } function askPasswordV14( #[\SensitiveParameter] array $config, string $csrfToken) { if (!isset($config['password']) || $config['password'] == '') { return; } if (isset($_POST['configpassword'])) { checkSetupToken(); } if (isset($_COOKIE['mtt-v14token'])) { if (validateTokenV14($config, $_COOKIE['mtt-v14token'])) { return; //authorized } if (MTT_DEBUG) error_log("Failed validation of v14token"); } if ( !isset($_POST['configpassword']) || $_POST['configpassword'] != $config['password'] ) { exitMessage("Enter current password to continue.
    "); } $token = generateTokenV14($config); if (PHP_VERSION_ID < 70300) { setcookie('mtt-v14token', $token, 0, url_dir(getRequestUri()). '; samesite=lax', '', false, true ) ; } else { /** @disregard P1006 available in php 7.3 */ setcookie('mtt-v14token', $token, [ 'path' => url_dir(getRequestUri()), 'httponly' => true, 'samesite' => 'lax' ]); } } function generateTokenV14( #[\SensitiveParameter] array $config) : ?string { if (!isset($config['password']) || $config['password'] == '') { return null; } $payload = base64_encode(json_encode([ 'exp' => time() + 300 # 5 min lifetime ])); return $payload. '.'. base64_encode(hash_hmac('sha256', $payload, $config['password'], true)); } function validateTokenV14( #[\SensitiveParameter] array $config, string $token) : bool { if (!isset($config['password']) || $config['password'] == '') { return true; } $parts = explode('.', $token); if (count($parts) != 2) { return false; } $signature = base64_decode($parts[1]); //binary if ($signature === false) { return false; } if ( !hash_equals($signature, hash_hmac('sha256', $parts[0], $config['password'], true)) ) { return false; } $payload = json_decode(base64_decode($parts[0]), true); if (!isset($payload['exp']) || time() > $payload['exp']) { return false; } return true; } function createAllTables($db, $dbtype) { if ($dbtype == 'mysql') { createMysqlTables($db); } else if ($dbtype == 'postgres') { createPostgresTables($db); } else { createSqliteTables($db); } } /* ===== mysql ========================================================= */ function createMysqlTables(Database_Abstract $db) { //$collation = hasMysqlUnicode520($db) ? 'utf8mb4_unicode_520_ci' : 'utf8mb4_unicode_ci'; $collation = 'utf8mb4_unicode_520_ci'; //TODO: use BIGINT for time() timestamp to avoid the Year-2038 problem (2106 here) // Mysql does not support transactions while executing DDL $db->ex( "CREATE TABLE {$db->prefix}lists ( `id` INT UNSIGNED NOT NULL auto_increment, `uuid` CHAR(36) CHARACTER SET latin1 NOT NULL default '', `ow` INT NOT NULL default 0, `name` VARCHAR(250) NOT NULL default '', `d_created` INT UNSIGNED NOT NULL default 0, `d_edited` INT UNSIGNED NOT NULL default 0, `sorting` TINYINT UNSIGNED NOT NULL default 0, `published` TINYINT UNSIGNED NOT NULL default 0, `taskview` INT UNSIGNED NOT NULL default 0, `extra` TEXT, PRIMARY KEY(`id`), UNIQUE KEY(`uuid`) ) CHARSET=utf8mb4 COLLATE $collation "); $db->ex( "CREATE TABLE {$db->prefix}todolist ( `id` INT UNSIGNED NOT NULL auto_increment, `uuid` CHAR(36) CHARACTER SET latin1 NOT NULL default '', `list_id` INT UNSIGNED NOT NULL default 0, `d_created` INT UNSIGNED NOT NULL default 0, /* time() timestamp */ `d_completed` INT UNSIGNED NOT NULL default 0, /* time() timestamp */ `d_edited` INT UNSIGNED NOT NULL default 0, /* time() timestamp */ `compl` TINYINT UNSIGNED NOT NULL default 0, `title` VARCHAR(250) NOT NULL, `note` TEXT, `prio` TINYINT NOT NULL default 0, /* priority -,0,+ */ `ow` INT NOT NULL default 0, /* order weight */ `duedate` DATE default NULL, PRIMARY KEY(`id`), KEY(`list_id`), UNIQUE KEY(`uuid`) ) CHARSET=utf8mb4 COLLATE $collation "); // Max length of varchar of utf8mb4 with UNIQUE index is 191 until Mysql 5.7 and MariaDB 10.2 $db->ex( "CREATE TABLE {$db->prefix}tags ( `id` INT UNSIGNED NOT NULL auto_increment, `name` VARCHAR(250) NOT NULL default '', PRIMARY KEY(`id`), UNIQUE KEY `name` (`name`) ) CHARSET=utf8mb4 COLLATE $collation "); $db->ex( "CREATE TABLE {$db->prefix}tag2task ( `tag_id` INT UNSIGNED NOT NULL, `task_id` INT UNSIGNED NOT NULL, `list_id` INT UNSIGNED NOT NULL, KEY(`tag_id`), KEY(`task_id`), KEY(`list_id`) /* for tagcloud */ ) CHARSET=utf8mb4 COLLATE $collation "); $db->ex( "CREATE TABLE {$db->prefix}settings ( `param_key` VARCHAR(250) CHARACTER SET latin1 NOT NULL default '', `param_value` TEXT, UNIQUE KEY `param_key` (`param_key`) ) CHARSET=utf8mb4 COLLATE $collation "); $db->ex( "CREATE TABLE {$db->prefix}sessions ( `id` VARCHAR(64) CHARACTER SET latin1 NOT NULL default '', /* upto 64 bytes for sha256 */ `data` TEXT, `last_access` INT UNSIGNED NOT NULL default 0, /* time() timestamp */ `expires` INT UNSIGNED NOT NULL default 0, /* time() timestamp */ UNIQUE KEY `id` (`id`) ) CHARSET=utf8mb4 COLLATE $collation "); } /* ===== postgres =============================================== */ function createPostgresTables(Database_Abstract $db) { //TODO: use BIGINT for time() timestamp to avoid the Year-2038 problem $db->ex( "CREATE TABLE {$db->prefix}lists ( id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, uuid CHAR(36) NOT NULL default '', ow INTEGER NOT NULL default 0, name VARCHAR(250) NOT NULL default '', d_created INTEGER NOT NULL default 0, d_edited INTEGER NOT NULL default 0, sorting SMALLINT NOT NULL default 0, published SMALLINT NOT NULL default 0, taskview INTEGER NOT NULL default 0, extra TEXT ) "); $db->ex("CREATE UNIQUE INDEX {$db->prefix}lists_uuid ON {$db->prefix}lists (uuid)"); $db->ex( "CREATE TABLE {$db->prefix}todolist ( id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, uuid CHAR(36) NOT NULL default '', list_id INTEGER NOT NULL default 0, d_created INTEGER NOT NULL default 0, d_completed INTEGER NOT NULL default 0, d_edited INTEGER NOT NULL default 0, compl SMALLINT NOT NULL default 0, title VARCHAR(250) NOT NULL default '', note TEXT default NULL, prio SMALLINT NOT NULL default 0, ow INTEGER NOT NULL default 0, duedate DATE default NULL ) "); $db->ex("CREATE INDEX {$db->prefix}todo_list_id ON {$db->prefix}todolist (list_id)"); $db->ex("CREATE UNIQUE INDEX {$db->prefix}todo_uuid ON {$db->prefix}todolist (uuid)"); $db->ex( "CREATE TABLE {$db->prefix}tags ( id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(250) NOT NULL DEFAULT '' ) "); $db->ex("CREATE UNIQUE INDEX {$db->prefix}tags_lower_name ON {$db->prefix}tags ((LOWER(name)))"); $db->ex( "CREATE TABLE {$db->prefix}tag2task ( tag_id INTEGER NOT NULL, task_id INTEGER NOT NULL, list_id INTEGER NOT NULL ) "); $db->ex("CREATE INDEX {$db->prefix}tag2task_tag_id ON {$db->prefix}tag2task (tag_id)"); $db->ex("CREATE INDEX {$db->prefix}tag2task_task_id ON {$db->prefix}tag2task (task_id)"); $db->ex("CREATE INDEX {$db->prefix}tag2task_list_id ON {$db->prefix}tag2task (list_id)"); $db->ex( "CREATE TABLE {$db->prefix}settings ( param_key VARCHAR(250) NOT NULL default '', param_value TEXT ) "); $db->ex("CREATE UNIQUE INDEX {$db->prefix}settings_key ON {$db->prefix}settings (param_key)"); $db->ex( "CREATE TABLE {$db->prefix}sessions ( id VARCHAR(64) NOT NULL default '', data TEXT, last_access INTEGER NOT NULL default 0, expires INTEGER NOT NULL default 0 ) "); $db->ex("CREATE UNIQUE INDEX {$db->prefix}sessions_id ON {$db->prefix}sessions (id)"); } /* ===== sqlite =============================================== */ function createSqliteTables(Database_Abstract $db) { $db->ex( "CREATE TABLE {$db->prefix}lists ( id INTEGER PRIMARY KEY, uuid CHAR(36) NOT NULL, ow INTEGER NOT NULL default 0, name VARCHAR(250) NOT NULL, d_created INTEGER UNSIGNED NOT NULL default 0, d_edited INTEGER UNSIGNED NOT NULL default 0, sorting TINYINT UNSIGNED NOT NULL default 0, published TINYINT UNSIGNED NOT NULL default 0, taskview INTEGER UNSIGNED NOT NULL default 0, extra TEXT ) "); $db->ex("CREATE UNIQUE INDEX lists_uuid ON {$db->prefix}lists (uuid)"); $db->ex( "CREATE TABLE {$db->prefix}todolist ( id INTEGER PRIMARY KEY, uuid CHAR(36) NOT NULL default '', list_id INTEGER UNSIGNED NOT NULL default 0, d_created INTEGER UNSIGNED NOT NULL default 0, d_completed INTEGER UNSIGNED NOT NULL default 0, d_edited INTEGER UNSIGNED NOT NULL default 0, compl TINYINT UNSIGNED NOT NULL default 0, title VARCHAR(250) NOT NULL default '' COLLATE UTF8CI, note TEXT COLLATE UTF8CI default NULL, prio TINYINT NOT NULL default 0, ow INTEGER NOT NULL default 0, duedate DATE default NULL ) "); $db->ex("CREATE INDEX todo_list_id ON {$db->prefix}todolist (list_id)"); $db->ex("CREATE UNIQUE INDEX todo_uuid ON {$db->prefix}todolist (uuid)"); $db->ex( "CREATE TABLE {$db->prefix}tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(250) NOT NULL DEFAULT '' COLLATE UTF8CI ) "); $db->ex("CREATE INDEX tags_name ON {$db->prefix}tags (name)"); //NB: unique in mysql $db->ex( "CREATE TABLE {$db->prefix}tag2task ( tag_id INTEGER NOT NULL, task_id INTEGER NOT NULL, list_id INTEGER NOT NULL ) "); $db->ex("CREATE INDEX tag2task_tag_id ON {$db->prefix}tag2task (tag_id)"); $db->ex("CREATE INDEX tag2task_task_id ON {$db->prefix}tag2task (task_id)"); $db->ex("CREATE INDEX tag2task_list_id ON {$db->prefix}tag2task (list_id)"); /* for tagcloud */ $db->ex( "CREATE TABLE {$db->prefix}settings ( param_key VARCHAR(250) NOT NULL default '', param_value TEXT ) "); $db->ex("CREATE UNIQUE INDEX settings_key ON {$db->prefix}settings (param_key COLLATE NOCASE)"); $db->ex( "CREATE TABLE {$db->prefix}sessions ( id VARCHAR(64) NOT NULL default '', data TEXT, last_access INTEGER UNSIGNED NOT NULL default 0, expires INTEGER UNSIGNED NOT NULL default 0 ) "); $db->ex("CREATE UNIQUE INDEX sessions_id ON {$db->prefix}sessions (id COLLATE NOCASE)"); } function databaseVersion(Database_Abstract $db): string { if ( !$db ) return ''; if ( !$db->tableExists($db->prefix.'todolist') ) return ''; $v = '1.0'; if ( !$db->tableExists($db->prefix.'tags') ) return $v; $v = '1.1'; if ( !$db->tableFieldExists($db->prefix.'todolist', 'duedate') ) return $v; $v = '1.2'; if ( !$db->tableExists($db->prefix.'lists') ) return $v; $v = '1.3.0'; if ( !$db->tableFieldExists($db->prefix.'todolist', 'd_completed') ) return $v; $v = '1.3.1'; if ( !$db->tableFieldExists($db->prefix.'todolist', 'd_edited') ) return $v; $v = '1.4'; if ( !$db->tableExists($db->prefix.'settings') ) return $v; $v = '1.7'; if ( $db->tableFieldExists($db->prefix.'todolist', 'tags') ) return $v; $v = '1.8'; return $v; } function hasMysqlUnicode520(Database_Abstract $db): bool { $r = $db->sq("SHOW COLLATION WHERE Charset='utf8mb4' and Collation='utf8mb4_unicode_520_ci'"); return $r ? true : false; } function exitMessage($s) { echo $s; printFooter(); exit; } function printFooter() { echo ""; } function tryToSaveDbConfig() { if (!file_exists(MTTPATH.'config.php')) { @touch(MTTPATH.'config.php'); } if (!is_writable(MTTPATH.'config.php')) { exitMessage("Database connection config file ('config.php') is not writable. You need to edit it manually, set contents to this and run setup once more.

    \n". "\n". "" ); } Config::saveDbConfig(); } function testConnect(&$error) { $db = null; try { if (!defined('MTT_DB_TYPE')) { throw new Exception("MTT_DB_TYPE is not defined"); } if (MTT_DB_TYPE == 'mysql') { $hasPDO = false; $hasMysqli = false; if (defined('PDO::MYSQL_ATTR_FOUND_ROWS')) { $hasPDO = true; } if (function_exists("mysqli_connect")) { $hasMysqli = true; } $driver = ''; if (defined('MTT_DB_DRIVER')) { // forced to use specific mysql interface if ( in_array(MTT_DB_DRIVER, ['mysqli', 'pdo', '']) ) { $driver = MTT_DB_DRIVER; if ($driver == '') $driver = 'pdo'; // default } else { throw new Exception("Unknown database driver"); } } if ($driver == '') { // auto-detect driver if ($hasPDO) $driver = 'pdo'; else if ($hasMysqli) $driver = 'mysqli'; } $db = null; if ($driver == 'mysqli') { if ($hasMysqli) { require_once(MTTINC. 'class.db.mysqli.php'); if (!defined('MTT_DB_DRIVER')) define('MTT_DB_DRIVER', 'mysqli'); $db = new Database_Mysqli(); } else { throw new Exception("Required PHP extension 'MySQLi' is not installed."); } } else { if ($hasPDO) { require_once(MTTINC. 'class.db.mysql.php'); if (!defined('MTT_DB_DRIVER')) define('MTT_DB_DRIVER', ''); // set pdo? $db = new Database_Mysql(); } else { throw new Exception("Required PHP extension 'PDO_MySQL' is not installed."); } } foreach (['MTT_DB_HOST', 'MTT_DB_USER', 'MTT_DB_PASSWORD', 'MTT_DB_NAME', 'MTT_DB_PREFIX'] as $c) { if (!defined($c)) throw new Exception("$c is not defined"); } $db->connect([ 'host' => MTT_DB_HOST, 'user' => MTT_DB_USER, 'password' => MTT_DB_PASSWORD, 'db' => MTT_DB_NAME ]); } else if (MTT_DB_TYPE == 'postgres') { if (!defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) { throw new Exception("Required PHP extension 'PDO_PostgreSQL' is not installed."); } require_once(MTTINC. 'class.db.postgres.php'); foreach (['MTT_DB_HOST', 'MTT_DB_USER', 'MTT_DB_PASSWORD', 'MTT_DB_NAME', 'MTT_DB_PREFIX'] as $c) { if (!defined($c)) throw new Exception("$c is not defined"); } $db = new Database_Postgres; $db->connect([ 'host' => MTT_DB_HOST, 'user' => MTT_DB_USER, 'password' => MTT_DB_PASSWORD, 'db' => MTT_DB_NAME ]); } else if (MTT_DB_TYPE == 'sqlite') { if (false === $f = @fopen(MTTPATH. 'db/todolist.db', 'a+')) { throw new Exception("database file is not readable/writable"); } else { fclose($f); } if (!is_writable(MTTPATH. 'db/')) { throw new Exception("database directory ('db') is not writable"); } require_once(MTTINC. 'class.db.sqlite3.php'); $db = new Database_Sqlite3; $db->connect([ 'filename' => MTTPATH. 'db/todolist.db' ]); } else { throw new Exception("Unsupported database type: ". MTT_DB_TYPE); } if (!defined('MTT_DB_PREFIX')) define('MTT_DB_PREFIX', ''); $db->setPrefix(MTT_DB_PREFIX); } catch(Exception $e) { //if (MTT_DEBUG) throw $e; $error = $e->getMessage(); return null; } $error = ''; return $db; } function debugExceptionHandler(Throwable $e) { echo '
    Error ('. htmlspecialchars(get_class($e)) .'): \''. htmlspecialchars($e->getMessage()) .'\' in '. htmlspecialchars($e->getFile() .':'. $e->getLine()). ''. "\n
    ". htmlspecialchars($e->getTraceAsString()) . "
    \n"; exit; } function myExceptionHandler(Throwable $e) { $called = ''; foreach ($e->getTrace() as $a) { if ($a['file'] == __FILE__) { $called = " in ". htmlspecialchars(basename($a['file']). ':'. $a['line']). ""; break; } } echo '
    Error: \''. htmlspecialchars($e->getMessage()). '\''. $called ; exit; } function databaseTypeName(Database_Abstract $db) { switch ($db::DBTYPE) { case DBConnection::DBTYPE_MYSQL: return "MySQL"; case DBConnection::DBTYPE_POSTGRES: return "PostgreSQL"; case DBConnection::DBTYPE_SQLITE: return "SQLite"; default: throw new Exception("Unsupported database type: ". $db::DBTYPE); } } ### update v1.4 to v1.7 ########## function update_14_17(Database_Abstract $db, $dbtype) { $db->ex("BEGIN"); if($dbtype=='mysql') { $db->ex("ALTER TABLE {$db->prefix}lists ADD `extra` TEXT"); # increase the length of list and tag name # (not applicable to sqlite because it uses VARCHAR fields of any length as TEXT) $db->ex("ALTER TABLE {$db->prefix}todolist CHANGE `tags` `tags` VARCHAR(2000) NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}tags CHANGE `name` `name` VARCHAR(250) NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}lists CHANGE `name` `name` VARCHAR(250) NOT NULL default '' "); # convert charset to utf8mb4 $db->ex("ALTER TABLE {$db->prefix}lists CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); $db->ex("ALTER TABLE {$db->prefix}todolist CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); $db->ex("ALTER TABLE {$db->prefix}tags CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); $db->ex("ALTER TABLE {$db->prefix}tag2task CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); # create settings table $db->ex( "CREATE TABLE {$db->prefix}settings ( `param_key` VARCHAR(250) NOT NULL default '', `param_value` TEXT, UNIQUE KEY `param_key` (`param_key`) ) CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); # create sessions table $db->ex( "CREATE TABLE {$db->prefix}sessions ( `id` VARCHAR(64) NOT NULL default '', `data` TEXT, `last_access` INT UNSIGNED NOT NULL default 0, `expires` INT UNSIGNED NOT NULL default 0, UNIQUE KEY `id` (`id`) ) CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); } else #sqlite { $db->ex("ALTER TABLE {$db->prefix}lists ADD extra TEXT"); # settings $db->ex( "CREATE TABLE {$db->prefix}settings ( param_key VARCHAR(100) NOT NULL default '', param_value TEXT ) "); $db->ex("CREATE UNIQUE INDEX settings_key ON {$db->prefix}settings (param_key COLLATE NOCASE)"); # sessions $db->ex( "CREATE TABLE {$db->prefix}sessions ( id VARCHAR(250) NOT NULL default '', data TEXT, last_access INTEGER UNSIGNED NOT NULL default 0, expires INTEGER UNSIGNED NOT NULL default 0 ) "); $db->ex("CREATE UNIQUE INDEX sessions_id ON {$db->prefix}sessions (id COLLATE NOCASE)"); } $db->ex("COMMIT"); Config::save(); Config::saveDbConfig(); } ### end of 1.7 ##### ### update v1.7 to v1.8 ########## function update_17_18(Database_Abstract $db, $dbtype) { $db->ex("BEGIN"); if ($dbtype == 'sqlite') { // Use UTF8CI collate. Old sqlite does not support DROP COLUMN (before v3.35.0 2021-03-12) $db->ex("DROP INDEX todo_list_id"); $db->ex("DROP INDEX todo_uuid"); $db->ex("ALTER TABLE {$db->prefix}todolist RENAME TO {$db->prefix}todolist_old"); $db->ex( "CREATE TABLE {$db->prefix}todolist ( id INTEGER PRIMARY KEY, uuid CHAR(36) NOT NULL default '', list_id INTEGER UNSIGNED NOT NULL default 0, d_created INTEGER UNSIGNED NOT NULL default 0, d_completed INTEGER UNSIGNED NOT NULL default 0, d_edited INTEGER UNSIGNED NOT NULL default 0, compl TINYINT UNSIGNED NOT NULL default 0, title VARCHAR(250) NOT NULL default '' COLLATE UTF8CI, note TEXT COLLATE UTF8CI default NULL, prio TINYINT NOT NULL default 0, ow INTEGER NOT NULL default 0, duedate DATE default NULL )" ); $db->ex("INSERT INTO {$db->prefix}todolist SELECT id,uuid,list_id,d_created,d_completed,d_edited,compl,title,note,prio,ow,duedate FROM {$db->prefix}todolist_old"); $db->ex("CREATE INDEX todo_list_id ON {$db->prefix}todolist (list_id)"); $db->ex("CREATE UNIQUE INDEX todo_uuid ON {$db->prefix}todolist (uuid)"); $db->ex("DROP TABLE {$db->prefix}todolist_old"); $db->ex("DROP INDEX tags_name"); $db->ex("ALTER TABLE {$db->prefix}tags RENAME TO {$db->prefix}tags_old"); $db->ex( "CREATE TABLE {$db->prefix}tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(250) NOT NULL DEFAULT '' COLLATE UTF8CI )" ); $db->ex("INSERT INTO {$db->prefix}tags SELECT * FROM {$db->prefix}tags_old"); $db->ex("CREATE INDEX tags_name ON {$db->prefix}tags (name)"); $db->ex("DROP TABLE {$db->prefix}tags_old"); } else // mysql { $db->ex("ALTER TABLE {$db->prefix}todolist DROP COLUMN tags"); $db->ex("ALTER TABLE {$db->prefix}todolist DROP COLUMN tags_ids"); // if mysql db was created in v1.7.x then // tags.name field has length of 50 instead of 250, // settings.param_key field has length of 100 instead of 250 $db->ex("ALTER TABLE {$db->prefix}lists MODIFY `uuid` CHAR(36) CHARACTER SET latin1 NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}lists MODIFY `name` VARCHAR(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}lists MODIFY `extra` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci "); $db->ex("ALTER TABLE {$db->prefix}todolist MODIFY `uuid` CHAR(36) CHARACTER SET latin1 NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}todolist MODIFY `title` VARCHAR(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}todolist MODIFY `note` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci "); $db->ex("ALTER TABLE {$db->prefix}tags MODIFY `name` VARCHAR(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}settings MODIFY `param_key` VARCHAR(250) CHARACTER SET latin1 NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}settings MODIFY `param_value` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci "); $db->ex("ALTER TABLE {$db->prefix}sessions MODIFY `id` VARCHAR(64) CHARACTER SET latin1 NOT NULL default '' "); $db->ex("ALTER TABLE {$db->prefix}sessions MODIFY `data` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci "); } $db->ex("COMMIT"); if ($dbtype == 'sqlite') { $db->ex("VACUUM"); } } ### end of 1.8 #####