summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrédéric Guillot <contact@fredericguillot.com>2014-01-25 14:56:02 -0500
committerFrédéric Guillot <contact@fredericguillot.com>2014-01-25 14:56:02 -0500
commit9383a15af699ede77142d040b65118e15754a2ca (patch)
treeb550b5adf5bcf8f5a8793c188cc5630f26a27d49
First commit
-rw-r--r--.gitignore44
-rw-r--r--LICENSE661
-rw-r--r--README.markdown122
-rw-r--r--assets/css/app.css540
-rw-r--r--assets/js/board.js197
-rw-r--r--check_setup.php29
-rw-r--r--controllers/.htaccess1
-rw-r--r--controllers/app.php16
-rw-r--r--controllers/base.php79
-rw-r--r--controllers/board.php185
-rw-r--r--controllers/config.php70
-rw-r--r--controllers/project.php162
-rw-r--r--controllers/task.php201
-rw-r--r--controllers/user.php198
-rw-r--r--data/.htaccess1
-rw-r--r--index.php8
-rw-r--r--lib/.htaccess1
-rw-r--r--lib/helper.php217
-rw-r--r--lib/request.php44
-rw-r--r--lib/response.php135
-rw-r--r--lib/router.php46
-rw-r--r--lib/session.php34
-rw-r--r--lib/template.php38
-rw-r--r--lib/translator.php124
-rw-r--r--locales/fr_FR/translations.php170
-rw-r--r--models/.htaccess1
-rw-r--r--models/base.php48
-rw-r--r--models/board.php166
-rw-r--r--models/config.php88
-rw-r--r--models/project.php162
-rw-r--r--models/schema.php71
-rw-r--r--models/task.php180
-rw-r--r--models/user.php154
-rw-r--r--robots.txt2
-rw-r--r--templates/.htaccess1
-rw-r--r--templates/board_edit.php40
-rw-r--r--templates/board_index.php57
-rw-r--r--templates/board_remove.php17
-rw-r--r--templates/config_index.php55
-rw-r--r--templates/layout.php51
-rw-r--r--templates/project_edit.php24
-rw-r--r--templates/project_index.php70
-rw-r--r--templates/project_new.php20
-rw-r--r--templates/project_remove.php16
-rw-r--r--templates/task_close.php16
-rw-r--r--templates/task_edit.php36
-rw-r--r--templates/task_new.php34
-rw-r--r--templates/task_open.php16
-rw-r--r--templates/task_show.php54
-rw-r--r--templates/user_edit.php34
-rw-r--r--templates/user_forbidden.php10
-rw-r--r--templates/user_index.php46
-rw-r--r--templates/user_login.php20
-rw-r--r--templates/user_new.php31
-rw-r--r--templates/user_remove.php14
-rw-r--r--vendor/.htaccess1
-rw-r--r--vendor/Parsedown/LICENSE.txt20
-rw-r--r--vendor/Parsedown/Parsedown.php991
-rw-r--r--vendor/PicoDb/Database.php108
-rw-r--r--vendor/PicoDb/Drivers/Sqlite.php48
-rw-r--r--vendor/PicoDb/Schema.php60
-rw-r--r--vendor/PicoDb/Table.php430
-rw-r--r--vendor/SimpleValidator/Base.php44
-rw-r--r--vendor/SimpleValidator/Validator.php67
-rw-r--r--vendor/SimpleValidator/Validators/Alpha.php33
-rw-r--r--vendor/SimpleValidator/Validators/AlphaNumeric.php33
-rw-r--r--vendor/SimpleValidator/Validators/Email.php81
-rw-r--r--vendor/SimpleValidator/Validators/Equals.php43
-rw-r--r--vendor/SimpleValidator/Validators/Integer.php42
-rw-r--r--vendor/SimpleValidator/Validators/Ip.php33
-rw-r--r--vendor/SimpleValidator/Validators/Length.php48
-rw-r--r--vendor/SimpleValidator/Validators/MacAddress.php37
-rw-r--r--vendor/SimpleValidator/Validators/MaxLength.php46
-rw-r--r--vendor/SimpleValidator/Validators/MinLength.php46
-rw-r--r--vendor/SimpleValidator/Validators/Numeric.php33
-rw-r--r--vendor/SimpleValidator/Validators/Range.php51
-rw-r--r--vendor/SimpleValidator/Validators/Required.php30
-rw-r--r--vendor/SimpleValidator/Validators/Unique.php78
-rw-r--r--vendor/SimpleValidator/Validators/Version.php32
-rw-r--r--vendor/password.php227
80 files changed, 7519 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..f3b71811
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,44 @@
+# Compiled source #
+###################
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+*.pyc
+
+# Packages #
+############
+# it's better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Logs and databases #
+######################
+*.log
+*.sql
+*.sqlite
+*.sqlite-journal
+
+# OS generated files #
+######################
+.DS_Store
+ehthumbs.db
+Icon?
+Thumbs.db
+*.swp
+.*.swp
+*~
+*.lock
+*.out
+
+# App specific #
+################
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/README.markdown b/README.markdown
new file mode 100644
index 00000000..b508a49a
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,122 @@
+Kanboard
+========
+
+Kanboard is a simple visual task board web application.
+
+- Inspired by the [Kanban methodology](http://en.wikipedia.org/wiki/Kanban_(development))
+- Get a visual and clear overview of your project
+- Multiple boards with the ability to drag and drop tasks
+- Minimalist software, focus only on essential features (Less is more)
+- Open source and self-hosted
+- Super simple installation
+
+Usage examples
+--------------
+
+You can customize your boards according to your business activities:
+
+- Software management: Backlog, Ready, Work in Progress, To be tested, Validated
+- Bug tracking: Received, Confirmed, Work in progress, Tested, Fixed
+- Sales: Prospect, Meeting, Proposal, Sale
+- Lean business management: Ideas, Developement, Measure, Analysis, Done
+- Recruiting: Candidates Pool, Phone Screens, Job Interviews, Hires
+- E-Commerce Shop: Orders, Packaged, Shipped
+- Construction Planning: Materials ordered, Materials received, Work in progress, Work done, Invoice sent, Paid
+
+Features
+--------
+
+- Multiple boards/projects
+- Boards customization, rename or add columns
+- Tasks with different colors, Markdown support for the description
+- Users management with a basic privileges separation (administrator or regular user)
+- Webhooks to create tasks from an external software
+- Host anywhere (shared hosting, VPS, Raspberry Pi or localhost)
+- No external dependencies
+- **Super easy setup**, copy and paste files and you are done!
+- Translations in English and French
+
+Todo
+----
+
+- Touch devices support (tablets)
+- Task search
+- Task limit for each column
+- File attachments
+- Comments
+- API
+- Basic reporting
+- Tasks export in CSV
+
+Todo and known bugs
+-------------------
+
+- See Issues: <https://github.com/fguillot/kanboard/issues>
+
+License
+-------
+
+- GNU Affero General Public License version 3: <http://www.gnu.org/licenses/agpl-3.0.txt>
+
+Authors
+-------
+
+Original author: [Frédéric Guillot](http://fredericguillot.com/)
+
+Requirements
+------------
+
+- Apache or Nginx
+- PHP >= 5.3.7
+- PHP Sqlite extension
+- A web browser with HTML5 drag and drop support
+
+Installation
+------------
+
+From the archive:
+
+1. You must have a web server with PHP installed
+2. Download the source code and copy the directory `kanboard` where you want
+3. Check if the directory `data` is writeable (Kanboard stores everything inside a Sqlite database)
+4. With your browser go to <http://yourpersonalserver/kanboard>
+5. The default login and password is **admin/admin**
+6. Start to use the software
+7. Don't forget to change your password!
+
+From the repository:
+
+1. `git clone https://github.com/fguillot/kanboard.git`
+2. Go to the third step just above
+
+Update
+------
+
+From the archive:
+
+1. Close your session (logout)
+2. Rename your actual Kanboard directory (to keep a backup)
+3. Uncompress the new archive and copy your database file `db.sqlite` in the directory `data`
+4. Make the directory `data` writeable by the web server user
+5. Login and check if everything is ok
+6. Remove the old Kanboard directory
+
+From the repository:
+
+1. Close your session (logout)
+2. `git pull`
+3. Login and check if everything is ok
+
+Security
+--------
+
+- Don't forget to change the default user/password
+- Don't allow everybody to access to the directory `data` from the URL. There is already a `.htaccess` for Apache but nothing for Nginx.
+
+FAQ
+---
+
+### Which web browsers are supported?
+
+Desktop version of Mozilla Firefox, Safari and Google Chrome.
+
diff --git a/assets/css/app.css b/assets/css/app.css
new file mode 100644
index 00000000..2e3e6987
--- /dev/null
+++ b/assets/css/app.css
@@ -0,0 +1,540 @@
+/* reset */
+figure,
+li,
+ul,
+ol,
+table,
+tr,
+td,
+th,
+p,
+blockquote,
+body {
+ margin: 0;
+ padding: 0;
+ font-size: 100%;
+}
+
+/* layout */
+body {
+ max-width: 1500px;
+ margin-left: 10px;
+ margin-right: 10px;
+ color: #333;
+ font-family: HelveticaNeue, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+
+/* links */
+a {
+ color: #3366CC;
+ border: 1px solid rgba(255, 255, 255, 0);
+}
+
+a:focus {
+ outline: 0;
+ color: red;
+ text-decoration: none;
+ border: 1px dotted #aaa;
+}
+
+a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+/* titles */
+h1, h2, h3 {
+ font-weight: normal;
+ color: #333;
+}
+
+h2 {
+ font-size: 1.6em;
+}
+
+h3 {
+ margin-top: 10px;
+ font-size: 1.2em;
+}
+
+/* tables */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+table caption {
+ font-weight: bold;
+ font-size: 1.0em;
+ text-align: left;
+ padding-bottom: 0.5em;
+ padding-top: 0.5em;
+}
+
+th,
+td {
+ border: 1px solid #ccc;
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+ padding-left: 5px;
+}
+
+th {
+ text-align: left;
+ background: #f0f0f0;
+}
+
+tr:nth-child(odd) td {
+ background: #fcfcfc;
+}
+
+td li {
+ margin-left: 20px;
+}
+
+/* forms */
+form {
+ padding-top: 5px;
+ padding-bottom: 5px;
+ padding-left: 15px;
+ margin-bottom: 20px;
+ border-left: 2px dotted #ddd;
+}
+
+label {
+ cursor: pointer;
+ display: block;
+ margin-top: 10px;
+}
+
+input[type="checkbox"] {
+ border: 1px solid #ccc;
+}
+
+input[type="date"],
+input[type="email"],
+input[type="tel"],
+input[type="password"],
+input[type="text"] {
+ border: 1px solid #ccc;
+ padding: 3px;
+ line-height: 15px;
+ width: 400px;
+ font-size: 99%;
+ margin-top: 5px;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+input[type="date"]:focus,
+input[type="email"]:focus,
+input[type="tel"]:focus,
+input[type="password"]:focus,
+input[type="text"]:focus,
+textarea:focus {
+ color: #000;
+ border-color: rgba(82, 168, 236, 0.8);
+ outline: 0;
+ box-shadow: 0 0 8px rgba(82, 168, 236, 0.6);
+}
+
+textarea {
+ border: 1px solid #ccc;
+ padding: 3px;
+ width: 400px;
+ height: 200px;
+ font-size: 99%;
+}
+
+select {
+}
+
+::-webkit-input-placeholder {
+ color: #bbb;
+ padding-top: 2px;
+}
+
+::-ms-input-placeholder {
+ color: #bbb;
+ padding-top: 2px;
+}
+
+::-moz-placeholder {
+ color: #bbb;
+ padding-top: 2px;
+}
+
+.form-actions {
+ margin-top: 20px;
+}
+
+input.form-error,
+textarea.form-error {
+ border: 2px solid #b94a48;
+}
+
+.form-errors {
+ color: #b94a48;
+ list-style-type: none;
+}
+
+.form-help {
+ font-size: 0.9em;
+ color: brown;
+ margin-bottom: 15px;
+}
+
+.form-inline {
+ padding: 0;
+ margin: 0;
+ border: none;
+}
+
+.form-inline label {
+ display: inline;
+}
+
+.form-inline input,
+.form-inline select {
+ margin: 0;
+ margin-right: 15px;
+}
+
+/* alerts */
+.alert {
+ padding: 8px 35px 8px 14px;
+ margin-bottom: 20px;
+ color: #c09853;
+ background-color: #fcf8e3;
+ border: 1px solid #fbeed5;
+ border-radius: 4px;
+}
+
+.alert-success {
+ color: #468847;
+ background-color: #dff0d8;
+ border-color: #d6e9c6;
+}
+
+.alert-error {
+ color: #b94a48;
+ background-color: #f2dede;
+ border-color: #eed3d7;
+}
+
+.alert-info {
+ color: #3a87ad;
+ background-color: #d9edf7;
+ border-color: #bce8f1;
+}
+
+.alert-normal {
+ color: #333;
+ background-color: #f0f0f0;
+ border-color: #ddd;
+}
+
+/* labels */
+a.label {
+ text-decoration: none;
+}
+
+a.label:hover,
+a.label:focus {
+ text-decoration: underline;
+}
+
+.label {
+ font-size: 0.9em;
+ border-radius: 5px 5px 5px 5px;
+ border: 1px solid #000;
+ border-color: rgba(0, 0, 0, 0.3);
+ background: #fff;
+ color: #000;
+ color: rgba(0, 0, 0, 0.8);
+ display: inline-block;
+ padding: 2px 5px;
+ vertical-align: top;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.label-yellow {
+ background-color: #ffee92;
+}
+
+.label-red {
+ background-color: #eea2a0;
+}
+
+.label-green {
+ background-color: #b3e494;
+}
+
+.label-blue {
+ background-color: #d5eeff;
+}
+
+.label-purple {
+ background-color: #dca9de;
+}
+
+/* buttons */
+.btn {
+ -webkit-appearance: none;
+ appearance: none;
+ display: inline-block;
+ color: #333;
+ border: 1px solid #ccc;
+ background: #efefef;
+ padding: 5px;
+ padding-left: 15px;
+ padding-right: 15px;
+ font-size: 0.9em;
+ cursor: pointer;
+ border-radius: 2px;
+}
+
+.btn-small {
+ padding: 2px;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+a.btn {
+ text-decoration: none;
+ font-weight: bold;
+}
+
+.btn-red {
+ border-color: #b0281a;;
+ background: #d14836;
+ color: #fff;
+}
+
+a.btn-red:hover,
+.btn-red:hover,
+.btn-red:focus {
+ color: #fff;
+ background: #c53727;
+}
+
+.btn-blue {
+ border-color: #3079ed;
+ background: #4d90fe;
+ color: #fff;
+}
+
+.btn-blue:hover,
+.btn-blue:focus {
+ border-color: #2f5bb7;
+ background: #357ae8;
+}
+
+/* header */
+header {
+ margin-bottom: 25px;
+ margin-top: 10px;
+}
+
+header ul {
+ text-align: right;
+ font-size: 90%;
+}
+
+header li {
+ display: inline;
+ padding-left: 30px;
+}
+
+header a {
+ color: #777;
+ text-decoration: none;
+}
+
+nav .active a {
+ color: #333;
+ font-weight: bold;
+}
+
+.logo {
+ color: #DF5353;
+ letter-spacing: 1px;
+ float: left;
+}
+
+.page-section {
+ margin-top: 30px;
+}
+
+.page-section,
+.page-header {
+ margin-bottom: 25px;
+}
+
+.page-section h2,
+.page-header h2 {
+ margin: 0;
+ padding: 0;
+ font-size: 140%;
+ border-bottom: 1px dotted red;
+}
+
+.page-header ul {
+ text-align: left;
+ margin-top: 5px;
+}
+
+.page-header li {
+ display: inline;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-left: 1px dotted #ccc;
+}
+
+.page-header li:first-child {
+ border: none;
+ padding-left: 0;
+}
+
+/* boards */
+#board th a {
+ text-decoration: none;
+ font-size: 150%;
+}
+
+#board td {
+ vertical-align: top;
+}
+
+.task-title {
+ margin-top: 10px;
+ font-size: 110%;
+}
+
+.task-user {
+ font-size: 80%;
+}
+
+.task-nobody {
+ font-style: italic;
+}
+
+.task {
+ border: 1px solid #000;
+ padding: 5px;
+ font-size: 95%;
+}
+
+td.over {
+ background-color: #f0f0f0;
+}
+
+td div.over {
+ border: 2px dashed #000;
+}
+
+.draggable-item {
+ margin-right: 5px;
+ margin-bottom: 10px;
+}
+
+[draggable] {
+ user-select: none;
+}
+
+[draggable=true]:hover {
+ box-shadow: 0 0 3px #333;
+}
+
+div.task a {
+ color: #000;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+div.task a:focus,
+div.task a:hover {
+ text-decoration: underline;
+}
+
+article.task li {
+ margin-left: 20px;
+ list-style-type: square;
+}
+
+.task-blue {
+ background-color: rgb(219, 235, 255);
+ border-color: rgb(168, 207, 255);
+}
+
+.task-purple {
+ background-color: rgb(223, 176, 255);
+ border-color: rgb(205, 133, 254);
+}
+
+.task-grey {
+ background-color: rgb(238, 238, 238);
+ border-color: rgb(204, 204, 204);
+}
+
+.task-red {
+ background-color: rgb(255, 187, 187);
+ border-color: rgb(255, 151, 151);
+}
+
+.task-green {
+ background-color: rgb(189, 244, 203);
+ border-color: rgb(74, 227, 113);
+}
+
+.task-yellow {
+ background-color: rgb(245, 247, 196);
+ border-color: rgb(223, 227, 45);
+}
+
+.task-orange {
+ background-color: rgb(255, 215, 179);
+ border-color: rgb(255, 172, 98);
+}
+
+#description {
+ border-left: 5px solid #000;
+ background: #f0f0f0;
+ padding-left: 10px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+#description li {
+ margin-left: 25px;
+}
+
+/* config page */
+.settings {
+ border-radius: 4px;
+ padding: 8px 35px 8px 14px;
+ margin-bottom: 20px;
+ border: 1px solid #ddd;
+ color: #333;
+ background-color: #f0f0f0;
+}
+
+.settings li {
+ list-style-type: square;
+ margin-left: 20px;
+ margin-bottom: 3px;
+}
+
+/* confirmation box */
+.confirm {
+ max-width: 700px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ padding-left: 15px;
+ border-left: 2px dotted #ddd;
+}
diff --git a/assets/js/board.js b/assets/js/board.js
new file mode 100644
index 00000000..a9bd35d1
--- /dev/null
+++ b/assets/js/board.js
@@ -0,0 +1,197 @@
+(function () {
+
+ function handleItemDragStart(e)
+ {
+ this.style.opacity = '0.4';
+
+ dragSrcItem = this;
+ dragSrcColumn = this.parentNode;
+
+ e.dataTransfer.effectAllowed = 'copy';
+ e.dataTransfer.setData('text/plain', this.innerHTML);
+ }
+
+ function handleItemDragEnd(e)
+ {
+ // Restore styles
+ removeOver();
+ this.style.opacity = '1.0';
+
+ dragSrcColumn = null;
+ dragSrcItem = null;
+ }
+
+ function handleItemDragOver(e)
+ {
+ if (e.preventDefault) e.preventDefault();
+
+ e.dataTransfer.dropEffect = 'copy';
+
+ return false;
+ }
+
+ function handleItemDragEnter(e)
+ {
+ if (dragSrcItem != this) {
+ removeOver();
+ this.classList.add('over');
+ }
+ }
+
+ function handleItemDrop(e)
+ {
+ if (e.preventDefault) e.preventDefault();
+ if (e.stopPropagation) e.stopPropagation();
+
+ // Drop the element if the item is not the same
+ if (dragSrcItem != this) {
+
+ var position = getItemPosition(this);
+ var item = createItem(e.dataTransfer.getData('text/plain'));
+
+ if (countColumnItems(this.parentNode) == position) {
+ this.parentNode.appendChild(item);
+ }
+ else {
+ this.parentNode.insertBefore(item, this);
+ }
+
+ dragSrcItem.parentNode.removeChild(dragSrcItem);
+
+ saveBoard();
+ }
+
+ dragSrcColumn = null;
+ dragSrcItem = null;
+
+ return false;
+ }
+
+
+ function handleColumnDragOver(e)
+ {
+ if (e.preventDefault) e.preventDefault();
+
+ e.dataTransfer.dropEffect = 'copy';
+
+ return false;
+ }
+
+ function handleColumnDragEnter(e)
+ {
+ if (dragSrcColumn != this) {
+ removeOver();
+ this.classList.add('over');
+ }
+ }
+
+ function handleColumnDrop(e)
+ {
+ if (e.preventDefault) e.preventDefault();
+ if (e.stopPropagation) e.stopPropagation();
+
+ // Drop the element if the column is not the same
+ if (dragSrcColumn != this) {
+
+ var item = createItem(e.dataTransfer.getData('text/plain'));
+ this.appendChild(item);
+ dragSrcColumn.removeChild(dragSrcItem);
+
+ saveBoard();
+ }
+
+ return false;
+ }
+
+ function saveBoard()
+ {
+ var data = [];
+ var projectId = document.getElementById("board").getAttribute("data-project-id");
+ var cols = document.querySelectorAll('.column');
+
+ [].forEach.call(cols, function(col) {
+
+ [].forEach.call(col.children, function(item) {
+
+ data.push({
+ "task_id": item.firstElementChild.getAttribute("data-task-id"),
+ "position": getItemPosition(item),
+ "column_id": col.getAttribute("data-column-id")
+ })
+ });
+ });
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "?controller=board&action=save&project_id=" + projectId, true);
+ xhr.send(JSON.stringify(data));
+ }
+
+ function getItemPosition(element)
+ {
+ var i = 0;
+
+ while ((element = element.previousSibling) != null) {
+
+ if (element.nodeName == "DIV" && element.className == "draggable-item") {
+ i++;
+ }
+ }
+
+ return i + 1;
+ }
+
+ function countColumnItems(element)
+ {
+ return element.children.length;
+ }
+
+ function createItem(html)
+ {
+ var item = document.createElement("div");
+ item.className = "draggable-item";
+ item.draggable = true;
+ item.innerHTML = html;
+ item.ondragstart = handleItemDragStart;
+ item.ondragend = handleItemDragEnd;
+ item.ondragenter = handleItemDragEnter;
+ item.ondragover = handleItemDragOver;
+ item.ondrop = handleItemDrop;
+
+ return item;
+ }
+
+ function removeOver()
+ {
+ // Remove column over
+ [].forEach.call(document.querySelectorAll('.column'), function (col) {
+ col.classList.remove('over');
+ });
+
+ // Remove item over
+ [].forEach.call(document.querySelectorAll('.draggable-item'), function (item) {
+ item.classList.remove('over');
+ });
+ }
+
+ var dragSrcItem = null;
+ var dragSrcColumn = null;
+
+ var items = document.querySelectorAll('.draggable-item');
+
+ [].forEach.call(items, function(item) {
+ item.addEventListener('dragstart', handleItemDragStart, false);
+ item.addEventListener('dragend', handleItemDragEnd, false);
+ item.addEventListener('dragenter', handleItemDragEnter, false);
+ item.addEventListener('dragover', handleItemDragOver, false);
+ item.addEventListener('drop', handleItemDrop, false);
+ });
+
+ var cols = document.querySelectorAll('.column');
+
+ [].forEach.call(cols, function(col) {
+ col.addEventListener('dragenter', handleColumnDragEnter, false);
+ col.addEventListener('dragover', handleColumnDragOver, false);
+ col.addEventListener('drop', handleColumnDrop, false);
+ });
+
+}()); \ No newline at end of file
diff --git a/check_setup.php b/check_setup.php
new file mode 100644
index 00000000..46260ff1
--- /dev/null
+++ b/check_setup.php
@@ -0,0 +1,29 @@
+<?php
+
+// PHP 5.3 minimum
+if (version_compare(PHP_VERSION, '5.3.7', '<')) {
+ die('This software require PHP 5.3.7 minimum');
+}
+
+// Short tags must be enabled for PHP < 5.4
+if (version_compare(PHP_VERSION, '5.4.0', '<')) {
+
+ if (! ini_get('short_open_tag')) {
+ die('This software require to have short tags enabled, check your php.ini => "short_open_tag = On"');
+ }
+}
+
+// Check PDO Sqlite
+if (! extension_loaded('pdo_sqlite')) {
+ die('PHP extension required: pdo_sqlite');
+}
+
+// Check if /data is writeable
+if (! is_writable('data')) {
+ die('The directory "data" must be writeable by your web server user');
+}
+
+// Include password_compat for PHP < 5.5
+if (version_compare(PHP_VERSION, '5.5.0', '<')) {
+ require __DIR__.'/vendor/password.php';
+}
diff --git a/controllers/.htaccess b/controllers/.htaccess
new file mode 100644
index 00000000..14249c50
--- /dev/null
+++ b/controllers/.htaccess
@@ -0,0 +1 @@
+Deny from all \ No newline at end of file
diff --git a/controllers/app.php b/controllers/app.php
new file mode 100644
index 00000000..981abbbe
--- /dev/null
+++ b/controllers/app.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Controller;
+
+class App extends Base
+{
+ public function index()
+ {
+ if ($this->project->countByStatus(\Model\Project::ACTIVE)) {
+ $this->response->redirect('?controller=board');
+ }
+ else {
+ $this->redirectNoProject();
+ }
+ }
+}
diff --git a/controllers/base.php b/controllers/base.php
new file mode 100644
index 00000000..f0ae5bd2
--- /dev/null
+++ b/controllers/base.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Controller;
+
+require __DIR__.'/../lib/request.php';
+require __DIR__.'/../lib/response.php';
+require __DIR__.'/../lib/session.php';
+require __DIR__.'/../lib/template.php';
+require __DIR__.'/../lib/helper.php';
+require __DIR__.'/../lib/translator.php';
+require __DIR__.'/../models/base.php';
+require __DIR__.'/../models/config.php';
+require __DIR__.'/../models/user.php';
+require __DIR__.'/../models/project.php';
+require __DIR__.'/../models/task.php';
+require __DIR__.'/../models/board.php';
+
+abstract class Base
+{
+ protected $request;
+ protected $response;
+ protected $session;
+ protected $template;
+ protected $user;
+ protected $project;
+ protected $task;
+ protected $board;
+ protected $config;
+
+ public function __construct()
+ {
+ $this->request = new \Request;
+ $this->response = new \Response;
+ $this->session = new \Session;
+ $this->template = new \Template;
+ $this->config = new \Model\Config;
+ $this->user = new \Model\User;
+ $this->project = new \Model\Project;
+ $this->task = new \Model\Task;
+ $this->board = new \Model\Board;
+ }
+
+ public function beforeAction($controller, $action)
+ {
+ $this->session->open();
+
+ $public = array(
+ 'user' => array('login', 'check'),
+ 'task' => array('add'),
+ );
+
+ if (! isset($_SESSION['user']) && ! isset($public[$controller]) && ! in_array($action, $public[$controller])) {
+ $this->response->redirect('?controller=user&action=login');
+ }
+
+ // Load translations
+ $language = $this->config->get('language', 'en_US');
+ if ($language !== 'en_US') \Translator\load($language);
+
+ $this->response->csp();
+ $this->response->nosniff();
+ $this->response->xss();
+ $this->response->hsts();
+ $this->response->xframe();
+ }
+
+ public function checkPermissions()
+ {
+ if ($_SESSION['user']['is_admin'] == 0) {
+ $this->response->redirect('?controller=user&action=forbidden');
+ }
+ }
+
+ public function redirectNoProject()
+ {
+ $this->session->flash(t('There is no active project, the first step is to create a new project.'));
+ $this->response->redirect('?controller=project&action=create');
+ }
+}
diff --git a/controllers/board.php b/controllers/board.php
new file mode 100644
index 00000000..ec32e8a0
--- /dev/null
+++ b/controllers/board.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Controller;
+
+class Board extends Base
+{
+ // Display current board
+ public function index()
+ {
+ $projects = $this->project->getListByStatus(\Model\Project::ACTIVE);
+
+ if (! count($projects)) {
+ $this->redirectNoProject();
+ }
+ else if (! empty($_SESSION['user']['default_project_id']) && isset($projects[$_SESSION['user']['default_project_id']])) {
+ $project_id = $_SESSION['user']['default_project_id'];
+ $project_name = $projects[$_SESSION['user']['default_project_id']];
+ }
+ else {
+ list($project_id, $project_name) = each($projects);
+ }
+
+ $this->response->html($this->template->layout('board_index', array(
+ 'projects' => $projects,
+ 'current_project_id' => $project_id,
+ 'current_project_name' => $project_name,
+ 'columns' => $this->board->get($project_id),
+ 'menu' => 'boards',
+ 'title' => $project_name
+ )));
+ }
+
+ // Show a board
+ public function show()
+ {
+ $projects = $this->project->getListByStatus(\Model\Project::ACTIVE);
+ $project_id = $this->request->getIntegerParam('project_id');
+ $project_name = $projects[$project_id];
+
+ $this->response->html($this->template->layout('board_index', array(
+ 'projects' => $projects,
+ 'current_project_id' => $project_id,
+ 'current_project_name' => $project_name,
+ 'columns' => $this->board->get($project_id),
+ 'menu' => 'boards',
+ 'title' => $project_name
+ )));
+ }
+
+ // Display a form to edit a board
+ public function edit()
+ {
+ $this->checkPermissions();
+
+ $project_id = $this->request->getIntegerParam('project_id');
+ $project = $this->project->get($project_id);
+ $columns = $this->board->getColumnsList($project_id);
+ $values = array();
+
+ foreach ($columns as $column_id => $column_title) {
+ $values['title['.$column_id.']'] = $column_title;
+ }
+
+ $this->response->html($this->template->layout('board_edit', array(
+ 'errors' => array(),
+ 'values' => $values + array('project_id' => $project_id),
+ 'columns' => $columns,
+ 'project' => $project,
+ 'menu' => 'projects',
+ 'title' => t('Edit board')
+ )));
+ }
+
+ // Validate and update a board
+ public function update()
+ {
+ $this->checkPermissions();
+
+ $project_id = $this->request->getIntegerParam('project_id');
+ $project = $this->project->get($project_id);
+ $columns = $this->board->getColumnsList($project_id);
+ $data = $this->request->getValues();
+ $values = array();
+
+ foreach ($columns as $column_id => $column_title) {
+ $values['title['.$column_id.']'] = isset($data['title'][$column_id]) ? $data['title'][$column_id] : '';
+ }
+
+ list($valid, $errors) = $this->board->validateModification($columns, $values);
+
+ if ($valid) {
+
+ if ($this->board->update($data['title'])) {
+ $this->session->flash(t('Board updated successfully.'));
+ $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
+ }
+ else {
+ $this->session->flashError(t('Unable to update this board.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('board_edit', array(
+ 'errors' => $errors,
+ 'values' => $values + array('project_id' => $project_id),
+ 'columns' => $columns,
+ 'project' => $project,
+ 'menu' => 'projects',
+ 'title' => t('Edit board')
+ )));
+ }
+
+ // Validate and add a new column
+ public function add()
+ {
+ $this->checkPermissions();
+
+ $project_id = $this->request->getIntegerParam('project_id');
+ $project = $this->project->get($project_id);
+ $columns = $this->board->getColumnsList($project_id);
+ $data = $this->request->getValues();
+ $values = array();
+
+ foreach ($columns as $column_id => $column_title) {
+ $values['title['.$column_id.']'] = $column_title;
+ }
+
+ list($valid, $errors) = $this->board->validateCreation($data);
+
+ if ($valid) {
+
+ if ($this->board->add($data)) {
+ $this->session->flash(t('Board updated successfully.'));
+ $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
+ }
+ else {
+ $this->session->flashError(t('Unable to update this board.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('board_edit', array(
+ 'errors' => $errors,
+ 'values' => $values + $data,
+ 'columns' => $columns,
+ 'project' => $project,
+ 'menu' => 'projects',
+ 'title' => t('Edit board')
+ )));
+ }
+
+ // Confirmation dialog before removing a column
+ public function confirm()
+ {
+ $this->checkPermissions();
+
+ $this->response->html($this->template->layout('board_remove', array(
+ 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
+ 'menu' => 'projects',
+ 'title' => t('Remove a column from a board')
+ )));
+ }
+
+ // Remove a column
+ public function remove()
+ {
+ $this->checkPermissions();
+
+ $column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
+
+ if ($column && $this->board->removeColumn($column['id'])) {
+ $this->session->flash(t('Column removed successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to remove this column.'));
+ }
+
+ $this->response->redirect('?controller=board&action=edit&project_id='.$column['project_id']);
+ }
+
+ // Save the board (Ajax request made by drag and drop)
+ public function save()
+ {
+ $this->response->json(array(
+ 'result' => $this->board->saveTasksPosition($this->request->getValues())
+ ));
+ }
+}
diff --git a/controllers/config.php b/controllers/config.php
new file mode 100644
index 00000000..96a6a085
--- /dev/null
+++ b/controllers/config.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Controller;
+
+class Config extends Base
+{
+ // Settings page
+ public function index()
+ {
+ $this->response->html($this->template->layout('config_index', array(
+ 'db_size' => $this->config->getDatabaseSize(),
+ 'user' => $_SESSION['user'],
+ 'projects' => $this->project->getList(),
+ 'languages' => $this->config->getLanguages(),
+ 'values' => $this->config->getAll(),
+ 'errors' => array(),
+ 'menu' => 'config',
+ 'title' => t('Settings')
+ )));
+ }
+
+ // Validate and save settings
+ public function save()
+ {
+ $this->checkPermissions();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->config->validateModification($values);
+
+ if ($valid) {
+
+ if ($this->config->save($values)) {
+ $this->config->reload();
+ $this->session->flash(t('Settings saved successfully.'));
+ $this->response->redirect('?controller=config');
+ }
+ else {
+ $this->session->flashError(t('Unable to save your settings.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('config_index', array(
+ 'db_size' => $this->config->getDatabaseSize(),
+ 'user' => $_SESSION['user'],
+ 'projects' => $this->project->getList(),
+ 'languages' => $this->config->getLanguages(),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'menu' => 'config',
+ 'title' => t('Settings')
+ )));
+ }
+
+ // Download the database
+ public function downloadDb()
+ {
+ $this->checkPermissions();
+ $this->response->forceDownload('db.sqlite.gz');
+ $this->response->binary($this->config->downloadDatabase());
+ }
+
+ // Optimize the database
+ public function optimizeDb()
+ {
+ $this->checkPermissions();
+ $this->config->optimizeDatabase();
+ $this->session->flash(t('Database optimization done.'));
+ $this->response->redirect('?controller=config');
+ }
+}
diff --git a/controllers/project.php b/controllers/project.php
new file mode 100644
index 00000000..a384be67
--- /dev/null
+++ b/controllers/project.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Controller;
+
+class Project extends Base
+{
+ // List of projects
+ public function index()
+ {
+ $projects = $this->project->getAll(true);
+ $nb_projects = count($projects);
+
+ $this->response->html($this->template->layout('project_index', array(
+ 'projects' => $projects,
+ 'nb_projects' => $nb_projects,
+ 'menu' => 'projects',
+ 'title' => t('Projects').' ('.$nb_projects.')'
+ )));
+ }
+
+ // Display a form to create a new project
+ public function create()
+ {
+ $this->checkPermissions();
+
+ $this->response->html($this->template->layout('project_new', array(
+ 'errors' => array(),
+ 'values' => array(),
+ 'menu' => 'projects',
+ 'title' => t('New project')
+ )));
+ }
+
+ // Validate and save a new project
+ public function save()
+ {
+ $this->checkPermissions();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->project->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->project->create($values)) {
+ $this->session->flash(t('Your project have been created successfully.'));
+ $this->response->redirect('?controller=project');
+ }
+ else {
+ $this->session->flashError(t('Unable to create your project.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('project_new', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'menu' => 'projects',
+ 'title' => t('New Project')
+ )));
+ }
+
+ // Display a form to edit a project
+ public function edit()
+ {
+ $this->checkPermissions();
+
+ $project = $this->project->get($this->request->getIntegerParam('project_id'));
+
+ $this->response->html($this->template->layout('project_edit', array(
+ 'errors' => array(),
+ 'values' => $project,
+ 'menu' => 'projects',
+ 'title' => t('Edit project')
+ )));
+ }
+
+ // Validate and update a project
+ public function update()
+ {
+ $this->checkPermissions();
+
+ $values = $this->request->getValues() + array('is_active' => 0);
+ list($valid, $errors) = $this->project->validateModification($values);
+
+ if ($valid) {
+
+ if ($this->project->update($values)) {
+ $this->session->flash(t('Project updated successfully.'));
+ $this->response->redirect('?controller=project');
+ }
+ else {
+ $this->session->flashError(t('Unable to update this project.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('project_edit', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'menu' => 'projects',
+ 'title' => t('Edit Project')
+ )));
+ }
+
+ // Confirmation dialog before to remove a project
+ public function confirm()
+ {
+ $this->checkPermissions();
+
+ $this->response->html($this->template->layout('project_remove', array(
+ 'project' => $this->project->get($this->request->getIntegerParam('project_id')),
+ 'menu' => 'projects',
+ 'title' => t('Remove project')
+ )));
+ }
+
+ // Remove a project
+ public function remove()
+ {
+ $this->checkPermissions();
+
+ $project_id = $this->request->getIntegerParam('project_id');
+
+ if ($project_id && $this->project->remove($project_id)) {
+ $this->session->flash(t('Project removed successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to remove this project.'));
+ }
+
+ $this->response->redirect('?controller=project');
+ }
+
+ // Enable a project
+ public function enable()
+ {
+ $this->checkPermissions();
+
+ $project_id = $this->request->getIntegerParam('project_id');
+
+ if ($project_id && $this->project->enable($project_id)) {
+ $this->session->flash(t('Project activated successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to activate this project.'));
+ }
+
+ $this->response->redirect('?controller=project');
+ }
+
+ // Disable a project
+ public function disable()
+ {
+ $this->checkPermissions();
+
+ $project_id = $this->request->getIntegerParam('project_id');
+
+ if ($project_id && $this->project->disable($project_id)) {
+ $this->session->flash(t('Project disabled successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to disable this project.'));
+ }
+
+ $this->response->redirect('?controller=project');
+ }
+}
diff --git a/controllers/task.php b/controllers/task.php
new file mode 100644
index 00000000..b022f37c
--- /dev/null
+++ b/controllers/task.php
@@ -0,0 +1,201 @@
+<?php
+
+namespace Controller;
+
+class Task extends Base
+{
+ // Webhook to create a task (useful for external software)
+ public function add()
+ {
+ $token = $this->request->getStringParam('token');
+
+ if ($this->config->get('webhooks_token') !== $token) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ $values = array(
+ 'title' => $this->request->getStringParam('title'),
+ 'description' => $this->request->getStringParam('description'),
+ 'color_id' => $this->request->getStringParam('color_id'),
+ 'project_id' => $this->request->getIntegerParam('project_id'),
+ 'owner_id' => $this->request->getIntegerParam('owner_id'),
+ 'column_id' => $this->request->getIntegerParam('column_id'),
+ );
+
+ list($valid,) = $this->task->validateCreation($values);
+
+ if ($valid && $this->task->create($values)) {
+ $this->response->text('OK');
+ }
+
+ $this->response->text('FAILED');
+ }
+
+ // Show a task
+ public function show()
+ {
+ $task = $this->task->getById($this->request->getIntegerParam('task_id'), true);
+
+ $this->response->html($this->template->layout('task_show', array(
+ 'task' => $task,
+ 'columns_list' => $this->board->getColumnsList($task['project_id']),
+ 'colors_list' => $this->task->getColors(),
+ 'menu' => 'tasks',
+ 'title' => $task['title']
+ )));
+ }
+
+ // Display a form to create a new task
+ public function create()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+
+ $this->response->html($this->template->layout('task_new', array(
+ 'errors' => array(),
+ 'values' => array(
+ 'project_id' => $project_id,
+ 'column_id' => $this->request->getIntegerParam('column_id'),
+ 'color_id' => $this->request->getStringParam('color_id'),
+ 'owner_id' => $this->request->getIntegerParam('owner_id'),
+ 'another_task' => $this->request->getIntegerParam('another_task'),
+ ),
+ 'projects_list' => $this->project->getListByStatus(\Model\Project::ACTIVE),
+ 'columns_list' => $this->board->getColumnsList($project_id),
+ 'users_list' => $this->user->getList(),
+ 'colors_list' => $this->task->getColors(),
+ 'menu' => 'tasks',
+ 'title' => t('New task')
+ )));
+ }
+
+ // Validate and save a new task
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->task->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->task->create($values)) {
+ $this->session->flash(t('Task created successfully.'));
+
+ if (isset($values['another_task']) && $values['another_task'] == 1) {
+ unset($values['title']);
+ unset($values['description']);
+ $this->response->redirect('?controller=task&action=create&'.http_build_query($values));
+ }
+ else {
+ $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']);
+ }
+ }
+ else {
+ $this->session->flashError(t('Unable to create your task.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('task_new', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'projects_list' => $this->project->getListByStatus(\Model\Project::ACTIVE),
+ 'columns_list' => $this->board->getColumnsList($values['project_id']),
+ 'users_list' => $this->user->getList(),
+ 'colors_list' => $this->task->getColors(),
+ 'menu' => 'tasks',
+ 'title' => t('New task')
+ )));
+ }
+
+ // Display a form to edit a task
+ public function edit()
+ {
+ $task = $this->task->getById($this->request->getIntegerParam('task_id'));
+
+ $this->response->html($this->template->layout('task_edit', array(
+ 'errors' => array(),
+ 'values' => $task,
+ 'projects_list' => $this->project->getListByStatus(\Model\Project::ACTIVE),
+ 'columns_list' => $this->board->getColumnsList($task['project_id']),
+ 'users_list' => $this->user->getList(),
+ 'colors_list' => $this->task->getColors(),
+ 'menu' => 'tasks',
+ 'title' => t('Edit a task')
+ )));
+ }
+
+ // Validate and update a task
+ public function update()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->task->validateModification($values);
+
+ if ($valid) {
+
+ if ($this->task->update($values)) {
+ $this->session->flash(t('Task updated successfully.'));
+ $this->response->redirect('?controller=task&action=show&task_id='.$values['id']);
+ }
+ else {
+ $this->session->flashError(t('Unable to update your task.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('task_edit', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'projects_list' => $this->project->getListByStatus(\Model\Project::ACTIVE),
+ 'columns_list' => $this->board->getColumnsList($task['project_id']),
+ 'users_list' => $this->user->getList(),
+ 'colors_list' => $this->task->getColors(),
+ 'menu' => 'tasks',
+ 'title' => t('Edit a task')
+ )));
+ }
+
+ // Hide a task
+ public function close()
+ {
+ $task = $this->task->getById($this->request->getIntegerParam('task_id'));
+
+ if ($task && $this->task->close($task['id'])) {
+ $this->session->flash(t('Task closed successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to close this task.'));
+ }
+
+ $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
+ }
+
+ // Confirmation dialog before to close a task
+ public function confirmClose()
+ {
+ $this->response->html($this->template->layout('task_close', array(
+ 'task' => $this->task->getById($this->request->getIntegerParam('task_id')),
+ 'menu' => 'tasks',
+ 'title' => t('Close a task')
+ )));
+ }
+
+ // Open a task
+ public function open()
+ {
+ $task = $this->task->getById($this->request->getIntegerParam('task_id'));
+
+ if ($task && $this->task->close($task['id'])) {
+ $this->session->flash(t('Task opened successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to open this task.'));
+ }
+
+ $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
+ }
+
+ // Confirmation dialog before to open a task
+ public function confirmOpen()
+ {
+ $this->response->html($this->template->layout('task_open', array(
+ 'task' => $this->task->getById($this->request->getIntegerParam('task_id')),
+ 'menu' => 'tasks',
+ 'title' => t('Open a task')
+ )));
+ }
+}
diff --git a/controllers/user.php b/controllers/user.php
new file mode 100644
index 00000000..0fdd9d1e
--- /dev/null
+++ b/controllers/user.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace Controller;
+
+class User extends Base
+{
+ // Display access forbidden page
+ public function forbidden()
+ {
+ $this->response->html($this->template->layout('user_forbidden', array(
+ 'menu' => 'users',
+ 'title' => t('Access Forbidden')
+ )));
+ }
+
+ // Logout and destroy session
+ public function logout()
+ {
+ $this->session->close();
+ $this->response->redirect('?controller=user&action=login');
+ }
+
+ // Display the form login
+ public function login()
+ {
+ if (isset($_SESSION['user'])) $this->response->redirect('?controller=app');
+
+ $this->response->html($this->template->layout('user_login', array(
+ 'errors' => array(),
+ 'values' => array(),
+ 'no_layout' => true,
+ 'title' => t('Login')
+ )));
+ }
+
+ // Check credentials
+ public function check()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->user->validateLogin($values);
+
+ if ($valid) $this->response->redirect('?controller=app');
+
+ $this->response->html($this->template->layout('user_login', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'no_layout' => true,
+ 'title' => t('Login')
+ )));
+ }
+
+ // List all users
+ public function index()
+ {
+ $users = $this->user->getAll();
+ $nb_users = count($users);
+
+ $this->response->html(
+ $this->template->layout('user_index', array(
+ 'projects' => $this->project->getList(),
+ 'users' => $users,
+ 'nb_users' => $nb_users,
+ 'menu' => 'users',
+ 'title' => t('Users').' ('.$nb_users.')'
+ )));
+ }
+
+ // Display a form to create a new user
+ public function create()
+ {
+ $this->checkPermissions();
+
+ $this->response->html($this->template->layout('user_new', array(
+ 'projects' => $this->project->getList(),
+ 'errors' => array(),
+ 'values' => array(),
+ 'menu' => 'users',
+ 'title' => t('New user')
+ )));
+ }
+
+ // Validate and save a new user
+ public function save()
+ {
+ $this->checkPermissions();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->user->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->user->create($values)) {
+ $this->session->flash(t('User created successfully.'));
+ $this->response->redirect('?controller=user');
+ }
+ else {
+ $this->session->flashError(t('Unable to create your user.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('user_new', array(
+ 'projects' => $this->project->getList(),
+ 'errors' => $errors,
+ 'values' => $values,
+ 'menu' => 'users',
+ 'title' => t('New user')
+ )));
+ }
+
+ // Display a form to edit a user
+ public function edit()
+ {
+ $user = $this->user->getById($this->request->getIntegerParam('user_id'));
+
+ if (! $_SESSION['user']['is_admin'] && $_SESSION['user']['id'] != $user['id']) {
+ $this->response->redirect('?controller=user&action=forbidden');
+ }
+
+ if (! empty($user)) unset($user['password']);
+
+ $this->response->html($this->template->layout('user_edit', array(
+ 'projects' => $this->project->getList(),
+ 'errors' => array(),
+ 'values' => $user,
+ 'menu' => 'users',
+ 'title' => t('Edit user')
+ )));
+ }
+
+ // Validate and update a user
+ public function update()
+ {
+ $values = $this->request->getValues();
+
+ if ($_SESSION['user']['is_admin'] == 1) {
+ $values += array('is_admin' => 0);
+ }
+ else {
+
+ if ($_SESSION['user']['id'] != $values['id']) {
+ $this->response->redirect('?controller=user&action=forbidden');
+ }
+
+ if (isset($values['is_admin'])) {
+ unset($values['is_admin']); // Regular users can't be admin
+ }
+ }
+
+ list($valid, $errors) = $this->user->validateModification($values);
+
+ if ($valid) {
+
+ if ($this->user->update($values)) {
+ $this->session->flash(t('User updated successfully.'));
+ $this->response->redirect('?controller=user');
+ }
+ else {
+ $this->session->flashError(t('Unable to update your user.'));
+ }
+ }
+
+ $this->response->html($this->template->layout('user_edit', array(
+ 'projects' => $this->project->getList(),
+ 'errors' => $errors,
+ 'values' => $values,
+ 'menu' => 'users',
+ 'title' => t('Edit user')
+ )));
+ }
+
+ // Confirmation dialog before to remove a user
+ public function confirm()
+ {
+ $this->checkPermissions();
+
+ $this->response->html($this->template->layout('user_remove', array(
+ 'user' => $this->user->getById($this->request->getIntegerParam('user_id')),
+ 'menu' => 'users',
+ 'title' => t('Remove user')
+ )));
+ }
+
+ // Remove a user
+ public function remove()
+ {
+ $this->checkPermissions();
+
+ $user_id = $this->request->getIntegerParam('user_id');
+
+ if ($user_id && $this->user->remove($user_id)) {
+ $this->session->flash(t('User removed successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to remove this user.'));
+ }
+
+ $this->response->redirect('?controller=user');
+ }
+}
diff --git a/data/.htaccess b/data/.htaccess
new file mode 100644
index 00000000..14249c50
--- /dev/null
+++ b/data/.htaccess
@@ -0,0 +1 @@
+Deny from all \ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 00000000..7d4b70cf
--- /dev/null
+++ b/index.php
@@ -0,0 +1,8 @@
+<?php
+
+require __DIR__.'/check_setup.php';
+require __DIR__.'/controllers/base.php';
+require __DIR__.'/lib/router.php';
+
+$router = new Router;
+$router->execute();
diff --git a/lib/.htaccess b/lib/.htaccess
new file mode 100644
index 00000000..14249c50
--- /dev/null
+++ b/lib/.htaccess
@@ -0,0 +1 @@
+Deny from all \ No newline at end of file
diff --git a/lib/helper.php b/lib/helper.php
new file mode 100644
index 00000000..a0279681
--- /dev/null
+++ b/lib/helper.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace Helper;
+
+function markdown($text)
+{
+ require_once __DIR__.'/../vendor/Parsedown/Parsedown.php';
+ return \Parsedown::instance()->parse($text);
+}
+
+function get_current_base_url()
+{
+ $url = isset($_SERVER['HTTPS']) ? 'https://' : 'http://';
+ $url .= $_SERVER['SERVER_NAME'];
+ $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
+ $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/';
+
+ return $url;
+}
+
+function escape($value)
+{
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
+}
+
+function flash($html)
+{
+ $data = '';
+
+ if (isset($_SESSION['flash_message'])) {
+ $data = sprintf($html, escape($_SESSION['flash_message']));
+ unset($_SESSION['flash_message']);
+ }
+
+ return $data;
+}
+
+function flash_error($html)
+{
+ $data = '';
+
+ if (isset($_SESSION['flash_error_message'])) {
+ $data = sprintf($html, escape($_SESSION['flash_error_message']));
+ unset($_SESSION['flash_error_message']);
+ }
+
+ return $data;
+}
+
+function format_bytes($size, $precision = 2)
+{
+ $base = log($size) / log(1024);
+ $suffixes = array('', 'k', 'M', 'G', 'T');
+
+ return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)];
+}
+
+function get_host_from_url($url)
+{
+ return escape(parse_url($url, PHP_URL_HOST)) ?: $url;
+}
+
+function summary($value, $min_length = 5, $max_length = 120, $end = '[...]')
+{
+ $length = strlen($value);
+
+ if ($length > $max_length) {
+ return substr($value, 0, strpos($value, ' ', $max_length)).' '.$end;
+ }
+ else if ($length < $min_length) {
+ return '';
+ }
+
+ return $value;
+}
+
+function in_list($id, array $listing)
+{
+ if (isset($listing[$id])) {
+ return escape($listing[$id]);
+ }
+
+ return '?';
+}
+
+function error_class(array $errors, $name)
+{
+ return ! isset($errors[$name]) ? '' : ' form-error';
+}
+
+function error_list(array $errors, $name)
+{
+ $html = '';
+
+ if (isset($errors[$name])) {
+
+ $html .= '<ul class="form-errors">';
+
+ foreach ($errors[$name] as $error) {
+ $html .= '<li>'.escape($error).'</li>';
+ }
+
+ $html .= '</ul>';
+ }
+
+ return $html;
+}
+
+function form_value($values, $name)
+{
+ if (isset($values->$name)) {
+ return 'value="'.escape($values->$name).'"';
+ }
+
+ return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : '';
+}
+
+function form_hidden($name, $values = array())
+{
+ return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).'/>';
+}
+
+function form_default_select($name, array $options, $values = array(), array $errors = array(), $class = '')
+{
+ $options = array('' => '?') + $options;
+ return form_select($name, $options, $values, $errors, $class);
+}
+
+function form_select($name, array $options, $values = array(), array $errors = array(), $class = '')
+{
+ $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'">';
+
+ foreach ($options as $id => $value) {
+
+ $html .= '<option value="'.escape($id).'"';
+
+ if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"';
+ if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"';
+
+ $html .= '>'.escape($value).'</option>';
+ }
+
+ $html .= '</select>';
+ $html .= error_list($errors, $name);
+
+ return $html;
+}
+
+function form_radios($name, array $options, array $values = array())
+{
+ $html = '';
+
+ foreach ($options as $value => $label) {
+ $html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value);
+ }
+
+ return $html;
+}
+
+function form_radio($name, $label, $value, $selected = false, $class = '')
+{
+ return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($selected ? 'selected="selected"' : '').'>'.escape($label).'</label>';
+}
+
+function form_checkbox($name, $label, $value, $checked = false, $class = '')
+{
+ return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($checked ? 'checked="checked"' : '').'>&nbsp;'.escape($label).'</label>';
+}
+
+function form_label($label, $name, $class = '')
+{
+ return '<label for="form-'.$name.'" class="'.$class.'">'.escape($label).'</label>';
+}
+
+function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+{
+ $class .= error_class($errors, $name);
+
+ $html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" ';
+ $html .= implode(' ', $attributes).'>';
+ $html .= isset($values->$name) ? escape($values->$name) : isset($values[$name]) ? $values[$name] : '';
+ $html .= '</textarea>';
+ $html .= error_list($errors, $name);
+
+ return $html;
+}
+
+function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+{
+ $class .= error_class($errors, $name);
+
+ $html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
+ $html .= implode(' ', $attributes).'/>';
+ $html .= error_list($errors, $name);
+
+ return $html;
+}
+
+function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+{
+ return form_input('text', $name, $values, $errors, $attributes, $class);
+}
+
+function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+{
+ return form_input('password', $name, $values, $errors, $attributes, $class);
+}
+
+function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+{
+ return form_input('email', $name, $values, $errors, $attributes, $class);
+}
+
+function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+{
+ return form_input('date', $name, $values, $errors, $attributes, $class);
+}
diff --git a/lib/request.php b/lib/request.php
new file mode 100644
index 00000000..8840e7a4
--- /dev/null
+++ b/lib/request.php
@@ -0,0 +1,44 @@
+<?php
+
+class Request
+{
+ public function getStringParam($name, $default_value = '')
+ {
+ return isset($_GET[$name]) ? $_GET[$name] : $default_value;
+ }
+
+ public function getIntegerParam($name, $default_value = 0)
+ {
+ return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value;
+ }
+
+ public function getValue($name)
+ {
+ $values = $this->getValues();
+ return isset($values[$name]) ? $values[$name] : null;
+ }
+
+ public function getValues()
+ {
+ if (! empty($_POST)) return $_POST;
+
+ $result = json_decode($this->getBody(), true);
+ if ($result) return $result;
+
+ return array();
+ }
+
+ public function getBody()
+ {
+ return file_get_contents('php://input');
+ }
+
+ public function getFileContent($name)
+ {
+ if (isset($_FILES[$name])) {
+ return file_get_contents($_FILES[$name]['tmp_name']);
+ }
+
+ return '';
+ }
+}
diff --git a/lib/response.php b/lib/response.php
new file mode 100644
index 00000000..e1b808bf
--- /dev/null
+++ b/lib/response.php
@@ -0,0 +1,135 @@
+<?php
+
+class Response
+{
+ public function forceDownload($filename)
+ {
+ header('Content-Disposition: attachment; filename="'.$filename.'"');
+ }
+
+ public function status($status_code)
+ {
+ if (strpos(php_sapi_name(), 'apache') !== false) {
+ header('HTTP/1.0 '.$status_code);
+ }
+ else {
+ header('Status: '.$status_code);
+ }
+ }
+
+ public function redirect($url)
+ {
+ header('Location: '.$url);
+ exit;
+ }
+
+ public function json(array $data, $status_code = 200)
+ {
+ $this->status($status_code);
+
+ header('Content-Type: application/json');
+ echo json_encode($data);
+
+ exit;
+ }
+
+ public function text($data, $status_code = 200)
+ {
+ $this->status($status_code);
+
+ header('Content-Type: text/plain; charset=utf-8');
+ echo $data;
+
+ exit;
+ }
+
+ public function html($data, $status_code = 200)
+ {
+ $this->status($status_code);
+
+ header('Content-Type: text/html; charset=utf-8');
+ echo $data;
+
+ exit;
+ }
+
+ public function xml($data, $status_code = 200)
+ {
+ $this->status($status_code);
+
+ header('Content-Type: text/xml; charset=utf-8');
+ echo $data;
+
+ exit;
+ }
+
+ public function js($data, $status_code = 200)
+ {
+ $this->status($status_code);
+
+ header('Content-Type: text/javascript; charset=utf-8');
+ echo $data;
+
+ exit;
+ }
+
+ public function binary($data, $status_code = 200)
+ {
+ $this->status($status_code);
+
+ header('Content-Transfer-Encoding: binary');
+ header('Content-Type: application/octet-stream');
+ echo $data;
+
+ exit;
+ }
+
+ public function csp(array $policies = array())
+ {
+ $policies['default-src'] = "'self'";
+ $values = '';
+
+ foreach ($policies as $policy => $hosts) {
+
+ if (is_array($hosts)) {
+
+ $acl = '';
+
+ foreach ($hosts as &$host) {
+
+ if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) {
+ $acl .= $host.' ';
+ }
+ }
+ }
+ else {
+
+ $acl = $hosts;
+ }
+
+ $values .= $policy.' '.trim($acl).'; ';
+ }
+
+ header('Content-Security-Policy: '.$values);
+ }
+
+ public function nosniff()
+ {
+ header('X-Content-Type-Options: nosniff');
+ }
+
+ public function xss()
+ {
+ header('X-XSS-Protection: 1; mode=block');
+ }
+
+ public function hsts()
+ {
+ header('Strict-Transport-Security: max-age=31536000');
+ }
+
+ public function xframe($mode = 'DENY', array $urls = array())
+ {
+ header('X-Frame-Options: '.$mode.' '.implode(' ', $urls));
+ }
+}
diff --git a/lib/router.php b/lib/router.php
new file mode 100644
index 00000000..979968d4
--- /dev/null
+++ b/lib/router.php
@@ -0,0 +1,46 @@
+<?php
+
+class Router
+{
+ private $controller = '';
+ private $action = '';
+
+ public function __construct($controller = '', $action = '')
+ {
+ $this->controller = empty($_GET['controller']) ? $controller : $_GET['controller'];
+ $this->action = empty($_GET['action']) ? $controller : $_GET['action'];
+ }
+
+ public function sanitize($value, $default_value)
+ {
+ return ! ctype_alpha($value) || empty($value) ? $default_value : strtolower($value);
+ }
+
+ public function loadController($filename, $class, $method)
+ {
+ if (file_exists($filename)) {
+
+ require $filename;
+
+ if (! method_exists($class, $method)) return false;
+
+ $instance = new $class;
+ $instance->beforeAction($this->controller, $this->action);
+ $instance->$method();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public function execute()
+ {
+ $this->controller = $this->sanitize($this->controller, 'app');
+ $this->action = $this->sanitize($this->action, 'index');
+
+ if (! $this->loadController('controllers/'.$this->controller.'.php', '\Controller\\'.$this->controller, $this->action)) {
+ die('Page not found!');
+ }
+ }
+}
diff --git a/lib/session.php b/lib/session.php
new file mode 100644
index 00000000..c5a8271f
--- /dev/null
+++ b/lib/session.php
@@ -0,0 +1,34 @@
+<?php
+
+class Session
+{
+ const SESSION_LIFETIME = 2678400;
+
+ public function open($base_path = '/')
+ {
+ session_set_cookie_params(
+ self::SESSION_LIFETIME,
+ $base_path,
+ null,
+ isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
+ true
+ );
+
+ session_start();
+ }
+
+ public function close()
+ {
+ session_destroy();
+ }
+
+ public function flash($message)
+ {
+ $_SESSION['flash_message'] = $message;
+ }
+
+ public function flashError($message)
+ {
+ $_SESSION['flash_error_message'] = $message;
+ }
+}
diff --git a/lib/template.php b/lib/template.php
new file mode 100644
index 00000000..09f9aa29
--- /dev/null
+++ b/lib/template.php
@@ -0,0 +1,38 @@
+<?php
+
+class Template
+{
+ const PATH = 'templates/';
+
+ // Template\load('template_name', ['bla' => 'value']);
+ public function load()
+ {
+ if (func_num_args() < 1 || func_num_args() > 2) {
+ die('Invalid template arguments');
+ }
+
+ if (! file_exists(self::PATH.func_get_arg(0).'.php')) {
+ die('Unable to load the template: "'.func_get_arg(0).'"');
+ }
+
+ if (func_num_args() === 2) {
+
+ if (! is_array(func_get_arg(1))) {
+ die('Template variables must be an array');
+ }
+
+ extract(func_get_arg(1));
+ }
+
+ ob_start();
+
+ include self::PATH.func_get_arg(0).'.php';
+
+ return ob_get_clean();
+ }
+
+ public function layout($template_name, array $template_args = array(), $layout_name = 'layout')
+ {
+ return $this->load($layout_name, $template_args + array('content_for_layout' => $this->load($template_name, $template_args)));
+ }
+}
diff --git a/lib/translator.php b/lib/translator.php
new file mode 100644
index 00000000..c485a94c
--- /dev/null
+++ b/lib/translator.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Translator {
+
+ const PATH = 'locales/';
+
+ function translate($identifier)
+ {
+ $args = \func_get_args();
+
+ \array_shift($args);
+ \array_unshift($args, get($identifier, $identifier));
+
+ return \call_user_func_array(
+ 'sprintf',
+ $args
+ );
+ }
+
+ function number($number)
+ {
+ return number_format(
+ $number,
+ get('number.decimals', 2),
+ get('number.decimals_separator', '.'),
+ get('number.thousands_separator', ',')
+ );
+ }
+
+ function currency($amount)
+ {
+ $position = get('currency.position', 'before');
+ $symbol = get('currency.symbol', '$');
+ $str = '';
+
+ if ($position === 'before') {
+
+ $str .= $symbol;
+ }
+
+ $str .= number($amount);
+
+ if ($position === 'after') {
+
+ $str .= ' '.$symbol;
+ }
+
+ return $str;
+ }
+
+ function datetime($format, $timestamp)
+ {
+ return strftime(get($format), (int) $timestamp);
+ }
+
+ function get($identifier, $default = '')
+ {
+ $locales = container();
+
+ if (isset($locales[$identifier])) {
+
+ return $locales[$identifier];
+ }
+ else {
+
+ return $default;
+ }
+ }
+
+ function load($language)
+ {
+ setlocale(LC_TIME, $language.'.UTF-8');
+
+ $path = PATH.$language;
+ $locales = array();
+
+ if (is_dir($path)) {
+
+ $dir = new \DirectoryIterator($path);
+
+ foreach ($dir as $fileinfo) {
+
+ if (strpos($fileinfo->getFilename(), '.php') !== false) {
+
+ $locales = array_merge($locales, include $fileinfo->getPathname());
+ }
+ }
+ }
+
+ container($locales);
+ }
+
+ function container($locales = null)
+ {
+ static $values = array();
+
+ if ($locales !== null) {
+
+ $values = $locales;
+ }
+
+ return $values;
+ }
+}
+
+
+namespace {
+
+ function t() {
+ return call_user_func_array('\Translator\translate', func_get_args());
+ }
+
+ function c() {
+ return call_user_func_array('\Translator\currency', func_get_args());
+ }
+
+ function n() {
+ return call_user_func_array('\Translator\number', func_get_args());
+ }
+
+ function dt() {
+ return call_user_func_array('\Translator\datetime', func_get_args());
+ }
+}
diff --git a/locales/fr_FR/translations.php b/locales/fr_FR/translations.php
new file mode 100644
index 00000000..809120b3
--- /dev/null
+++ b/locales/fr_FR/translations.php
@@ -0,0 +1,170 @@
+<?php
+
+return array(
+ 'English' => 'Anglais',
+ 'French' => 'Français',
+ 'None' => 'Aucun',
+ 'edit' => 'modifier',
+ 'Edit' => 'Modifier',
+ 'remove' => 'supprimer',
+ 'Remove' => 'Supprimer',
+ 'Update' => 'Mettre à jour',
+ 'Yes' => 'Oui',
+ 'No' => 'Non',
+ 'cancel' => 'annuler',
+ 'or' => 'ou',
+ 'Yellow' => 'Jaune',
+ 'Blue' => 'Bleu',
+ 'Green' => 'Vert',
+ 'Purple' => 'Violet',
+ 'Red' => 'Rouge',
+ 'Orange' => 'Orange',
+ 'Grey' => 'Gris',
+ 'Save' => 'Enregistrer',
+ 'Login' => 'Connexion',
+ 'Official website:' => 'Site web officiel :',
+ 'Unassigned' => 'Non assigné',
+ 'View this task' => 'Visualiser cette tâche',
+ 'Remove user' => 'Supprimer un utilisateur',
+ 'Do you really want to remove this user: "%s"?' => 'Voulez-vous vraiment supprimer cet utilisateur : "%s" ?',
+ 'New user' => 'Ajouter un utilisateur',
+ 'All users' => 'Tous les utilisateurs',
+ 'Username' => 'Nom d\'utilisateur',
+ 'Password' => 'Mot de passe',
+ 'Default Project' => 'Projet par défaut',
+ 'Administrator' => 'Administrateur',
+ 'Sign in' => 'Connexion',
+ 'Users' => 'Utilisateurs',
+ 'No user' => 'Aucun utilisateur',
+ 'Forbidden' => 'Accès interdit',
+ 'Access Forbidden' => 'Accès interdit',
+ 'Only administrators can access to this page.' => 'Uniquement les administrateurs peuvent accéder à cette page.',
+ 'Edit user' => 'Modifier un utilisateur',
+ 'logout' => 'déconnexion',
+ 'Bad username or password' => 'Identifiant ou mot de passe incorrect',
+ 'users' => 'utilisateurs',
+ 'projects' => 'projets',
+ 'Edit project' => 'Modifier le projet',
+ 'Name' => 'Nom',
+ 'Activated' => 'Actif',
+ 'Projects' => 'Projets',
+ 'No project' => 'Aucun projet',
+ 'Project' => 'Projet',
+ 'Status' => 'État',
+ 'Tasks' => 'Tâches',
+ 'Board' => 'Tableau',
+ 'Inactive' => 'Inactif',
+ 'Active' => 'Actif',
+ 'Column %d' => 'Colonne %d',
+ 'Add this column' => 'Ajouter cette colonne',
+ '%d tasks on the board' => '%d tâches sur le tableau',
+ '%d tasks in total' => '%d tâches au total',
+ 'Unable to update this board.' => 'Impossible de mettre à jour ce tableau.',
+ 'Edit board' => 'Modifier le tableau',
+ 'Disable' => 'Désactiver',
+ 'Enable' => 'Activer',
+ 'New project' => 'Nouveau projet',
+ 'Do you really want to remove this project: "%s"?' => 'Voulez-vous vraiment supprimer ce projet : "%s" ?',
+ 'Remove project' => 'Supprimer le projet',
+ 'boards' => 'tableaux',
+ 'Edit the board for "%s"' => 'Modifier le tableau pour "%s"',
+ 'All projects' => 'Tous les projets',
+ 'Change columns' => 'Changer les colonnes',
+ 'Add a new column' => 'Ajouter une nouvelle colonne',
+ 'Title' => 'Titre',
+ 'Add Column' => 'Nouvelle colonne',
+ 'Project "%s"' => 'Projet "%s"',
+ 'No body assigned' => 'Personne assigné',
+ 'Assigned to %s' => 'Assigné à %s',
+ 'Remove a column' => 'Supprimer une colonne',
+ 'Remove a column from a board' => 'Supprimer une colonne d\'un tableau',
+ 'Unable to remove this column.' => 'Impossible de supprimer cette colonne.',
+ 'Do you really want to remove this column: "%s"?' => 'Voulez vraiment supprimer cette colonne : "%s" ?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Cette action va supprimer toutes les tâches associées à cette colonne !',
+ 'settings' => 'préférences',
+ 'Application Settings' => 'Paramètres de l\'application',
+ 'Language' => 'Langue',
+ 'Webhooks token' => 'Jeton de securité pour les webhooks',
+ 'More information' => 'Plus d\'informations',
+ 'Database size:' => 'Taille de la base de données :',
+ 'Download the database' => 'Télécharger la base de données',
+ 'Optimize the database' => 'Optimiser la base de données',
+ '(VACUUM command)' => '(Commande VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(Fichier Sqlite compressé en Gzip)',
+ 'User Settings' => 'Paramètres utilisateur',
+ 'My default project:' => 'Mon projet par défaut : ',
+ 'Close a task' => 'Fermer une tâche',
+ 'Do you really want to close this task: "%s"?' => 'Voulez-vous vraiment fermer cettre tâche : "%s" ?',
+ 'Edit a task' => 'Modifier une tâche',
+ 'Column' => 'Colonne',
+ 'Color' => 'Couleur',
+ 'Assignee' => 'Affectation',
+ 'Create another task' => 'Créer une autre tâche',
+ 'New task' => 'Nouvelle tâche',
+ 'Open a task' => 'Ouvrir une tâche',
+ 'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : "%s" ?',
+ 'Back to the board' => 'Retour au tableau',
+ 'Created on %B %e, %G at %k:%M %p' => 'Créé le %e %B %G à %k:%M',
+ 'There is no body assigned' => 'Il n\'y a personne d\'assigné à cette tâche',
+ 'Column on the board:' => 'Colonne sur le tableau : ',
+ 'Status is open' => 'État ouvert',
+ 'Status is closed' => 'État fermé',
+ 'close this task' => 'fermer cette tâche',
+ 'open this task' => 'ouvrir cette tâche',
+ 'There is no description.' => 'Il n\'y a pas de description.',
+ 'Add a new task' => 'Ajouter une nouvelle tâche',
+ 'The username is required' => 'Le nom d\'utilisateur est obligatoire',
+ 'The maximum length is %d characters' => 'La longueur maximale est de %d caractères',
+ 'The minimum length is %d characters' => 'La longueur minimale est de %d caractères',
+ 'The password is required' => 'Le mot de passe est obligatoire',
+ 'This value must be an integer' => 'Cette valeur doit être un entier',
+ 'The username must be unique' => 'Le nom d\'utilisateur doit être unique',
+ 'The username must be alphanumeric' => 'Le nom d\'utilisateur doit être alpha-numérique',
+ 'The user id is required' => 'L\'id de l\'utilisateur est obligatoire',
+ 'Passwords doesn\'t matches' => 'Les mots de passe ne correspondent pas',
+ 'The confirmation is required' => 'Le confirmation est requise',
+ 'The password is required' => 'Le mot de passe est obligatoire',
+ 'The title is required' => 'Le titre est obligatoire',
+ 'The column is required' => 'La colonne est obligatoire',
+ 'The project is required' => 'Le projet est obligatoire',
+ 'The color is required' => 'La couleur est obligatoire',
+ 'The id is required' => 'L\'identifiant est obligatoire',
+ 'The project id is required' => 'L\'identifiant du projet est obligatoire',
+ 'The project name is required' => 'Le nom du projet est obligatoire',
+ 'This project must be unique' => 'Le nom du projet doit être unique',
+ 'The title is required' => 'Le titre est obligatoire',
+ 'The language is required' => 'La langue est obligatoire',
+ 'There is no active project, the first step is to create a new project.' => 'Il n\'y a aucun projet actif, la première étape est de créer un nouveau projet.',
+ 'Settings saved successfully.' => 'Paramètres sauvegardés avec succès.',
+ 'Unable to save your settings.' => 'Impossible de sauvegarder vos réglages.',
+ 'Database optimization done.' => 'Optmisation de la base de données terminée.',
+ 'Your project have been created successfully.' => 'Votre projet a été créé avec succès.',
+ 'Unable to create your project.' => 'Impossible de créer un projet.',
+ 'Project updated successfully.' => 'Votre projet a été mis à jour avec succès.',
+ 'Unable to update this project.' => 'Impossible de mettre à jour ce projet.',
+ 'Unable to remove this project.' => 'Impossible de supprimer ce projet.',
+ 'Project removed successfully.' => 'Votre projet a été supprimé avec succès.',
+ 'Project activated successfully.' => 'Votre projet a été activé avec succès.',
+ 'Unable to activate this project.' => 'Impossible d\'activer ce projet.',
+ 'Project disabled successfully.' => 'Votre projet a été désactivé avec succès.',
+ 'Unable to disable this project.' => 'Impossible de désactiver ce projet.',
+ 'Unable to open this task.' => 'Impossible d\'ouvrir cette tâche.',
+ 'Task opened successfully.' => 'Tâche ouverte avec succès.',
+ 'Unable to close this task.' => 'Impossible de fermer cette tâche.',
+ 'Task closed successfully.' => 'Tâche fermé avec succès.',
+ 'Unable to update your task.' => 'Impossible de fermer cette tâche.',
+ 'Task updated successfully.' => 'Tâche mise à jour avec succès.',
+ 'Unable to create your task.' => 'Impossible de créer cette tâche.',
+ 'Task created successfully.' => 'Tâche créée avec succès.',
+ 'User created successfully.' => 'Utilisateur créé avec succès.',
+ 'Unable to create your user.' => 'Impossible de créer cet utilisateur.',
+ 'User updated successfully.' => 'Utilisateur mis à jour avec succès.',
+ 'Unable to update your user.' => 'Impossible de mettre à jour cet utilisateur.',
+ 'User removed successfully.' => 'Utilisateur supprimé avec succès.',
+ 'Unable to remove this user.' => 'Impossible de supprimer cet utilisateur.',
+ 'Board updated successfully.' => 'Tableau mis à jour avec succès.',
+ 'Ready' => 'Prêt',
+ 'Backlog' => 'En attente',
+ 'Work in progress' => 'En cours',
+ 'Done' => 'Terminé',
+);
diff --git a/models/.htaccess b/models/.htaccess
new file mode 100644
index 00000000..14249c50
--- /dev/null
+++ b/models/.htaccess
@@ -0,0 +1 @@
+Deny from all \ No newline at end of file
diff --git a/models/base.php b/models/base.php
new file mode 100644
index 00000000..85a8f252
--- /dev/null
+++ b/models/base.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Model;
+
+require 'vendor/SimpleValidator/Validator.php';
+require 'vendor/SimpleValidator/Base.php';
+require 'vendor/SimpleValidator/Validators/Required.php';
+require 'vendor/SimpleValidator/Validators/Unique.php';
+require 'vendor/SimpleValidator/Validators/MaxLength.php';
+require 'vendor/SimpleValidator/Validators/MinLength.php';
+require 'vendor/SimpleValidator/Validators/Integer.php';
+require 'vendor/SimpleValidator/Validators/Equals.php';
+require 'vendor/SimpleValidator/Validators/AlphaNumeric.php';
+require 'vendor/PicoDb/Database.php';
+require __DIR__.'/schema.php';
+
+abstract class Base
+{
+ const DB_VERSION = 1;
+ const DB_FILENAME = 'data/db.sqlite';
+
+ private static $dbInstance = null;
+ protected $db;
+
+ public function __construct()
+ {
+ if (self::$dbInstance === null) {
+ self::$dbInstance = $this->getDatabaseInstance();
+ }
+
+ $this->db = self::$dbInstance;
+ }
+
+ public function getDatabaseInstance()
+ {
+ $db = new \PicoDb\Database(array(
+ 'driver' => 'sqlite',
+ 'filename' => self::DB_FILENAME
+ ));
+
+ if ($db->schema()->check(self::DB_VERSION)) {
+ return $db;
+ }
+ else {
+ die('Unable to migrate database schema!');
+ }
+ }
+}
diff --git a/models/board.php b/models/board.php
new file mode 100644
index 00000000..4a46d6c5
--- /dev/null
+++ b/models/board.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace Model;
+
+use \SimpleValidator\Validator;
+use \SimpleValidator\Validators;
+
+class Board extends Base
+{
+ const TABLE = 'columns';
+
+ // Save the board (each task position/column)
+ public function saveTasksPosition(array $values)
+ {
+ $this->db->startTransaction();
+
+ $taskModel = new \Model\Task;
+ $results = array();
+
+ foreach ($values as $value) {
+ $results[] = $taskModel->move(
+ $value['task_id'],
+ $value['column_id'],
+ $value['position']
+ );
+ }
+
+ $this->db->closeTransaction();
+
+ return ! in_array(false, $results, true);
+ }
+
+ // Create board with default columns => must executed inside a transaction
+ public function create($project_id, array $columns)
+ {
+ $position = 0;
+
+ foreach ($columns as $title) {
+
+ $values = array(
+ 'title' => $title,
+ 'position' => ++$i,
+ 'project_id' => $project_id,
+ );
+
+ $this->db->table(self::TABLE)->save($values);
+ }
+
+ return true;
+ }
+
+ // Add a new column to the board
+ public function add(array $values)
+ {
+ $values['position'] = $this->getLastColumnPosition($values['project_id']) + 1;
+ return $this->db->table(self::TABLE)->save($values);
+ }
+
+ // Update columns
+ public function update(array $values)
+ {
+ $this->db->startTransaction();
+
+ foreach ($values as $column_id => $column_title) {
+ $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('title' => $column_title));
+ }
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ // Get columns and tasks for each column
+ public function get($project_id)
+ {
+ $taskModel = new \Model\Task;
+
+ $this->db->startTransaction();
+
+ $columns = $this->getColumns($project_id);
+
+ foreach ($columns as &$column) {
+ $column['tasks'] = $taskModel->getAllByColumnId($project_id, $column['id'], array(1));
+ }
+
+ $this->db->closeTransaction();
+
+ return $columns;
+ }
+
+ // Get list of columns
+ public function getColumnsList($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'title');
+ }
+
+ // Get all columns information for a project
+ public function getColumns($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll();
+ }
+
+ // Get the number of columns for a project
+ public function countColumns($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->count();
+ }
+
+ // Get just one column
+ public function getColumn($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne();
+ }
+
+ // Get the position of the last column for a project
+ public function getLastColumnPosition($project_id)
+ {
+ return (int) $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->desc('position')
+ ->findOneColumn('position');
+ }
+
+ // Remove a column and all tasks associated to this column
+ public function removeColumn($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->remove();
+ }
+
+ // Validate columns update
+ public function validateModification(array $columns, array $values)
+ {
+ $rules = array();
+
+ foreach ($columns as $column_id => $column_title) {
+ $rules[] = new Validators\Required('title['.$column_id.']', t('The title is required'));
+ $rules[] = new Validators\MaxLength('title['.$column_id.']', t('The maximum length is %d characters', 50), 50);
+ }
+
+ $v = new Validator($values, $rules);
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ // Validate column creation
+ public function validateCreation(array $values)
+ {
+ $rules = array();
+
+ $v = new Validator($values, array(
+ new Validators\Required('project_id', t('The project id is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('title', t('The title is required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+} \ No newline at end of file
diff --git a/models/config.php b/models/config.php
new file mode 100644
index 00000000..d854047d
--- /dev/null
+++ b/models/config.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Model;
+
+use \SimpleValidator\Validator;
+use \SimpleValidator\Validators;
+
+class Config extends Base
+{
+ const TABLE = 'config';
+
+ public function getLanguages()
+ {
+ return array(
+ 'en_US' => t('English'),
+ 'fr_FR' => t('French'),
+ );
+ }
+
+ public function get($name, $default_value = '')
+ {
+ if (! isset($_SESSION['config'][$name])) {
+ $_SESSION['config'] = $this->getAll();
+ }
+
+ if (isset($_SESSION['config'][$name])) {
+ return $_SESSION['config'][$name];
+ }
+
+ return $default_value;
+ }
+
+ public function getAll()
+ {
+ return $this->db->table(self::TABLE)->findOne();
+ }
+
+ public function save(array $values)
+ {
+ $_SESSION['config'] = $values;
+ return $this->db->table(self::TABLE)->update($values);
+ }
+
+ public function reload()
+ {
+ $_SESSION['config'] = $this->getAll();
+
+ $language = $this->get('language', 'en_US');
+ if ($language !== 'en_US') \Translator\load($language);
+ }
+
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('language', t('The language is required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ public static function generateToken()
+ {
+ if (ini_get('open_basedir') === '') {
+ return substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
+ }
+ else {
+ return substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
+ }
+ }
+
+ public function optimizeDatabase()
+ {
+ $this->db->getconnection()->exec("VACUUM");
+ }
+
+ public function downloadDatabase()
+ {
+ return gzencode(file_get_contents(self::DB_FILENAME));
+ }
+
+ public function getDatabaseSize()
+ {
+ return filesize(self::DB_FILENAME);
+ }
+}
diff --git a/models/project.php b/models/project.php
new file mode 100644
index 00000000..7a0fb2b1
--- /dev/null
+++ b/models/project.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Model;
+
+use \SimpleValidator\Validator;
+use \SimpleValidator\Validators;
+
+class Project extends Base
+{
+ const TABLE = 'projects';
+ const ACTIVE = 1;
+ const INACTIVE = 0;
+
+ public function get($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $project_id)->findOne();
+ }
+
+ public function getAll($fetch_stats = false)
+ {
+ if (! $fetch_stats) {
+ return $this->db->table(self::TABLE)->asc('name')->findAll();
+ }
+
+ $this->db->startTransaction();
+
+ $projects = $this->db
+ ->table(self::TABLE)
+ ->asc('name')
+ ->findAll();
+
+ $taskModel = new \Model\Task;
+ $boardModel = new \Model\Board;
+
+ foreach ($projects as &$project) {
+
+ $columns = $boardModel->getcolumns($project['id']);
+ $project['nb_active_tasks'] = 0;
+
+ foreach ($columns as &$column) {
+ $column['nb_active_tasks'] = $taskModel->countByColumnId($project['id'], $column['id']);
+ $project['nb_active_tasks'] += $column['nb_active_tasks'];
+ }
+
+ $project['columns'] = $columns;
+ $project['nb_tasks'] = $taskModel->countByProjectId($project['id']);
+ }
+
+ $this->db->closeTransaction();
+
+ return $projects;
+ }
+
+ public function getList()
+ {
+ return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name');
+ }
+
+ public function getAllByStatus($status)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->asc('name')
+ ->eq('is_active', $status)
+ ->findAll();
+ }
+
+ public function getListByStatus($status)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->asc('name')
+ ->eq('is_active', $status)
+ ->listing('id', 'name');
+ }
+
+ public function countByStatus($status)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('is_active', $status)
+ ->count();
+ }
+
+ public function create(array $values)
+ {
+ $this->db->startTransaction();
+
+ $this->db->table(self::TABLE)->save($values);
+
+ $project_id = $this->db->getConnection()->getLastId();
+
+ $boardModel = new \Model\Board;
+
+ $boardModel->create($project_id, array(
+ t('Backlog'),
+ t('Ready'),
+ t('Work in progress'),
+ t('Done'),
+ ));
+
+ $this->db->closeTransaction();
+
+ return $project_id;
+ }
+
+ public function update(array $values)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
+ }
+
+ public function remove($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $project_id)->remove();
+ }
+
+ public function enable($project_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $project_id)
+ ->save(array('is_active' => 1));
+ }
+
+ public function disable($project_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $project_id)
+ ->save(array('is_active' => 0));
+ }
+
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('name', t('The project name is required')),
+ new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
+ new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE)
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('The project id is required')),
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Required('name', t('The project name is required')),
+ new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
+ new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE)
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/models/schema.php b/models/schema.php
new file mode 100644
index 00000000..3217663a
--- /dev/null
+++ b/models/schema.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Schema;
+
+function version_1($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE config (
+ language TEXT,
+ webhooks_token TEXT
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE users (
+ id INTEGER PRIMARY KEY,
+ username TEXT,
+ password TEXT,
+ is_admin INTEGER DEFAULT 0,
+ default_project_id DEFAULT 0
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE projects (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOCASE UNIQUE,
+ is_active INTEGER DEFAULT 1
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE columns (
+ id INTEGER PRIMARY KEY,
+ title TEXT,
+ position INTEGER,
+ project_id INTEGER,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE (title, project_id)
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE tasks (
+ id INTEGER PRIMARY KEY,
+ title TEXT,
+ description TEXT,
+ date_creation INTEGER,
+ color_id TEXT,
+ project_id INTEGER,
+ column_id INTEGER,
+ owner_id INTEGER DEFAULT '0',
+ position INTEGER,
+ is_active INTEGER DEFAULT 1,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE
+ )
+ ");
+
+ $pdo->exec("
+ INSERT INTO users
+ (username, password, is_admin)
+ VALUES ('admin', '".\password_hash('admin', PASSWORD_BCRYPT)."', '1')
+ ");
+
+ $pdo->exec("
+ INSERT INTO config
+ (language, webhooks_token)
+ VALUES ('en_US', '".\Model\Config::generateToken()."')
+ ");
+}
diff --git a/models/task.php b/models/task.php
new file mode 100644
index 00000000..db27c650
--- /dev/null
+++ b/models/task.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace Model;
+
+use \SimpleValidator\Validator;
+use \SimpleValidator\Validators;
+
+class Task extends Base
+{
+ const TABLE = 'tasks';
+
+ public function getColors()
+ {
+ return array(
+ 'yellow' => t('Yellow'),
+ 'blue' => t('Blue'),
+ 'green' => t('Green'),
+ 'purple' => t('Purple'),
+ 'red' => t('Red'),
+ 'orange' => t('Orange'),
+ 'grey' => t('Grey'),
+ );
+ }
+
+ public function getById($task_id, $more = false)
+ {
+ if ($more) {
+
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.title',
+ self::TABLE.'.description',
+ self::TABLE.'.date_creation',
+ self::TABLE.'.color_id',
+ self::TABLE.'.project_id',
+ self::TABLE.'.column_id',
+ self::TABLE.'.owner_id',
+ self::TABLE.'.position',
+ self::TABLE.'.is_active',
+ \Model\Project::TABLE.'.name AS project_name',
+ \Model\Board::TABLE.'.title AS column_title',
+ \Model\User::TABLE.'.username'
+ )
+ ->join(\Model\Project::TABLE, 'id', 'project_id')
+ ->join(\Model\Board::TABLE, 'id', 'column_id')
+ ->join(\Model\User::TABLE, 'id', 'owner_id')
+ ->eq(self::TABLE.'.id', $task_id)
+ ->findOne();
+ }
+ else {
+
+ return $this->db->table(self::TABLE)->eq('id', $task_id)->findOne();
+ }
+ }
+
+ public function getAllByProjectId($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll();
+ }
+
+ public function countByProjectId($project_id, $status = array(1, 0))
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ public function getAllByColumnId($project_id, $column_id, $status = array(1))
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns('tasks.id', 'title', 'color_id', 'project_id', 'owner_id', 'column_id', 'position', 'users.username')
+ ->join('users', 'id', 'owner_id')
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->in('is_active', $status)
+ ->asc('position')
+ ->findAll();
+ }
+
+ public function countByColumnId($project_id, $column_id, $status = array(1))
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ public function create(array $values)
+ {
+ $this->db->startTransaction();
+
+ unset($values['another_task']);
+
+ $values['date_creation'] = time();
+ $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']);
+
+ $this->db->table(self::TABLE)->save($values);
+
+ $task_id = $this->db->getConnection()->getLastId();
+
+ $this->db->closeTransaction();
+
+ return $task_id;
+ }
+
+ public function update(array $values)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+
+ public function close($task_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $task_id)->update(array('is_active' => 0));
+ }
+
+ public function open($task_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $task_id)->update(array('is_active' => 1));
+ }
+
+ public function remove($task_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
+ }
+
+ public function move($task_id, $column_id, $position)
+ {
+ return (bool) $this->db
+ ->table(self::TABLE)
+ ->eq('id', $task_id)
+ ->update(array('column_id' => $column_id, 'position' => $position));
+ }
+
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('color_id', t('The color is required')),
+ new Validators\Required('project_id', t('The project is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('column_id', t('The column is required')),
+ new Validators\Integer('column_id', t('This value must be an integer')),
+ new Validators\Integer('owner_id', t('This value must be an integer')),
+ new Validators\Required('title', t('The title is required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ public function validateModification(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('The id is required')),
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Required('color_id', t('The color is required')),
+ new Validators\Required('project_id', t('The project is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('column_id', t('The column is required')),
+ new Validators\Integer('column_id', t('This value must be an integer')),
+ new Validators\Integer('owner_id', t('This value must be an integer')),
+ new Validators\Required('title', t('The title is required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/models/user.php b/models/user.php
new file mode 100644
index 00000000..50e02fef
--- /dev/null
+++ b/models/user.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Model;
+
+use \SimpleValidator\Validator;
+use \SimpleValidator\Validators;
+
+class User extends Base
+{
+ const TABLE = 'users';
+
+ public function getById($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->findOne();
+ }
+
+ public function getByUsername($username)
+ {
+ return $this->db->table(self::TABLE)->eq('username', $username)->findOne();
+ }
+
+ public function getAll()
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->asc('username')
+ ->columns('id', 'username', 'is_admin', 'default_project_id')
+ ->findAll();
+ }
+
+ public function getList()
+ {
+ return array(t('Unassigned')) + $this->db->table(self::TABLE)->asc('username')->listing('id', 'username');
+ }
+
+ public function create(array $values)
+ {
+ unset($values['confirmation']);
+ $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
+
+ return $this->db->table(self::TABLE)->save($values);
+ }
+
+ public function update(array $values)
+ {
+ if (! empty($values['password'])) {
+ $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
+ }
+ else {
+ unset($values['password']);
+ }
+
+ unset($values['confirmation']);
+
+ $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
+
+ if ($_SESSION['user']['id'] == $values['id']) {
+ $this->updateSession();
+ }
+
+ return true;
+ }
+
+ public function remove($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->remove();
+ }
+
+ public function updateSession(array $user = array())
+ {
+ if (empty($user)) {
+ $user = $this->getById($_SESSION['user']['id']);
+ }
+
+ if (isset($user['password'])) unset($user['password']);
+
+ $_SESSION['user'] = $user;
+ }
+
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('username', t('The username is required')),
+ new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
+ new Validators\AlphaNumeric('username', t('The username must be alphanumeric')),
+ new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
+ new Validators\Required('password', t('The password is required')),
+ new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
+ new Validators\Required('confirmation', t('The confirmation is required')),
+ new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t matches')),
+ new Validators\Integer('default_project_id', t('The value must be an integer')),
+ new Validators\Integer('is_admin', t('This value must be an integer')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ public function validateModification(array $values)
+ {
+ if (! empty($values['password'])) {
+ return $this->validateCreation($values);
+ }
+ else {
+
+ $v = new Validator($values, array(
+ new Validators\Required('id', t('The user id is required')),
+ new Validators\Required('username', t('The username is required')),
+ new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
+ new Validators\AlphaNumeric('username', t('The username must be alphanumeric')),
+ new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
+ new Validators\Integer('default_project_id', t('This value must be an integer')),
+ new Validators\Integer('is_admin', t('This value must be an integer')),
+ ));
+ }
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ public function validateLogin(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('username', t('The username is required')),
+ new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
+ new Validators\Required('password', t('The password is required')),
+ ));
+
+ $result = $v->execute();
+ $errors = $v->getErrors();
+
+ if ($result) {
+
+ $user = $this->getByUsername($values['username']);
+
+ if ($user !== false && \password_verify($values['password'], $user['password'])) {
+ $this->updateSession($user);
+ }
+ else {
+ $result = false;
+ $errors['login'] = t('Bad username or password');
+ }
+ }
+
+ return array(
+ $result,
+ $errors
+ );
+ }
+}
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 00000000..77470cb3
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: / \ No newline at end of file
diff --git a/templates/.htaccess b/templates/.htaccess
new file mode 100644
index 00000000..14249c50
--- /dev/null
+++ b/templates/.htaccess
@@ -0,0 +1 @@
+Deny from all \ No newline at end of file
diff --git a/templates/board_edit.php b/templates/board_edit.php
new file mode 100644
index 00000000..605773ae
--- /dev/null
+++ b/templates/board_edit.php
@@ -0,0 +1,40 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2>
+ <ul>
+ <li><a href="?controller=project"><?= t('All projects') ?></a></li>
+ </ul>
+ </div>
+ <section>
+
+ <h3><?= t('Change columns') ?></h3>
+ <form method="post" action="?controller=board&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
+
+ <?php $i = 0; ?>
+
+ <?php foreach ($columns as $column_id => $column_title): ?>
+ <?= Helper\form_label(t('Column %d', ++$i), 'title['.$column_id.']') ?>
+ <?= Helper\form_text('title['.$column_id.']', $values, $errors, array('required')) ?>
+ <a href="?controller=board&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column_id ?>"><?= t('Remove') ?></a>
+ <?php endforeach ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
+ <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
+ </div>
+ </form>
+
+ <h3><?= t('Add a new column') ?></h3>
+ <form method="post" action="?controller=board&amp;action=add&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
+
+ <?= Helper\form_hidden('project_id', $values) ?>
+ <?= Helper\form_label(t('Title'), 'title') ?>
+ <?= Helper\form_text('title', $values, $errors, array('required')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/>
+ <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/board_index.php b/templates/board_index.php
new file mode 100644
index 00000000..777d601b
--- /dev/null
+++ b/templates/board_index.php
@@ -0,0 +1,57 @@
+<section id="main">
+
+ <div class="page-header">
+ <h2><?= t('Project "%s"', $current_project_name) ?></h2>
+ <ul>
+ <?php foreach ($projects as $project_id => $project_name): ?>
+ <?php if ($project_id != $current_project_id): ?>
+ <li>
+ <a href="?controller=board&amp;action=show&amp;project_id=<?= $project_id ?>"><?= Helper\escape($project_name) ?></a>
+ </li>
+ <?php endif ?>
+ <?php endforeach ?>
+ </ul>
+ </div>
+
+ <table id="board" data-project-id="<?= $current_project_id ?>">
+ <tr>
+ <?php $column_with = round(100 / count($columns), 2); ?>
+ <?php foreach ($columns as $column): ?>
+ <th width="<?= $column_with ?>%">
+ <a href="?controller=task&amp;action=create&amp;project_id=<?= $column['project_id'] ?>&amp;column_id=<?= $column['id'] ?>" title="<?= t('Add a new task') ?>">+</a>
+ <?= Helper\escape($column['title']) ?>
+ </th>
+ <?php endforeach ?>
+ </tr>
+ <tr>
+ <?php foreach ($columns as $column): ?>
+ <td id="column-<?= $column['id'] ?>" class="column" data-column-id="<?= $column['id'] ?>" dropzone="copy">
+ <?php foreach ($column['tasks'] as $task): ?>
+ <div class="draggable-item" draggable="true">
+ <div class="task task-<?= $task['color_id'] ?>" data-task-id="<?= $task['id'] ?>">
+
+ <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>">#<?= $task['id'] ?></a> -
+
+ <span class="task-user">
+ <?php if (! empty($task['owner_id'])): ?>
+ <?= t('Assigned to %s', $task['username']) ?>
+ <?php else: ?>
+ <span class="task-nobody"><?= t('No body assigned') ?></span>
+ <?php endif ?>
+ </span>
+
+ <div class="task-title">
+ <?= Helper\escape($task['title']) ?>
+ </div>
+
+ </div>
+ </div>
+ <?php endforeach ?>
+ </td>
+ <?php endforeach ?>
+ </tr>
+ </table>
+
+</section>
+
+<script type="text/javascript" src="assets/js/board.js"></script> \ No newline at end of file
diff --git a/templates/board_remove.php b/templates/board_remove.php
new file mode 100644
index 00000000..c95c8a28
--- /dev/null
+++ b/templates/board_remove.php
@@ -0,0 +1,17 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Remove a column') ?></h2>
+ </div>
+
+ <div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this column: "%s"?', Helper\escape($column['title'])) ?>
+ <?= t('This action will REMOVE ALL TASKS associated to this column!') ?>
+ </p>
+
+ <div class="form-actions">
+ <a href="?controller=board&amp;action=remove&amp;column_id=<?= $column['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <?= t('or') ?> <a href="?controller=board&amp;action=edit&amp;project_id=<?= $column['project_id'] ?>"><?= t('cancel') ?></a>
+ </div>
+ </div>
+</section> \ No newline at end of file
diff --git a/templates/config_index.php b/templates/config_index.php
new file mode 100644
index 00000000..43145caf
--- /dev/null
+++ b/templates/config_index.php
@@ -0,0 +1,55 @@
+<section id="main">
+
+ <?php if ($user['is_admin']): ?>
+ <div class="page-header">
+ <h2><?= t('Application Settings') ?></h2>
+ </div>
+ <section>
+ <form method="post" action="?controller=config&amp;action=save" autocomplete="off">
+
+ <?= Helper\form_label(t('Language'), 'language') ?>
+ <?= Helper\form_select('language', $languages, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Webhooks token'), 'webhooks_token') ?>
+ <?= Helper\form_text('webhooks_token', $values, $errors, array('readonly')) ?><br/>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+ </form>
+ </section>
+ <div class="page-header">
+ <h2><?= t('More information') ?></h2>
+ </div>
+ <section class="settings">
+ <ul>
+ <li><?= t('Database size:') ?> <strong><?= Helper\format_bytes($db_size) ?></strong></li>
+ <li>
+ <a href="?controller=config&amp;action=downloadDb"><?= t('Download the database') ?></a>
+ <?= t('(Gzip compressed Sqlite file)') ?>
+ </li>
+ <li>
+ <a href="?controller=config&amp;action=optimizeDb"><?= t('Optimize the database') ?></a>
+ <?= t('(VACUUM command)') ?>
+ </li>
+ <li>
+ <?= t('Official website:') ?>
+ <a href="http://kanboard.net/" target="_blank">http://kanboard.net/</a>
+ </li>
+ </ul>
+ </section>
+ <?php endif ?>
+
+ <div class="page-header">
+ <h2><?= t('User Settings') ?></h2>
+ </div>
+ <section class="settings">
+ <ul>
+ <li>
+ <strong><?= t('My default project:') ?> </strong>
+ <?= isset($user['default_project_id']) ? $projects[$user['default_project_id']] : t('None') ?>,
+ <a href="?controller=user&amp;action=edit&amp;user_id=<?= $user['id'] ?>"><?= t('edit') ?></a>
+ </li>
+ </ul>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/layout.php b/templates/layout.php
new file mode 100644
index 00000000..7739db1a
--- /dev/null
+++ b/templates/layout.php
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <link rel="stylesheet" href="assets/css/app.css" media="screen">
+ <!--
+ <link rel="icon" type="image/png" href="assets/img/favicon.png">
+ <link rel="shortcut icon" href="favicon.ico">
+ <link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png">
+ <link rel="apple-touch-icon" sizes="72x72" href="assets/img/touch-icon-ipad.png">
+ <link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png">
+ <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png">
+ -->
+ <title><?= isset($title) ? Helper\escape($title) : 'Kanboard' ?></title>
+ </head>
+ <body>
+ <?php if (isset($no_layout)): ?>
+ <?= $content_for_layout ?>
+ <?php else: ?>
+ <header>
+ <nav>
+ <a class="logo" href="?">kan<span>board</span></a>
+ <ul>
+ <li <?= isset($menu) && $menu === 'boards' ? 'class="active"' : '' ?>>
+ <a href="?controller=board"><?= t('boards') ?></a>
+ </li>
+ <li <?= isset($menu) && $menu === 'projects' ? 'class="active"' : '' ?>>
+ <a href="?controller=project"><?= t('projects') ?></a>
+ </li>
+ <li <?= isset($menu) && $menu === 'users' ? 'class="active"' : '' ?>>
+ <a href="?controller=user"><?= t('users') ?></a>
+ </li>
+ <li <?= isset($menu) && $menu === 'config' ? 'class="active"' : '' ?>>
+ <a href="?controller=config"><?= t('settings') ?></a>
+ </li>
+ <li>
+ <a href="?controller=user&amp;action=logout"><?= t('logout') ?></a>
+ (<?= Helper\escape($_SESSION['user']['username']) ?>)
+ </li>
+ </ul>
+ </nav>
+ </header>
+ <section class="page">
+ <?= Helper\flash('<div class="alert alert-success">%s</div>') ?>
+ <?= Helper\flash_error('<div class="alert alert-error">%s</div>') ?>
+ <?= $content_for_layout ?>
+ </section>
+ <?php endif ?>
+ </body>
+</html> \ No newline at end of file
diff --git a/templates/project_edit.php b/templates/project_edit.php
new file mode 100644
index 00000000..518e72df
--- /dev/null
+++ b/templates/project_edit.php
@@ -0,0 +1,24 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Edit project') ?></h2>
+ <ul>
+ <li><a href="?controller=project"><?= t('All projects') ?></a></li>
+ </ul>
+ </div>
+ <section>
+ <form method="post" action="?controller=project&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
+
+ <?= Helper\form_hidden('id', $values) ?>
+
+ <?= Helper\form_label(t('Name'), 'name') ?>
+ <?= Helper\form_text('name', $values, $errors, array('required')) ?>
+
+ <?= Helper\form_checkbox('is_active', t('Activated'), 1, isset($values['is_active']) && $values['is_active'] == 1 ? true : false) ?><br/>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/project_index.php b/templates/project_index.php
new file mode 100644
index 00000000..8bb09a62
--- /dev/null
+++ b/templates/project_index.php
@@ -0,0 +1,70 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Projects') ?><span id="page-counter"> (<?= $nb_projects ?>)</span></h2>
+ <ul>
+ <li><a href="?controller=project&amp;action=create"><?= t('New project') ?></a></li>
+ </ul>
+ </div>
+ <section>
+ <?php if (empty($projects)): ?>
+ <p class="alert"><?= t('No project') ?></p>
+ <?php else: ?>
+ <table>
+ <tr>
+ <th><?= t('Project') ?></th>
+ <th><?= t('Status') ?></th>
+ <th><?= t('Tasks') ?></th>
+ <th><?= t('Board') ?></th>
+
+ <?php if ($_SESSION['user']['is_admin'] == 1): ?>
+ <th><?= t('Actions') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($projects as $project): ?>
+ <tr>
+ <td>
+ <a href="?controller=board&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= Helper\escape($project['name']) ?></a>
+ </td>
+ <td>
+ <?= $project['is_active'] ? t('Active') : t('Inactive') ?>
+ </td>
+ <td>
+ <?= t('%d tasks on the board', $project['nb_active_tasks']) ?>, <?= t('%d tasks in total', $project['nb_tasks']) ?>
+ </td>
+ <td>
+ <ul>
+ <?php foreach ($project['columns'] as $column): ?>
+ <li>
+ <?= Helper\escape($column['title']) ?> (<?= $column['nb_active_tasks'] ?>)
+ </li>
+ <?php endforeach ?>
+ </ul>
+ </td>
+ <?php if ($_SESSION['user']['is_admin'] == 1): ?>
+ <td>
+ <ul>
+ <li>
+ <a href="?controller=project&amp;action=edit&amp;project_id=<?= $project['id'] ?>"><?= t('Edit project') ?></a>
+ </li>
+ <li>
+ <a href="?controller=board&amp;action=edit&amp;project_id=<?= $project['id'] ?>"><?= t('Edit board') ?></a>
+ </li>
+ <li>
+ <?php if ($project['is_active']): ?>
+ <a href="?controller=project&amp;action=disable&amp;project_id=<?= $project['id'] ?>"><?= t('Disable') ?></a>
+ <?php else: ?>
+ <a href="?controller=project&amp;action=enable&amp;project_id=<?= $project['id'] ?>"><?= t('Enable') ?></a>
+ <?php endif ?>
+ </li>
+ <li>
+ <a href="?controller=project&amp;action=confirm&amp;project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a>
+ </li>
+ </ul>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/project_new.php b/templates/project_new.php
new file mode 100644
index 00000000..5ce6f97d
--- /dev/null
+++ b/templates/project_new.php
@@ -0,0 +1,20 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('New project') ?></h2>
+ <ul>
+ <li><a href="?controller=project"><?= t('All projects') ?></a></li>
+ </ul>
+ </div>
+ <section>
+ <form method="post" action="?controller=project&amp;action=save" autocomplete="off">
+
+ <?= Helper\form_label(t('Name'), 'name') ?>
+ <?= Helper\form_text('name', $values, $errors, array('autofocus required')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/project_remove.php b/templates/project_remove.php
new file mode 100644
index 00000000..f63c4031
--- /dev/null
+++ b/templates/project_remove.php
@@ -0,0 +1,16 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Remove project') ?></h2>
+ </div>
+
+ <div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this project: "%s"?', Helper\escape($project['name'])) ?>
+ </p>
+
+ <div class="form-actions">
+ <a href="?controller=project&amp;action=remove&amp;project_id=<?= $project['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
+ </div>
+ </div>
+</section> \ No newline at end of file
diff --git a/templates/task_close.php b/templates/task_close.php
new file mode 100644
index 00000000..6bc32813
--- /dev/null
+++ b/templates/task_close.php
@@ -0,0 +1,16 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Close a task') ?></h2>
+ </div>
+
+ <div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to close this task: "%s"?', Helper\escape($task['title'])) ?>
+ </p>
+
+ <div class="form-actions">
+ <a href="?controller=task&amp;action=close&amp;task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
+ </div>
+ </div>
+</section> \ No newline at end of file
diff --git a/templates/task_edit.php b/templates/task_edit.php
new file mode 100644
index 00000000..cf01c4a7
--- /dev/null
+++ b/templates/task_edit.php
@@ -0,0 +1,36 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Edit a task') ?></h2>
+ </div>
+ <section>
+ <form method="post" action="?controller=task&amp;action=update" autocomplete="off">
+
+ <?= Helper\form_hidden('id', $values) ?>
+
+ <?= Helper\form_label(t('Title'), 'title') ?>
+ <?= Helper\form_text('title', $values, $errors, array('required')) ?><br/>
+
+ <?= Helper\form_label(t('Project'), 'project_id') ?>
+ <?= Helper\form_select('project_id', $projects_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Column'), 'column_id') ?>
+ <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Color'), 'color_id') ?>
+ <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Assignee'), 'owner_id') ?>
+ <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Description'), 'description') ?>
+ <?= Helper\form_textarea('description', $values, $errors) ?><br/>
+
+ <?= Helper\form_checkbox('another_task', t('Create another task'), 1) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?> <a href="?controller=board&amp;action=show&amp;project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/task_new.php b/templates/task_new.php
new file mode 100644
index 00000000..e418748c
--- /dev/null
+++ b/templates/task_new.php
@@ -0,0 +1,34 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('New task') ?></h2>
+ </div>
+ <section>
+ <form method="post" action="?controller=task&amp;action=save" autocomplete="off">
+
+ <?= Helper\form_label(t('Title'), 'title') ?>
+ <?= Helper\form_text('title', $values, $errors, array('autofocus required')) ?><br/>
+
+ <?= Helper\form_label(t('Project'), 'project_id') ?>
+ <?= Helper\form_select('project_id', $projects_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Column'), 'column_id') ?>
+ <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Color'), 'color_id') ?>
+ <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Assignee'), 'owner_id') ?>
+ <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Description'), 'description') ?>
+ <?= Helper\form_textarea('description', $values, $errors) ?><br/>
+
+ <?= Helper\form_checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?> <a href="?controller=board&amp;action=show&amp;project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/task_open.php b/templates/task_open.php
new file mode 100644
index 00000000..54cc11f0
--- /dev/null
+++ b/templates/task_open.php
@@ -0,0 +1,16 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Open a task') ?></h2>
+ </div>
+
+ <div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to open this task: "%s"?', Helper\escape($task['title'])) ?>
+ </p>
+
+ <div class="form-actions">
+ <a href="?controller=task&amp;action=open&amp;task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
+ </div>
+ </div>
+</section> \ No newline at end of file
diff --git a/templates/task_show.php b/templates/task_show.php
new file mode 100644
index 00000000..e2789fca
--- /dev/null
+++ b/templates/task_show.php
@@ -0,0 +1,54 @@
+<section id="main">
+ <div class="page-header">
+ <h2>#<?= $task['id'] ?> - <?= Helper\escape($task['title']) ?></h2>
+ <ul>
+ <li><a href="?controller=board&amp;action=show&amp;project_id=<?= $task['project_id'] ?>"><?= t('Back to the board') ?></a></li>
+ </ul>
+ </div>
+ <section>
+ <h3><?= t('Details') ?></h3>
+ <article id="infos" class="task task-<?= $task['color_id'] ?>">
+ <ul>
+ <li>
+ <?= dt('Created on %B %e, %G at %k:%M %p', $task['date_creation']) ?>
+ </li>
+ <li>
+ <strong>
+ <?php if ($task['username']): ?>
+ <?= t('Assigned to %s', $task['username']) ?>
+ <?php else: ?>
+ <?= t('There is no body assigned') ?>
+ <?php endif ?>
+ </strong>
+ </li>
+ <li>
+ <?= t('Column on the board:') ?>
+ <strong><?= Helper\escape($task['column_title']) ?></strong>
+ (<?= Helper\escape($task['project_name']) ?>)
+ </li>
+ <li>
+ <?php if ($task['is_active'] == 1): ?>
+ <?= t('Status is open') ?>
+ <?php else: ?>
+ <?= t('Status is closed') ?>
+ <?php endif ?>
+ </li>
+ <li>
+ <a href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>"><?= t('Edit') ?></a>
+ <?= t('or') ?>
+ <?php if ($task['is_active'] == 1): ?>
+ <a href="?controller=task&amp;action=confirmClose&amp;task_id=<?= $task['id'] ?>"><?= t('close this task') ?></a>
+ <?php else: ?>
+ <a href="?controller=task&amp;action=confirmOpen&amp;task_id=<?= $task['id'] ?>"><?= t('open this task') ?></a>
+ <?php endif ?>
+ </li>
+ </ul>
+ </article>
+
+ <h3><?= t('Description') ?></h3>
+ <article id="description">
+ <?= Helper\markdown($task['description']) ?: t('There is no description.') ?>
+ </article>
+
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/user_edit.php b/templates/user_edit.php
new file mode 100644
index 00000000..a6f5d600
--- /dev/null
+++ b/templates/user_edit.php
@@ -0,0 +1,34 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Edit user') ?></h2>
+ <ul>
+ <li><a href="?action=users"><?= t('All users') ?></a></li>
+ </ul>
+ </div>
+ <section>
+ <form method="post" action="?controller=user&amp;action=update&amp;username=<?= Helper\escape($username) ?>" autocomplete="off">
+
+ <?= Helper\form_hidden('id', $values) ?>
+
+ <?= Helper\form_label(t('Username'), 'username') ?>
+ <?= Helper\form_text('username', $values, $errors, array('required')) ?><br/>
+
+ <?= Helper\form_label(t('Password'), 'password') ?>
+ <?= Helper\form_password('password', $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Confirmation'), 'confirmation') ?>
+ <?= Helper\form_password('confirmation', $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Default Project'), 'default_project_id') ?>
+ <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/>
+
+ <?php if ($values['is_admin'] == 1): ?>
+ <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
+ <?php endif ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/user_forbidden.php b/templates/user_forbidden.php
new file mode 100644
index 00000000..bb71d9b1
--- /dev/null
+++ b/templates/user_forbidden.php
@@ -0,0 +1,10 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Forbidden') ?></h2>
+ </div>
+
+ <p class="alert alert-error">
+ <?= t('Only administrators can access to this page.') ?>
+ </p>
+
+</section> \ No newline at end of file
diff --git a/templates/user_index.php b/templates/user_index.php
new file mode 100644
index 00000000..c83a5417
--- /dev/null
+++ b/templates/user_index.php
@@ -0,0 +1,46 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Users') ?><span id="page-counter"> (<?= $nb_users ?>)</span></h2>
+ <ul>
+ <li><a href="?controller=user&amp;action=create"><?= t('New user') ?></a></li>
+ </ul>
+ </div>
+ <section>
+ <?php if (empty($users)): ?>
+ <p class="alert"><?= t('No user') ?></p>
+ <?php else: ?>
+ <table>
+ <tr>
+ <th><?= t('Username') ?></th>
+ <th><?= t('Administrator') ?></th>
+ <th><?= t('Default Project') ?></th>
+ <?php if ($_SESSION['user']['is_admin'] == 1): ?>
+ <th><?= t('Actions') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($users as $user): ?>
+ <tr>
+ <td>
+ <?= Helper\escape($user['username']) ?>
+ </td>
+ <td>
+ <?= $user['is_admin'] ? t('Yes') : t('No') ?>
+ </td>
+ <td>
+ <?= $projects[$user['default_project_id']] ?>
+ </td>
+ <?php if ($_SESSION['user']['is_admin'] == 1): ?>
+ <td>
+ <a href="?controller=user&amp;action=edit&amp;user_id=<?= $user['id'] ?>"><?= t('edit') ?></a>
+ <?php if (count($users) > 1): ?>
+ <?= t('or') ?>
+ <a href="?controller=user&amp;action=confirm&amp;user_id=<?= $user['id'] ?>"><?= t('remove') ?></a>
+ <?php endif ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/user_login.php b/templates/user_login.php
new file mode 100644
index 00000000..0bbd48a1
--- /dev/null
+++ b/templates/user_login.php
@@ -0,0 +1,20 @@
+<div class="page-header">
+ <h1><?= t('Sign in') ?></h1>
+</div>
+
+<?php if (isset($errors['login'])): ?>
+ <p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p>
+<?php endif ?>
+
+<form method="post" action="?controller=user&amp;action=check">
+
+ <?= Helper\form_label(t('Username'), 'username') ?>
+ <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
+
+ <?= Helper\form_label(t('Password'), 'password') ?>
+ <?= Helper\form_password('password', $values, $errors, array('required')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/templates/user_new.php b/templates/user_new.php
new file mode 100644
index 00000000..0c753c2a
--- /dev/null
+++ b/templates/user_new.php
@@ -0,0 +1,31 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('New user') ?></h2>
+ <ul>
+ <li><a href="?controller=user"><?= t('All users') ?></a></li>
+ </ul>
+ </div>
+ <section>
+ <form method="post" action="?controller=user&amp;action=save" autocomplete="off">
+
+ <?= Helper\form_label(t('Username'), 'username') ?>
+ <?= Helper\form_text('username', $values, $errors, array('autofocus required')) ?><br/>
+
+ <?= Helper\form_label(t('Password'), 'password') ?>
+ <?= Helper\form_password('password', $values, $errors, array('required')) ?><br/>
+
+ <?= Helper\form_label(t('Confirmation'), 'confirmation') ?>
+ <?= Helper\form_password('confirmation', $values, $errors, array('required')) ?><br/>
+
+ <?= Helper\form_label(t('Default Project'), 'default_project_id') ?>
+ <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/>
+
+ <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/templates/user_remove.php b/templates/user_remove.php
new file mode 100644
index 00000000..e1dc6f7b
--- /dev/null
+++ b/templates/user_remove.php
@@ -0,0 +1,14 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Remove user') ?></h2>
+ </div>
+
+ <div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', Helper\escape($user['username'])) ?></p>
+
+ <div class="form-actions">
+ <a href="?controller=user&amp;action=remove&amp;user_id=<?= $user['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a>
+ </div>
+ </div>
+</section> \ No newline at end of file
diff --git a/vendor/.htaccess b/vendor/.htaccess
new file mode 100644
index 00000000..14249c50
--- /dev/null
+++ b/vendor/.htaccess
@@ -0,0 +1 @@
+Deny from all \ No newline at end of file
diff --git a/vendor/Parsedown/LICENSE.txt b/vendor/Parsedown/LICENSE.txt
new file mode 100644
index 00000000..baca86f5
--- /dev/null
+++ b/vendor/Parsedown/LICENSE.txt
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Emanuil Rusev, erusev.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/vendor/Parsedown/Parsedown.php b/vendor/Parsedown/Parsedown.php
new file mode 100644
index 00000000..b5014b1a
--- /dev/null
+++ b/vendor/Parsedown/Parsedown.php
@@ -0,0 +1,991 @@
+<?php
+
+#
+#
+# Parsedown
+# http://parsedown.org
+#
+# (c) Emanuil Rusev
+# http://erusev.com
+#
+# For the full license information, please view the LICENSE file that was
+# distributed with this source code.
+#
+#
+
+class Parsedown
+{
+ #
+ # Multiton (http://en.wikipedia.org/wiki/Multiton_pattern)
+ #
+
+ static function instance($name = 'default')
+ {
+ if (isset(self::$instances[$name]))
+ return self::$instances[$name];
+
+ $instance = new Parsedown();
+
+ self::$instances[$name] = $instance;
+
+ return $instance;
+ }
+
+ private static $instances = array();
+
+ #
+ # Setters
+ #
+
+ private $break_marker = " \n";
+
+ function set_breaks_enabled($breaks_enabled)
+ {
+ $this->break_marker = $breaks_enabled ? "\n" : " \n";
+
+ return $this;
+ }
+
+ #
+ # Fields
+ #
+
+ private $reference_map = array();
+ private $escape_sequence_map = array();
+
+ #
+ # Public Methods
+ #
+
+ function parse($text)
+ {
+ # removes UTF-8 BOM and marker characters
+ $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+ # removes \r characters
+ $text = str_replace("\r\n", "\n", $text);
+ $text = str_replace("\r", "\n", $text);
+
+ # replaces tabs with spaces
+ $text = str_replace("\t", ' ', $text);
+
+ # encodes escape sequences
+
+ if (strpos($text, '\\') !== FALSE)
+ {
+ $escape_sequences = array('\\\\', '\`', '\*', '\_', '\{', '\}', '\[', '\]', '\(', '\)', '\>', '\#', '\+', '\-', '\.', '\!');
+
+ foreach ($escape_sequences as $index => $escape_sequence)
+ {
+ if (strpos($text, $escape_sequence) !== FALSE)
+ {
+ $code = "\x1A".'\\'.$index.';';
+
+ $text = str_replace($escape_sequence, $code, $text);
+
+ $this->escape_sequence_map[$code] = $escape_sequence;
+ }
+ }
+ }
+
+ # ~
+
+ $text = preg_replace('/\n\s*\n/', "\n\n", $text);
+ $text = trim($text, "\n");
+
+ $lines = explode("\n", $text);
+
+ $text = $this->parse_block_elements($lines);
+
+ # decodes escape sequences
+
+ foreach ($this->escape_sequence_map as $code => $escape_sequence)
+ {
+ $text = str_replace($code, $escape_sequence[1], $text);
+ }
+
+ # ~
+
+ $text = rtrim($text, "\n");
+
+ return $text;
+ }
+
+ #
+ # Private Methods
+ #
+
+ private function parse_block_elements(array $lines, $context = '')
+ {
+ $elements = array();
+
+ $element = array(
+ 'type' => '',
+ );
+
+ foreach ($lines as $line)
+ {
+ # fenced elements
+
+ switch ($element['type'])
+ {
+ case 'fenced_code_block':
+
+ if ( ! isset($element['closed']))
+ {
+ if (preg_match('/^[ ]*'.$element['fence'][0].'{3,}[ ]*$/', $line))
+ {
+ $element['closed'] = true;
+ }
+ else
+ {
+ $element['text'] !== '' and $element['text'] .= "\n";
+
+ $element['text'] .= $line;
+ }
+
+ continue 2;
+ }
+
+ break;
+
+ case 'markup':
+
+ if ( ! isset($element['closed']))
+ {
+ if (preg_match('{<'.$element['subtype'].'>$}', $line)) # opening tag
+ {
+ $element['depth']++;
+ }
+
+ if (preg_match('{</'.$element['subtype'].'>$}', $line)) # closing tag
+ {
+ $element['depth'] > 0
+ ? $element['depth']--
+ : $element['closed'] = true;
+ }
+
+ $element['text'] .= "\n".$line;
+
+ continue 2;
+ }
+
+ break;
+ }
+
+ # *
+
+ if ($line === '')
+ {
+ $element['interrupted'] = true;
+
+ continue;
+ }
+
+ # composite elements
+
+ switch ($element['type'])
+ {
+ case 'blockquote':
+
+ if ( ! isset($element['interrupted']))
+ {
+ $line = preg_replace('/^[ ]*>[ ]?/', '', $line);
+
+ $element['lines'] []= $line;
+
+ continue 2;
+ }
+
+ break;
+
+ case 'li':
+
+ if (preg_match('/^([ ]{0,3})(\d+[.]|[*+-])[ ](.*)/', $line, $matches))
+ {
+ if ($element['indentation'] !== $matches[1])
+ {
+ $element['lines'] []= $line;
+ }
+ else
+ {
+ unset($element['last']);
+
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'li',
+ 'indentation' => $matches[1],
+ 'last' => true,
+ 'lines' => array(
+ preg_replace('/^[ ]{0,4}/', '', $matches[3]),
+ ),
+ );
+ }
+
+ continue 2;
+ }
+
+ if (isset($element['interrupted']))
+ {
+ if ($line[0] === ' ')
+ {
+ $element['lines'] []= '';
+
+ $line = preg_replace('/^[ ]{0,4}/', '', $line);
+
+ $element['lines'] []= $line;
+
+ unset($element['interrupted']);
+
+ continue 2;
+ }
+ }
+ else
+ {
+ $line = preg_replace('/^[ ]{0,4}/', '', $line);
+
+ $element['lines'] []= $line;
+
+ continue 2;
+ }
+
+ break;
+ }
+
+ # indentation sensitive types
+
+ $deindented_line = $line;
+
+ switch ($line[0])
+ {
+ case ' ':
+
+ # ~
+
+ $deindented_line = ltrim($line);
+
+ if ($deindented_line === '')
+ {
+ continue 2;
+ }
+
+ # code block
+
+ if (preg_match('/^[ ]{4}(.*)/', $line, $matches))
+ {
+ if ($element['type'] === 'code_block')
+ {
+ if (isset($element['interrupted']))
+ {
+ $element['text'] .= "\n";
+
+ unset ($element['interrupted']);
+ }
+
+ $element['text'] .= "\n".$matches[1];
+ }
+ else
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'code_block',
+ 'text' => $matches[1],
+ );
+ }
+
+ continue 2;
+ }
+
+ break;
+
+ case '#':
+
+ # atx heading (#)
+
+ if (preg_match('/^(#{1,6})[ ]*(.+?)[ ]*#*$/', $line, $matches))
+ {
+ $elements []= $element;
+
+ $level = strlen($matches[1]);
+
+ $element = array(
+ 'type' => 'h.',
+ 'text' => $matches[2],
+ 'level' => $level,
+ );
+
+ continue 2;
+ }
+
+ break;
+
+ case '-':
+
+ # setext heading (---)
+
+ if ($line[0] === '-' and $element['type'] === 'p' and ! isset($element['interrupted']) and preg_match('/^[-]+[ ]*$/', $line))
+ {
+ $element['type'] = 'h.';
+ $element['level'] = 2;
+
+ continue 2;
+ }
+
+ break;
+
+ case '=':
+
+ # setext heading (===)
+
+ if ($line[0] === '=' and $element['type'] === 'p' and ! isset($element['interrupted']) and preg_match('/^[=]+[ ]*$/', $line))
+ {
+ $element['type'] = 'h.';
+ $element['level'] = 1;
+
+ continue 2;
+ }
+
+ break;
+ }
+
+ # indentation insensitive types
+
+ switch ($deindented_line[0])
+ {
+ case '<':
+
+ # self-closing tag
+
+ if (preg_match('{^<.+?/>$}', $deindented_line))
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => '',
+ 'text' => $deindented_line,
+ );
+
+ continue 2;
+ }
+
+ # opening tag
+
+ if (preg_match('{^<(\w+)(?:[ ].*?)?>}', $deindented_line, $matches))
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'markup',
+ 'subtype' => strtolower($matches[1]),
+ 'text' => $deindented_line,
+ 'depth' => 0,
+ );
+
+ preg_match('{</'.$matches[1].'>\s*$}', $deindented_line) and $element['closed'] = true;
+
+ continue 2;
+ }
+
+ break;
+
+ case '>':
+
+ # quote
+
+ if (preg_match('/^>[ ]?(.*)/', $deindented_line, $matches))
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'blockquote',
+ 'lines' => array(
+ $matches[1],
+ ),
+ );
+
+ continue 2;
+ }
+
+ break;
+
+ case '[':
+
+ # reference
+
+ if (preg_match('/^\[(.+?)\]:[ ]*(.+?)(?:[ ]+[\'"](.+?)[\'"])?[ ]*$/', $deindented_line, $matches))
+ {
+ $label = strtolower($matches[1]);
+
+ $this->reference_map[$label] = array(
+ '»' => trim($matches[2], '<>'),
+ );
+
+ if (isset($matches[3]))
+ {
+ $this->reference_map[$label]['#'] = $matches[3];
+ }
+
+ continue 2;
+ }
+
+ break;
+
+ case '`':
+ case '~':
+
+ # fenced code block
+
+ if (preg_match('/^([`]{3,}|[~]{3,})[ ]*(\S+)?[ ]*$/', $deindented_line, $matches))
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'fenced_code_block',
+ 'text' => '',
+ 'fence' => $matches[1],
+ );
+
+ isset($matches[2]) and $element['language'] = $matches[2];
+
+ continue 2;
+ }
+
+ break;
+
+ case '*':
+ case '+':
+ case '-':
+ case '_':
+
+ # hr
+
+ if (preg_match('/^([-*_])([ ]{0,2}\1){2,}[ ]*$/', $deindented_line))
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'hr',
+ );
+
+ continue 2;
+ }
+
+ # li
+
+ if (preg_match('/^([ ]*)[*+-][ ](.*)/', $line, $matches))
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'li',
+ 'ordered' => false,
+ 'indentation' => $matches[1],
+ 'last' => true,
+ 'lines' => array(
+ preg_replace('/^[ ]{0,4}/', '', $matches[2]),
+ ),
+ );
+
+ continue 2;
+ }
+ }
+
+ # li
+
+ if ($deindented_line[0] <= '9' and $deindented_line >= '0' and preg_match('/^([ ]*)\d+[.][ ](.*)/', $line, $matches))
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'li',
+ 'ordered' => true,
+ 'indentation' => $matches[1],
+ 'last' => true,
+ 'lines' => array(
+ preg_replace('/^[ ]{0,4}/', '', $matches[2]),
+ ),
+ );
+
+ continue;
+ }
+
+ # paragraph
+
+ if ($element['type'] === 'p')
+ {
+ if (isset($element['interrupted']))
+ {
+ $elements []= $element;
+
+ $element['text'] = $line;
+
+ unset($element['interrupted']);
+ }
+ else
+ {
+ $element['text'] .= "\n".$line;
+ }
+ }
+ else
+ {
+ $elements []= $element;
+
+ $element = array(
+ 'type' => 'p',
+ 'text' => $line,
+ );
+ }
+ }
+
+ $elements []= $element;
+
+ unset($elements[0]);
+
+ #
+ # ~
+ #
+
+ $markup = '';
+
+ foreach ($elements as $element)
+ {
+ switch ($element['type'])
+ {
+ case 'p':
+
+ $text = $this->parse_span_elements($element['text']);
+
+ if ($context === 'li' and $markup === '')
+ {
+ if (isset($element['interrupted']))
+ {
+ $markup .= "\n".'<p>'.$text.'</p>'."\n";
+ }
+ else
+ {
+ $markup .= $text;
+ }
+ }
+ else
+ {
+ $markup .= '<p>'.$text.'</p>'."\n";
+ }
+
+ break;
+
+ case 'blockquote':
+
+ $text = $this->parse_block_elements($element['lines']);
+
+ $markup .= '<blockquote>'."\n".$text.'</blockquote>'."\n";
+
+ break;
+
+ case 'code_block':
+ case 'fenced_code_block':
+
+ $text = htmlspecialchars($element['text'], ENT_NOQUOTES, 'UTF-8');
+
+ strpos($text, "\x1A\\") !== FALSE and $text = strtr($text, $this->escape_sequence_map);
+
+ $markup .= isset($element['language'])
+ ? '<pre><code class="language-'.$element['language'].'">'.$text.'</code></pre>'
+ : '<pre><code>'.$text.'</code></pre>';
+
+ $markup .= "\n";
+
+ break;
+
+ case 'h.':
+
+ $text = $this->parse_span_elements($element['text']);
+
+ $markup .= '<h'.$element['level'].'>'.$text.'</h'.$element['level'].'>'."\n";
+
+ break;
+
+ case 'hr':
+
+ $markup .= '<hr />'."\n";
+
+ break;
+
+ case 'li':
+
+ if (isset($element['ordered'])) # first
+ {
+ $list_type = $element['ordered'] ? 'ol' : 'ul';
+
+ $markup .= '<'.$list_type.'>'."\n";
+ }
+
+ if (isset($element['interrupted']) and ! isset($element['last']))
+ {
+ $element['lines'] []= '';
+ }
+
+ $text = $this->parse_block_elements($element['lines'], 'li');
+
+ $markup .= '<li>'.$text.'</li>'."\n";
+
+ isset($element['last']) and $markup .= '</'.$list_type.'>'."\n";
+
+ break;
+
+ case 'markup':
+
+ $markup .= $this->parse_span_elements($element['text'])."\n";
+
+ break;
+
+ default:
+
+ $markup .= $element['text']."\n";
+ }
+ }
+
+ return $markup;
+ }
+
+ # ~
+
+ private $strong_regex = array(
+ '*' => '/^[*]{2}([^*]+?)[*]{2}(?![*])/s',
+ '_' => '/^__([^_]+?)__(?!_)/s',
+ );
+
+ private $em_regex = array(
+ '*' => '/^[*]([^*]+?)[*](?![*])/s',
+ '_' => '/^_([^_]+?)[_](?![_])\b/s',
+ );
+
+ private $strong_em_regex = array(
+ '*' => '/^[*]{2}(.*?)[*](.+?)[*](.*?)[*]{2}/s',
+ '_' => '/^__(.*?)_(.+?)_(.*?)__/s',
+ );
+
+ private $em_strong_regex = array(
+ '*' => '/^[*](.*?)[*]{2}(.+?)[*]{2}(.*?)[*]/s',
+ '_' => '/^_(.*?)__(.+?)__(.*?)_/s',
+ );
+
+ private function parse_span_elements($text, $markers = array('![', '&', '*', '<', '[', '_', '`', 'http', '~~'))
+ {
+ if (isset($text[2]) === false or $markers === array())
+ {
+ return $text;
+ }
+
+ # ~
+
+ $markup = '';
+
+ while ($markers)
+ {
+ $closest_marker = null;
+ $closest_marker_index = 0;
+ $closest_marker_position = null;
+
+ foreach ($markers as $index => $marker)
+ {
+ $marker_position = strpos($text, $marker);
+
+ if ($marker_position === false)
+ {
+ unset($markers[$index]);
+
+ continue;
+ }
+
+ if ($closest_marker === null or $marker_position < $closest_marker_position)
+ {
+ $closest_marker = $marker;
+ $closest_marker_index = $index;
+ $closest_marker_position = $marker_position;
+ }
+ }
+
+ # ~
+
+ if ($closest_marker === null or isset($text[$closest_marker_position + 2]) === false)
+ {
+ $markup .= $text;
+
+ break;
+ }
+ else
+ {
+ $markup .= substr($text, 0, $closest_marker_position);
+ }
+
+ $text = substr($text, $closest_marker_position);
+
+ # ~
+
+ unset($markers[$closest_marker_index]);
+
+ # ~
+
+ switch ($closest_marker)
+ {
+ case '![':
+ case '[':
+
+ if (strpos($text, ']') and preg_match('/\[((?:[^][]|(?R))*)\]/', $text, $matches))
+ {
+ $element = array(
+ '!' => $text[0] === '!',
+ 'a' => $matches[1],
+ );
+
+ $offset = strlen($matches[0]);
+
+ $element['!'] and $offset++;
+
+ $remaining_text = substr($text, $offset);
+
+ if ($remaining_text[0] === '(' and preg_match('/\([ ]*(.*?)(?:[ ]+[\'"](.+?)[\'"])?[ ]*\)/', $remaining_text, $matches))
+ {
+ $element['»'] = $matches[1];
+
+ if (isset($matches[2]))
+ {
+ $element['#'] = $matches[2];
+ }
+
+ $offset += strlen($matches[0]);
+ }
+ elseif ($this->reference_map)
+ {
+ $reference = $element['a'];
+
+ if (preg_match('/^\s*\[(.*?)\]/', $remaining_text, $matches))
+ {
+ $reference = $matches[1] ? $matches[1] : $element['a'];
+
+ $offset += strlen($matches[0]);
+ }
+
+ $reference = strtolower($reference);
+
+ if (isset($this->reference_map[$reference]))
+ {
+ $element['»'] = $this->reference_map[$reference]['»'];
+
+ if (isset($this->reference_map[$reference]['#']))
+ {
+ $element['#'] = $this->reference_map[$reference]['#'];
+ }
+ }
+ else
+ {
+ unset($element);
+ }
+ }
+ else
+ {
+ unset($element);
+ }
+ }
+
+ if (isset($element))
+ {
+ $element['»'] = str_replace('&', '&amp;', $element['»']);
+ $element['»'] = str_replace('<', '&lt;', $element['»']);
+
+ if ($element['!'])
+ {
+ $markup .= '<img alt="'.$element['a'].'" src="'.$element['»'].'" />';
+ }
+ else
+ {
+ $element['a'] = $this->parse_span_elements($element['a'], $markers);
+
+ $markup .= isset($element['#'])
+ ? '<a href="'.$element['»'].'" title="'.$element['#'].'">'.$element['a'].'</a>'
+ : '<a href="'.$element['»'].'">'.$element['a'].'</a>';
+ }
+
+ unset($element);
+ }
+ else
+ {
+ $markup .= $closest_marker;
+
+ $offset = $closest_marker === '![' ? 2 : 1;
+ }
+
+ break;
+
+ case '&':
+
+ $markup .= '&amp;';
+
+ $offset = substr($text, 0, 5) === '&amp;' ? 5 : 1;
+
+ break;
+
+ case '*':
+ case '_':
+
+ if ($text[1] === $closest_marker and preg_match($this->strong_regex[$closest_marker], $text, $matches))
+ {
+ $matches[1] = $this->parse_span_elements($matches[1], $markers);
+
+ $markup .= '<strong>'.$matches[1].'</strong>';
+ }
+ elseif (preg_match($this->em_regex[$closest_marker], $text, $matches))
+ {
+ $matches[1] = $this->parse_span_elements($matches[1], $markers);
+
+ $markup .= '<em>'.$matches[1].'</em>';
+ }
+ elseif ($text[1] === $closest_marker and preg_match($this->strong_em_regex[$closest_marker], $text, $matches))
+ {
+ $matches[2] = $this->parse_span_elements($matches[2], $markers);
+
+ $matches[1] and $matches[1] = $this->parse_span_elements($matches[1], $markers);
+ $matches[3] and $matches[3] = $this->parse_span_elements($matches[3], $markers);
+
+ $markup .= '<strong>'.$matches[1].'<em>'.$matches[2].'</em>'.$matches[3].'</strong>';
+ }
+ elseif (preg_match($this->em_strong_regex[$closest_marker], $text, $matches))
+ {
+ $matches[2] = $this->parse_span_elements($matches[2], $markers);
+
+ $matches[1] and $matches[1] = $this->parse_span_elements($matches[1], $markers);
+ $matches[3] and $matches[3] = $this->parse_span_elements($matches[3], $markers);
+
+ $markup .= '<em>'.$matches[1].'<strong>'.$matches[2].'</strong>'.$matches[3].'</em>';
+ }
+
+ if (isset($matches) and $matches)
+ {
+ $offset = strlen($matches[0]);
+ }
+ else
+ {
+ $markup .= $closest_marker;
+
+ $offset = 1;
+ }
+
+ break;
+
+ case '<':
+
+ if (strpos($text, '>') !== false)
+ {
+ if ($text[1] === 'h' and preg_match('/^<(https?:[\/]{2}[^\s]+?)>/i', $text, $matches))
+ {
+ $element_url = $matches[1];
+ $element_url = str_replace('&', '&amp;', $element_url);
+ $element_url = str_replace('<', '&lt;', $element_url);
+
+ $markup .= '<a href="'.$element_url.'">'.$element_url.'</a>';
+
+ $offset = strlen($matches[0]);
+ }
+ elseif (preg_match('/^<\/?\w.*?>/', $text, $matches))
+ {
+ $markup .= $matches[0];
+
+ $offset = strlen($matches[0]);
+ }
+ else
+ {
+ $markup .= '&lt;';
+
+ $offset = 1;
+ }
+ }
+ else
+ {
+ $markup .= '&lt;';
+
+ $offset = 1;
+ }
+
+ break;
+
+ case '`':
+
+ if (preg_match('/^`(.+?)`/', $text, $matches))
+ {
+ $element_text = $matches[1];
+ $element_text = htmlspecialchars($element_text, ENT_NOQUOTES, 'UTF-8');
+
+ if ($this->escape_sequence_map and strpos($element_text, "\x1A") !== false)
+ {
+ $element_text = strtr($element_text, $this->escape_sequence_map);
+ }
+
+ $markup .= '<code>'.$element_text.'</code>';
+
+ $offset = strlen($matches[0]);
+ }
+ else
+ {
+ $markup .= '`';
+
+ $offset = 1;
+ }
+
+ break;
+
+ case 'http':
+
+ if (preg_match('/^https?:[\/]{2}[^\s]+\b/i', $text, $matches))
+ {
+ $element_url = $matches[0];
+ $element_url = str_replace('&', '&amp;', $element_url);
+ $element_url = str_replace('<', '&lt;', $element_url);
+
+ $markup .= '<a href="'.$element_url.'">'.$element_url.'</a>';
+
+ $offset = strlen($matches[0]);
+ }
+ else
+ {
+ $markup .= 'http';
+
+ $offset = 4;
+ }
+
+ break;
+
+ case '~~':
+
+ if (preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $text, $matches))
+ {
+ $matches[1] = $this->parse_span_elements($matches[1], $markers);
+
+ $markup .= '<del>'.$matches[1].'</del>';
+
+ $offset = strlen($matches[0]);
+ }
+ else
+ {
+ $markup .= '~~';
+
+ $offset = 2;
+ }
+
+ break;
+ }
+
+ if (isset($offset))
+ {
+ $text = substr($text, $offset);
+ }
+
+ $markers[$closest_marker_index] = $closest_marker;
+ }
+
+ $markup = str_replace($this->break_marker, '<br />'."\n", $markup);
+
+ return $markup;
+ }
+} \ No newline at end of file
diff --git a/vendor/PicoDb/Database.php b/vendor/PicoDb/Database.php
new file mode 100644
index 00000000..c3405f72
--- /dev/null
+++ b/vendor/PicoDb/Database.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace PicoDb;
+
+class Database
+{
+ private $logs = array();
+ private $pdo;
+
+
+ public function __construct(array $settings)
+ {
+ if (! isset($settings['driver'])) {
+
+ throw new \LogicException('You must define a database driver.');
+ }
+
+ switch ($settings['driver']) {
+
+ case 'sqlite':
+ require_once __DIR__.'/Drivers/Sqlite.php';
+ $this->pdo = new Sqlite($settings['filename']);
+ break;
+
+ default:
+ throw new \LogicException('This database driver is not supported.');
+ }
+
+ $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ }
+
+
+ public function setLogMessage($message)
+ {
+ $this->logs[] = $message;
+ }
+
+
+ public function getLogMessages()
+ {
+ return $this->logs;
+ }
+
+
+ public function getConnection()
+ {
+ return $this->pdo;
+ }
+
+
+ public function escapeIdentifier($value)
+ {
+ return $this->pdo->escapeIdentifier($value);
+ }
+
+
+ public function execute($sql, array $values = array())
+ {
+ try {
+
+ $this->setLogMessage($sql);
+ $this->setLogMessage(implode(', ', $values));
+
+ $rq = $this->pdo->prepare($sql);
+ $rq->execute($values);
+
+ return $rq;
+ }
+ catch (\PDOException $e) {
+
+ if ($this->pdo->inTransaction()) $this->pdo->rollback();
+ $this->setLogMessage($e->getMessage());
+ return false;
+ }
+ }
+
+
+ public function startTransaction()
+ {
+ $this->pdo->beginTransaction();
+ }
+
+
+ public function closeTransaction()
+ {
+ $this->pdo->commit();
+ }
+
+
+ public function cancelTransaction()
+ {
+ $this->pdo->rollback();
+ }
+
+
+ public function table($table_name)
+ {
+ require_once __DIR__.'/Table.php';
+ return new Table($this, $table_name);
+ }
+
+
+ public function schema()
+ {
+ require_once __DIR__.'/Schema.php';
+ return new Schema($this);
+ }
+} \ No newline at end of file
diff --git a/vendor/PicoDb/Drivers/Sqlite.php b/vendor/PicoDb/Drivers/Sqlite.php
new file mode 100644
index 00000000..6555e73d
--- /dev/null
+++ b/vendor/PicoDb/Drivers/Sqlite.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace PicoDb;
+
+class Sqlite extends \PDO {
+
+
+ public function __construct($filename)
+ {
+ parent::__construct('sqlite:'.$filename);
+
+ $this->exec('PRAGMA foreign_keys = ON');
+ }
+
+
+ public function getSchemaVersion()
+ {
+ $rq = $this->prepare('PRAGMA user_version');
+ $rq->execute();
+ $result = $rq->fetch(\PDO::FETCH_ASSOC);
+
+ if (isset($result['user_version'])) {
+
+ return $result['user_version'];
+ }
+
+ return 0;
+ }
+
+
+ public function setSchemaVersion($version)
+ {
+ $this->exec('PRAGMA user_version='.$version);
+ }
+
+
+ public function getLastId()
+ {
+ return $this->lastInsertId();
+ }
+
+
+ public function escapeIdentifier($value)
+ {
+ if (strpos($value, '.') !== false) return $value;
+ return '"'.$value.'"';
+ }
+} \ No newline at end of file
diff --git a/vendor/PicoDb/Schema.php b/vendor/PicoDb/Schema.php
new file mode 100644
index 00000000..2f52b846
--- /dev/null
+++ b/vendor/PicoDb/Schema.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace PicoDb;
+
+class Schema
+{
+ protected $db = null;
+
+
+ public function __construct(Database $db)
+ {
+ $this->db = $db;
+ }
+
+
+ public function check($last_version = 1)
+ {
+ $current_version = $this->db->getConnection()->getSchemaVersion();
+
+ if ($current_version < $last_version) {
+
+ return $this->migrateTo($current_version, $last_version);
+ }
+
+ return true;
+ }
+
+
+ public function migrateTo($current_version, $next_version)
+ {
+ try {
+
+ $this->db->startTransaction();
+
+ for ($i = $current_version + 1; $i <= $next_version; $i++) {
+
+ $function_name = '\Schema\version_'.$i;
+
+ if (function_exists($function_name)) {
+
+ call_user_func($function_name, $this->db->getConnection());
+ $this->db->getConnection()->setSchemaVersion($i);
+ }
+ else {
+
+ throw new \LogicException('To execute a database migration, you need to create this function: "'.$function_name.'".');
+ }
+ }
+
+ $this->db->closeTransaction();
+ }
+ catch (\PDOException $e) {
+
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/PicoDb/Table.php b/vendor/PicoDb/Table.php
new file mode 100644
index 00000000..4cdf35f7
--- /dev/null
+++ b/vendor/PicoDb/Table.php
@@ -0,0 +1,430 @@
+<?php
+
+namespace PicoDb;
+
+class Table
+{
+ private $table_name = '';
+ private $sql_limit = '';
+ private $sql_offset = '';
+ private $sql_order = '';
+ private $joins = array();
+ private $conditions = array();
+ private $or_conditions = array();
+ private $is_or_condition = false;
+ private $columns = array();
+ private $values = array();
+ private $distinct = false;
+ private $group_by = array();
+
+ private $db;
+
+
+ public function __construct(Database $db, $table_name)
+ {
+ $this->db = $db;
+ $this->table_name = $table_name;
+
+ return $this;
+ }
+
+
+ public function save(array $data)
+ {
+ if (! empty($this->conditions)) {
+
+ return $this->update($data);
+ }
+ else {
+
+ return $this->insert($data);
+ }
+ }
+
+
+ public function update(array $data)
+ {
+ $columns = array();
+ $values = array();
+
+ foreach ($data as $column => $value) {
+
+ $columns[] = $this->db->escapeIdentifier($column).'=?';
+ $values[] = $value;
+ }
+
+ foreach ($this->values as $value) {
+
+ $values[] = $value;
+ }
+
+ $sql = sprintf(
+ 'UPDATE %s SET %s %s',
+ $this->db->escapeIdentifier($this->table_name),
+ implode(', ', $columns),
+ $this->conditions()
+ );
+
+ $result = $this->db->execute($sql, $values);
+
+ if ($result !== false && $result->rowCount() > 0) {
+
+ return true;
+ }
+
+ return false;
+ }
+
+
+ public function insert(array $data)
+ {
+ $columns = array();
+
+ foreach ($data as $column => $value) {
+
+ $columns[] = $this->db->escapeIdentifier($column);
+ }
+
+ $sql = sprintf(
+ 'INSERT INTO %s (%s) VALUES (%s)',
+ $this->db->escapeIdentifier($this->table_name),
+ implode(', ', $columns),
+ implode(', ', array_fill(0, count($data), '?'))
+ );
+
+ return false !== $this->db->execute($sql, array_values($data));
+ }
+
+
+ public function remove()
+ {
+ $sql = sprintf(
+ 'DELETE FROM %s %s',
+ $this->db->escapeIdentifier($this->table_name),
+ $this->conditions()
+ );
+
+ return false !== $this->db->execute($sql, $this->values);
+ }
+
+
+ public function listing($key, $value)
+ {
+ $this->columns($key, $value);
+
+ $listing = array();
+ $results = $this->findAll();
+
+ if ($results) {
+
+ foreach ($results as $result) {
+
+ $listing[$result[$key]] = $result[$value];
+ }
+ }
+
+ return $listing;
+ }
+
+
+ public function findAll()
+ {
+ $rq = $this->db->execute($this->buildSelectQuery(), $this->values);
+ if (false === $rq) return false;
+
+ return $rq->fetchAll(\PDO::FETCH_ASSOC);
+ }
+
+
+ public function findAllByColumn($column)
+ {
+ $this->columns = array($column);
+ $rq = $this->db->execute($this->buildSelectQuery(), $this->values);
+ if (false === $rq) return false;
+
+ return $rq->fetchAll(\PDO::FETCH_COLUMN, 0);
+ }
+
+
+ public function findOne()
+ {
+ $this->limit(1);
+ $result = $this->findAll();
+
+ return isset($result[0]) ? $result[0] : null;
+ }
+
+
+ public function findOneColumn($column)
+ {
+ $this->limit(1);
+ $this->columns = array($column);
+
+ $rq = $this->db->execute($this->buildSelectQuery(), $this->values);
+ if (false === $rq) return false;
+
+ return $rq->fetchColumn();
+ }
+
+
+ public function buildSelectQuery()
+ {
+ return sprintf(
+ 'SELECT %s %s FROM %s %s %s %s %s %s %s',
+ $this->distinct ? 'DISTINCT' : '',
+ empty($this->columns) ? '*' : implode(', ', $this->columns),
+ $this->db->escapeIdentifier($this->table_name),
+ implode(' ', $this->joins),
+ $this->conditions(),
+ empty($this->group_by) ? '' : 'GROUP BY '.implode(', ', $this->group_by),
+ $this->sql_order,
+ $this->sql_limit,
+ $this->sql_offset
+ );
+ }
+
+
+ public function count()
+ {
+ $sql = sprintf(
+ 'SELECT COUNT(*) FROM %s'.$this->conditions().$this->sql_order.$this->sql_limit.$this->sql_offset,
+ $this->db->escapeIdentifier($this->table_name)
+ );
+
+ $rq = $this->db->execute($sql, $this->values);
+ if (false === $rq) return false;
+
+ $result = $rq->fetchColumn();
+ return $result ? (int) $result : 0;
+ }
+
+
+ public function join($table, $foreign_column, $local_column)
+ {
+ $this->joins[] = sprintf(
+ 'LEFT JOIN %s ON %s=%s',
+ $this->db->escapeIdentifier($table),
+ $this->db->escapeIdentifier($table).'.'.$this->db->escapeIdentifier($foreign_column),
+ $this->db->escapeIdentifier($this->table_name).'.'.$this->db->escapeIdentifier($local_column)
+ );
+
+ return $this;
+ }
+
+
+ public function conditions()
+ {
+ if (! empty($this->conditions)) {
+
+ return ' WHERE '.implode(' AND ', $this->conditions);
+ }
+ else {
+
+ return '';
+ }
+ }
+
+
+ public function addCondition($sql)
+ {
+ if ($this->is_or_condition) {
+
+ $this->or_conditions[] = $sql;
+ }
+ else {
+
+ $this->conditions[] = $sql;
+ }
+ }
+
+
+ public function beginOr()
+ {
+ $this->is_or_condition = true;
+ $this->or_conditions = array();
+
+ return $this;
+ }
+
+
+ public function closeOr()
+ {
+ $this->is_or_condition = false;
+
+ if (! empty($this->or_conditions)) {
+
+ $this->conditions[] = '('.implode(' OR ', $this->or_conditions).')';
+ }
+
+ return $this;
+ }
+
+
+ public function orderBy($column, $order = 'ASC')
+ {
+ $order = strtoupper($order);
+ $order = $order === 'ASC' || $order === 'DESC' ? $order : 'ASC';
+
+ if ($this->sql_order === '') {
+ $this->sql_order = ' ORDER BY '.$this->db->escapeIdentifier($column).' '.$order;
+ }
+ else {
+ $this->sql_order .= ', '.$this->db->escapeIdentifier($column).' '.$order;
+ }
+
+ return $this;
+ }
+
+
+ public function asc($column)
+ {
+ if ($this->sql_order === '') {
+ $this->sql_order = ' ORDER BY '.$this->db->escapeIdentifier($column).' ASC';
+ }
+ else {
+ $this->sql_order .= ', '.$this->db->escapeIdentifier($column).' ASC';
+ }
+
+ return $this;
+ }
+
+
+ public function desc($column)
+ {
+ if ($this->sql_order === '') {
+ $this->sql_order = ' ORDER BY '.$this->db->escapeIdentifier($column).' DESC';
+ }
+ else {
+ $this->sql_order .= ', '.$this->db->escapeIdentifier($column).' DESC';
+ }
+
+ return $this;
+ }
+
+
+ public function limit($value)
+ {
+ if (! is_null($value)) $this->sql_limit = ' LIMIT '.(int) $value;
+ return $this;
+ }
+
+
+ public function offset($value)
+ {
+ if (! is_null($value)) $this->sql_offset = ' OFFSET '.(int) $value;
+ return $this;
+ }
+
+
+ public function groupBy()
+ {
+ $this->group_by = \func_get_args();
+ return $this;
+ }
+
+
+ public function columns()
+ {
+ $this->columns = \func_get_args();
+ return $this;
+ }
+
+
+ public function distinct()
+ {
+ $this->columns = \func_get_args();
+ $this->distinct = true;
+ return $this;
+ }
+
+
+ public function __call($name, array $arguments)
+ {
+ $column = $arguments[0];
+ $sql = '';
+
+ switch (strtolower($name)) {
+
+ case 'in':
+ if (isset($arguments[1]) && is_array($arguments[1])) {
+
+ $sql = sprintf(
+ '%s IN (%s)',
+ $this->db->escapeIdentifier($column),
+ implode(', ', array_fill(0, count($arguments[1]), '?'))
+ );
+ }
+ break;
+
+ case 'notin':
+ if (isset($arguments[1]) && is_array($arguments[1])) {
+
+ $sql = sprintf(
+ '%s NOT IN (%s)',
+ $this->db->escapeIdentifier($column),
+ implode(', ', array_fill(0, count($arguments[1]), '?'))
+ );
+ }
+ break;
+
+ case 'like':
+ $sql = sprintf('%s LIKE ?', $this->db->escapeIdentifier($column));
+ break;
+
+ case 'eq':
+ case 'equal':
+ case 'equals':
+ $sql = sprintf('%s = ?', $this->db->escapeIdentifier($column));
+ break;
+
+ case 'gt':
+ case 'greaterthan':
+ $sql = sprintf('%s > ?', $this->db->escapeIdentifier($column));
+ break;
+
+ case 'lt':
+ case 'lowerthan':
+ $sql = sprintf('%s < ?', $this->db->escapeIdentifier($column));
+ break;
+
+ case 'gte':
+ case 'greaterthanorequals':
+ $sql = sprintf('%s >= ?', $this->db->escapeIdentifier($column));
+ break;
+
+ case 'lte':
+ case 'lowerthanorequals':
+ $sql = sprintf('%s <= ?', $this->db->escapeIdentifier($column));
+ break;
+
+ case 'isnull':
+ $sql = sprintf('%s IS NULL', $this->db->escapeIdentifier($column));
+ break;
+
+ case 'notnull':
+ $sql = sprintf('%s IS NOT NULL', $this->db->escapeIdentifier($column));
+ break;
+ }
+
+ if ($sql !== '') {
+
+ $this->addCondition($sql);
+
+ if (isset($arguments[1])) {
+
+ if (is_array($arguments[1])) {
+
+ foreach ($arguments[1] as $value) {
+ $this->values[] = $value;
+ }
+ }
+ else {
+
+ $this->values[] = $arguments[1];
+ }
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/vendor/SimpleValidator/Base.php b/vendor/SimpleValidator/Base.php
new file mode 100644
index 00000000..45c01a6e
--- /dev/null
+++ b/vendor/SimpleValidator/Base.php
@@ -0,0 +1,44 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+abstract class Base
+{
+ protected $field = '';
+ protected $error_message = '';
+ protected $data = array();
+
+
+ abstract public function execute(array $data);
+
+
+ public function __construct($field, $error_message)
+ {
+ $this->field = $field;
+ $this->error_message = $error_message;
+ }
+
+
+ public function getErrorMessage()
+ {
+ return $this->error_message;
+ }
+
+
+ public function getField()
+ {
+ return $this->field;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validator.php b/vendor/SimpleValidator/Validator.php
new file mode 100644
index 00000000..8bb4d620
--- /dev/null
+++ b/vendor/SimpleValidator/Validator.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Validator
+{
+ private $data = array();
+ private $errors = array();
+ private $validators = array();
+
+
+ public function __construct(array $data, array $validators)
+ {
+ $this->data = $data;
+ $this->validators = $validators;
+ }
+
+
+ public function execute()
+ {
+ $result = true;
+
+ foreach ($this->validators as $validator) {
+
+ if (! $validator->execute($this->data)) {
+
+ $this->addError(
+ $validator->getField(),
+ $validator->getErrorMessage()
+ );
+
+ $result = false;
+ }
+ }
+
+ return $result;
+ }
+
+
+ public function addError($field, $message)
+ {
+ if (! isset($this->errors[$field])) {
+
+ $this->errors[$field] = array();
+ }
+
+ $this->errors[$field][] = $message;
+ }
+
+
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Alpha.php b/vendor/SimpleValidator/Validators/Alpha.php
new file mode 100644
index 00000000..b00b819b
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Alpha.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Alpha extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (! ctype_alpha($data[$this->field])) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/AlphaNumeric.php b/vendor/SimpleValidator/Validators/AlphaNumeric.php
new file mode 100644
index 00000000..e1762d67
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/AlphaNumeric.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class AlphaNumeric extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (! ctype_alnum($data[$this->field])) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Email.php b/vendor/SimpleValidator/Validators/Email.php
new file mode 100644
index 00000000..e4e3d5d6
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Email.php
@@ -0,0 +1,81 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Email extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ // I use the same validation method as Firefox
+ // http://hg.mozilla.org/mozilla-central/file/cf5da681d577/content/html/content/src/nsHTMLInputElement.cpp#l3967
+
+ $value = $data[$this->field];
+ $length = strlen($value);
+
+ // If the email address begins with a '@' or ends with a '.',
+ // we know it's invalid.
+ if ($value[0] === '@' || $value[$length - 1] === '.') {
+
+ return false;
+ }
+
+ // Check the username
+ for ($i = 0; $i < $length && $value[$i] !== '@'; ++$i) {
+
+ $c = $value[$i];
+
+ if (! (ctype_alnum($c) || $c === '.' || $c === '!' || $c === '#' || $c === '$' ||
+ $c === '%' || $c === '&' || $c === '\'' || $c === '*' || $c === '+' ||
+ $c === '-' || $c === '/' || $c === '=' || $c === '?' || $c === '^' ||
+ $c === '_' || $c === '`' || $c === '{' || $c === '|' || $c === '}' ||
+ $c === '~')) {
+
+ return false;
+ }
+ }
+
+ // There is no domain name (or it's one-character long),
+ // that's not a valid email address.
+ if (++$i >= $length) return false;
+ if (($i + 1) === $length) return false;
+
+ // The domain name can't begin with a dot.
+ if ($value[$i] === '.') return false;
+
+ // Parsing the domain name.
+ for (; $i < $length; ++$i) {
+
+ $c = $value[$i];
+
+ if ($c === '.') {
+
+ // A dot can't follow a dot.
+ if ($value[$i - 1] === '.') return false;
+ }
+ elseif (! (ctype_alnum($c) || $c === '-')) {
+
+ // The domain characters have to be in this list to be valid.
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Equals.php b/vendor/SimpleValidator/Validators/Equals.php
new file mode 100644
index 00000000..91f34e4b
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Equals.php
@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Equals extends Base
+{
+ private $field2;
+
+
+ public function __construct($field1, $field2, $error_message)
+ {
+ parent::__construct($field1, $error_message);
+
+ $this->field2 = $field2;
+ }
+
+
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (! isset($data[$this->field2])) return false;
+
+ return $data[$this->field] === $data[$this->field2];
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Integer.php b/vendor/SimpleValidator/Validators/Integer.php
new file mode 100644
index 00000000..150558a3
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Integer.php
@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Integer extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (is_string($data[$this->field])) {
+
+ if ($data[$this->field][0] === '-') {
+
+ return ctype_digit(substr($data[$this->field], 1));
+ }
+
+ return ctype_digit($data[$this->field]);
+ }
+ else {
+
+ return is_int($data[$this->field]);
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Ip.php b/vendor/SimpleValidator/Validators/Ip.php
new file mode 100644
index 00000000..48afe569
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Ip.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Ip extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (! filter_var($data[$this->field], FILTER_VALIDATE_IP)) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Length.php b/vendor/SimpleValidator/Validators/Length.php
new file mode 100644
index 00000000..36e50b37
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Length.php
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Length extends Base
+{
+ private $min;
+ private $max;
+
+
+ public function __construct($field, $error_message, $min, $max)
+ {
+ parent::__construct($field, $error_message);
+
+ $this->min = $min;
+ $this->max = $max;
+ }
+
+
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ $length = mb_strlen($data[$this->field], 'UTF-8');
+
+ if ($length < $this->min || $length > $this->max) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/MacAddress.php b/vendor/SimpleValidator/Validators/MacAddress.php
new file mode 100644
index 00000000..d9348417
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/MacAddress.php
@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class MacAddress extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ $groups = explode(':', $data[$this->field]);
+
+ if (count($groups) !== 6) return false;
+
+ foreach ($groups as $group) {
+
+ if (! ctype_xdigit($group)) return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/MaxLength.php b/vendor/SimpleValidator/Validators/MaxLength.php
new file mode 100644
index 00000000..d8e032b0
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/MaxLength.php
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class MaxLength extends Base
+{
+ private $max;
+
+
+ public function __construct($field, $error_message, $max)
+ {
+ parent::__construct($field, $error_message);
+
+ $this->max = $max;
+ }
+
+
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ $length = mb_strlen($data[$this->field], 'UTF-8');
+
+ if ($length > $this->max) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/MinLength.php b/vendor/SimpleValidator/Validators/MinLength.php
new file mode 100644
index 00000000..4b7f7d24
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/MinLength.php
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class MinLength extends Base
+{
+ private $min;
+
+
+ public function __construct($field, $error_message, $min)
+ {
+ parent::__construct($field, $error_message);
+
+ $this->min = $min;
+ }
+
+
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ $length = mb_strlen($data[$this->field], 'UTF-8');
+
+ if ($length < $this->min) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Numeric.php b/vendor/SimpleValidator/Validators/Numeric.php
new file mode 100644
index 00000000..a958df1a
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Numeric.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Numeric extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (! is_numeric($data[$this->field])) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Range.php b/vendor/SimpleValidator/Validators/Range.php
new file mode 100644
index 00000000..1d71b926
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Range.php
@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Range extends Base
+{
+ private $min;
+ private $max;
+
+
+ public function __construct($field, $error_message, $min, $max)
+ {
+ parent::__construct($field, $error_message);
+
+ $this->min = $min;
+ $this->max = $max;
+ }
+
+
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (! is_numeric($data[$this->field])) {
+
+ return false;
+ }
+
+ if ($data[$this->field] < $this->min || $data[$this->field] > $this->max) {
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Required.php b/vendor/SimpleValidator/Validators/Required.php
new file mode 100644
index 00000000..e7ef2714
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Required.php
@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Required extends Base
+{
+ public function execute(array $data)
+ {
+ if (! isset($data[$this->field]) || $data[$this->field] === '') {
+
+ return false;
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Unique.php b/vendor/SimpleValidator/Validators/Unique.php
new file mode 100644
index 00000000..c20dbe11
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Unique.php
@@ -0,0 +1,78 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ */
+class Unique extends Base
+{
+ private $pdo;
+ private $primary_key;
+ private $table;
+
+
+ public function __construct($field, $error_message, \PDO $pdo, $table, $primary_key = 'id')
+ {
+ parent::__construct($field, $error_message);
+
+ $this->pdo = $pdo;
+ $this->primary_key = $primary_key;
+ $this->table = $table;
+ }
+
+
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ if (! isset($data[$this->primary_key])) {
+
+ $rq = $this->pdo->prepare('SELECT COUNT(*) FROM '.$this->table.' WHERE '.$this->field.'=?');
+
+ $rq->execute(array(
+ $data[$this->field]
+ ));
+
+ $result = $rq->fetch(\PDO::FETCH_NUM);
+
+ if (isset($result[0]) && $result[0] === '1') {
+
+ return false;
+ }
+ }
+ else {
+
+ $rq = $this->pdo->prepare(
+ 'SELECT COUNT(*) FROM '.$this->table.'
+ WHERE '.$this->field.'=? AND '.$this->primary_key.' != ?'
+ );
+
+ $rq->execute(array(
+ $data[$this->field],
+ $data[$this->primary_key]
+ ));
+
+ $result = $rq->fetch(\PDO::FETCH_NUM);
+
+ if (isset($result[0]) && $result[0] === '1') {
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/SimpleValidator/Validators/Version.php b/vendor/SimpleValidator/Validators/Version.php
new file mode 100644
index 00000000..273a28a5
--- /dev/null
+++ b/vendor/SimpleValidator/Validators/Version.php
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Simple Validator.
+ *
+ * (c) Frédéric Guillot <contact@fredericguillot.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace SimpleValidator\Validators;
+
+use SimpleValidator\Base;
+
+/**
+ * @author Frédéric Guillot <contact@fredericguillot.com>
+ * @link http://semver.org/
+ */
+class Version extends Base
+{
+ public function execute(array $data)
+ {
+ if (isset($data[$this->field]) && $data[$this->field] !== '') {
+
+ $pattern = '/^[0-9]+\.[0-9]+\.[0-9]+([+-][^+-][0-9A-Za-z-.]*)?$/';
+ return (bool) preg_match($pattern, $data[$this->field]);
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/vendor/password.php b/vendor/password.php
new file mode 100644
index 00000000..c6e84cbd
--- /dev/null
+++ b/vendor/password.php
@@ -0,0 +1,227 @@
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ */
+
+if (!defined('PASSWORD_BCRYPT')) {
+
+ define('PASSWORD_BCRYPT', 1);
+ define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+
+ if (version_compare(PHP_VERSION, '5.3.7', '<')) {
+
+ define('PASSWORD_PREFIX', '$2a$');
+ }
+ else {
+
+ define('PASSWORD_PREFIX', '$2y$');
+ }
+
+ /**
+ * Hash the password using the specified algorithm
+ *
+ * @param string $password The password to hash
+ * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
+ * @param array $options The options for the algorithm to use
+ *
+ * @return string|false The hashed password, or false on error.
+ */
+ function password_hash($password, $algo, array $options = array()) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+ return null;
+ }
+ if (!is_string($password)) {
+ trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+ return null;
+ }
+ if (!is_int($algo)) {
+ trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+ return null;
+ }
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
+ $cost = 10;
+ if (isset($options['cost'])) {
+ $cost = $options['cost'];
+ if ($cost < 4 || $cost > 31) {
+ trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+ return null;
+ }
+ }
+ $required_salt_len = 22;
+ $hash_format = sprintf("%s%02d$", PASSWORD_PREFIX, $cost);
+ break;
+ default:
+ trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+ return null;
+ }
+ if (isset($options['salt'])) {
+ switch (gettype($options['salt'])) {
+ case 'NULL':
+ case 'boolean':
+ case 'integer':
+ case 'double':
+ case 'string':
+ $salt = (string) $options['salt'];
+ break;
+ case 'object':
+ if (method_exists($options['salt'], '__tostring')) {
+ $salt = (string) $options['salt'];
+ break;
+ }
+ case 'array':
+ case 'resource':
+ default:
+ trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
+ return null;
+ }
+ if (strlen($salt) < $required_salt_len) {
+ trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
+ return null;
+ } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+ $salt = str_replace('+', '.', base64_encode($salt));
+ }
+ } else {
+ $buffer = '';
+ $raw_length = (int) ($required_salt_len * 3 / 4 + 1);
+ $buffer_valid = false;
+ if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
+ $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+ $buffer = openssl_random_pseudo_bytes($raw_length);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && is_readable('/dev/urandom')) {
+ $f = fopen('/dev/urandom', 'r');
+ $read = strlen($buffer);
+ while ($read < $raw_length) {
+ $buffer .= fread($f, $raw_length - $read);
+ $read = strlen($buffer);
+ }
+ fclose($f);
+ if ($read >= $raw_length) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid || strlen($buffer) < $raw_length) {
+ $bl = strlen($buffer);
+ for ($i = 0; $i < $raw_length; $i++) {
+ if ($i < $bl) {
+ $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+ } else {
+ $buffer .= chr(mt_rand(0, 255));
+ }
+ }
+ }
+ $salt = str_replace('+', '.', base64_encode($buffer));
+
+ }
+ $salt = substr($salt, 0, $required_salt_len);
+
+ $hash = $hash_format . $salt;
+
+ $ret = crypt($password, $hash);
+
+ if (!is_string($ret) || strlen($ret) <= 13) {
+ return false;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get information about the password hash. Returns an array of the information
+ * that was used to generate the password hash.
+ *
+ * array(
+ * 'algo' => 1,
+ * 'algoName' => 'bcrypt',
+ * 'options' => array(
+ * 'cost' => 10,
+ * ),
+ * )
+ *
+ * @param string $hash The password hash to extract info from
+ *
+ * @return array The array of information about the hash.
+ */
+ function password_get_info($hash) {
+ $return = array(
+ 'algo' => 0,
+ 'algoName' => 'unknown',
+ 'options' => array(),
+ );
+ if (substr($hash, 0, 4) == PASSWORD_PREFIX && strlen($hash) == 60) {
+ $return['algo'] = PASSWORD_BCRYPT;
+ $return['algoName'] = 'bcrypt';
+ list($cost) = sscanf($hash, PASSWORD_PREFIX."%d$");
+ $return['options']['cost'] = $cost;
+ }
+ return $return;
+ }
+
+ /**
+ * Determine if the password hash needs to be rehashed according to the options provided
+ *
+ * If the answer is true, after validating the password using password_verify, rehash it.
+ *
+ * @param string $hash The hash to test
+ * @param int $algo The algorithm used for new password hashes
+ * @param array $options The options array passed to password_hash
+ *
+ * @return boolean True if the password needs to be rehashed.
+ */
+ function password_needs_rehash($hash, $algo, array $options = array()) {
+ $info = password_get_info($hash);
+ if ($info['algo'] != $algo) {
+ return true;
+ }
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ $cost = isset($options['cost']) ? $options['cost'] : 10;
+ if ($cost != $info['options']['cost']) {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Verify a password against a hash using a timing attack resistant approach
+ *
+ * @param string $password The password to verify
+ * @param string $hash The hash to verify against
+ *
+ * @return boolean If the password matches the hash
+ */
+ function password_verify($password, $hash) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
+ return false;
+ }
+ $ret = crypt($password, $hash);
+ if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
+ return false;
+ }
+
+ $status = 0;
+ for ($i = 0; $i < strlen($ret); $i++) {
+ $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+ }
+
+ return $status === 0;
+ }
+}