├── .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 | [](https://github.com/Alanaktion/phproject/actions?query=workflow%3ACI)
2 | [](https://crowdin.com/project/phproject)
3 |
4 | Phproject
5 | =========
6 | *A high-performance project management system in PHP*
7 |
8 | [](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 | {{ @dict.name }} |
15 | {{ @dict.cols.author }} |
16 | {{ @dict.version }} |
17 |
18 | |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{ @meta.package | esc }} |
27 | {{ @meta.author | esc }} |
28 | {{ @meta.version }} |
29 |
30 |
31 | Details
32 |
33 | |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/view/admin/plugins/single.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | - {{ @dict.plugins }}
12 | - {{ @plugin->_package() }}
13 |
14 | {~ @plugin->_admin() ~}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/view/admin/sprints.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 | {{ @dict.cols.id }} |
20 | {{ @dict.name }} |
21 | {{ @dict.start_date }} |
22 | {{ @dict.end_date }} |
23 |
24 |
25 |
26 |
27 |
28 | {{ @sprint.id }} |
29 | {{ @sprint.name | esc }} |
30 | {{ @sprint.start_date }} |
31 | {{ @sprint.end_date }} |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/view/admin/sprints/edit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/view/admin/sprints/new.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/view/admin/users/deleted.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 | Back
17 | Deactivated Users
18 |
19 |
20 |
21 |
22 | {{ @dict.cols.id }} |
23 | {{ @dict.username }} |
24 | {{ @dict.email }} |
25 | {{ @dict.name }} |
26 | {{ @dict.role }} |
27 | {{ @dict.task_color }} |
28 | |
29 |
30 |
31 |
32 |
33 |
34 | {{ @user.id }} |
35 | {{ @user.username | esc }} |
36 | {{ @user.email | esc }} |
37 | {{ @user.name | esc }} |
38 | {{ ucfirst(@user.role) }} |
39 | #{{ @user.task_color }} |
40 |
41 |
47 | |
48 |
49 |
50 |
51 |
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 |
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 |
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 |
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 | {{ @item.author_name | esc }} |
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 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/view/index/reset_complete.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/view/index/reset_forced.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 | {{ @col }} |
19 |
20 |
21 | {~ endwhile ~}
22 |
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 |
16 | Showing {{ (@issues.limit * @issues.pos) + 1 }}–{{ @issues.limit * (@issues.pos + 1) > @issues.total ? @issues.total : @issues.limit * (@issues.pos + 1) }} of {{ @issues.total }}
17 |
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 |
17 |
18 |
19 |
20 |
21 |
22 | |
23 | {{ @dict.file_name }} |
24 | {{ @dict.cols.parent }} |
25 | {{ @dict.uploaded_by }} |
26 | {{ @dict.upload_date }} |
27 | {{ @dict.file_size }} |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | |
36 | {{ @file.filename | esc }} |
37 | {{ @file.issue_id }} |
38 | {{ @file.user_name | esc }} |
39 | {{ date('M j, Y \a\t g:ia', @this->utc2local(strtotime(@file.created_date))) }} |
40 | {{ @file.filesize | formatFilesize }} |
41 |
42 |
43 |
44 |
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 | {{ @dict.cols.id }} |
6 | {{ @dict.cols.title }} |
7 | {{ @dict.cols.type }} |
8 | {{ @dict.cols.assignee }} |
9 | {{ @dict.cols.author }} |
10 | {{ @dict.cols.priority }} |
11 | {{ @dict.cols.due_date }} |
12 | {{ @dict.cols.sprint }} |
13 | {{ @dict.cols.hours_spent }} |
14 |
15 |
16 |
17 | {~ @renderTree(@project) ~}
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/view/issues/search.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 | {{ @dict.under_n,'#' . @parent.id . ' ' . @this->esc(@parent.name) . '' | format }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {{ @dict.cols.id }} |
17 | {{ @dict.cols.title }} |
18 | {{ @dict.cols.author }} |
19 | {{ @dict.cols.assignee }} |
20 | {{ @dict.cols.created }} |
21 | {{ @dict.cols.priority }} |
22 | {{ @dict.cols.due }} |
23 | {{ @dict.cols.status }} |
24 |
25 |
26 |
27 |
28 |
29 | {{ @item.id }} |
30 | {{ @item.name | esc }} |
31 | {{ @item.author_name | esc }} |
32 | {{ @item.owner_name | esc }} |
33 | {{ date("n/j/y", strtotime(@item.created_date)) }} |
34 | {{ @item.priority_name | esc }} |
35 | {{ !empty(@item.due_date) ? date("n/j", strtotime(@item.due_date)) : "" }} |
36 | {{ @item.status_name | esc }} |
37 |
38 |
39 |
40 |
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 |
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 |
10 |
11 |
12 |
13 | |
14 |
15 |
16 |
17 | View Issue
18 | |
19 |
20 |
21 | |
22 |
23 |
24 |
25 | |
26 | |
27 |
28 |
29 | Or reply to this message to leave a comment
30 |
31 | |
32 | |
33 |
34 |
35 | |
36 |
37 |
38 | |
39 |
40 |
--------------------------------------------------------------------------------
/app/view/notification/blocks/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
--------------------------------------------------------------------------------
/app/view/notification/blocks/logo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | {{ @dict.name }} |
27 | {{ @dict.count }} |
28 |
29 |
30 |
31 |
32 |
33 | {{ @item.tag }} |
34 | {{ @item.freq }} |
35 |
36 |
37 |
38 |
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 |
60 |
61 |
62 |
63 |
64 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/view/tag/single.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/bugs.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/issue_tree.html:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/my_comments.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/open_comments.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/projects.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/recent_comments.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/repeat_work.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/subprojects.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/tasks.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/view/user/dashboard-widgets/watchlist.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/view/user/dashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/view/user/single/tree.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
 | esc }})
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 | {{ @dict.cols.id }} |
29 | {{ @dict.cols.title }} |
30 | {{ @dict.cols.type }} |
31 | {{ @dict.cols.assignee }} |
32 | {{ @dict.cols.author }} |
33 | {{ @dict.cols.priority }} |
34 | {{ @dict.cols.due_date }} |
35 | {{ @dict.cols.sprint }} |
36 | {{ @dict.cols.hours_spent }} |
37 |
38 |
39 |
40 |
41 | {~ @renderTree(@issue) ~}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/view/user/single/tree/ajax.html:
--------------------------------------------------------------------------------
1 | {{ @dict.assigned_issues }}
2 |
3 |
4 |
5 |
6 | {{ @dict.cols.id }} |
7 | {{ @dict.cols.title }} |
8 | {{ @dict.cols.type }} |
9 | {{ @dict.cols.assignee }} |
10 | {{ @dict.cols.author }} |
11 | {{ @dict.cols.priority }} |
12 | {{ @dict.cols.due_date }} |
13 | {{ @dict.cols.sprint }} |
14 | {{ @dict.cols.hours_spent }} |
15 |
16 |
17 |
18 |
19 | {~ @renderTree(@issue) ~}
20 |
21 |
22 |
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 |
32 |
--------------------------------------------------------------------------------
/img/spinner-8-3-16.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
32 |
--------------------------------------------------------------------------------
/img/spinner-8-3-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
32 |
--------------------------------------------------------------------------------
/img/spinner-8-3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------