First commit
This commit is contained in:
commit
9383a15af6
|
|
@ -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 #
|
||||
################
|
||||
|
|
@ -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/>.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
}());
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Deny from all
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Deny from all
|
||||
|
|
@ -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();
|
||||
|
|
@ -0,0 +1 @@
|
|||
Deny from all
|
||||
|
|
@ -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"' : '').'> '.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);
|
||||
}
|
||||
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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é',
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
Deny from all
|
||||
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()."')
|
||||
");
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
|
|
@ -0,0 +1 @@
|
|||
Deny from all
|
||||
|
|
@ -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&action=update&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&action=confirm&project_id=<?= $project['id'] ?>&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&action=add&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>
|
||||
|
|
@ -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&action=show&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&action=create&project_id=<?= $column['project_id'] ?>&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&action=show&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>
|
||||
|
|
@ -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&action=remove&column_id=<?= $column['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
|
||||
<?= t('or') ?> <a href="?controller=board&action=edit&project_id=<?= $column['project_id'] ?>"><?= t('cancel') ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -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&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&action=downloadDb"><?= t('Download the database') ?></a>
|
||||
<?= t('(Gzip compressed Sqlite file)') ?>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?controller=config&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&action=edit&user_id=<?= $user['id'] ?>"><?= t('edit') ?></a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -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&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>
|
||||
|
|
@ -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&action=update&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>
|
||||
|
|
@ -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&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&action=show&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&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit project') ?></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?controller=board&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit board') ?></a>
|
||||
</li>
|
||||
<li>
|
||||
<?php if ($project['is_active']): ?>
|
||||
<a href="?controller=project&action=disable&project_id=<?= $project['id'] ?>"><?= t('Disable') ?></a>
|
||||
<?php else: ?>
|
||||
<a href="?controller=project&action=enable&project_id=<?= $project['id'] ?>"><?= t('Enable') ?></a>
|
||||
<?php endif ?>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?controller=project&action=confirm&project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -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&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>
|
||||
|
|
@ -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&action=remove&project_id=<?= $project['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
|
||||
<?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -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&action=close&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
|
||||
<?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -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&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&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<section id="main">
|
||||
<div class="page-header">
|
||||
<h2><?= t('New task') ?></h2>
|
||||
</div>
|
||||
<section>
|
||||
<form method="post" action="?controller=task&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&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -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&action=open&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
|
||||
<?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -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&action=show&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&action=edit&task_id=<?= $task['id'] ?>"><?= t('Edit') ?></a>
|
||||
<?= t('or') ?>
|
||||
<?php if ($task['is_active'] == 1): ?>
|
||||
<a href="?controller=task&action=confirmClose&task_id=<?= $task['id'] ?>"><?= t('close this task') ?></a>
|
||||
<?php else: ?>
|
||||
<a href="?controller=task&action=confirmOpen&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>
|
||||
|
|
@ -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&action=update&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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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&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&action=edit&user_id=<?= $user['id'] ?>"><?= t('edit') ?></a>
|
||||
<?php if (count($users) > 1): ?>
|
||||
<?= t('or') ?>
|
||||
<a href="?controller=user&action=confirm&user_id=<?= $user['id'] ?>"><?= t('remove') ?></a>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -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&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>
|
||||
|
|
@ -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&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>
|
||||
|
|
@ -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&action=remove&user_id=<?= $user['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
|
||||
<?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1 @@
|
|||
Deny from all
|
||||
|
|
@ -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.
|
||||
|
|
@ -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('&', '&', $element['»']);
|
||||
$element['»'] = str_replace('<', '<', $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 .= '&';
|
||||
|
||||
$offset = substr($text, 0, 5) === '&' ? 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('&', '&', $element_url);
|
||||
$element_url = str_replace('<', '<', $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 .= '<';
|
||||
|
||||
$offset = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$markup .= '<';
|
||||
|
||||
$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('&', '&', $element_url);
|
||||
$element_url = str_replace('<', '<', $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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.'"';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue