├── .htaccess ├── .phpcs.xml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── controller.php ├── controller │ ├── admin.php │ ├── api.php │ ├── api │ │ ├── issues.php │ │ └── user.php │ ├── backlog.php │ ├── files.php │ ├── index.php │ ├── issues.php │ ├── issues │ │ └── project.php │ ├── tag.php │ ├── taskboard.php │ └── user.php ├── dict │ ├── cs.ini │ ├── de.ini │ ├── en-GB.ini │ ├── en.ini │ ├── es.ini │ ├── et.ini │ ├── fr.ini │ ├── it.ini │ ├── ja.ini │ ├── ko.ini │ ├── nl.ini │ ├── pl.ini │ ├── pt.ini │ ├── ru.ini │ └── zh.ini ├── helper │ ├── cli.php │ ├── dashboard.php │ ├── file.php │ ├── matrix.php │ ├── notification.php │ ├── plugin.php │ ├── security.php │ ├── security │ │ └── antixss.php │ ├── template.php │ ├── update.php │ └── view.php ├── model.php ├── model │ ├── config.php │ ├── custom.php │ ├── issue.php │ ├── issue │ │ ├── backlog.php │ │ ├── comment.php │ │ ├── comment │ │ │ ├── detail.php │ │ │ └── user.php │ │ ├── dependency.php │ │ ├── detail.php │ │ ├── file.php │ │ ├── file │ │ │ └── detail.php │ │ ├── priority.php │ │ ├── status.php │ │ ├── tag.php │ │ ├── type.php │ │ ├── update.php │ │ ├── update │ │ │ └── field.php │ │ └── watcher.php │ ├── session.php │ ├── sprint.php │ ├── user.php │ └── user │ │ └── group.php ├── plugin.php ├── plugin │ └── README.md ├── routes.ini └── view │ ├── admin │ ├── config.html │ ├── groups.html │ ├── groups │ │ └── edit.html │ ├── index.html │ ├── plugins.html │ ├── plugins │ │ └── single.html │ ├── sprints.html │ ├── sprints │ │ ├── edit.html │ │ └── new.html │ ├── users.html │ └── users │ │ ├── deleted.html │ │ └── edit.html │ ├── backlog │ ├── index.html │ ├── item.html │ └── old.html │ ├── blocks │ ├── admin │ │ └── tabs.html │ ├── dashboard-issue-list.html │ ├── file │ │ └── thumb.html │ ├── footer-modals.html │ ├── footer-scripts.html │ ├── footer.html │ ├── head.html │ ├── issue-comment.html │ ├── issue-list.html │ ├── issue-list │ │ ├── bulk-update.html │ │ ├── filters.html │ │ └── issue.html │ ├── navbar-public.html │ └── navbar.html │ ├── error │ ├── 404.html │ ├── 500.html │ ├── general.html │ └── inline.html │ ├── index │ ├── atom.xml │ ├── index.html │ ├── login.html │ ├── opensearch.xml │ ├── reset.html │ ├── reset_complete.html │ └── reset_forced.html │ ├── install.html │ ├── issues │ ├── edit-form.html │ ├── edit.html │ ├── file │ │ └── preview │ │ │ └── table.html │ ├── index.html │ ├── new.html │ ├── project.html │ ├── project │ │ ├── files.html │ │ ├── tree-item.html │ │ └── tree.html │ ├── search.html │ ├── single.html │ └── single │ │ ├── dependencies.html │ │ ├── history.html │ │ ├── related.html │ │ └── watchers.html │ ├── notification │ ├── blocks │ │ ├── _head.html │ │ ├── actions.html │ │ ├── footer.html │ │ ├── logo.html │ │ └── preview.html │ ├── comment.html │ ├── comment.txt │ ├── file.html │ ├── file.txt │ ├── new.html │ ├── new.txt │ ├── update.html │ ├── update.txt │ ├── user_due_issues.html │ ├── user_due_issues.txt │ ├── user_reset.html │ └── user_reset.txt │ ├── tag │ ├── index.html │ └── single.html │ ├── taskboard │ └── index.html │ └── user │ ├── account.html │ ├── dashboard-widget-modal.html │ ├── dashboard-widgets │ ├── bugs.html │ ├── issue_tree.html │ ├── my_comments.html │ ├── open_comments.html │ ├── projects.html │ ├── recent_comments.html │ ├── repeat_work.html │ ├── subprojects.html │ ├── tasks.html │ └── watchlist.html │ ├── dashboard.html │ ├── single.html │ └── single │ ├── tree.html │ └── tree │ └── ajax.html ├── composer.json ├── composer.lock ├── cron ├── base.php ├── checkmail.php ├── checkmail2.php ├── due_alerts.php ├── sprintbreaker.php └── update_sprints.php ├── css ├── backlog.css ├── bootstrap-cerulean.min.css ├── bootstrap-cyborg.min.css ├── bootstrap-geo.css ├── bootstrap-phproject-dark.css ├── bootstrap-phproject.css ├── bootstrap-sandstone.min.css ├── bootstrap-slate.min.css ├── bootstrap-spacelab.min.css ├── bootstrap-theme.min.css ├── bootstrap-thrive-2015-dark.css ├── bootstrap-thrive-2015.css ├── bootstrap-thrive.css ├── bootstrap.min.css ├── bootstrap.min.css.map ├── datepicker3.css.map ├── datepicker3.min.css ├── easymde.min.css ├── font-awesome.min.css ├── style.css └── taskboard.css ├── db ├── 14.12.11.sql ├── 14.12.21.sql ├── 14.12.29.sql ├── 14.12.30.sql ├── 15.01.31.sql ├── 15.02.07.sql ├── 15.02.26.sql ├── 15.03.14.sql ├── 15.03.20.sql ├── 15.04.06.sql ├── 15.04.07.sql ├── 15.04.17.sql ├── 15.06.02.sql ├── 15.10.07.sql ├── 16.02.04.1.sql ├── 16.02.04.sql ├── 16.02.05.sql ├── 16.04.13.sql ├── 16.06.28.sql ├── 16.09.12.sql ├── 16.11.23.sql ├── 16.11.25.sql ├── 16.11.29.1.sql ├── 16.11.29.sql ├── 16.11.30.sql ├── 16.12.01.sql ├── 16.12.29.sql ├── 17.03.17.sql ├── 17.03.23.sql ├── 17.08.25.sql ├── 17.09.20.sql ├── 18.02.07.sql ├── 18.02.19.sql ├── 20.04.20.sql ├── 20.10.14.sql ├── 21.03.18.sql ├── database.sql └── demo-content.sql ├── fonts ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── img ├── 404.png ├── ajax-loader-dark.gif ├── ajax-loader.gif ├── backlog │ └── i_has_grip.png ├── checkmark.png ├── geo │ ├── flames.gif │ ├── microfab.gif │ ├── progress.gif │ ├── rainbow.gif │ ├── seamlessfire-web.gif │ ├── seamlessfire.gif │ └── stars.gif ├── mime │ ├── 16 │ │ ├── _404.svg │ │ ├── _archive.svg │ │ ├── _audio.svg │ │ ├── _blank.svg │ │ ├── _code.svg │ │ ├── _image.svg │ │ ├── _video.svg │ │ ├── csv.svg │ │ ├── doc.svg │ │ ├── odg.svg │ │ ├── odp.svg │ │ ├── ods.svg │ │ ├── odt.svg │ │ ├── pdf.svg │ │ ├── ppt.svg │ │ ├── txt.svg │ │ └── xls.svg │ └── 96 │ │ ├── _404.svg │ │ ├── _archive.svg │ │ ├── _audio.svg │ │ ├── _blank.svg │ │ ├── _code.svg │ │ ├── _image.svg │ │ ├── _video.svg │ │ ├── csv.svg │ │ ├── doc.svg │ │ ├── odg.svg │ │ ├── odp.svg │ │ ├── ods.svg │ │ ├── odt.svg │ │ ├── pdf.svg │ │ ├── ppt.svg │ │ ├── txt.svg │ │ └── xls.svg ├── sparks.gif ├── spinner-8-3-16-white.svg ├── spinner-8-3-16.svg ├── spinner-8-3-white.svg ├── spinner-8-3.svg └── taskboard │ ├── ajax-loader.gif │ ├── burn.gif │ └── warning.png ├── index.php ├── install.php ├── js ├── autosize.min.js ├── backlog.js ├── bootstrap-datepicker.de.min.js ├── bootstrap-datepicker.en-GB.min.js ├── bootstrap-datepicker.es.min.js ├── bootstrap-datepicker.et.min.js ├── bootstrap-datepicker.fr.min.js ├── bootstrap-datepicker.it.min.js ├── bootstrap-datepicker.ja.min.js ├── bootstrap-datepicker.js ├── bootstrap-datepicker.ko.min.js ├── bootstrap-datepicker.min.js ├── bootstrap-datepicker.nl.min.js ├── bootstrap-datepicker.pl.min.js ├── bootstrap-datepicker.ru.min.js ├── bootstrap-datepicker.zh-CN.min.js ├── bootstrap-datepicker.zh-TW.min.js ├── bootstrap.min.js ├── burndown.js ├── chart.min.js ├── easymde.min.js ├── global.js ├── jquery-3.6.3.min.js ├── jquery-ui-dragsort.min.js ├── jquery.ui.touch-punch.min.js ├── mousetrap-1.6.5.min.js ├── sortable.min.js ├── stupidtable.min.js ├── taskboard.js └── typeahead.jquery.js ├── nginx-example.conf ├── phpunit.xml ├── rector.php ├── tests ├── apiTest.php ├── bootstrap.php ├── pluginTest.php └── stringTest.php └── uploads └── .htaccess /.htaccess: -------------------------------------------------------------------------------- 1 | # Enable rewrite engine and route requests to framework 2 | RewriteEngine On 3 | 4 | # Some servers require you to specify the `RewriteBase` directive 5 | # In such cases, it should be the path (relative to the document root) 6 | # containing this .htaccess file 7 | # 8 | # RewriteBase / 9 | 10 | RewriteRule ^app\/(controller|dict|helper|model|view) - [R=404] 11 | RewriteRule ^(tmp|log)\/|\.ini$ - [R=404] 12 | 13 | RewriteCond %{REQUEST_FILENAME} !-l 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteCond %{REQUEST_FILENAME} !-d 16 | RewriteRule .* index.php [L,QSA] 17 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] 18 | -------------------------------------------------------------------------------- /.phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | app/ 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Phproject 2 | 3 | ## Issues 4 | 5 | Found a bug, or want to request a new feature? Great! Just make sure there isn't an existing issue before creating a new one, and try to provide as much detail as possible. 6 | 7 | ## Pull Requests 8 | 9 | Thanks for contributing! We have a few basic guidelines for helping us accept your pull requests. 10 | 11 | 1. New translations should be added on [Crowdin](https://crowdin.com/project/phproject) rather than through a pull request. 12 | 2. Code style should follow the [PSR-12 standard](https://www.php-fig.org/psr/psr-12/). 13 | 3. New features may be best implemented through [a plugin](https://www.phproject.org/plugins.html) rather than into the core code. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/Alanaktion/phproject/workflows/CI/badge.svg)](https://github.com/Alanaktion/phproject/actions?query=workflow%3ACI) 2 | [![Crowdin](https://badges.crowdin.net/phproject/localized.svg)](https://crowdin.com/project/phproject) 3 | 4 | Phproject 5 | ========= 6 | *A high-performance project management system in PHP* 7 | 8 | [![20i FOSS Awards](https://i.imgur.com/KkovAqB.png)](https://www.20i.com/foss-awards/category/project-management) 9 | 10 | ### Installation 11 | Download and extract [the latest release](https://github.com/Alanaktion/phproject/releases/latest) a web accessible directory, go to the page in a browser, and fill in your database connection details. 12 | 13 | Detailed requirements and installation instructions are available at [phproject.org](http://www.phproject.org/install.html). 14 | 15 | ### Development 16 | Phproject uses [Composer](https://getcomposer.org/) for dependency management. After cloning the repository, run `composer install` to install the required packages. 17 | 18 | ### Contributing 19 | Phproject is maintained as an open source project for use by anyone around the world under the [GNU General Public License](http://www.gnu.org/licenses/gpl-3.0.txt). If you find a bug or would like a new feature added, [open an issue](https://github.com/Alanaktion/phproject/issues/new) or [submit a pull request](https://github.com/Alanaktion/phproject/compare/) with new code. If you want to help with translation, [you can submit translations via Crowdin](https://crowdin.com/project/phproject). 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Since our team is small, and we don't update often, we only support the latest release of Phproject. It is highly recommended that you update whenever we release a new version, as it likely includes major bug fixes, and can impact the security of your site. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you find an issue with the security of Phproject, you may report the vulnerability on [huntr.dev](https://huntr.dev/bounties/disclose), or let me know personally via email at alan@phpizza.com. If emailing, I recommend encrypting your communication with [my PGP public key](https://keybase.io/alanaktion/pgp_keys.asc). 10 | 11 | I'll do my best to get back to you within 48 hours, at least with an idea of when we can fix the issue. Major issues that involve significant changes to the application can take several weeks since no one works on this project full time, but we'll do what we can to fix things. 12 | 13 | If it's been more than a week and we haven't replied, or if we have replied but it's been a while and we haven't communicated or fixed the issue you found, let us know and we'll invite you to a private fork where we can collaborate on a fix. 14 | -------------------------------------------------------------------------------- /app/controller/api.php: -------------------------------------------------------------------------------- 1 | set("ONERROR", function (\Base $f3): void { 13 | if (!headers_sent()) { 14 | header("Content-type: application/json"); 15 | } 16 | $out = [ 17 | "status" => $f3->get("ERROR.code"), 18 | "error" => $f3->get("ERROR.text"), 19 | ]; 20 | if ($f3->get("DEBUG") >= 2) { 21 | $out["trace"] = strip_tags($f3->get("ERROR.trace")); 22 | } 23 | echo json_encode($out, JSON_THROW_ON_ERROR); 24 | }); 25 | 26 | $this->_userId = $this->_requireAuth(); 27 | } 28 | 29 | /** 30 | * Require an API key. Sends an HTTP 401 if one is not supplied. 31 | * @return int|bool 32 | */ 33 | protected function _requireAuth() 34 | { 35 | $f3 = \Base::instance(); 36 | 37 | $user = new \Model\User(); 38 | 39 | // Use the logged in user if there is one 40 | if ($f3->get("user.api_key")) { 41 | $key = $f3->get("user.api_key"); 42 | } else { 43 | $key = false; 44 | } 45 | 46 | // Check all supported key methods 47 | if (!empty($_GET["key"])) { 48 | $key = $_GET["key"]; 49 | } elseif ($f3->get("HEADERS.X-Redmine-API-Key")) { 50 | $key = $f3->get("HEADERS.X-Redmine-API-Key"); 51 | } elseif ($f3->get("HEADERS.X-API-Key")) { 52 | $key = $f3->get("HEADERS.X-API-Key"); 53 | } elseif ($f3->get("HEADERS.X-Api-Key")) { 54 | $key = $f3->get("HEADERS.X-Api-Key"); 55 | } elseif (isset($_SERVER['HTTP_X_API_KEY'])) { 56 | $key = $_SERVER['HTTP_X_API_KEY']; 57 | } 58 | 59 | $user->load(["api_key = ?", $key]); 60 | 61 | if ($key && $user->id && $user->api_key) { 62 | $f3->set("user", $user->cast()); 63 | $f3->set("user_obj", $user); 64 | return $user->id; 65 | } else { 66 | $f3->error(401); 67 | return false; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/controller/tag.php: -------------------------------------------------------------------------------- 1 | _userId = $this->_requireLogin(); 12 | } 13 | 14 | /** 15 | * Tag index route (/tag/) 16 | * @param \Base $f3 17 | */ 18 | public function index($f3) 19 | { 20 | $tag = new \Model\Issue\Tag(); 21 | $cloud = $tag->cloud(); 22 | $f3->set("list", $cloud); 23 | shuffle($cloud); 24 | $f3->set("cloud", $cloud); 25 | 26 | $f3->set("title", $f3->get("dict.issue_tags")); 27 | $this->_render("tag/index.html"); 28 | } 29 | 30 | /** 31 | * Single tag route (/tag/@tag) 32 | * @param \Base $f3 33 | * @param array $params 34 | */ 35 | public function single($f3, $params) 36 | { 37 | $tag = new \Model\Issue\Tag(); 38 | $tag->load(["tag = ?", $params["tag"]]); 39 | 40 | if (!$tag->id) { 41 | $f3->error(404); 42 | return; 43 | } 44 | 45 | $issue = new \Model\Issue\Detail(); 46 | $issue_ids = implode(',', $tag->issues()); 47 | 48 | $f3->set("title", "#" . $params["tag"] . " - " . $f3->get("dict.issue_tags")); 49 | $f3->set("tag", $tag); 50 | $f3->set("issues.subset", $issue->find("id IN ($issue_ids) AND deleted_date IS NULL")); 51 | $this->_render("tag/single.html"); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/helper/cli.php: -------------------------------------------------------------------------------- 1 | default values 14 | * @param array $argv 15 | * @return array|null 16 | */ 17 | public function parseOptions(array $options, ?array $argv = null): ?array 18 | { 19 | $keys = array_keys($options); 20 | if ($argv === null) { 21 | $argv = $_SERVER['argv']; 22 | } 23 | 24 | // Show argument help 25 | if (getopt('h', ['help']) || (is_countable($argv) ? count($argv) : 0) == 1) { 26 | $this->showHelp($keys, $options); 27 | return null; 28 | } 29 | 30 | // Parse options 31 | $data = getopt('', $keys); 32 | 33 | // Check that required options are set 34 | foreach ($keys as $key) { 35 | if (substr_count($key, ':') != 1) { 36 | continue; 37 | } 38 | $o = rtrim($key, ':'); 39 | if (!array_key_exists($o, $data)) { 40 | echo "Required argument --$o not specified.", PHP_EOL; 41 | exit(1); 42 | } 43 | } 44 | 45 | // Fill result with defaults 46 | $result = []; 47 | foreach ($keys as $o) { 48 | $key = rtrim($o, ':'); 49 | $result[$key] = $data[$key] ?? $options[$o]; 50 | } 51 | return $result; 52 | } 53 | 54 | /** 55 | * Output help message showing parseOptions() options and defaults 56 | */ 57 | protected function showHelp(array $options, array $defaultMap): void 58 | { 59 | $required = []; 60 | $optional = []; 61 | $flags = []; 62 | foreach ($options as $o) { 63 | $colons = substr_count($o, ':'); 64 | if ($colons == 2) { 65 | $optional[] = $o; 66 | } elseif ($colons == 1) { 67 | $required[] = $o; 68 | } else { 69 | $flags[] = $o; 70 | } 71 | } 72 | 73 | echo 'Required values:', PHP_EOL; 74 | foreach ($required as $key) { 75 | $o = rtrim($key, ':'); 76 | echo "--$o={$defaultMap[$key]}", PHP_EOL; 77 | } 78 | echo PHP_EOL; 79 | 80 | echo 'Optional values:', PHP_EOL; 81 | foreach ($optional as $key) { 82 | $o = rtrim($key, ':'); 83 | echo "--$o={$defaultMap[$key]}", PHP_EOL; 84 | } 85 | echo PHP_EOL; 86 | 87 | echo 'Optional flags:', PHP_EOL; 88 | foreach ($flags as $key) { 89 | $o = rtrim($key, ':'); 90 | echo "--$o", PHP_EOL; 91 | } 92 | echo PHP_EOL; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/helper/file.php: -------------------------------------------------------------------------------- 1 | [ 9 | "image/jpeg", 10 | "image/png", 11 | "image/gif", 12 | "image/bmp", 13 | ], 14 | "icon" => [ 15 | "audio/.+" => "_audio", 16 | "application/.*zip" => "_archive", 17 | "application/x-php" => "_code", 18 | "(application|text)/xml" => "_code", 19 | "text/html" => "_code", 20 | "image/.+" => "_image", 21 | "application/x-photoshop" => "_image", 22 | "video/.+" => "_video", 23 | "application/.*pdf" => "pdf", 24 | "text/[ct]sv" => "csv", 25 | "text/.+-separated-values" => "csv", 26 | "text/.+" => "txt", 27 | "application/sql" => "txt", 28 | "application/vnd\.oasis\.opendocument\.graphics" => "odg", 29 | "application/vnd\.oasis\.opendocument\.spreadsheet" => "ods", 30 | "application/vnd\.oasis\.opendocument\.presentation" => "odp", 31 | "application/vnd\.oasis\.opendocument\.text" => "odt", 32 | "application/(msword|vnd\.(ms-word|openxmlformats-officedocument\.wordprocessingml.+))" => "doc", 33 | "application/(msexcel|vnd\.(ms-excel|openxmlformats-officedocument\.spreadsheetml.+))" => "xls", 34 | "application/(mspowerpoint|vnd\.(ms-powerpoint|openxmlformats-officedocument\.presentationml.+))" => "ppt", 35 | ], 36 | ]; 37 | 38 | /** 39 | * Get an icon name by MIME type 40 | * 41 | * Returns "_blank" when no icon matches 42 | * 43 | * @param string $contentType 44 | * @return string 45 | */ 46 | public static function mimeIcon($contentType) 47 | { 48 | foreach (self::$mimeMap["icon"] as $regex => $name) { 49 | if (preg_match("@^" . $regex . "$@i", $contentType)) { 50 | return $name; 51 | } 52 | } 53 | return "_blank"; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/helper/matrix.php: -------------------------------------------------------------------------------- 1 | $v) { 17 | $lengths[$k] = is_countable($v) ? count($v) : 0; 18 | } 19 | $max = max($lengths); 20 | $result = []; 21 | for ($i = 0; $i < $max; $i++) { 22 | foreach ($lengths as $k => $l) { 23 | if ($l > $i) { 24 | $result[] = $arrays[$k][$i]; 25 | } 26 | } 27 | } 28 | return $result; 29 | } 30 | 31 | /** 32 | * Run array_merge on an array of arrays 33 | * 34 | * @param array $arrays 35 | * @return array 36 | */ 37 | public function merge(array $arrays) 38 | { 39 | return call_user_func_array("array_merge", $arrays); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/helper/template.php: -------------------------------------------------------------------------------- 1 | esc($csrf_token) ?>" />'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/model/custom.php: -------------------------------------------------------------------------------- 1 | _table_name = $tableName; 16 | parent::__construct(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/model/issue/backlog.php: -------------------------------------------------------------------------------- 1 | issue_comment($item->issue_id, $item->id); 37 | } 38 | return $item; 39 | } 40 | 41 | /** 42 | * Save the comment 43 | * @return Comment 44 | */ 45 | public function save(): Comment 46 | { 47 | // Censor credit card numbers if enabled 48 | if (\Base::instance()->get("security.block_ccs") && preg_match("/[0-9-]{9,15}[0-9]{4}/", $this->get("text"))) { 49 | $this->set("text", preg_replace("/[0-9-]{9,15}([0-9]{4})/", "************$1", $this->get("text"))); 50 | } 51 | 52 | return parent::save(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/model/issue/comment/detail.php: -------------------------------------------------------------------------------- 1 | db->exec( 26 | 'SELECT d.id as d_id,i.id, i.name, i.start_date, i.due_date, i.status_closed, i.author_name, i.author_username, i.owner_name, i.owner_username, i.status_name, i.status, d.dependency_type ' . 27 | 'FROM issue_detail i JOIN issue_dependency d on i.id = d.dependency_id ' . 28 | 'WHERE d.issue_id = :issue_id AND i.deleted_date IS NULL ' . 29 | 'ORDER BY :orderby', 30 | [':issue_id' => $issueId, ':orderby' => $orderby] 31 | ); 32 | } 33 | 34 | /** 35 | * Find dependent issues by issue ID 36 | * @param int $issueId 37 | * @param string $orderby 38 | * @return array 39 | */ 40 | public function findby_dependent($issueId, $orderby = 'due_date'): array 41 | { 42 | return $this->db->exec( 43 | 'SELECT d.id as d_id, i.id, i.name, i.start_date, i.due_date, i.status_closed, i.author_name, i.author_username, i.owner_name, i.owner_username, i.status_name, i.status, d.dependency_type ' . 44 | 'FROM issue_detail i JOIN issue_dependency d on i.id = d.issue_id ' . 45 | 'WHERE d.dependency_id = :issue_id AND i.deleted_date IS NULL ' . 46 | 'ORDER BY :orderby', 47 | [':issue_id' => $issueId, ':orderby' => $orderby] 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/model/issue/detail.php: -------------------------------------------------------------------------------- 1 | issue_file($item->issue_id, $item->id); 39 | } 40 | return $item; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/model/issue/file/detail.php: -------------------------------------------------------------------------------- 1 | db->exec("DELETE FROM {$this->_table_name} WHERE issue_id = ?", $issueId); 24 | return $this; 25 | } 26 | 27 | /** 28 | * Get a multidimensional array representing a tag cloud 29 | * @return array 30 | */ 31 | public function cloud(): array 32 | { 33 | return $this->db->exec("SELECT tag, COUNT(*) AS freq FROM {$this->_table_name} GROUP BY tag ORDER BY freq DESC"); 34 | } 35 | 36 | /** 37 | * Find issues with the given/current tag 38 | * @param string $tag 39 | * @return array Issue IDs 40 | */ 41 | public function issues($tag = ''): array 42 | { 43 | if (!$tag) { 44 | $tag = $this->get("tag"); 45 | } 46 | $result = $this->db->exec("SELECT DISTINCT issue_id FROM {$this->_table_name} WHERE tag = ?", $tag); 47 | $return = []; 48 | foreach ($result as $r) { 49 | $return[] = $r["issue_id"]; 50 | } 51 | return $return; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/model/issue/type.php: -------------------------------------------------------------------------------- 1 | db->exec( 25 | 'SELECT i.* FROM issue_detail i JOIN issue_watcher w on i.id = w.issue_id ' . 26 | 'WHERE w.user_id = :user_id AND i.deleted_date IS NULL AND i.closed_date IS NULL AND i.status_closed = 0 AND i.owner_id != :user_id2 ' . 27 | 'ORDER BY :orderby', 28 | [':user_id' => $userId, ':user_id2' => $userId, ':orderby' => $orderby] 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/model/sprint.php: -------------------------------------------------------------------------------- 1 | start_date)); 20 | if ($weekDay == 0) { 21 | return date("Y-m-d", strtotime($this->start_date . " +1 day")); 22 | } elseif ($weekDay == 6) { 23 | return date("Y-m-d", strtotime($this->start_date . " +2 days")); 24 | } 25 | return $this->start_date; 26 | } 27 | 28 | public function getLastWeekday() 29 | { 30 | $weekDay = date("w", strtotime($this->end_date)); 31 | if ($weekDay == 0) { 32 | return date("Y-m-d", strtotime($this->end_date . " -2 days")); 33 | } elseif ($weekDay == 6) { 34 | return date("Y-m-d", strtotime($this->end_date . " -1 day")); 35 | } 36 | return $this->end_date; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/model/user/group.php: -------------------------------------------------------------------------------- 1 | get("db.instance"); 26 | 27 | if ($user_id === null) { 28 | $user_id = $f3->get("user.id"); 29 | } 30 | 31 | $query_groups = "SELECT u.id, u.name, u.username 32 | FROM user u 33 | JOIN user_group g ON u.id = g.group_id 34 | WHERE g.user_id = :user AND u.deleted_date IS NULL ORDER BY u.name"; 35 | 36 | $result = $db->exec($query_groups, [":user" => $user_id]); 37 | return $result; 38 | } 39 | 40 | /** 41 | * Check if a user is in a group 42 | * @param int $group_id 43 | * @param int $user_id 44 | * @return bool 45 | */ 46 | public static function userIsInGroup(int $group_id, $user_id = null): bool 47 | { 48 | $f3 = \Base::instance(); 49 | 50 | if ($user_id === null) { 51 | $user_id = $f3->get("user.id"); 52 | } 53 | 54 | $group = new static(); 55 | $group->load(['user_id = ? AND group_id = ?', $user_id, $group_id]); 56 | 57 | return $group->id ? true : false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/plugin/README.md: -------------------------------------------------------------------------------- 1 | Phproject plugins should be placed in this directory. 2 | 3 | * Plugins must have a `Base` class in `base.php` that extends `\Plugin` 4 | * This class should contain all core code for the plugin, including all methods and properties in the Plugin Standards 5 | * Plugins must have a `_load()` method which is called when initializing the plugin 6 | * Any hooks and routes used in the plugin should be initialized in this method 7 | * Plugins must have a PHPDoc comment block at the start of the `base.php` file 8 | * Block must contain at least the @package tag 9 | * Block should contain the @author tag 10 | * Plugins must have an `_installed()` method which will be called to check the installation status of the plugin 11 | * Plugins may have an `_install()` method which will be called if `_installed()` returns `false` 12 | * Plugins may have a `dict` directory, which will be loaded for localization 13 | * Plugins should follow the [Code Standards](http://www.phproject.org/contribute.html) 14 | -------------------------------------------------------------------------------- /app/view/admin/plugins.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 |
{{ @dict.name }}{{ @dict.cols.author }}{{ @dict.version }}
{{ @meta.package | esc }}{{ @meta.author | esc }}{{ @meta.version }} 30 | 31 | Details 32 | 33 |
38 |
39 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /app/view/admin/plugins/single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 14 | {~ @plugin->_admin() ~} 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /app/view/admin/sprints.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |  {{ @dict.new }} 12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
{{ @dict.cols.id }}{{ @dict.name }}{{ @dict.start_date }}{{ @dict.end_date }}
{{ @sprint.id }}{{ @sprint.name | esc }}{{ @sprint.start_date }}{{ @sprint.end_date }}
36 |
37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /app/view/admin/sprints/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | {{ @dict.edit_sprint }} 13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 | {{ @dict.cancel }} 35 |
36 |
37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/view/admin/sprints/new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | {{ @dict.new_sprint }} 13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 | {{ @dict.cancel }} 35 |
36 |
37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/view/admin/users/deleted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 13 | 14 |
15 |

16 |  Back 17 |  Deactivated Users 18 |

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | 50 | 51 |
{{ @dict.cols.id }}{{ @dict.username }}{{ @dict.email }}{{ @dict.name }}{{ @dict.role }}{{ @dict.task_color }}
{{ @user.id }}{{ @user.username | esc }}{{ @user.email | esc }}{{ @user.name | esc }}{{ ucfirst(@user.role) }} #{{ @user.task_color }} 41 |
42 | 43 | 46 | 47 |
52 | 53 | 54 | 59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /app/view/backlog/item.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{ @project.priority_name }} 3 | 4 | {{ @project.type_name }} 5 | #{{ @project.id }}  6 | {{ @project.name | esc }} 7 | 8 | - {{ @project.size_estimate }} 9 | 10 | 11 |
  • 12 | -------------------------------------------------------------------------------- /app/view/backlog/old.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    11 |
    12 |

    13 | 14 |  {{ @dict.show_future_sprints }} 15 | 16 |

    17 |
    18 |
    19 | {{ @dict.backlog }} 20 |
    21 |
    22 |
      23 |
    24 |
    25 |

    26 | {{ @dict.backlog_old_help_text }} 27 |

    28 |
    29 |
    30 | 49 |
    50 | 51 | 52 | 53 | 54 |
    55 | 56 | 57 | -------------------------------------------------------------------------------- /app/view/blocks/admin/tabs.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/view/blocks/dashboard-issue-list.html: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /app/view/blocks/file/thumb.html: -------------------------------------------------------------------------------- 1 |
  • 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ @file.filename | esc }} 17 | 18 | 19 |
  • 20 | -------------------------------------------------------------------------------- /app/view/blocks/footer-scripts.html: -------------------------------------------------------------------------------- 1 | {~ 2 | @types = array(); 3 | foreach(@issue_types as @type) { 4 | if(@type.id > 0 && @type.id < 10) { 5 | array_push(@types, @type.id); 6 | } 7 | } 8 | ~} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{ @code }} 20 | 21 | -------------------------------------------------------------------------------- /app/view/blocks/footer.html: -------------------------------------------------------------------------------- 1 | {~ 2 | @dblog = @db.instance->log(); 3 | @total = preg_match_all('/\([0-9\.]{3,}ms\)/', @dblog, $matches); 4 | @cached = substr_count(@dblog, '[CACHED]'); 5 | ~} 6 | 7 |
    8 |
    {{ @dblog | esc }}
    9 |
    10 |
    11 |
    12 |
    13 | 14 | 17 | 18 |
    19 |
    20 |

    21 | 22 | {{ @dict.n_queries,(@total - @cached) | format }} · 23 | 24 | 25 | 26 | {{ round(@pagemtime * 1000, 0) }}ms · 27 | 28 | {{ \Helper\View::instance()->formatFilesize(memory_get_peak_usage()) }} 29 | 30 | · {{ substr(@revision, 0, 7) }} 31 | 32 | 33 | · {{ count(@plugins) }} {{ @dict.plugins }} 34 | 35 |

    36 |
    37 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /app/view/blocks/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ empty(@title) ? @site.name : @title . ' - ' . @site.name | esc }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/view/blocks/issue-comment.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 |
    6 |

    7 | {{ @comment.user_name | esc }} 8 | 9 | > {{ @issue.name | esc }} 10 | 11 | 12 | × 13 | 14 |

    15 |
    {{ @comment.text, array('hashtags' => false) | parseText }}
    16 | 17 |
    18 | 19 | 20 |
    21 | 22 | {{ @comment.file_filename | esc }} 23 | [{{ @dict.deleted }}] 24 |
    25 |
    26 | 27 | 28 | 29 | {{ @comment.file_filename | esc }} 30 | 31 | 32 |
    33 |
    34 |
    35 |

    36 | {{ date("D, M j, Y \\a\\t g:ia", $this->utc2local(@comment.created_date)) }} 37 |

    38 |
    39 |
    40 | -------------------------------------------------------------------------------- /app/view/blocks/issue-list/issue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ @item.id }} 6 | {{ @item.name | esc }} 7 | {{ isset(@dict[@item.type_name]) ? @dict[@item.type_name] : str_replace('_', ' ', @item.type_name) }} 8 | {{ isset(@dict[@item.priority_name]) ? @dict[@item.priority_name] : str_replace('_', ' ', @item.priority_name) }} 9 | {{ isset(@dict[@item.status_name]) ? @dict[@item.status_name] : str_replace('_', ' ', @item.status_name) }} 10 | {{ @item.parent_id ?: '' }} 11 | 12 | 13 | 14 | 15 | 16 | {{ @item.author_name | esc }} 17 | 18 | 19 | 20 | 21 | {{ @item.owner_name | esc }} 22 | 23 | 24 | {{ @item.owner_name | esc }} 25 | 26 | 27 | {{ !empty(@item.sprint_start_date) ? date("n/j/y", strtotime(@item.sprint_start_date)) : "" }} 28 | {{ isset(@dict[@item.repeat_cycle]) ? ucwords(@dict[@item.repeat_cycle]) : @dict.not_repeating }} 29 | {{ date("n/j/y", @this->utc2local(@item.created_date)) }} 30 | {{ !empty(@item.due_date) ? date("n/j/y", strtotime(@item.due_date)) : "" }} 31 | 32 | {{ !empty(@item.closed_date) ? date("n/j/y", @this->utc2local(@item.closed_date)) : "" }} 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/view/blocks/navbar-public.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /app/view/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 |
    17 |

    {{ @ERROR.code }} Not Found

    18 |

    {{ @dict.error.404_text }}

    19 |
    20 | 21 |
    22 | 23 | 24 | -------------------------------------------------------------------------------- /app/view/error/500.html: -------------------------------------------------------------------------------- 1 |

    get('ERROR.title'); ?>

    2 |

    An error occurred processing your request. Please try again.

    3 | get('DEBUG') == 3) { ?> 4 |

    get('ERROR.text'); ?>

    5 |
    get('ERROR.trace')); ?>
    6 | 7 | -------------------------------------------------------------------------------- /app/view/error/general.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |

    {{ @ERROR.code }} {{ @ERROR.text }}

    11 |

    An unknown error occurred.

    12 |
    13 | 14 |
    15 | 16 | 17 | -------------------------------------------------------------------------------- /app/view/error/inline.html: -------------------------------------------------------------------------------- 1 |
    2 |

    get("ERROR.code"); ?> get("ERROR.status"); ?>

    3 |

    get("ERROR.text"); ?>

    4 | get("DEBUG") >= 2) { ?> 5 |
    6 | get("ERROR.trace")); ?> 7 |
    8 | 9 | get("DEBUG") >= 3) { ?> 10 |
    get("db.instance")->log(); ?>
    11 | 12 |
    13 | 15 | Fatal Error 16 | 17 | */ ?> 18 | -------------------------------------------------------------------------------- /app/view/index/atom.xml: -------------------------------------------------------------------------------- 1 | {{ '' }} 2 | 3 | 4 | {{ @get.type == 'all' ? 'All Issues' : htmlentities(@feed_user.name) }} 5 | 6 | {{ @url }} 7 | {{ @PACKAGE }} 8 | 9 | 10 | 11 | {{ htmlentities(@issue.name) }} 12 | {{ @site.url . 'issues/' . @issue.id }} 13 | 14 | {{ @site.url . 'issues/' . @issue.id }} 15 | {{ date(DATE_RSS, strtotime(@issue.created_date)) }} 16 | {{ htmlentities(@issue.author_name) }} 17 | {{ htmlentities(@issue.owner_name) }} 18 | {{ htmlentities(@issue.type_name) }} 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/view/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
    8 | 9 |
    10 |

    {{ @site.name | esc }}

    11 |

    {{ @site.description | esc }}

    12 |
    13 | 14 |
    15 | 16 | 17 | -------------------------------------------------------------------------------- /app/view/index/opensearch.xml: -------------------------------------------------------------------------------- 1 | 2 | {{ @site.name | esc }} 3 | {{ @site.description | esc }} 4 | UTF-8 5 | 6 | {{ @site.url }}search 7 | 8 | -------------------------------------------------------------------------------- /app/view/index/reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 | 13 |
    14 | {{ @dict.reset_password }} 15 | 16 |

    {{ @reset.success | esc }}

    17 |
    18 | 19 |

    {{ @reset.error | esc }}

    20 |
    21 |
    22 | 23 |
    24 | 25 |
    26 |
    27 |
    28 |
    29 | 30 | {{ @dict.cancel }} 31 |
    32 |
    33 |
    34 | 35 |
    36 |
    37 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /app/view/index/reset_complete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 | 13 |
    14 | {{ @dict.reset_password }} 15 | 16 |

    {{ @reset.success | esc }}

    17 |
    18 | 19 |

    {{ @reset.error | esc }}

    20 |
    21 |
    22 | 23 |
    24 | 25 |
    26 |
    27 |
    28 | 29 |
    30 | 31 |
    32 |
    33 |
    34 | 35 |
    36 | 37 |
    38 |
    39 |
    40 |
    41 | 42 | {{ @dict.cancel }} 43 |
    44 |
    45 |
    46 | 47 |
    48 |
    49 |
    50 | 51 | 52 | -------------------------------------------------------------------------------- /app/view/index/reset_forced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 | 13 |
    14 | {{ @dict.reset_password }} 15 | 16 |

    {{ @reset.error }}

    17 |
    18 |
    19 | 20 |
    21 | 22 |
    23 |
    24 |
    25 | 26 |
    27 | 28 |
    29 |
    30 |
    31 | 32 |
    33 | 34 |
    35 |
    36 |
    37 |
    38 | 39 | {{ @dict.cancel }} 40 |
    41 |
    42 |
    43 | 44 |
    45 |
    46 |
    47 | 48 | 49 | -------------------------------------------------------------------------------- /app/view/issues/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
    12 | 13 | 14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/view/issues/file/preview/table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ @file.filename }} 5 | 6 | 7 | 8 | 9 |

    10 | 11 |  Download 12 | 13 |

    14 | 15 | {~ while((@row = fgetcsv(@fh, 0, @delimiter)) !== false): ~} 16 | 17 | 18 | 19 | 20 | 21 | {~ endwhile ~} 22 |
    {{ @col }}
    23 | {~ fclose(@fh) ~} 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/view/issues/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 | 11 |

    {{ @dict.deleted_success,intval(@GET.deleted) | format }} {{ @dict.restore_issue }}

    12 |
    13 | 14 |
    15 | 18 |
    19 | 20 |
    21 | 28 |
    29 |
    30 | 31 |
    32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/view/issues/new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |
    11 |

    {{ @dict.new_n, @dict.issues | format }}

    12 | 19 |
    20 |
    21 | 22 |
    23 | 24 | 25 | -------------------------------------------------------------------------------- /app/view/issues/project/files.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 |
    8 | 9 |
    10 |
    11 |
      12 | 13 | 14 | 15 |
    16 |
    17 |
    18 |
    19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
    {{ @dict.file_name }}{{ @dict.cols.parent }}{{ @dict.uploaded_by }}{{ @dict.upload_date }}{{ @dict.file_size }}
    34 | 35 | {{ @file.filename | esc }}{{ @file.issue_id }}{{ @file.user_name | esc }}{{ date('M j, Y \a\t g:ia', @this->utc2local(strtotime(@file.created_date))) }}{{ @file.filesize | formatFilesize }}
    45 |
    46 |
    47 |
    48 |
    49 | 50 |
    51 | {{ @dict.project_no_files_attached }} 52 |
    53 |
    54 |
    55 | -------------------------------------------------------------------------------- /app/view/issues/project/tree-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ str_repeat(" ", @level) }} 4 | {{ @issue.id }} – 5 | 6 | 7 | {{ @issue.name | esc }} 8 | 9 | 10 | {{ @issue.name | esc }} 11 | 12 | 13 | 14 | {{ @issue.type_name }} 15 | {{ @issue.owner_name }} 16 | {{ @issue.author_name }} 17 | {{ @issue.priority_name }} 18 | 19 | 20 | 21 | {{ date("n/j/y", strtotime(@issue.due_date)) }} 22 | 23 | 24 | 25 | {{ date("n/j/y", strtotime(@issue.sprint_end_date)) }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{ date("n/j/y", strtotime(@issue.sprint_start_date)) }} 33 | 34 | 35 | {{ @issue.hours_spent }} 36 | 37 | -------------------------------------------------------------------------------- /app/view/issues/project/tree.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {~ @renderTree(@project) ~} 18 | 19 |
    {{ @dict.cols.id }}{{ @dict.cols.title }}{{ @dict.cols.type }}{{ @dict.cols.assignee }}{{ @dict.cols.author }}{{ @dict.cols.priority }}{{ @dict.cols.due_date }}{{ @dict.cols.sprint }}{{ @dict.cols.hours_spent }}
    20 |
    21 | -------------------------------------------------------------------------------- /app/view/issues/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |

    11 | {{ empty(@GET.closed) ? 'Include closed issues' : 'Exclude closed issues' }} 12 |

    13 | 14 |
    15 | 18 |
    19 | 20 |
    21 | 34 |
    35 |
    36 | 37 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /app/view/issues/single/related.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 | {{ @dict.new_sub_project }} 4 | 5 | {{ @dict.new_task }} 6 | {{ @dict.browse }} 7 | 8 |

    9 | 10 | 11 | 12 |
    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
    {{ @dict.cols.id }}{{ @dict.cols.title }}{{ @dict.cols.author }}{{ @dict.cols.assignee }}{{ @dict.cols.created }}{{ @dict.cols.priority }}{{ @dict.cols.due }}{{ @dict.cols.status }}
    {{ @item.id }}{{ @item.name | esc }}{{ @item.author_name | esc }}{{ @item.owner_name | esc }}{{ date("n/j/y", strtotime(@item.created_date)) }}{{ @item.priority_name | esc }}{{ !empty(@item.due_date) ? date("n/j", strtotime(@item.due_date)) : "" }}{{ @item.status_name | esc }}
    41 |
    42 |
    43 | 44 |

    {{ @dict.no_related_issues }}

    45 |
    46 |
    47 | -------------------------------------------------------------------------------- /app/view/issues/single/watchers.html: -------------------------------------------------------------------------------- 1 |
      2 | 3 |
    • 4 |  {{ @watcher.name | esc }} 5 |
    • 6 |
      7 |
    8 |
    9 |
    10 | 15 |
    16 |
    17 | 18 |
    19 |
    20 | -------------------------------------------------------------------------------- /app/view/notification/blocks/_head.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /app/view/notification/blocks/actions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | 37 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 |
    17 | View Issue 18 |
    24 |
    25 |
    28 | 29 | Or reply to this message to leave a comment 30 | 31 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /app/view/notification/blocks/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 |
    9 | {{ @@footer_msg ?: 'You receieved this message because you are a watcher, author, or assignee on this issue.' }} 10 |
    16 | 17 | 18 | -------------------------------------------------------------------------------- /app/view/notification/blocks/logo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 | 20 | 21 | 22 |
    9 | 10 | 11 | 12 | {{ @site.name | esc }} 13 | 14 | {{ @site.name | esc }} 15 | 16 | 17 |
    23 | 24 | 25 | -------------------------------------------------------------------------------- /app/view/notification/blocks/preview.html: -------------------------------------------------------------------------------- 1 |
    2 | {{ @previewText | esc }} 3 |
    4 | -------------------------------------------------------------------------------- /app/view/notification/comment.txt: -------------------------------------------------------------------------------- 1 | --- --- 2 | {{ @site.name }} 3 | New comment on #{{ @issue.id }} - {{ @issue.name }} 4 | 5 | {{ @comment.user_name }}: 6 | {{ @comment.text }} 7 | 8 | 9 | Attached file: {{ @comment.file_filename }} - {{ @site.url }}files/{{ @comment.file_id }}/{{ @comment.file_filename }} 10 | 11 | 12 | View issue: {{ @site.url }}issues/{{ @issue.id }} 13 | 14 | {{ date("D, M j, Y \\a\\t g:ia", $this->utc2local(strtotime(@comment.created_date))) }} 15 | -------------------------------------------------------------------------------- /app/view/notification/file.txt: -------------------------------------------------------------------------------- 1 | --- --- 2 | {{ @site.name }} 3 | New file uploaded to #{{ @issue.id }} - {{ @issue.name }}: 4 | 5 | {{ @file.user_name }} 6 | 7 | {{ @file.filename }} - {{ @site.url }}files/{{ @file.id }}/{{ @file.filename }} ({{ \Helper\View::instance()->formatFilesize(@file.filesize) }}) 8 | 9 | {{ date("D, M j, Y \\a\\t g:ia", $this->utc2local(strtotime(@file.created_date))) }} 10 | -------------------------------------------------------------------------------- /app/view/notification/new.txt: -------------------------------------------------------------------------------- 1 | --- --- 2 | {{ @site.name }} 3 | Issue #{{ @issue.id }} {{ @issue.name }} created 4 | 5 | Author: {{ @issue.author_name }} 6 | Assigned to: {{ @issue.owner_name }} 7 | Priority: {{ @issue.priority_name }} 8 | Status: {{ @issue.status_name }} 9 | Planned Hours: {{ @issue.hours_total ?: 'None' }} 10 | Due Date: {{ @issue.due_date ? date("D, M j, Y", strtotime(@issue.due_date)) : 'None' }} 11 | 12 | Description: 13 | {{ @issue.description }} 14 | 15 | View issue: {{ @site.url }}issues/{{ @issue.id }} 16 | 17 | {{ date("D, M j, Y \\a\\t g:ia", $this->utc2local(strtotime(@issue.created_date))) }} 18 | -------------------------------------------------------------------------------- /app/view/notification/update.txt: -------------------------------------------------------------------------------- 1 | --- --- 2 | {{ @site.name }} 3 | Issue #{{ @issue.id }} {{ @issue.name }} updated 4 | 5 | {{ @update.user_name }}: 6 | 7 | {~ 8 | foreach(@changes as @change) { 9 | @human_readable = \Helper\Update::instance()->humanReadableValues(@change.field, @change.old_value, @change.new_value); 10 | if(@change.old_value && @change.new_value) { 11 | echo @human_readable.field, ' changed from ', @human_readable.old, ' to ', @human_readable.new, "\n"; 12 | } elseif(@change.old_value) { 13 | echo @human_readable.field, ' removed', "\n"; 14 | } else { 15 | echo @human_readable.field, ' set to ', @human_readable.new, "\n"; 16 | } 17 | } 18 | ~} 19 | 20 | View issue: {{ @site.url }}issues/{{ @issue.id }} 21 | 22 | {{ date("D, M j, Y \\a\\t g:ia", $this->utc2local(strtotime(@update.created_date))) }} 23 | -------------------------------------------------------------------------------- /app/view/notification/user_due_issues.txt: -------------------------------------------------------------------------------- 1 | {{ @site.name }} 2 | 3 | 4 | Issues due today: 5 | 6 | 7 | #{{ @issue.id }}: {{ @issue.name }} - {{ @site.url }}issues/{{ @issue.id }} 8 | 9 | 10 | 11 | 12 | Overdue issues: 13 | 14 | 15 | #{{ @issue.id }}: {{ @issue.name }} - {{ @site.url }}issues/{{ @issue.id }} - Due {{ date("D, M j, Y", strtotime(@issue.due_date)) }} 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/view/notification/user_reset.txt: -------------------------------------------------------------------------------- 1 | {{ @site.name }} 2 | 3 | Someone requested to reset your password on {{ date("F jS \\a\\t g:ia", $this->utc2local()) }}. If this was you, click the link below to reset your password. Otherwise, you can safely ignore this email. 4 | 5 | {{ @site.url }}reset/{{ @token }} 6 | -------------------------------------------------------------------------------- /app/view/tag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 |

    {{ @dict.issue_tags }}

    10 | 11 |

    {{ @dict.no_tags_created }}

    12 |
    13 |

    {{ @dict.tag_help_1 }}
    14 | {{ @dict.tag_help_2 }} 15 |

    16 | 17 | 21 |
    22 |
    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
    {{ @dict.name }}{{ @dict.count }}
    {{ @item.tag }}{{ @item.freq }}
    39 |
    40 |
    41 | {~ 42 | @tagSizeMin = 14; 43 | @tagSizeMax = 60; 44 | @tagFreqMin = null; 45 | @tagFreqMax = 0; 46 | foreach (@cloud as @item) { 47 | if (@tagFreqMin === null || @item.freq < @tagFreqMin) @tagFreqMin = @item.freq; 48 | if (@tagFreqMax < @item.freq) @tagFreqMax = @item.freq; 49 | } 50 | @sizes = []; 51 | foreach (@cloud as @item) { 52 | @sizes[@item.tag] = (@item.freq - @tagFreqMin) / (@tagFreqMax - @tagFreqMin) * (@tagSizeMax - @tagSizeMin) + @tagSizeMin; 53 | } 54 | ~} 55 |
    56 | 57 | {{ @item.tag }}  58 | 59 |
    60 |
    61 |
    62 | 63 | 64 | 69 |
    70 | 71 | 72 | -------------------------------------------------------------------------------- /app/view/tag/single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 | 10 |  {{ @dict.view_all_tags }} 11 | 12 |

    #{{ @tag.tag }}

    13 |

    {{ @dict.tag_help_1 }}
    14 | {{ @dict.tag_help_2 }} 15 |

    16 | 17 | 18 |
    19 | 20 | 21 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/bugs.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.my_bugs }} {{ count(@bugs) }}  3 | 4 |

    5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/issue_tree.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.issue_tree }}

    3 |
    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {~ @renderTree(@issue) ~} 21 | 22 | 23 |
    {{ @dict.cols.id }}{{ @dict.cols.title }}{{ @dict.cols.type }}{{ @dict.cols.assignee }}{{ @dict.cols.author }}{{ @dict.cols.priority }}{{ @dict.cols.due }}{{ @dict.cols.sprint }}{{ @dict.cols.hours_spent }}
    24 |
    25 |
    26 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/my_comments.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.my_comments }}

    3 |
    4 | {~ @issue = new \Model\Issue() ~} 5 | 6 | {~ @issue->load(@comment.issue_id) ~} 7 | 8 | 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/open_comments.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.open_comments }}

    3 |
    4 | {~ @issue = new \Model\Issue() ~} 5 | 6 | {~ @issue->load(@comment.issue_id) ~} 7 | 8 | 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/projects.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.my_projects }} {{ count(@projects) }}  3 | 4 |

    5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/recent_comments.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.recent_comments }}

    3 |
    4 | {~ @issue = new \Model\Issue() ~} 5 | 6 | {~ @issue->load(@comment.issue_id) ~} 7 | 8 | 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/repeat_work.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.repeat_work }} {{ count(@repeat_work) }}

    3 | 4 | 5 |
    6 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/subprojects.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.my_subprojects }} {{ count(@subprojects) }}

    3 | 4 | 5 |
    6 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/tasks.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.my_tasks }} {{ count(@tasks) }}  3 | 4 |

    5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /app/view/user/dashboard-widgets/watchlist.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ @dict.my_watchlist }} {{ count(@watchlist) }}

    3 | 4 | 5 |
    6 | -------------------------------------------------------------------------------- /app/view/user/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 | 17 |  {{ @dict.manage_widgets }} 18 | 19 | 20 | 21 |  {{ @dict.taskboard }} 22 | 23 | 24 | 25 |
    26 |
    27 | 28 | 29 | 30 |
    31 |
    32 | 33 | 34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 | -------------------------------------------------------------------------------- /app/view/user/single/tree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 | 10 |
    11 |
    12 | 13 |
    14 |
    15 |

    {{ @this_user.name | esc }}

    16 |

    {{ @dict.username }}: {{ @this_user.username | esc }}

    17 | 18 |

    {{ @dict.email }}: {{ @this_user.email | esc }}

    19 |
    20 |
    21 |
    22 | 23 |

    {{ @dict.assigned_issues }}

    24 |
    25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {~ @renderTree(@issue) ~} 42 | 43 | 44 |
    {{ @dict.cols.id }}{{ @dict.cols.title }}{{ @dict.cols.type }}{{ @dict.cols.assignee }}{{ @dict.cols.author }}{{ @dict.cols.priority }}{{ @dict.cols.due_date }}{{ @dict.cols.sprint }}{{ @dict.cols.hours_spent }}
    45 |
    46 | 47 | 48 |
    49 | 50 | 51 | -------------------------------------------------------------------------------- /app/view/user/single/tree/ajax.html: -------------------------------------------------------------------------------- 1 |

    {{ @dict.assigned_issues }}

    2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {~ @renderTree(@issue) ~} 20 | 21 | 22 |
    {{ @dict.cols.id }}{{ @dict.cols.title }}{{ @dict.cols.type }}{{ @dict.cols.assignee }}{{ @dict.cols.author }}{{ @dict.cols.priority }}{{ @dict.cols.due_date }}{{ @dict.cols.sprint }}{{ @dict.cols.hours_spent }}
    23 |
    24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alanaktion/phproject", 3 | "description": "A high performance full-featured project management system", 4 | "type": "project", 5 | "require": { 6 | "php": ">=7.4.0", 7 | "bcosca/fatfree-core": "^3.8", 8 | "netcarver/textile": "^4.0", 9 | "neos/diff": "^7.3", 10 | "league/commonmark": "^2.6" 11 | }, 12 | "license": "GPL-3.0-or-later", 13 | "authors": [ 14 | { 15 | "name": "Alan Hardman", 16 | "email": "alan@phpizza.com" 17 | } 18 | ], 19 | "config": { 20 | "platform": { 21 | "php": "7.4.33" 22 | } 23 | }, 24 | "require-dev": { 25 | "squizlabs/php_codesniffer": "3.*", 26 | "rector/rector": "^2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cron/base.php: -------------------------------------------------------------------------------- 1 | mset([ 18 | "UI" => $homedir . "app/view/", 19 | "LOGS" => $homedir . "log/", 20 | "AUTOLOAD" => $homedir . "app/;" . $homedir . "lib/vendor/", 21 | "TEMP" => $homedir . "tmp/", 22 | "TZ" => "UTC", 23 | ]); 24 | 25 | // Load local configuration 26 | $f3->mset(require_once('config.php')); 27 | 28 | // Connect to database 29 | $f3->set("db.instance", new DB\SQL( 30 | "mysql:host=" . $f3->get("db.host") . ";port=3306;dbname=" . $f3->get("db.name"), 31 | $f3->get("db.user"), 32 | $f3->get("db.pass") 33 | )); 34 | 35 | // Load database-backed config 36 | \Model\Config::loadAll(); 37 | -------------------------------------------------------------------------------- /cron/due_alerts.php: -------------------------------------------------------------------------------- 1 | getAll(); 12 | 13 | foreach ($users as $u) { 14 | if ($u->option('disable_due_alerts')) { 15 | continue; 16 | } 17 | $u->sendDueAlert(); 18 | } 19 | -------------------------------------------------------------------------------- /cron/update_sprints.php: -------------------------------------------------------------------------------- 1 | find(["type_id = ? AND deleted_date IS NULL", $f3->get("issue_type.project")]); 12 | 13 | foreach ($issues as $issue) { 14 | $issue->resetChildren(false); 15 | } 16 | -------------------------------------------------------------------------------- /css/backlog.css: -------------------------------------------------------------------------------- 1 | body.is-loading #backlog .list-group.sortable { 2 | opacity: 0; 3 | } 4 | #backlog .list-group.sortable { 5 | min-height: 80px; 6 | transition: all 0.3s ease; 7 | margin: 0; 8 | } 9 | #backlog .list-group-item { 10 | -webkit-box-sizing: content-box; 11 | -moz-box-sizing: content-box; 12 | box-sizing: content-box; 13 | position: relative; 14 | padding: 6px 6px 6px 14px; 15 | } 16 | body.is-sortable #backlog .list-group-item { 17 | cursor: move; 18 | } 19 | #backlog .hidden-group, 20 | #backlog .hidden-type { 21 | display: none; 22 | visibility: hidden; 23 | } 24 | #backlog .list-group-item.placeholder { 25 | opacity: 0.5; 26 | } 27 | body.is-sortable #backlog .list-group-item:hover { 28 | background-image: url(../img/backlog/i_has_grip.png); 29 | background-position: 2px center; 30 | background-repeat: no-repeat; 31 | } 32 | #backlog .list-group-item.completed { 33 | text-decoration: line-through; 34 | } 35 | #backlog .panel-heading { 36 | position: relative; 37 | } 38 | @supports (position: sticky) { 39 | #backlog .panel-heading { 40 | position: sticky; 41 | top: 60px; 42 | z-index: 2; 43 | } 44 | } 45 | #backlog .panel-heading > a:first-child { 46 | cursor: pointer; 47 | font-weight: bold; 48 | } 49 | .badge.status { 50 | background-color: #C6C6C6; 51 | color: #fff; 52 | } 53 | #backlog .badge.status.new { 54 | background-color: #00537D; 55 | } 56 | #backlog .badge.status.active { 57 | background-color: #FFA700; 58 | } 59 | #backlog .badge.status.completed { 60 | background-color: #7EC500 61 | } 62 | -------------------------------------------------------------------------------- /db/14.12.11.sql: -------------------------------------------------------------------------------- 1 | # This database update occured after commit f7c42f23b8 2 | 3 | # Add Version checking to the database 4 | CREATE TABLE IF NOT EXISTS `config` ( 5 | `id` int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, 6 | `attribute` varchar(255) NULL, 7 | `value` varchar(255) NULL, 8 | UNIQUE KEY `attribute` (`attribute`) 9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 10 | 11 | INSERT INTO `config` (`attribute`, `value`) VALUES ('version', '0.0.0'); 12 | 13 | # Add start_date to issues and issue_detail view 14 | ALTER TABLE `issue` ADD `start_date` date NULL AFTER `deleted_date`; 15 | 16 | CREATE OR REPLACE VIEW `issue_detail` AS 17 | select `issue`.`id` AS `id`,`issue`.`status` AS `status`,`issue`.`type_id` AS `type_id`,`issue`.`name` AS `name`,`issue`.`description` AS `description`,`issue`.`parent_id` AS `parent_id`,`issue`.`author_id` AS `author_id`,`issue`.`owner_id` AS `owner_id`,`issue`.`priority` AS `priority`,`issue`.`hours_total` AS `hours_total`,`issue`.`hours_remaining` AS `hours_remaining`,`issue`.`hours_spent` AS `hours_spent`,`issue`.`created_date` AS `created_date`,`issue`.`closed_date` AS `closed_date`,`issue`.`deleted_date` AS `deleted_date`,`issue`.`start_date` AS `start_date`,`issue`.`due_date` AS `due_date`,isnull(`issue`.`due_date`) AS `has_due_date`,`issue`.`repeat_cycle` AS `repeat_cycle`,`issue`.`sprint_id` AS `sprint_id`,`sprint`.`name` AS `sprint_name`,`sprint`.`start_date` AS `sprint_start_date`,`sprint`.`end_date` AS `sprint_end_date`,`type`.`name` AS `type_name`,`status`.`name` AS `status_name`,`status`.`closed` AS `status_closed`,`priority`.`id` AS `priority_id`,`priority`.`name` AS `priority_name`,`author`.`username` AS `author_username`,`author`.`name` AS `author_name`,`author`.`email` AS `author_email`,`author`.`task_color` AS `author_task_color`,`owner`.`username` AS `owner_username`,`owner`.`name` AS `owner_name`,`owner`.`email` AS `owner_email`,`owner`.`task_color` AS `owner_task_color` from ((((((`issue` left join `user` `author` on((`issue`.`author_id` = `author`.`id`))) left join `user` `owner` on((`issue`.`owner_id` = `owner`.`id`))) left join `issue_status` `status` on((`issue`.`status` = `status`.`id`))) left join `issue_priority` `priority` on((`issue`.`priority` = `priority`.`value`))) left join `issue_type` `type` on((`issue`.`type_id` = `type`.`id`))) left join `sprint` on((`issue`.`sprint_id` = `sprint`.`id`))); 18 | 19 | # Update Version 20 | UPDATE `config` SET `value` = '14.12.11' WHERE `attribute` = 'version'; 21 | -------------------------------------------------------------------------------- /db/14.12.21.sql: -------------------------------------------------------------------------------- 1 | # This database update occured after commit 58697b3b04 2 | 3 | # Add language field to users 4 | ALTER TABLE `user` ADD COLUMN `language` VARCHAR(5) NULL AFTER `theme`; 5 | 6 | # Update Version 7 | UPDATE `config` SET `value` = '14.12.21' WHERE `attribute` = 'version'; 8 | -------------------------------------------------------------------------------- /db/14.12.29.sql: -------------------------------------------------------------------------------- 1 | # This database update occured after commit a2a868d3 2 | 3 | # Adding index to parent_id column 4 | ALTER TABLE `issue` ADD INDEX `parent_id` (`parent_id`); 5 | 6 | # Update Version 7 | UPDATE `config` SET `value` = '14.12.29' WHERE `attribute` = 'version'; 8 | -------------------------------------------------------------------------------- /db/14.12.30.sql: -------------------------------------------------------------------------------- 1 | # This database update occured after commit f6f4e8de 2 | # This update may take a while to run on large databases! 3 | 4 | # Clean potentially messy data that could break the upgrade process 5 | UPDATE issue SET owner_id = NULL WHERE owner_id = 0; 6 | UPDATE issue SET sprint_id = NULL WHERE sprint_id = 0; 7 | 8 | # Adding foreign key constraints to issue metadata 9 | ALTER TABLE `issue` 10 | CHANGE `status` `status` INT(10) UNSIGNED DEFAULT 1 NOT NULL; 11 | ALTER TABLE `issue` 12 | ADD INDEX `status` (`status`); 13 | ALTER TABLE `issue` 14 | ADD CONSTRAINT `issue_type_id` FOREIGN KEY (`type_id`) REFERENCES `issue_type`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT, 15 | ADD CONSTRAINT `issue_sprint_id` FOREIGN KEY (`sprint_id`) REFERENCES `sprint`(`id`) ON UPDATE CASCADE ON DELETE SET NULL, 16 | ADD CONSTRAINT `issue_owner_id` FOREIGN KEY (`owner_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE SET NULL, 17 | ADD CONSTRAINT `issue_status` FOREIGN KEY (`status`) REFERENCES `issue_status`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT; 18 | 19 | # Prevent deleting users with live comments 20 | ALTER TABLE `issue_comment` DROP FOREIGN KEY `comment_user`; 21 | ALTER TABLE `issue_comment` ADD CONSTRAINT `comment_user` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT; 22 | 23 | # Update Version 24 | UPDATE `config` SET `value` = '14.12.30' WHERE `attribute` = 'version'; 25 | -------------------------------------------------------------------------------- /db/15.01.31.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `issue_tag`; 2 | CREATE TABLE `issue_tag`( 3 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 4 | `tag` VARCHAR(60) NOT NULL, 5 | `issue_id` INT UNSIGNED NOT NULL, 6 | PRIMARY KEY (`id`), 7 | INDEX `issue_tag_tag` (`tag`, `issue_id`), 8 | CONSTRAINT `issue_tag_issue` FOREIGN KEY (`issue_id`) REFERENCES `issue`(`id`) ON UPDATE CASCADE ON DELETE CASCADE 9 | ) ENGINE=INNODB CHARSET=utf8mb4; 10 | 11 | UPDATE `config` SET `value` = '15.01.31' WHERE `attribute` = 'version'; 12 | -------------------------------------------------------------------------------- /db/15.02.07.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Rank 0: guest - read-only access 3 | Rank 1: client - read-only access + comments 4 | Rank 2: user - current user permissions 5 | Rank 3: manager - delete issues/comments 6 | Rank 4: admin - current admin privileges, minus plugin config 7 | Rank 5: superadmin - able to change config file values from web interface 8 | */ 9 | 10 | ALTER TABLE user ADD COLUMN rank tinyint(1) UNSIGNED DEFAULT 0 NOT NULL AFTER `role`; 11 | UPDATE user SET rank = '2' WHERE `role` = 'user'; 12 | UPDATE user SET rank = '4' WHERE `role` = 'admin'; 13 | UPDATE config SET value = '15.02.07' WHERE attribute = 'version'; 14 | -------------------------------------------------------------------------------- /db/15.02.26.sql: -------------------------------------------------------------------------------- 1 | # Update issue_detail view 2 | ALTER VIEW `issue_detail` AS 3 | SELECT 4 | `issue`.`id` AS `id`, 5 | `issue`.`status` AS `status`, 6 | `issue`.`type_id` AS `type_id`, 7 | `issue`.`name` AS `name`, 8 | `issue`.`description` AS `description`, 9 | `issue`.`parent_id` AS `parent_id`, 10 | `issue`.`author_id` AS `author_id`, 11 | `issue`.`owner_id` AS `owner_id`, 12 | `issue`.`priority` AS `priority`, 13 | `issue`.`hours_total` AS `hours_total`, 14 | `issue`.`hours_remaining` AS `hours_remaining`, 15 | `issue`.`hours_spent` AS `hours_spent`, 16 | `issue`.`created_date` AS `created_date`, 17 | `issue`.`closed_date` AS `closed_date`, 18 | `issue`.`deleted_date` AS `deleted_date`, 19 | `issue`.`start_date` AS `start_date`, 20 | `issue`.`due_date` AS `due_date`, 21 | ISNULL(`issue`.`due_date`) AS `has_due_date`, 22 | `issue`.`repeat_cycle` AS `repeat_cycle`, 23 | `issue`.`sprint_id` AS `sprint_id`, 24 | `sprint`.`name` AS `sprint_name`, 25 | `sprint`.`start_date` AS `sprint_start_date`, 26 | `sprint`.`end_date` AS `sprint_end_date`, 27 | `type`.`name` AS `type_name`, 28 | `status`.`name` AS `status_name`, 29 | `status`.`closed` AS `status_closed`, 30 | `priority`.`id` AS `priority_id`, 31 | `priority`.`name` AS `priority_name`, 32 | `author`.`username` AS `author_username`, 33 | `author`.`name` AS `author_name`, 34 | `author`.`email` AS `author_email`, 35 | `author`.`task_color` AS `author_task_color`, 36 | `owner`.`username` AS `owner_username`, 37 | `owner`.`name` AS `owner_name`, 38 | `owner`.`email` AS `owner_email`, 39 | `owner`.`task_color` AS `owner_task_color`, 40 | `parent`.`name` AS `parent_name` 41 | FROM `issue` 42 | LEFT JOIN `user` `author` ON `issue`.`author_id` = `author`.`id` 43 | LEFT JOIN `user` `owner` ON `issue`.`owner_id` = `owner`.`id` 44 | LEFT JOIN `issue_status` `status` ON `issue`.`status` = `status`.`id` 45 | LEFT JOIN `issue_priority` `priority` ON `issue`.`priority` = `priority`.`value` 46 | LEFT JOIN `issue_type` `type` ON `issue`.`type_id` = `type`.`id` 47 | LEFT JOIN `sprint` ON `issue`.`sprint_id` = `sprint`.`id` 48 | LEFT JOIN `issue` `parent` ON `issue`.`parent_id` = `parent`.`id`; 49 | 50 | # Update Version 51 | UPDATE `config` SET `value` = '15.02.26' WHERE `attribute` = 'version'; 52 | -------------------------------------------------------------------------------- /db/15.03.14.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `issue_update` ADD COLUMN `notify` TINYINT(1) UNSIGNED NULL AFTER `comment_id`; 2 | UPDATE `config` SET `value` = '15.03.14' WHERE `attribute` = 'version'; 3 | -------------------------------------------------------------------------------- /db/15.03.20.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `session`; 2 | CREATE TABLE `session`( 3 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 4 | `token` VARBINARY(64) NOT NULL, 5 | `user_id` INT UNSIGNED NOT NULL, 6 | `created` DATETIME NOT NULL, 7 | PRIMARY KEY (`id`), 8 | UNIQUE KEY `session_token` (`token`), 9 | KEY `session_user_id` (`user_id`), 10 | CONSTRAINT `session_user_id` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE 11 | ) ENGINE=INNODB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 12 | 13 | UPDATE `config` SET `value` = '15.03.20' WHERE `attribute` = 'version'; 14 | -------------------------------------------------------------------------------- /db/15.04.06.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `session` 2 | ADD COLUMN `ip` VARBINARY(39) NOT NULL AFTER `token`, 3 | DROP INDEX `session_token`, ADD UNIQUE INDEX `session_token` (`token`, `ip`); 4 | TRUNCATE `session`; 5 | 6 | UPDATE `config` SET `value` = '15.04.06' WHERE `attribute` = 'version'; 7 | -------------------------------------------------------------------------------- /db/15.04.07.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM `issue_update_field` 2 | WHERE `issue_update_id` NOT IN ( 3 | SELECT `id` FROM `issue_update` 4 | ); 5 | 6 | ALTER TABLE `issue_update_field` 7 | ADD CONSTRAINT `issue_update_field_update` FOREIGN KEY (`issue_update_id`) REFERENCES `issue_update`(`id`) ON UPDATE CASCADE ON DELETE CASCADE, 8 | ENGINE=INNODB; 9 | 10 | UPDATE `config` SET `value` = '15.04.07' WHERE `attribute` = 'version'; 11 | -------------------------------------------------------------------------------- /db/15.04.17.sql: -------------------------------------------------------------------------------- 1 | # Add Issue dependency 2 | DROP TABLE IF EXISTS `issue_dependency`; 3 | CREATE TABLE `issue_dependency` ( 4 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 5 | `issue_id` int(10) unsigned NOT NULL, 6 | `dependency_id` int(11) unsigned NOT NULL, 7 | `dependency_type` char(2) COLLATE utf8mb4_unicode_ci NOT NULL, 8 | PRIMARY KEY (`id`), 9 | UNIQUE KEY `issue_id_dependency_id` (`issue_id`,`dependency_id`), 10 | KEY `dependency_id` (`dependency_id`), 11 | CONSTRAINT `issue_dependency_ibfk_2` FOREIGN KEY (`issue_id`) REFERENCES `issue` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 12 | CONSTRAINT `issue_dependency_ibfk_3` FOREIGN KEY (`dependency_id`) REFERENCES `issue` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 14 | 15 | # Update Version 16 | UPDATE `config` SET `value` = '15.04.17' WHERE `attribute` = 'version'; 17 | -------------------------------------------------------------------------------- /db/15.06.02.sql: -------------------------------------------------------------------------------- 1 | # Update issue update detail view 2 | ALTER VIEW `issue_update_detail` AS ( 3 | select 4 | `i`.`id` AS `id`, 5 | `i`.`issue_id` AS `issue_id`, 6 | `i`.`user_id` AS `user_id`, 7 | `i`.`created_date` AS `created_date`, 8 | `u`.`username` AS `user_username`, 9 | `u`.`name` AS `user_name`, 10 | `u`.`email` AS `user_email`, 11 | `i`.`comment_id` AS `comment_id`, 12 | `c`.`text` AS `comment_text`, 13 | `i`.`notify` AS `notify` 14 | from `issue_update` `i` 15 | inner join `user` `u` on `i`.`user_id` = `u`.`id` 16 | left join `issue_comment` `c` on `i`.`comment_id` = `c`.`id` 17 | ); 18 | 19 | # Update Version 20 | UPDATE `config` SET `value` = '15.06.12' WHERE `attribute` = 'version'; 21 | -------------------------------------------------------------------------------- /db/15.10.07.sql: -------------------------------------------------------------------------------- 1 | # Add options column to user 2 | ALTER TABLE `user` ADD `options` BLOB NULL AFTER `api_key`; 3 | 4 | # Update Version 5 | UPDATE `config` SET `value` = '15.10.07' WHERE `attribute` = 'version'; 6 | -------------------------------------------------------------------------------- /db/16.02.04.1.sql: -------------------------------------------------------------------------------- 1 | # Add issue_backlog table 2 | CREATE TABLE `issue_backlog`( 3 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 4 | `user_id` INT UNSIGNED NOT NULL, 5 | `issues` BLOB NOT NULL, 6 | PRIMARY KEY (`id`), 7 | CONSTRAINT `issue_backlog_user_id` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE 8 | ) ENGINE=INNODB CHARSET=utf8mb4; 9 | 10 | # Update version 11 | UPDATE `config` SET `value` = '16.02.04.1' WHERE `attribute` = 'version'; 12 | -------------------------------------------------------------------------------- /db/16.02.04.sql: -------------------------------------------------------------------------------- 1 | # Add taskboard_sort column to issue_status 2 | ALTER TABLE `issue_status` 3 | ADD COLUMN `taskboard_sort` INT UNSIGNED NULL AFTER `taskboard`; 4 | UPDATE `issue_status` SET `taskboard_sort` = '1' WHERE `taskboard` > 0; 5 | 6 | # Update version 7 | UPDATE `config` SET `value` = '16.02.04' WHERE `attribute` = 'version'; 8 | -------------------------------------------------------------------------------- /db/16.02.05.sql: -------------------------------------------------------------------------------- 1 | # Add sprint_id column to issue_backlog 2 | ALTER TABLE `issue_backlog` 3 | ADD COLUMN `sprint_id` INT UNSIGNED NULL AFTER `user_id`, 4 | ADD CONSTRAINT `issue_backlog_sprint_id` FOREIGN KEY (`sprint_id`) REFERENCES `sprint`(`id`) ON UPDATE CASCADE ON DELETE CASCADE; 5 | 6 | # Update version 7 | UPDATE `config` SET `value` = '16.02.05' WHERE `attribute` = 'version'; 8 | -------------------------------------------------------------------------------- /db/16.04.13.sql: -------------------------------------------------------------------------------- 1 | # Add api_visible column to user table 2 | ALTER TABLE `user` 3 | ADD COLUMN `api_visible` TINYINT(1) UNSIGNED DEFAULT 1 NOT NULL AFTER `api_key`; 4 | 5 | # Update version 6 | UPDATE `config` SET `value` = '16.04.13' WHERE `attribute` = 'version'; 7 | -------------------------------------------------------------------------------- /db/16.06.28.sql: -------------------------------------------------------------------------------- 1 | # Update issues table to allow null repeat_cycle 2 | ALTER TABLE `issue` CHANGE `repeat_cycle` 3 | `repeat_cycle` VARCHAR(10) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci NULL; 4 | 5 | # Change existing repeat_cycle 'none' values to NULL 6 | UPDATE issue SET repeat_cycle = NULL WHERE repeat_cycle IN('none', ''); 7 | 8 | # Update version 9 | UPDATE `config` SET `value` = '16.06.28' WHERE `attribute` = 'version'; 10 | -------------------------------------------------------------------------------- /db/16.09.12.sql: -------------------------------------------------------------------------------- 1 | # Update issue_backlog table structure 2 | ALTER TABLE issue_backlog 3 | ADD COLUMN type_id INT(10) UNSIGNED NULL AFTER user_id; 4 | 5 | UPDATE issue_backlog b 6 | JOIN config c ON c.attribute = 'issue_type.project' 7 | SET b.type_id = c.value; 8 | 9 | ALTER TABLE issue_backlog 10 | CHANGE type_id type_id INT(10) UNSIGNED NOT NULL, 11 | ADD CONSTRAINT issue_backlog_type_id FOREIGN KEY (type_id) 12 | REFERENCES issue_type(id) ON UPDATE CASCADE ON DELETE CASCADE; 13 | 14 | # Update version 15 | UPDATE `config` SET `value` = '16.09.12' WHERE `attribute` = 'version'; 16 | -------------------------------------------------------------------------------- /db/16.11.23.sql: -------------------------------------------------------------------------------- 1 | # Update issue_type to support roles 2 | ALTER TABLE `issue_type` 3 | ADD COLUMN `role` ENUM('task','project','bug') DEFAULT 'task' NOT NULL, 4 | ADD INDEX `issue_type_role` (`role`); 5 | 6 | UPDATE issue_type 7 | JOIN config ON config.value = issue_type.id AND config.attribute = 'issue_type.project' 8 | SET issue_type.`role` = 'project'; 9 | 10 | UPDATE issue_type 11 | JOIN config ON config.value = issue_type.id AND config.attribute = 'issue_type.bug' 12 | SET issue_type.`role` = 'bug'; 13 | 14 | UPDATE `config` SET `value` = '16.11.23' WHERE `attribute` = 'version'; 15 | -------------------------------------------------------------------------------- /db/16.11.25.sql: -------------------------------------------------------------------------------- 1 | # Merge group- and type-based backlog into single per-sprint lists 2 | SET SESSION group_concat_max_len = 8192; 3 | 4 | CREATE TEMPORARY TABLE issue_backlog_converting 5 | SELECT sprint_id, REPLACE(GROUP_CONCAT(issues), '],[', ',') 6 | FROM issue_backlog 7 | GROUP BY sprint_id; 8 | 9 | TRUNCATE issue_backlog; 10 | 11 | ALTER TABLE issue_backlog 12 | DROP COLUMN user_id, 13 | DROP COLUMN type_id, 14 | DROP INDEX issue_backlog_user_id, 15 | DROP INDEX issue_backlog_type_id, 16 | DROP FOREIGN KEY issue_backlog_type_id, 17 | DROP FOREIGN KEY issue_backlog_user_id; 18 | 19 | INSERT INTO issue_backlog (sprint_id, issues) 20 | SELECT * FROM issue_backlog_converting; 21 | 22 | DROP TEMPORARY TABLE issue_backlog_converting; 23 | 24 | UPDATE `config` SET `value` = '16.11.25' WHERE `attribute` = 'version'; 25 | -------------------------------------------------------------------------------- /db/16.11.29.1.sql: -------------------------------------------------------------------------------- 1 | # Reset user theme selection for removed themes 2 | UPDATE user SET theme = NULL WHERE 3 | theme = 'css/bootstrap-amelia.min.css' 4 | OR theme = 'css/bootstrap-cosmo.min.css' 5 | OR theme = 'css/bootstrap-cupid.min.css' 6 | OR theme = 'css/bootstrap-google.css' 7 | OR theme = 'css/bootstrap-journal.min.css' 8 | OR theme = 'css/bootstrap-lumen.min.css' 9 | OR theme = 'css/bootstrap-paper.min.css' 10 | OR theme = 'css/bootstrap-readable.min.css' 11 | OR theme = 'css/bootstrap-shamrock.min.css' 12 | OR theme = 'css/bootstrap-simplex.min.css' 13 | OR theme = 'css/bootstrap-superhero.min.css' 14 | OR theme = 'css/bootstrap-united.min.css' 15 | OR theme = 'css/bootstrap-yeti.min.css'; 16 | 17 | UPDATE `config` SET `value` = '16.11.29.1' WHERE `attribute` = 'version'; 18 | -------------------------------------------------------------------------------- /db/16.11.29.sql: -------------------------------------------------------------------------------- 1 | # New backlog uses only one row per sprint 2 | ALTER TABLE issue_backlog 3 | DROP INDEX issue_backlog_sprint_id, 4 | ADD UNIQUE INDEX issue_backlog_sprint_id (sprint_id); 5 | 6 | UPDATE `config` SET `value` = '16.11.29' WHERE `attribute` = 'version'; 7 | -------------------------------------------------------------------------------- /db/16.11.30.sql: -------------------------------------------------------------------------------- 1 | ALTER VIEW `issue_comment_detail` AS ( 2 | SELECT 3 | `c`.`id` AS `id`, 4 | `c`.`issue_id` AS `issue_id`, 5 | `c`.`user_id` AS `user_id`, 6 | `c`.`text` AS `text`, 7 | `c`.`file_id` AS `file_id`, 8 | `c`.`created_date` AS `created_date`, 9 | `u`.`username` AS `user_username`, 10 | `u`.`email` AS `user_email`, 11 | `u`.`name` AS `user_name`, 12 | `u`.`role` AS `user_role`, 13 | `u`.`task_color` AS `user_task_color`, 14 | `f`.`filename` AS `file_filename`, 15 | `f`.`filesize` AS `file_filesize`, 16 | `f`.`content_type` AS `file_content_type`, 17 | `f`.`downloads` AS `file_downloads`, 18 | `f`.`created_date` AS `file_created_date`, 19 | `f`.`deleted_date` AS `file_deleted_date`, 20 | `i`.`deleted_date` AS `issue_deleted_date` 21 | FROM `issue_comment` `c` 22 | JOIN `user` `u` ON `c`.`user_id` = `u`.`id` 23 | LEFT JOIN `issue_file` `f` ON `c`.`file_id` = `f`.`id` 24 | JOIN `issue` `i` ON `i`.`id` = `c`.`issue_id` 25 | ); 26 | 27 | UPDATE `config` SET `value` = '16.11.30' WHERE `attribute` = 'version'; 28 | -------------------------------------------------------------------------------- /db/16.12.01.sql: -------------------------------------------------------------------------------- 1 | # Update issue table to include due_date_sprint setting 2 | ALTER TABLE `issue` 3 | ADD COLUMN `due_date_sprint` TINYINT(1) UNSIGNED DEFAULT 0 NOT NULL; 4 | 5 | ALTER VIEW `issue_detail` AS 6 | SELECT 7 | `issue`.`id` AS `id`, 8 | `issue`.`status` AS `status`, 9 | `issue`.`type_id` AS `type_id`, 10 | `issue`.`name` AS `name`, 11 | `issue`.`description` AS `description`, 12 | `issue`.`parent_id` AS `parent_id`, 13 | `issue`.`author_id` AS `author_id`, 14 | `issue`.`owner_id` AS `owner_id`, 15 | `issue`.`priority` AS `priority`, 16 | `issue`.`hours_total` AS `hours_total`, 17 | `issue`.`hours_remaining` AS `hours_remaining`, 18 | `issue`.`hours_spent` AS `hours_spent`, 19 | `issue`.`created_date` AS `created_date`, 20 | `issue`.`closed_date` AS `closed_date`, 21 | `issue`.`deleted_date` AS `deleted_date`, 22 | `issue`.`start_date` AS `start_date`, 23 | `issue`.`due_date` AS `due_date`, 24 | ISNULL(`issue`.`due_date`) AS `has_due_date`, 25 | `issue`.`repeat_cycle` AS `repeat_cycle`, 26 | `issue`.`sprint_id` AS `sprint_id`, 27 | `issue`.`due_date_sprint` AS `due_date_sprint`, 28 | `sprint`.`name` AS `sprint_name`, 29 | `sprint`.`start_date` AS `sprint_start_date`, 30 | `sprint`.`end_date` AS `sprint_end_date`, 31 | `type`.`name` AS `type_name`, 32 | `status`.`name` AS `status_name`, 33 | `status`.`closed` AS `status_closed`, 34 | `priority`.`id` AS `priority_id`, 35 | `priority`.`name` AS `priority_name`, 36 | `author`.`username` AS `author_username`, 37 | `author`.`name` AS `author_name`, 38 | `author`.`email` AS `author_email`, 39 | `author`.`task_color` AS `author_task_color`, 40 | `owner`.`username` AS `owner_username`, 41 | `owner`.`name` AS `owner_name`, 42 | `owner`.`email` AS `owner_email`, 43 | `owner`.`task_color` AS `owner_task_color`, 44 | `parent`.`name` AS `parent_name` 45 | FROM `issue` 46 | LEFT JOIN `user` `author` ON `issue`.`author_id` = `author`.`id` 47 | LEFT JOIN `user` `owner` ON `issue`.`owner_id` = `owner`.`id` 48 | LEFT JOIN `issue_status` `status` ON `issue`.`status` = `status`.`id` 49 | LEFT JOIN `issue_priority` `priority` ON `issue`.`priority` = `priority`.`value` 50 | LEFT JOIN `issue_type` `type` ON `issue`.`type_id` = `type`.`id` 51 | LEFT JOIN `sprint` ON `issue`.`sprint_id` = `sprint`.`id` 52 | LEFT JOIN `issue` `parent` ON `issue`.`parent_id` = `parent`.`id`; 53 | 54 | # Update version 55 | UPDATE `config` SET `value` = '16.12.01' WHERE `attribute` = 'version'; 56 | -------------------------------------------------------------------------------- /db/16.12.29.sql: -------------------------------------------------------------------------------- 1 | # Add reset_token column to user table 2 | ALTER TABLE `user` 3 | ADD COLUMN `reset_token` CHAR(96) NULL AFTER `salt`; 4 | 5 | # Add default config entry for reset TTL 6 | INSERT INTO `config` (`attribute`,`value`) VALUES ('security.reset_ttl', '86400'); 7 | 8 | # Update version 9 | UPDATE `config` SET `value` = '16.12.29' WHERE `attribute` = 'version'; 10 | -------------------------------------------------------------------------------- /db/17.03.17.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE issue ADD size_estimate VARCHAR(20) AFTER name; 2 | 3 | ALTER VIEW `issue_detail` 4 | AS ( 5 | SELECT 6 | `issue`.`id` AS `id`, 7 | `issue`.`status` AS `status`, 8 | `issue`.`type_id` AS `type_id`, 9 | `issue`.`name` AS `name`, 10 | `issue`.`size_estimate` AS `size_estimate`, 11 | `issue`.`description` AS `description`, 12 | `issue`.`parent_id` AS `parent_id`, 13 | `issue`.`author_id` AS `author_id`, 14 | `issue`.`owner_id` AS `owner_id`, 15 | `issue`.`priority` AS `priority`, 16 | `issue`.`hours_total` AS `hours_total`, 17 | `issue`.`hours_remaining` AS `hours_remaining`, 18 | `issue`.`hours_spent` AS `hours_spent`, 19 | `issue`.`created_date` AS `created_date`, 20 | `issue`.`closed_date` AS `closed_date`, 21 | `issue`.`deleted_date` AS `deleted_date`, 22 | `issue`.`start_date` AS `start_date`, 23 | `issue`.`due_date` AS `due_date`, 24 | isnull(`issue`.`due_date`) AS `has_due_date`, 25 | `issue`.`repeat_cycle` AS `repeat_cycle`, 26 | `issue`.`sprint_id` AS `sprint_id`, 27 | `issue`.`due_date_sprint` AS `due_date_sprint`, 28 | `sprint`.`name` AS `sprint_name`, 29 | `sprint`.`start_date` AS `sprint_start_date`, 30 | `sprint`.`end_date` AS `sprint_end_date`, 31 | `type`.`name` AS `type_name`, 32 | `status`.`name` AS `status_name`, 33 | `status`.`closed` AS `status_closed`, 34 | `priority`.`id` AS `priority_id`, 35 | `priority`.`name` AS `priority_name`, 36 | `author`.`username` AS `author_username`, 37 | `author`.`name` AS `author_name`, 38 | `author`.`email` AS `author_email`, 39 | `author`.`task_color` AS `author_task_color`, 40 | `owner`.`username` AS `owner_username`, 41 | `owner`.`name` AS `owner_name`, 42 | `owner`.`email` AS `owner_email`, 43 | `owner`.`task_color` AS `owner_task_color` 44 | FROM `issue` 45 | LEFT JOIN `user` `author` ON `issue`.`author_id` = `author`.`id` 46 | LEFT JOIN `user` `owner` ON `issue`.`owner_id` = `owner`.`id` 47 | LEFT JOIN `issue_status` `status` ON `issue`.`status` = `status`.`id` 48 | LEFT JOIN `issue_priority` `priority` ON `issue`.`priority` = `priority`.`value` 49 | LEFT JOIN `issue_type` `type` ON `issue`.`type_id` = `type`.`id` 50 | LEFT JOIN `sprint` ON `issue`.`sprint_id` = `sprint`.`id` 51 | ); 52 | 53 | # Update version 54 | UPDATE `config` SET `value` = '17.03.17' WHERE `attribute` = 'version'; 55 | -------------------------------------------------------------------------------- /db/17.03.23.sql: -------------------------------------------------------------------------------- 1 | -- Re-add a column removed in 17.03.17 2 | ALTER VIEW `issue_detail` AS 3 | SELECT 4 | `issue`.`id` AS `id`, 5 | `issue`.`status` AS `status`, 6 | `issue`.`type_id` AS `type_id`, 7 | `issue`.`name` AS `name`, 8 | `issue`.`size_estimate` AS `size_estimate`, 9 | `issue`.`description` AS `description`, 10 | `issue`.`parent_id` AS `parent_id`, 11 | `issue`.`author_id` AS `author_id`, 12 | `issue`.`owner_id` AS `owner_id`, 13 | `issue`.`priority` AS `priority`, 14 | `issue`.`hours_total` AS `hours_total`, 15 | `issue`.`hours_remaining` AS `hours_remaining`, 16 | `issue`.`hours_spent` AS `hours_spent`, 17 | `issue`.`created_date` AS `created_date`, 18 | `issue`.`closed_date` AS `closed_date`, 19 | `issue`.`deleted_date` AS `deleted_date`, 20 | `issue`.`start_date` AS `start_date`, 21 | `issue`.`due_date` AS `due_date`, 22 | ISNULL(`issue`.`due_date`) AS `has_due_date`, 23 | `issue`.`repeat_cycle` AS `repeat_cycle`, 24 | `issue`.`sprint_id` AS `sprint_id`, 25 | `issue`.`due_date_sprint` AS `due_date_sprint`, 26 | `sprint`.`name` AS `sprint_name`, 27 | `sprint`.`start_date` AS `sprint_start_date`, 28 | `sprint`.`end_date` AS `sprint_end_date`, 29 | `type`.`name` AS `type_name`, 30 | `status`.`name` AS `status_name`, 31 | `status`.`closed` AS `status_closed`, 32 | `priority`.`id` AS `priority_id`, 33 | `priority`.`name` AS `priority_name`, 34 | `author`.`username` AS `author_username`, 35 | `author`.`name` AS `author_name`, 36 | `author`.`email` AS `author_email`, 37 | `author`.`task_color` AS `author_task_color`, 38 | `owner`.`username` AS `owner_username`, 39 | `owner`.`name` AS `owner_name`, 40 | `owner`.`email` AS `owner_email`, 41 | `owner`.`task_color` AS `owner_task_color`, 42 | `parent`.`name` AS `parent_name` 43 | FROM `issue` 44 | LEFT JOIN `user` `author` on`issue`.`author_id` = `author`.`id` 45 | LEFT JOIN `user` `owner` on`issue`.`owner_id` = `owner`.`id` 46 | LEFT JOIN `issue_status` `status` on`issue`.`status` = `status`.`id` 47 | LEFT JOIN `issue_priority` `priority` on`issue`.`priority` = `priority`.`value` 48 | LEFT JOIN `issue_type` `type` on`issue`.`type_id` = `type`.`id` 49 | LEFT JOIN `sprint` on`issue`.`sprint_id` = `sprint`.`id` 50 | LEFT JOIN `issue` `parent` ON `issue`.`parent_id` = `parent`.`id`; 51 | 52 | UPDATE `config` SET `value` = '17.03.23' WHERE `attribute` = 'version'; 53 | -------------------------------------------------------------------------------- /db/17.08.25.sql: -------------------------------------------------------------------------------- 1 | SET foreign_key_checks = 0; 2 | 3 | -- Remove unused attribute tables 4 | DROP TABLE IF EXISTS attribute; 5 | DROP TABLE IF EXISTS attribute_issue_type; 6 | DROP TABLE IF EXISTS attribute_value; 7 | DROP VIEW IF EXISTS `attribute_value_detail`; 8 | 9 | -- Make ID column types consistent between tables 10 | ALTER TABLE `issue` 11 | CHANGE `type_id` `type_id` INT(10) UNSIGNED DEFAULT 1 NOT NULL, 12 | CHANGE `parent_id` `parent_id` INT(10) UNSIGNED NULL, 13 | CHANGE `author_id` `author_id` INT(10) UNSIGNED NOT NULL, 14 | CHANGE `owner_id` `owner_id` INT(10) UNSIGNED NULL, 15 | CHANGE `priority` `priority` INT(10) DEFAULT 0 NOT NULL, 16 | CHANGE `sprint_id` `sprint_id` INT(10) UNSIGNED NULL; 17 | ALTER TABLE `issue_dependency` 18 | CHANGE `dependency_id` `dependency_id` INT(10) UNSIGNED NOT NULL; 19 | ALTER TABLE `issue_priority` 20 | CHANGE `value` `value` INT(10) NOT NULL, 21 | ADD UNIQUE INDEX `priority` (`value`); 22 | 23 | SET foreign_key_checks = 1; 24 | 25 | -- Remove invalid parent IDs 26 | UPDATE `issue` `i1` LEFT 27 | JOIN `issue` `i2` ON `i2`.`id` = `i1`.`parent_id` 28 | SET `i1`.`parent_id` = NULL 29 | WHERE `i1`.`parent_id` IS NOT NULL AND `i2`.`id` IS NULL; 30 | 31 | -- Reset invalid priorities 32 | UPDATE `issue` `i` 33 | LEFT JOIN `issue_priority` `p` ON `p`.`value` = `i`.`priority` 34 | SET `i`.`priority` = IFNULL((SELECT `value` FROM `config` WHERE `attribute` = 'issue_priority.default'), '0') 35 | WHERE `p`.`id` IS NULL; 36 | 37 | -- Reset invalid author IDs 38 | UPDATE `issue` `i` 39 | LEFT JOIN `user` `u` ON `u`.`id` = `i`.`author_id` 40 | SET i.`author_id` = (SELECT MIN(`id`) FROM `user`) 41 | WHERE `u`.`id` IS NULL; 42 | 43 | -- Add additional foreign keys 44 | ALTER TABLE `issue` 45 | ADD CONSTRAINT `issue_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `issue`(`id`) ON UPDATE CASCADE ON DELETE SET NULL, 46 | ADD CONSTRAINT `issue_priority` FOREIGN KEY (`priority`) REFERENCES `issue_priority`(`value`) ON UPDATE CASCADE ON DELETE RESTRICT, 47 | ADD CONSTRAINT `issue_author_id` FOREIGN KEY (`author_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT; 48 | 49 | -- Update version 50 | UPDATE `config` SET `value` = '17.08.25' WHERE `attribute` = 'version'; 51 | -------------------------------------------------------------------------------- /db/17.09.20.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `issue` CHANGE `repeat_cycle` `repeat_cycle` VARCHAR(20) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci NULL; 2 | UPDATE `config` SET `value` = '17.09.20' WHERE `attribute` = 'version'; 3 | -------------------------------------------------------------------------------- /db/18.02.07.sql: -------------------------------------------------------------------------------- 1 | UPDATE `config` SET `attribute` = 'session_lifetime' WHERE `attribute` = 'JAR.expire'; 2 | UPDATE `config` SET `value` = '18.02.07' WHERE `attribute` = 'version'; 3 | -------------------------------------------------------------------------------- /db/18.02.19.sql: -------------------------------------------------------------------------------- 1 | UPDATE `config` SET `attribute` = 'session_lifetime' WHERE `attribute` = 'JAR.lifetime'; 2 | UPDATE `config` SET `value` = '18.02.19' WHERE `attribute` = 'version'; 3 | -------------------------------------------------------------------------------- /db/20.04.20.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `config` (`attribute`,`value`) VALUES ('security.file_blacklist', '/\.(ph(p([3457s]|\-s)?|t|tml)|aspx?|shtml|exe|dll)$/i'); 2 | UPDATE `config` SET `value` = '20.04.20' WHERE `attribute` = 'version'; 3 | -------------------------------------------------------------------------------- /db/20.10.14.sql: -------------------------------------------------------------------------------- 1 | -- Convert core tables containing user strings to utf8mb4 2 | ALTER TABLE config CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 3 | ALTER TABLE issue CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 4 | ALTER TABLE issue_comment CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 5 | ALTER TABLE issue_file CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 6 | ALTER TABLE issue_priority CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 7 | ALTER TABLE issue_status CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 8 | ALTER TABLE issue_tag CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 9 | ALTER TABLE issue_type CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 10 | ALTER TABLE issue_update_field CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 11 | ALTER TABLE sprint CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 12 | ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 13 | 14 | UPDATE `config` SET `value` = '20.10.14' WHERE `attribute` = 'version'; 15 | -------------------------------------------------------------------------------- /db/21.03.18.sql: -------------------------------------------------------------------------------- 1 | -- Add default description to issue types 2 | ALTER TABLE `issue_type` 3 | ADD `default_description` text NULL AFTER `role`; 4 | 5 | UPDATE `config` SET `value` = '21.03.18' WHERE `attribute` = 'version'; 6 | -------------------------------------------------------------------------------- /db/demo-content.sql: -------------------------------------------------------------------------------- 1 | /* Demo content should be imported immediately after the database.sql file */ 2 | 3 | SET NAMES utf8mb4; 4 | 5 | INSERT INTO `user` (`username`, `email`, `name`, `password`, `salt`, `role`,`created_date`) VALUES 6 | ('demo', 'demo@demo', 'Demo User', NULL, NULL, 'user', NOW()), 7 | (NULL, NULL, 'Demo Group', NULL, NULL, 'group', NOW()); 8 | 9 | INSERT INTO `user_group`(`user_id`,`group_id`,`manager`) VALUES (1,3,0), (2,3,0); 10 | 11 | INSERT INTO `sprint` (`id`, `name`, `start_date`, `end_date`) VALUES 12 | (1, 'First Sprint', DATE_SUB(CURDATE(), INTERVAL 1 DAY), DATE_ADD(DATE_SUB(CURDATE(), INTERVAL 2 DAY), INTERVAL 2 WEEK)); 13 | 14 | INSERT INTO `issue` (`status`, `type_id`, `name`, `description`, `parent_id`, `author_id`, `owner_id`, `hours_total`, `hours_remaining`, `created_date`, `sprint_id`) VALUES 15 | (1, 2, 'A Big Project', 'This is a project. Projects group tasks and bugs, and can go into sprints.', NULL, 2, 2, NULL, NULL, NOW(), 1), 16 | (1, 1, 'A Simple Task', 'This is a sample task.', 1, 1, 2, 2, 2, NOW(), NULL); 17 | -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /img/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/404.png -------------------------------------------------------------------------------- /img/ajax-loader-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/ajax-loader-dark.gif -------------------------------------------------------------------------------- /img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/ajax-loader.gif -------------------------------------------------------------------------------- /img/backlog/i_has_grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/backlog/i_has_grip.png -------------------------------------------------------------------------------- /img/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/checkmark.png -------------------------------------------------------------------------------- /img/geo/flames.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/geo/flames.gif -------------------------------------------------------------------------------- /img/geo/microfab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/geo/microfab.gif -------------------------------------------------------------------------------- /img/geo/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/geo/progress.gif -------------------------------------------------------------------------------- /img/geo/rainbow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/geo/rainbow.gif -------------------------------------------------------------------------------- /img/geo/seamlessfire-web.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/geo/seamlessfire-web.gif -------------------------------------------------------------------------------- /img/geo/seamlessfire.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/geo/seamlessfire.gif -------------------------------------------------------------------------------- /img/geo/stars.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/geo/stars.gif -------------------------------------------------------------------------------- /img/mime/16/_archive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/_audio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/_blank.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/_code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/_video.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/odg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/odp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/ods.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/odt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/16/txt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/96/_blank.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/mime/96/_image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/sparks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/sparks.gif -------------------------------------------------------------------------------- /img/spinner-8-3-16-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /img/spinner-8-3-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /img/spinner-8-3-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /img/spinner-8-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /img/taskboard/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/taskboard/ajax-loader.gif -------------------------------------------------------------------------------- /img/taskboard/burn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/taskboard/burn.gif -------------------------------------------------------------------------------- /img/taskboard/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alanaktion/phproject/de7ee42dc2c68962359fc168920b7ec06fa4eae5/img/taskboard/warning.png -------------------------------------------------------------------------------- /js/bootstrap-datepicker.de.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.de={days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],daysShort:["Son","Mon","Die","Mit","Don","Fre","Sam"],daysMin:["So","Mo","Di","Mi","Do","Fr","Sa"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",monthsTitle:"Monate",clear:"Löschen",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.en-GB.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates["en-GB"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.es.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.es={days:["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"],daysShort:["Dom","Lun","Mar","Mié","Jue","Vie","Sáb"],daysMin:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],monthsShort:["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"],today:"Hoy",monthsTitle:"Meses",clear:"Borrar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.et.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.et={days:["Pühapäev","Esmaspäev","Teisipäev","Kolmapäev","Neljapäev","Reede","Laupäev"],daysShort:["Pühap","Esmasp","Teisip","Kolmap","Neljap","Reede","Laup"],daysMin:["P","E","T","K","N","R","L"],months:["Jaanuar","Veebruar","Märts","Aprill","Mai","Juuni","Juuli","August","September","Oktoober","November","Detsember"],monthsShort:["Jaan","Veebr","Märts","Apr","Mai","Juuni","Juuli","Aug","Sept","Okt","Nov","Dets"],today:"Täna",clear:"Tühjenda",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.fr.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.fr={days:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],daysShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],daysMin:["d","l","ma","me","j","v","s"],months:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthsShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],today:"Aujourd'hui",monthsTitle:"Mois",clear:"Effacer",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.it.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.it={days:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],daysShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],daysMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],months:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthsShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],today:"Oggi",monthsTitle:"Mesi",clear:"Cancella",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.ja.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.ja={days:["日曜","月曜","火曜","水曜","木曜","金曜","土曜"],daysShort:["日","月","火","水","木","金","土"],daysMin:["日","月","火","水","木","金","土"],months:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今日",format:"yyyy/mm/dd",titleFormat:"yyyy年mm月",clear:"クリア"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.ko.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.ko={days:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],daysShort:["일","월","화","수","목","금","토"],daysMin:["일","월","화","수","목","금","토"],months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthsShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],today:"오늘",clear:"삭제",format:"yyyy-mm-dd",titleFormat:"yyyy년mm월",weekStart:0}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.nl.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.nl={days:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],daysShort:["zo","ma","di","wo","do","vr","za"],daysMin:["zo","ma","di","wo","do","vr","za"],months:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthsShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],today:"Vandaag",monthsTitle:"Maanden",clear:"Wissen",weekStart:1,format:"dd-mm-yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.pl.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.pl={days:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],daysShort:["Niedz.","Pon.","Wt.","Śr.","Czw.","Piąt.","Sob."],daysMin:["Ndz.","Pn.","Wt.","Śr.","Czw.","Pt.","Sob."],months:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthsShort:["Sty.","Lut.","Mar.","Kwi.","Maj","Cze.","Lip.","Sie.","Wrz.","Paź.","Lis.","Gru."],today:"Dzisiaj",weekStart:1,clear:"Wyczyść",format:"dd.mm.yyyy"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.ru.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates.ru={days:["Воскресенье","Понедельник","Вторник","Среда","Четверг","Пятница","Суббота"],daysShort:["Вск","Пнд","Втр","Срд","Чтв","Птн","Суб"],daysMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],months:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthsShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],today:"Сегодня",clear:"Очистить",format:"dd.mm.yyyy",weekStart:1,monthsTitle:"Месяцы"}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.zh-CN.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates["zh-CN"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["周日","周一","周二","周三","周四","周五","周六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",monthsTitle:"选择月份",clear:"清除",format:"yyyy-mm-dd",titleFormat:"yyyy年mm月",weekStart:1}}(jQuery); -------------------------------------------------------------------------------- /js/bootstrap-datepicker.zh-TW.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a.fn.datepicker.dates["zh-TW"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["週日","週一","週二","週三","週四","週五","週六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",format:"yyyy年mm月dd日",weekStart:1,clear:"清除"}}(jQuery); -------------------------------------------------------------------------------- /js/burndown.js: -------------------------------------------------------------------------------- 1 | /* globals $ Chart BurndownLegendDict BurndownRange */ 2 | var Burndown = { 3 | initialized: false, 4 | chart: null, 5 | data: { 6 | datasets: [{ 7 | data: null, 8 | label: BurndownLegendDict.hours_remaining, 9 | borderColor: '#2ecc71', 10 | pointBackgroundColor: '#2ecc71', 11 | pointBorderColor: '#2ecc71', 12 | }, { 13 | data: null, 14 | label: BurndownLegendDict.man_hours_remaining, 15 | borderColor: '#9b59b6', 16 | pointBackgroundColor: '#9b59b6', 17 | pointBorderColor: '#9b59b6', 18 | }] 19 | }, 20 | options: { 21 | maintainAspectRatio: false, 22 | legend: { 23 | position: 'bottom', 24 | labels: { 25 | boxWidth: 1 26 | } 27 | }, 28 | animation: { 29 | duration: 250 30 | }, 31 | tooltips: { 32 | mode: 'x-axis' 33 | }, 34 | hover: { 35 | mode: 'x-axis' 36 | }, 37 | scales: { 38 | xAxes: [{ 39 | type: 'time', 40 | time: { 41 | min: BurndownRange.start, 42 | max: BurndownRange.end, 43 | minUnit: 'day', 44 | tooltipFormat: 'ddd MMM D, h A', 45 | displayFormats: { 46 | day: 'ddd MMM D', 47 | }, 48 | }, 49 | }], 50 | yAxes: [{ 51 | ticks: { 52 | beginAtZero: true 53 | } 54 | }] 55 | }, 56 | elements: { 57 | line: { 58 | tension: 0.05, 59 | borderWidth: 2, 60 | }, 61 | point: { 62 | radius: 0, 63 | hitRadius: 5, 64 | hoverRadius: 3, 65 | } 66 | } 67 | }, 68 | init: function(canvasId, dataUrl) { 69 | Chart.defaults.global.defaultFontColor = 'rgba(127,127,127,1)'; 70 | Chart.defaults.scale.gridLines.color = 'rgba(127,127,127,.3)'; 71 | Chart.defaults.scale.gridLines.zeroLineColor = 'rgba(127,127,127,.3)'; 72 | this.initialized = true; 73 | 74 | $.get(dataUrl, function(data) { 75 | var finalData = []; 76 | $.each(data, function(key, val) { 77 | finalData.push({ 78 | x: key, 79 | y: val 80 | }); 81 | }); 82 | 83 | Burndown.data.datasets[0].data = finalData; 84 | Burndown.data.datasets[1].data = [ 85 | {x: BurndownRange.start, y: BurndownManHours || 0}, 86 | {x: BurndownRange.end, y: 0} 87 | ]; 88 | 89 | $('#' + canvasId).parents('.modal-body').removeAttr('data-loading'); 90 | 91 | var ctx = document.getElementById(canvasId).getContext('2d'); 92 | Burndown.chart = new Chart(ctx, { 93 | type: 'line', 94 | data: Burndown.data, 95 | options: Burndown.options 96 | }); 97 | }, 'json').fail(function() { 98 | $('#' + canvasId).parents('.modal-body').removeAttr('data-loading') 99 | .html('

    Failed to load burndown data!

    '); 100 | }); 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /js/jquery.ui.touch-punch.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Touch Punch 0.2.3 3 | * 4 | * Copyright 2011–2014, Dave Furfero 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * 7 | * Depends: 8 | * jquery.ui.widget.js 9 | * jquery.ui.mouse.js 10 | */ 11 | !function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); 12 | -------------------------------------------------------------------------------- /nginx-example.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | #listen 443 ssl http2; 4 | 5 | #ssl_certificate /etc/nginx/ssl/phproject.crt; 6 | #ssl_certificate_key /etc/nginx/ssl/phproject.key; 7 | 8 | root /var/www/phproject; 9 | index index.php; 10 | 11 | server_name demo.phproject.org; 12 | 13 | # Dynamic URLs 14 | location / { 15 | try_files $uri $uri/ /index.php?$args; 16 | } 17 | 18 | location ~ ^/app/(controller|dict|helper|model|view) { 19 | deny all; 20 | } 21 | location ~ ^/uploads/ { 22 | deny all; 23 | } 24 | location ~ /\.ht { 25 | deny all; 26 | } 27 | 28 | location ~ \.php$ { 29 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 30 | fastcgi_pass 127.0.0.1:9000; 31 | fastcgi_index index.php; 32 | include fastcgi_params; 33 | } 34 | 35 | client_max_body_size 64M; 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/app', 10 | __DIR__ . '/cron', 11 | __DIR__ . '/tests', 12 | ]) 13 | // ->withPhpSets(php81: true) 14 | ->withPhp74Sets() 15 | ->withTypeCoverageLevel(10) 16 | ->withDeadCodeLevel(10) 17 | ->withCodeQualityLevel(10) 18 | ->withCodingStyleLevel(0) 19 | ->withSkip([ 20 | // StringClassNameToClassConstantRector::class, 21 | ]); 22 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | mset([ 10 | "UI" => "app/view/;app/plugin/", 11 | "ESCAPE" => false, 12 | "LOGS" => "log/", 13 | "TEMP" => "tmp/", 14 | "PREFIX" => "dict.", 15 | "LOCALES" => "app/dict/", 16 | "FALLBACK" => "en", 17 | "CACHE" => false, 18 | "AUTOLOAD" => "app/;lib/vendor/", 19 | "PACKAGE" => "Phproject", 20 | "TZ" => "UTC", 21 | "site.timezone" => "America/Phoenix" 22 | ]); 23 | -------------------------------------------------------------------------------- /tests/pluginTest.php: -------------------------------------------------------------------------------- 1 | addHook("test", function (): void { 15 | $GLOBALS["test--testHook"] = true; 16 | }); 17 | 18 | // Trigger hook 19 | $helper->callHook("test"); 20 | 21 | $this->assertArrayHasKey("test--testHook", $GLOBALS); 22 | } 23 | 24 | public function testNav(): void 25 | { 26 | $helper = \Helper\Plugin::instance(); 27 | 28 | // Add nav items 29 | $helper->addNavItem("test1", "Test 1", "/^\/test1/", "root"); 30 | $helper->addNavItem("test2", "Test 2", "/^\/test2/", "user"); 31 | 32 | // Get all nav items 33 | $result = $helper->getAllNavs("/test1"); 34 | $expected = [ 35 | "root" => [ 36 | [ 37 | "href" => "test1", 38 | "title" => "Test 1", 39 | "match" => "/^\/test1/", 40 | "location" => "root", 41 | "active" => true 42 | ] 43 | ], 44 | "user" => [ 45 | [ 46 | "href" => "test2", 47 | "title" => "Test 2", 48 | "match" => "/^\/test2/", 49 | "location" => "user", 50 | "active" => false 51 | ] 52 | ], 53 | "new" => [], 54 | "browse" => [], 55 | ]; 56 | 57 | $this->assertEquals($result, $expected); 58 | } 59 | 60 | public function testJsFiles(): void 61 | { 62 | $helper = \Helper\Plugin::instance(); 63 | 64 | // Add JS file 65 | $helper->addJsFile("test.js", "/^\/test1/"); 66 | 67 | // Get JS file list 68 | $result1 = $helper->getJsFiles("/test1"); 69 | $result2 = $helper->getJsFiles("/test2"); 70 | 71 | $expected = ["test.js"]; 72 | $this->assertEquals($result1, $expected); 73 | $this->assertNotEquals($result2, $expected); 74 | } 75 | 76 | public function testJsCode(): void 77 | { 78 | $helper = \Helper\Plugin::instance(); 79 | 80 | // Add JS code block 81 | $helper->addJsCode("'test';", "/^\/test1/"); 82 | 83 | // Get JS code block list 84 | $result1 = $helper->getJsCode("/test1"); 85 | $result2 = $helper->getJsCode("/test2"); 86 | 87 | $expected = ["'test';"]; 88 | $this->assertEquals($result1, $expected); 89 | $this->assertNotEquals($result2, $expected); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/stringTest.php: -------------------------------------------------------------------------------- 1 | salt(); 13 | $this->assertRegexp("/[0-9a-f]{32}/", $result); 14 | } 15 | 16 | public function testSaltSha1(): void 17 | { 18 | $helper = \Helper\Security::instance(); 19 | $result = $helper->salt_sha1(); 20 | $this->assertRegexp("/[0-9a-f]{40}/", $result); 21 | } 22 | 23 | public function testHash(): void 24 | { 25 | $helper = \Helper\Security::instance(); 26 | $string = "Hello world!"; 27 | $hash = $helper->hash($string); 28 | $result = $helper->hash($string, $hash["salt"]); 29 | $this->assertEquals($result, $hash["hash"]); 30 | } 31 | 32 | public function testFormatFilesize(): void 33 | { 34 | $helper = \Helper\View::instance(); 35 | $size = 1288490189; 36 | $result = $helper->formatFilesize($size); 37 | $this->assertContains($result, ["1.2 GB", "1.20 GB"]); 38 | } 39 | 40 | public function testGravatar(): void 41 | { 42 | $helper = \Helper\View::instance(); 43 | $email = "alan@phpizza.com"; 44 | $result = $helper->gravatar($email); 45 | $this->assertStringContainsString("gravatar.com/avatar/996df14", $result); 46 | } 47 | 48 | public function testUtc2local(): void 49 | { 50 | $helper = \Helper\View::instance(); 51 | $time = 1420498500; 52 | $result = $helper->utc2local($time); 53 | $this->assertEquals(1420473300, $result); 54 | } 55 | 56 | public function testConvertClosedDate(): void 57 | { 58 | $helper = \Helper\Update::instance(); 59 | $time = '2016-01-01 12:34:56'; 60 | $f3 = \Base::instance(); 61 | $tz = $f3->get('TZ'); 62 | $f3->set('TZ', 'America/Phoenix'); 63 | $result = $helper->convertClosedDate($time); 64 | $f3->set('TZ', $tz); 65 | $this->assertEquals('Fri, Jan 1, 2016 5:34am', $result); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /uploads/.htaccess: -------------------------------------------------------------------------------- 1 | Deny from all 2 | --------------------------------------------------------------------------------