From: Paul Hänsch Date: Sat, 20 Apr 2024 19:32:56 +0000 (+0200) Subject: styling for iban assignment X-Git-Url: https://git.plutz.net/?p=confetti;a=commitdiff_plain;h=HEAD;hp=fcfbf01dba9c932dbb66abffa8f65bf1c7a15081 styling for iban assignment --- diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24781a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: _subtrees + +_subtrees: _cgilite + +cgilite: + git subtree add --squash -P $@ https://git.plutz.net/git/$@ master + +_cgilite: cgilite + git subtree pull --squash -P $< https://git.plutz.net/git/$< master diff --git a/cards/edit_card.sh b/cards/edit_card.sh new file mode 100755 index 0000000..897bcc2 --- /dev/null +++ b/cards/edit_card.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# Copyright 2019 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +locktimeout=900 +. "$_EXEC"/session_lock.sh + +card="$(GET card |PATH)" +cardfile="$_DATA/vcard/${card##*/}" +filter="$(REF f)" +order="$(REF o)" + +if tempfile="$(SLOCK "$cardfile" "$locktimeout")"; then + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}" +elif [ -f "$tempfile" ]; then + SET_COOKIE session message="SESSLOCK" + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}" +else + SET_COOKIE session message="EDITLOCK" + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}" +fi diff --git a/cards/export_card.sh b/cards/export_card.sh new file mode 100755 index 0000000..0918032 --- /dev/null +++ b/cards/export_card.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# Copyright 2014, 2015, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +card="$(GET card |PATH)" +cardfile="$_DATA/vcard/${card##*/}" + +. $_EXEC/pdiread.sh +. $_EXEC/cgilite/file.sh + +printf 'Content-Disposition: inline; filename="%s.vcf"\r\n' "$(pdi_value "$(pdi_load "$cardfile")" FN)" + +FILE "$cardfile" "text/vcard; charset=utf-8" diff --git a/cards/export_csv.sh b/cards/export_csv.sh new file mode 100755 index 0000000..b566c03 --- /dev/null +++ b/cards/export_csv.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +. $_EXEC/pdiread.sh +. $_EXEC/cards/l10n.sh +. $_EXEC/cards/list.sh + +filter="$(GET f)" +order="$(GET o)" + +[ "$order" ] || order=firstname + +list_attendance() { + grep -F " ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do + { pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY || l10n "(unnamed course)"; } |unescape + done \ + | sed -E 's;";\\";g;' +} + +list_item() { + local item="$1" + local cnt="$(pdi_count "$card" "$item")" + local ret='' + + seq 1 $cnt |while read n; do case $item in + TEL) + tel="$(pdi_value "$card" "$item" "$n" |unescape)" + ttype="$(pdi_attrib "$card" "$item" "$n" TYPE)" + if [ "$tel" -a "$ttype" ]; then + printf '%s: %s\n' "$(l10n "TYPE=$ttype")" "$tel" + elif [ "$tel" ]; then + printf '%s\n' "$tel" + fi + ;; + GENDER) + gen="$(pdi_value "$card" "$item" "$n" |unescape)" + [ "$gen" ] && l10n "gender_$gen" + ;; + *) pdi_value "$card" "$item" "$n" |unescape + ;; + esac; done \ + | sed -E 's;";\\";g;' +} + +printf '%s\r\n' \ + 'Content-Type: text/csv; charset=utf-8' \ + 'Content-Disposition: inline; filename="confetti_export_'$(date +%F_%T)'.csv"' \ + '' + +printf '"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s"\n' \ + "$(l10n FN)" "$(l10n GENDER)" "$(l10n BDAY)" \ + "$(l10n TEL)" "$(l10n EMAIL)" "$(l10n ADR)" \ + "$(l10n NOTE)" "$(l10n courses)" "$(l10n CATEGORIES)" \ +| sed -E 's;­\;;;g;' + + +filter_cards \ +| order_cards \ +| while read cardfile; do + card="$(pdi_load "$cardfile")" + printf '"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s"\n' \ + "$(list_item FN)" "$(list_item GENDER)" "$(list_item BDAY)" \ + "$(list_item TEL)" "$(list_item EMAIL)" "$(list_item ADR)" \ + "$(list_item NOTE)" "$(list_attendance)" "$(list_item CATEGORIES)" +done diff --git a/cards/filter_card.sh b/cards/filter_card.sh new file mode 100755 index 0000000..504fb08 --- /dev/null +++ b/cards/filter_card.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +# Copyright 2014, 2017, 2019 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +filter="$( + seq 0 100 |while read n; do + filter_type="$(POST "filter_type${n}")" + filter_text="$(POST "filter_text${n}")" + if [ ! "$filter_type" -a ! "$filter_text" ]; then + break + elif [ "$filter_type" = CATEGORIES ]; then + printf '^CATEGORIES:' + seq 0 $(POST_COUNT filter_cat$n) |while read m; do + printf '|%s' "$(POST filter_cat$n $m)" + done + elif [ "$filter_type" = course ]; then + printf '^course:' + seq 0 $(POST_COUNT filter_course$n) |while read m; do + printf '|%s' "$(POST filter_course$n $m)" + done + else + printf '^%s:%s' "$filter_type" "$filter_text" + fi + done | sed -E \ + 's;\|+;\|;g; s;\^+;\^;g; s;:\|;:;g; + :X; s;\^[^:]*:\^;\^;g; /\^[^:]*:\^/bX; + s;^\^;;; s;\^[^:]*:$;;;' +)" + +case $(POST choice) in + filter) + REDIRECT "${_BASE}/cards/?o=$(POST order)&f=${filter}" + ;; + new_filter) + REDIRECT "${_BASE}/cards/?o=$(POST order)&f=${filter}&newfilter=yes" + ;; + export_csv) + REDIRECT "${_BASE}/cards/export_csv.sh?o=$(POST order)&f=${filter}" + ;; + *) + REDIRECT "${_BASE}/cards/" + ;; +esac diff --git a/cards/index.cgi b/cards/index.cgi new file mode 100755 index 0000000..5babc9f --- /dev/null +++ b/cards/index.cgi @@ -0,0 +1,23 @@ +#!/bin/sh + +. $_EXEC/pdiread.sh +. $_EXEC/cards/l10n.sh +. $_EXEC/cards/widgets.sh +. $_EXEC/cards/list.sh + +filter="$(GET f)" +order="$(GET o)" +edit="$(GET e |PATH)" + +[ "$order" ] || order=firstname +edit="${edit##*/}" + +{ w_filter_diag + printf ' + [form class="newcard" action="%s/cards/new_card.sh" method="POST" + [button type="submit" %s] + [input name="seed" placeholder="%s"] + ]' "${_BASE}" "$(l10n newcard)" "$(l10n vcf_seed_label)" + [ "$edit" ] && edit_card "$edit" + list_cards +} | yield_page cards #/cards/cards.css diff --git a/cards/l10n.sh b/cards/l10n.sh new file mode 100755 index 0000000..fde98e0 --- /dev/null +++ b/cards/l10n.sh @@ -0,0 +1,64 @@ +# Copyright 2014, 2016, 2019, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +l10n(){ + local word + [ $# -eq 0 ] && read -r word || word="$*" + case $word in + newcard) printf %s "Neuen Eintrag anlegen";; + + X-HEALTH-INSURANCE) printf %s "Kran­ken­ver­sich­er­ung";; + hi_from_list) printf %s "Aus Liste";; + hi_other) printf %s "Andere";; + hi_company) printf %s "Ver­sich­er­ungs­ge­sell­schaft";; + hi_number) printf %s "Ver­sich­er­ten­num­mer";; + hi_status) printf %s "Ver­sich­er­ten­sta­tus";; + X-HEALTH-INSURANCE-NOCONTRIB) printf %s "Zu­zahl­ungs­be­frei­ung";; + X-CLIENT-REFERRAL) printf %s "Empfehl­ung durch";; + prescriptions) printf %s "Verord­nungen";; + new_prescription) printf %s "Neue Verord­nung";; + no_icd) printf %s "Kein ICD Code";; + + X-ZACK-JOINDATE) printf %s "Anmelde­datum";; + X-ZACK-LEAVEDATE) printf %s "Abmelde­datum";; + X-ZACK-JOINDATE_short) printf %s "Anm.";; + X-ZACK-LEAVEDATE_short) printf %s "Abm.";; + X-IBAN) printf %s "IBAN";; + + *) l10n_global "$word";; + esac +} + +# BEGIN) printf %s "";; +# CALADRURI) printf %s "";; +# CALURI) printf %s "";; +# CLASS) printf %s "";; +# CLIENTPIDMAP) printf %s "";; +# END) printf %s "";; +# FBURL) printf %s "";; +# GEO) printf %s "";; +# MAILER) printf %s "";; +# NAME) printf %s "";; +# PRODID) printf %s "";; +# PROFILE) printf %s "";; +# REV) printf %s "";; +# SORT-STRING) printf %s "";; +# SOURCE) printf %s "";; +# TZ) printf %s "";; +# UID) printf %s "";; +# VERSION) printf %s "";; +# XML) printf %s "";; diff --git a/cards/list.sh b/cards/list.sh new file mode 100755 index 0000000..9511fc1 --- /dev/null +++ b/cards/list.sh @@ -0,0 +1,215 @@ +#!/bin/sh + +. "${_EXEC}"/pdiread.sh + +edit_card(){ + local cardfile="$_DATA/vcard/$1" + local tempfile card + + . $_EXEC/session_lock.sh + + if ! tempfile="$(CHECK_SLOCK "$cardfile")"; then + printf '[div .message %s]' "$(l10n "This card is not set up for editing within this session.")" + else + card="$(pdi_load "$tempfile")" + cat <<-EOF + [form .card #${cardfile##*/} action="${_BASE}/cards/update_card.sh" method="POST" + [input type="hidden" name="tid" value="$(transid ${tempfile})"] + [div .section .basic $( + edit_item "$card" N GENDER + [ "$(pdi_count "$card" NICKNAME)" -gt 0 ] \ + && edit_item "$card" NICKNAME + edit_item "$card" BDAY + edit_item "$card" X-ZACK-JOINDATE + [ "$(pdi_count "$card" X-ZACK-LEAVEDATE)" -gt 0 ] \ + && edit_item "$card" X-ZACK-LEAVEDATE + card_item "$card" SOUND PHOTO LOGO + )] + [div .section .phone $(edit_item "$card" TEL)] + [div .section .message $( + edit_item "$card" EMAIL + [ $(pdi_count "$card" IMPP) -gt 0 ] && edit_item "$card" IMPP + [ $(pdi_count "$card" URL ) -gt 0 ] && edit_item "$card" URL + )] + [div .section .address $( + edit_item "$card" ADR + [ $(pdi_count "$card" X-IBAN) -gt 0 ] && edit_item "$card" X-IBAN + )] + [div .section .note $(edit_item "$card" NOTE)] + [div .section .attendance + [h3 $(l10n course_attendance) ] [div .attendance $( + list_courses |while IFS=/ read course coursename; do + courseH="$(HTML "$course")" + printf '[input type="checkbox" id="cour%s" name="attendance" value="%s" %s][label for="cour%s" . %s]' \ + "$courseH" "$courseH" \ + "$(grep -qF "${course} ${cardfile##*/}" "$_DATA/mappings/attendance" \ + && printf 'checked="checked"' + )" \ + "$courseH" "$coursename" + done)] + [h3 $(l10n CATEGORIES) ] [div .categories $( + grep -xE '[^ ]+' "$_DATA"/mappings/categories |while read -r cat; do + catH="$(HTML "$cat")" + printf '[input type="checkbox" id="cat%s" name="CATEGORIES" value="%s" %s][label for="cat%s" . %s]' \ + "$catH" "$catH" \ + "$(seq 1 $(pdi_count "$card" CATEGORIES) |while read c; do + pdi_value "$card" CATEGORIES $c |grep -qxF "$cat" \ + && printf 'checked="checked"' && break + done)" \ + "$catH" "$catH" + done)] + ] + [div .control + [div .item .delete label="$(l10n edit_delete)" + [input type="checkbox" #delete] + [label for="delete" $(l10n edit_delete)] + [button type="submit" name="action" value="delete" $(l10n edit_delete)] + ] + [div .item .newfield + [select name="newfield" + [option value="" disabled="disabled" selected="selected" $(l10n edit_addfieldtext)] + $(for f in NICKNAME EMAIL TEL IMPP ADR URL NOTE X-ZACK-LEAVEDATE X-IBAN; do + printf '[option value="%s" %s] ' "$f" "$(l10n "$f")" + done) + ][button type="submit" name="action" value="addfield" $(l10n edit_addfield)] + ] + [button .item type="submit" name="action" value="update" $(l10n edit_update)] + [button .item type="submit" name="action" value="cancel" $(l10n edit_cancel)] + ] + [input type="hidden" name="UID" value="$(pdi_value "$card" UID |HTML)"] + [input type="hidden" name="card" value="${cardfile##*/}"] + ] + EOF + fi +} + +print_card(){ + local cardfile="$1" + local card="$(pdi_load "$cardfile")" + cat <<-EOF + [div .card #${cardfile##*/} + [div .section .basic . $( + card_item "$card" FN GENDER NICKNAME BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE SOUND PHOTO LOGO + )] + [div .section .phone . $(card_item "$card" TEL)] + [div .section .message . $(card_item "$card" EMAIL IMPP URL)] + [div .section .address . $(card_item "$card" ADR X-IBAN)] + [div .section .note . $(card_item "$card" NOTE)] + [div .section .attendance [h3 $(l10n course_attendance) ] [ul + $(grep -F " ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do + printf '[li [a .item .attendance href="%s/courses#%s" . %s]]' \ + "${_BASE}" "$each" \ + "$(pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY || l10n "(unnamed course)" |unescape |HTML)" + done |sort -k7)] + $(card_item "$card" CATEGORIES) + ] + [div .control + [a .button .item href="${_BASE}/cards/edit_card.sh?card=${cardfile##*/}" $(l10n edit)] + [a .button .item href="${_BASE}/cards/export_card.sh?card=${cardfile##*/}" $(l10n vcf_export)] + ] + ] + EOF +} + +print_cards(){ + local cardfile cachefile date size name ldate=0 lsize lname + + while read cardfile; do + cachefile="${_DATA}/cache/${cardfile##*/}.cache" + if [ -s "$cachefile" -a "$cachefile" -nt "$cardfile" ]; then + cat "$cachefile" + elif [ -s "$cardfile" ]; then + print_card "$cardfile" |tee "$cachefile" + fi + done +} + +filter_attendance(){ + fatt="$1" + attfile="$_DATA/mappings/attendance" + + if [ ! "$fatt" ]; then + # debug 'list all' + printf '%s\n' "$_DATA/vcard"/*.vcf + elif [ "${fatt#* }" = "${fatt}" ]; then + # debug "list $fatt" + grep -xiE "(${fatt}) .+vcf" "$attfile" \ + | while read vcf; do + printf '%s/vcard/%s\n' "$_DATA" "${vcf##* }" + done + else + # debug "filter ${fatt%% *}" + filter_attendance "${fatt#* }" \ + | while read vcf; do + grep -xiE "(${fatt%% *}) ${vcf##*/}" "$attfile" + done \ + | while read vcf; do + printf '%s/vcard/%s\n' "$_DATA" "${vcf##* }" + done + fi +} + +filter_cards(){ + local filter f fex='x;p;' + local upcase=' y;abcdefghijklmnopqrstuvwxyzäöüé;ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉ;; ' + + filter="$(printf %s "${filter}" \ + | sed -E 's;[]\/\(\)\\\$\?\.\+\*\;\[\{\}];\\&;g; + '"$upcase" + )^" + + while [ "$filter" ]; do + f="${filter%%^*}" filter="${filter#*^}" + case $f in + '') break + ;; + COURSE:*) fatt="${fatt}${fatt:+ }${f#*:}" + ;; + ANY:*) fex="/\n.*(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}" + ;; + NAME:*) fex="/\n(N|FN|NICKNAME)(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}" + ;; + STREET:*|ZIP:*) fex="/\nADR(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}" + ;; + *) fex="/\n${f%%:*}(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}" + ;; + esac + done + + # for cardfile in "${_DATA}"/vcard/*.vcf; do + filter_attendance "$fatt" |while read cardfile; do + printf '%s\n' "$cardfile" + cat "$cardfile" + done \ + | sed -nE ':X; /\nEND\;?:VCARD\r?$/!{ N; bX; }; h; s;\n.*$;;; x; s;^[^\n]+\n;;; + '"$upcase""$fex" +} + +order_cards() { + local cardfile card + + while read cardfile; do + card="$(pdi_load "$cardfile")" + + case $order in + firstname) + printf '%s %s\n' "$(pdi_value "$card" FN)" "$cardfile" + ;; + lastname) + printf '%s %s\n' "$(pdi_value "$card" N || pdi_value "$card" FN)" "$cardfile" + ;; + bdate) + printf '%s %s\n' "$(pdi_value "$card" BDAY || printf 0000-00-00)" "$cardfile" + ;; + esac + done \ + | sort \ + | sed -E 's;^.*\t;;g' +} + +list_cards(){ + filter_cards \ + | order_cards \ + | grep -xvF "$edit" \ + | print_cards +} diff --git a/cards/new_card.sh b/cards/new_card.sh new file mode 100755 index 0000000..69f89c6 --- /dev/null +++ b/cards/new_card.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +# Copyright 2014, 2019 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +filter="$(REF f)" +order="$(REF o)" + +uid="$(timeid)$(randomid)" # 32 Octets UID, starting with timestamp +card="${uid}.vcf" + +vcf_escape(){ + for each in "$@"; do + printf %s\\n "$each" \ + | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;' + done \ + | sed -E ':X;$!{N;bX}; s;\n;\;;g' +} + +IFS='|' read -r date fn ln bday bmonth byear tel tcell junk1 email junk2 note <<-EOF + $(POST seed |tr \\t \|) + EOF + +[ ${#byear} = 1 ] && byear="200$byear" +[ ${#byear} = 2 ] && byear="20$byear" +[ ${#bmonth} = 1 ] && bmonth="0$bmonth" +[ ${#bday} = 1 ] && bday="0$bday" + +mn="" +case $fn in + *\ *) + mn="${fn#* }" + fn="${fn%% *}" + ;; +esac + +mkdir -p "${_DATA}/lock/vcard/" +lockdir="${_DATA}/lock/vcard/${card}/" +lockfile=${lockdir}/${SESSION_ID} + +if mkdir "$lockdir"; then + cat >"$lockfile" <<-EOF + BEGIN:VCARD + VERSION:4.0 + N:$(vcf_escape "$ln" "$fn" "$mn" "" "") + FN:$(vcf_escape "${fn}${mn:+ }${mn} ${ln}") + BDAY:$(parse_date "${byear}-${bmonth}-${bday}") + TEL:$(vcf_escape "$tel") + TEL;TYPE=CELL:$(vcf_escape "$tcell") + EMAIL:$(vcf_escape "$email") + X-ZACK-JOINDATE:$(parse_date "$date") + ADR: + NOTE:$(vcf_escape "$note") + UID:${uid} + END:VCARD + EOF + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}" +else + SET_COOKIE session message="EDITLOCK" + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}" +fi diff --git a/cards/update_card.sh b/cards/update_card.sh new file mode 100755 index 0000000..d57f503 --- /dev/null +++ b/cards/update_card.sh @@ -0,0 +1,151 @@ +#!/bin/sh + +# Copyright 2014, 2016, 2019, 2020, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +. "$_EXEC/pdiread.sh" +. "$_EXEC/session_lock.sh" +. "$_EXEC/cgilite/storage.sh" + +unset filter order card action newfield +unset cardfile attfile tempfile +unset vcf field cnt delete_key + +filter="$(REF f)" +order="$(REF o)" + +card="$(POST card |PATH)"; card="${card##*/}" +cardfile="$_DATA/vcard/${card}" +attfile="$_DATA/mappings/attendance" + +action="$(POST action)" +newfield="$(POST newfield |grep -m 1 -xE '[A-Z][A-Z0-9-]*')" + +if printf '%s\n' "$action" |grep -qxE 'addfield [A-Z][A-Z0-9-]*'; then + newfield="${action##* }" + action=addfield +fi + +if ! tempfile=$(CHECK_SLOCK "$cardfile"); then + SET_COOKIE 0 message="NO VALID FILE LOCK" + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}" + exit 0 +elif [ "$(POST tid)" != "$(transid "$tempfile")" ]; then + SET_COOKIE 0 message="INVALID TRANSACTION ID" + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}" + exit 0 +fi + +vcf_escape(){ + for each in "$@"; do + printf %s\\n "$each" \ + | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;' + done \ + | sed -E ':X;$!{N;bX}; s;\n;\;;g' +} + +# [ "${_POST[hi_select]}" = "list" ] || _POST[hi_company]="${_POST[hi_other]}" +# [ -n "${_POST[hi_company]}${_POST[hi_number]}${_POST[hi_status]}" ] \ +# && _POST[X-HEALTH-INSURANCE]="$(vcf_escape "${_POST[hi_company]}" "${_POST[hi_number]}" "${_POST[hi_status]}")" + +vcf="$(pdi_load "$tempfile")" + +n1="$(POST 1N)" n2="$(POST 2N)" n3="$(POST 3N)" n4="$(POST 4N)" n5="$(POST 5N)" +# 3N (Middle Names) is not actually used +n3="${n2#${n2%% *}}" + +vcf="$(pdi_update_value "$vcf" N 1 "$(vcf_escape "$n1" "${n2%% *}" "${n3# }" "$n4" "$n5")")" +vcf="$(pdi_update_value "$vcf" FN 1 "$(vcf_escape "$n4 $n2 $n1 $n5" |sed -E 's;(^ +| +$);;g; s; +; ;g;')")" +vcf="$(printf '%s\n' "$vcf" |sed -E "/^CATEGORIES;[^:]*:.*$/d")" + +for field in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*'); do + for cnt in $(seq 1 $(POST_COUNT "$field")); do + case "$field" in + # (TEL) + # printf '%s;TYPE=%s:%s\r\n' "${field}" "${_POST[phonetype${key#TEL}]}" "$(vcf_escape "$(POST "$field" "$cnt")")" + # ;; + TEL) + vcf="$(pdi_update_attrib "$vcf" TEL $cnt TYPE="$(POST teltype $cnt |grep -Exm1 'HOME|WORK|CELL|FAX')")" + vcf="$(pdi_update_value "$vcf" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")" + ;; + *) + vcf="$(pdi_update_value "$vcf" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")" + ;; + esac +done; done + +# delete fields, first mark for deletion using delete_key +# this way the field enumeration is preserved during the process +# finally filter marked lines +delete_key="$(randomid)" +for delete in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*_delete_[0-9]+'); do + f="${delete%%_*}"; c="${delete##*_}"; + [ "$(POST "$delete")" = "true" ] && vcf="$(pdi_update_value "$vcf" "$f" "$c" "delete=${delete_key}")" +done +vcf="$(printf '%s\n' "$vcf" |sed -E "/^[^:]+:delete=${delete_key}\$/d")" + +if [ "$action" = addfield ]; then + vcf="$(pdi_update_value "$vcf" "$newfield" $(( $(pdi_count "$vcf" "$newfield") + 1 )) '')" +fi +printf '%s' "$vcf" |grep -vx '' >"$tempfile" + +case "$action" in + addfield) + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}" + ;; + update) + if LOCK "$attfile"; then + grep -F " ${card}" "$attfile" |while read course junk; do + touch "$_DATA/ical/${course}" + done + sed -i -E "/^.+ ${card}\$/d" "$attfile" + seq 1 $(POST_COUNT attendance) |while read n; do + printf '%s %s\n' "$(POST attendance $n)" "$card" + done >>"$attfile" + grep -F " ${card}" "$attfile" |while read course junk; do + touch "$_DATA/ical/${course}" + done + RELEASE "$attfile" + else + SET_COOKIE 0 message="COULD NOT UPDATE COURSE MAPPINGS" + fi + + cp "$tempfile" "$cardfile" + RELEASE_SLOCK "$cardfile" + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}" + ;; + cancel) + RELEASE_SLOCK "$cardfile" + [ -f "$cardfile" ] \ + && REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}" \ + || REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}" + ;; + delete) + rm "$cardfile" + RELEASE_SLOCK "$cardfile" + if LOCK "$attfile"; then + grep -F " ${card}" "$attfile" |while read course junk; do + touch "$_DATA/ical/${course}" + done + sed -i -E "/^.+ ${card}\$/d" "$attfile" + RELEASE "$attfile" + else + SET_COOKIE 0 message="COULD NOT UPDATE COURSE MAPPINGS" + fi + REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}" + ;; +esac diff --git a/cards/widgets.sh b/cards/widgets.sh new file mode 100755 index 0000000..eb9d0fd --- /dev/null +++ b/cards/widgets.sh @@ -0,0 +1,284 @@ +# Copyright 2014 - 2019, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +list_categories() { + grep -vxE '^[ ]*$' "${_DATA}/mappings/categories" +} + +list_courses() { + local file name cachefile="${_DATA}/cache/courses.ui.cache" + if [ $cachefile -nt "${_DATA}/ical" ]; then + cat "$cachefile" + else + for file in "$_DATA/ical"/*.ics; do + name="$(pdi_value "$(pdi_load "$file")" SUMMARY || l10n "(unnamed course)" |unescape |HTML)" + printf '%s/%s\n' "${file##*/}" "$name" + done \ + | sort -t/ -k2 |tee "$cachefile" + fi +} + +w_filter_item() { +n=$3 +cat <%s' \ + "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)" + done + printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)" + ;; + TEL) printf '[h3 %s]' "$(l10n "$item")" + seq 1 $cnt |while read c; do + printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \ + "$item" $c "$item" $c "$item" $c "$(l10n delete)" + teltype="$(pdi_attrib "$card" TEL $c TYPE)" + printf '[select .item .teltype name="teltype" + [option value="" disabled="disabled" %s %s] + [option value="HOME" %s %s] + [option value="WORK" %s %s] + [option value="CELL" %s %s] + [option value="FAX" %s %s] + ]\n' \ + "$([ "$teltype" = '' ] && printf 'selected="selected"')" "$(l10n teltype)" \ + "$([ "$teltype" = 'HOME' ] && printf 'selected="selected"')" "$(l10n TYPE=HOME)" \ + "$([ "$teltype" = 'WORK' ] && printf 'selected="selected"')" "$(l10n TYPE=WORK)" \ + "$([ "$teltype" = 'CELL' ] && printf 'selected="selected"')" "$(l10n TYPE=CELL)" \ + "$([ "$teltype" = 'FAX' ] && printf 'selected="selected"')" "$(l10n TYPE=FAX)" + + printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \ + "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)" "$(l10n "$item")" + done + printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)" + ;; + *)printf '[h3 %s]' "$(l10n "$item")" + seq 1 $cnt |while read c; do + printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \ + "$item" $c "$item" $c "$item" $c "$(l10n delete)" + printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \ + "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)" "$(l10n "$item")" + done + printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)" + ;; + esac + done +} diff --git a/categories/edit_categories.sh b/categories/edit_categories.sh new file mode 100755 index 0000000..2eef152 --- /dev/null +++ b/categories/edit_categories.sh @@ -0,0 +1,35 @@ +#!/bin/zsh + +# Copyright 2015 - 2016 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +catfile="${_DATA}/mappings/categories" + +remove="$(POST remove)" +newcat="$(POST newcat)" + +if [ "$(POST add)" = "add" ]; then + categories="$( { + cat "$catfile" + printf %s\\n "$newcat" + } |sort -u )" + printf %s\\n "$categories" >"$catfile" +elif [ "$remove" ]; then + sed -E -i '/^'"${remove}"'$/d' "$catfile" +fi + +REDIRECT "${_BASE}/categories/" diff --git a/categories/index.cgi b/categories/index.cgi new file mode 100755 index 0000000..236b1e3 --- /dev/null +++ b/categories/index.cgi @@ -0,0 +1,71 @@ +#!/bin/sh +# Copyright 2015, 2017, 2018, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +. $_EXEC/pdiread.sh +. $_EXEC/categories/l10n.sh + +catfile="${_DATA}/mappings/categories" + +list_categories() { + grep -vxE '[ ]*' "$catfile" |sort -u +} + +list_catsel(){ + local vcf="$1" card="$2" n=1 cats="${BR}" + while cats="${cats}${BR}$(pdi_value "$vcf" CATEGORIES $n)"; do n=$((n + 1)); done + + list_categories |while read cat; do + printf '[li [label [input %s type="checkbox" name="%s" value="%s"] %s]]' \ + "$([ "${cats%*${BR}${cat}${BR}*}" != "$cats" ] && printf checked=checked)" \ + "$(HTML "$card")" "$(HTML "$cat")" "$(HTML "$cat")" + done +} + +cat <. + +l10n(){ + local word + [ $# -eq 0 ] && read -r word || word="$*" + case $word in + cat_remove) printf %s "-";; + cat_add) printf %s "+";; + cat_newlabel) printf %s "neue Kategorie";; + cat_update) printf %s "Zuweisungen übernehmen";; + categories_label) printf %s "Kategorien";; + + *) l10n_global "$word";; + esac +} diff --git a/categories/update_categories.sh b/categories/update_categories.sh new file mode 100755 index 0000000..a26f93d --- /dev/null +++ b/categories/update_categories.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# Copyright 2016, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +. "$_EXEC"/cgilite/storage.sh +. "$_EXEC"/pdiread.sh + +catfile="${_DATA}/mappings/categories" + +for card in "${_DATA}"/vcard/*.vcf; do + n='' postcats='' cardcats='' + vcf="$(pdi_load "$card")" + + n=1; while postcats="${postcats}${postcats:+,}$(POST "${card##*/}" $n)"; do n=$((n+1)); done + n=1; while cardcats="${cardcats}${cardcats:+,}$(pdi_value "$vcf" CATEGORIES $n)"; do n=$((n+1)); done + + if [ "${postcats}" != "${cardcats}" ] && LOCK "$card"; then + sed -E -i ' + /^CATEGORIES[;:]/d + /^END;?:VCARD *\r?$/iCATEGORIES:'"${postcats%,}"'\r + ' "${card}" + RELEASE "$card" + fi +done + +REDIRECT "${_BASE}/categories/" diff --git a/cgilite.sh b/cgilite.sh deleted file mode 100755 index f766ee2..0000000 --- a/cgilite.sh +++ /dev/null @@ -1,268 +0,0 @@ -#!/bin/sh - -# Copyright 2017 - 2020 Paul Hänsch -# -# This is CGIlite. -# A collection of posix shell functions for writing CGI scripts. -# -# CGIlite 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. -# -# CGIlite 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 CGIlite. If not, see . - -[ -n "$include_cgilite" ] && return 0 -# guard set after webserver part - -# ksh and zsh workaround -# set -o posix # ksh, not portable -setopt -o OCTAL_ZEROES 2>&- - -CR=" " -BR=' -' -cgilite_timeout=2 - -PATH(){ - local str seg out - [ $# -eq 0 ] && str="$(cat)" || str="$*" - while [ "$str" ]; do - seg=${str%%/*}; str="${str#*/}" - case $seg in - ..) out="${out%/}"; out="${out%/*}/";; - .|'') out="${out%/}/";; - *) out="${out%/}/${seg}";; - esac; - [ "$seg" = "$str" ] && break - done - [ "${str}" -a "${out}" ] && printf %s "$out" || printf %s/ "${out%/}" -} - -HEX_DECODE=' - s;\\;\\\\;g; :HEXDECODE_X; s;%([^0-9A-F]);\\045\1;g; tHEXDECODE_X; - # Hexadecimal { %00 - %FF } will be transformed to octal { \000 - \377 } for posix printf - s;%[0123].;&\\0;g; s;%[4567].;&\\1;g; s;%[89AB].;&\\2;g; s;%[CDEF].;&\\3;g; - s;%[048C][0-7]\\.;&0;g; s;%[048C][89A-F]\\.;&1;g; s;%[159D][0-7]\\.;&2;g; s;%[159D][89A-F]\\.;&3;g; - s;%[26AE][0-7]\\.;&4;g; s;%[26AE][89A-F]\\.;&5;g; s;%[37BF][0-7]\\.;&6;g; s;%[37BF][89A-F]\\.;&7;g; - s;%.[08](\\..);\10;g; s;%.[19](\\..);\11;g; s;%.[2A](\\..);\12;g; s;%.[3B](\\..);\13;g; - s;%.[4C](\\..);\14;g; s;%.[5D](\\..);\15;g; s;%.[6E](\\..);\16;g; s;%.[7F](\\..);\17;g; -' - -HEX_DECODE(){ - printf -- "$(printf %s "$1" |sed -E "$HEX_DECODE")" -} - -if [ -z "$REQUEST_METHOD" ]; then - # no webserver variables means we are running via inetd / ncat - # so use builtin web server - - # Use env from inetd as webserver variables - REMOTE_ADDR="${TCPREMOTEIP}" - SERVER_NAME="${TCPLOCALIP}" - SERVER_PORT="${TCPLOCALPORT}" - - # Wait 2 seconds for request or kill connection through watchdog. - # Once Request is received the watchdog will be suspended (killed). - # At the end of the loop the watchdog will be restarted to enable - # timeout for the subsequent request. - - (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$! - while read REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL; do - [ "${SERVER_PROTOCOL#HTTP/1.[01]${CR}}" ] && break - kill $cgilite_watchdog - - SERVER_PROTOCOL="${SERVER_PROTOCOL%${CR}}" - PATH_INFO="$(HEX_DECODE "${REQUEST_URI%\?*}" |PATH)" - [ "${REQUEST_URI}" = "${REQUEST_URI#*\?}" ] \ - && QUERY_STRING='' \ - || QUERY_STRING="${REQUEST_URI#*\?}" - cgilite_headers=''; while read -r hl; do - hl="${hl%${CR}}"; [ "$hl" ] || break - case $hl in - 'Content-Length: '*) CONTENT_LENGTH="${hl#*: }";; - 'Content-Type: '*) CONTENT_TYPE="${hl#*: }";; - esac - cgilite_headers="${cgilite_headers}${hl}${BR}" - done - - export REMOTE_ADDR SERVER_NAME SERVER_PORT REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL \ - PATH_INFO QUERY_STRING CONTENT_TYPE CONTENT_LENGTH - - # Try to serve multiple requests, provided that script serves a - # Content-Length header. - # Without Content-Length header, connection will terminate after - # script. - - cgilite_status='200 OK'; cgilite_response=''; cgilite_cl="Connection: close${CR}${BR}"; - . "$0" | while read -r l; do case $l in - Status:*) - cgilite_status="${l#Status: }";; - Content-Length:*) - cgilite_cl="" - cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";; - Connection:*) - cgilite_cl="${l}${BR}";; - $CR) printf '%s %s\r\n%s%s\r\n' \ - 'HTTP/1.1' "${cgilite_status%${CR}}" \ - "${cgilite_response}${cgilite_response:+${BR}}" "${cgilite_cl}" - cat || kill $$ - [ "${cgilite_cl#Connection}" = "${cgilite_cl}" ]; exit;; - *) cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";; - esac; done || exit 0; - (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$! - done - kill $cgilite_watchdog - exit 0 -fi - -include_cgilite="$0" - -if [ "${REQUEST_METHOD}" = POST -a "${CONTENT_LENGTH:-0}" -gt 0 -a \ - "${CONTENT_TYPE}" = "application/x-www-form-urlencoded" ]; then - cgilite_post="$(head -c "$CONTENT_LENGTH")" -fi - -debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } -[ "${DEBUG+x}" ] && env >&2 - -cgilite_count(){ - printf %s "&$1" \ - | grep -oE '&'"$2"'=[^&]*' \ - | wc -l -} - -cgilite_value(){ - local str="&$1" name="$2" cnt="${3:-1}" - while [ $cnt -gt 0 ]; do - [ "${str}" = "${str#*&${name}=}" ] && return 1 - str="${str#*&${name}=}" - cnt=$((cnt - 1)) - done - printf -- "$(printf %s "${str%%&*}" |sed -E 's;\+; ;g;'"$HEX_DECODE")" -} - -cgilite_keys(){ - local str="&$1" - while [ "${str#*&}" != "${str}" ]; do - str="${str#*&}" - printf '%s\n' "${str%%=*}" - done \ - | sort -u -} - -GET(){ cgilite_value "${QUERY_STRING}" $@; } -GET_COUNT(){ cgilite_count "${QUERY_STRING}" $1; } -GET_KEYS(){ cgilite_keys "${QUERY_STRING}"; } - -POST(){ cgilite_value "${cgilite_post}" $@; } -POST_COUNT(){ cgilite_count "${cgilite_post}" $1; } -POST_KEYS(){ cgilite_keys "${cgilite_post}"; } - -REF(){ cgilite_value "${HTTP_REFERER#*\?}" $@; } -REF_COUNT(){ cgilite_count "${HTTP_REFERER#*\?}" $1; } -REF_KEYS(){ cgilite_keys "${HTTP_REFERER#*\?}"; } - -HEADER(){ - # Read value of header line. Use this instead of - # referencing HTTP_* environment variables. - if [ -n "${cgilite_headers+x}" ]; then - local str="${BR}${cgilite_headers}" - [ "${str}" = "${str#*${BR}${1}: }" ] && return 1 - str="${str#*${BR}${1}: }" - printf %s "${str%%${BR}*}" - else - local var="HTTP_$(printf %s "$1" |tr a-z- A-Z-)" - eval "[ \"\$$var\" ] && printf %s \"\$$var\" || return 1" - # eval "printf %s \"\$HTTP_$(printf %s "${1}" |tr a-z A-Z |tr -c A-Z _)\"" - fi -} - -COOKIE(){ - HEX_DECODE "$( - HEADER Cookie \ - | grep -oE '(^|; ?)'"$1"'=[^;]*' \ - | sed -En "${2:-1}"'{s;^[^=]+=;;; s;\+; ;g; p;}' - )" -} - -HTML(){ - # Escape HTML cahracters - # Also escape [, ], and \n for use in html-sh - local str out - [ $# -eq 0 ] && str="$(cat)" || str="$*" - while [ "$str" ]; do - case $str in - \&*) out="${out}&";; - \<*) out="${out}<";; - \>*) out="${out}>";; - \"*) out="${out}"";; - \'*) out="${out}'";; - \[*) out="${out}[";; - \]*) out="${out}]";; - "${CR}"*) out="${out} ";; - "${BR}"*) out="${out} ";; - *) out="${out}${str%"${str#?}"}";; - esac - str="${str#?}" - done - printf %s "$out" -} - -URL(){ - # Escape pathes, so they can be used in link tags and HTTP Headers - local str out - [ $# -eq 0 ] && str="$(cat)" || str="$*" - while [ "$str" ]; do - case $str in - \&*) out="${out}%26";; - \"*) out="${out}%22";; - \'*) out="${out}%27";; - \?*) out="${out}%3F";; - \#*) out="${out}%23";; - \[*) out="${out}%5B";; - \]*) out="${out}%5D";; - \ *) out="${out}%20";; - " "*) out="${out}%09";; - "${CR}"*) out="${out}%0D";; - "${BR}"*) out="${out}%0A";; - %*) out="${out}%25";; - *) out="${out}${str%"${str#?}"}";; - esac - str="${str#?}" - done - printf %s "$out" -} - -SET_COOKIE(){ - # Param: session | +seconds | [date] - # Param: name=value - # Param: Path= | Domain= | Secure - local expire cookie - case "$1" in - ''|0|session) expire='';; - [+-][0-9]*) expire="$(date -R -d @$(($(date +%s) + $1)))";; - *) expire="$(date -R -d "$1")";; - esac - cookie="$2" - - printf 'Set-Cookie: %s; HttpOnly; SameSite=Lax' "$cookie" - [ -n "$expire" ] && printf '; Expires=%s' "${expire%+????}${expire:+GMT}" - [ $# -ge 3 ] && shift 2 && printf '; %s' "$@" - printf '\r\n' -} - -REDIRECT(){ - printf '%s: %s\r\n' \ - Status "303 See Other" \ - Content-Length 0 \ - Location "$*" - printf '\r\n' - exit 0 -} diff --git a/cgilite/.gitignore b/cgilite/.gitignore new file mode 100644 index 0000000..5c9950a --- /dev/null +++ b/cgilite/.gitignore @@ -0,0 +1,3 @@ +cgilite +serverkey +users.db diff --git a/cgilite/cgilite.awk b/cgilite/cgilite.awk new file mode 100644 index 0000000..f16ed6a --- /dev/null +++ b/cgilite/cgilite.awk @@ -0,0 +1,158 @@ +#!/bin/env awk -f + +function PATH( str, seg, out ) { + while ( str ) { + seg = str; + sub( /\/.*$/, "", seg); + sub( /^[^\/]*\//, "", str); + + if ( seg == ".." ) sub(/\/[^\/]*\/?$/, "", out); + else if ( seg ~ /^\.?$/) sub(/\/?$/, "/", out); + else sub(/\/?$/, "/" seg, out); + + if (seg == str) break; + } + if (!(str && out)) sub(/\/?$/,"/" out); + return out; +} + +function HEX_DECODE( pfx, inp, out, n, k ) { + k = length(pfx); + gsub(/[].*+?^${}()|\\[]/,"\\\\&",pfx); + while ( inp ) if ( n = match(inp, pfx "[0-9a-fA-F][0-9a-fA-F]") ) { + out = out substr(inp, 1, n - 1); + inp = substr(inp, n + k); + if (inp ~ /^[0-9]/) n = 16 * substr(inp, 1, 1); + else if (inp ~ /^[aA]/) n = 160; + else if (inp ~ /^[bB]/) n = 176; + else if (inp ~ /^[cC]/) n = 192; + else if (inp ~ /^[dD]/) n = 208; + else if (inp ~ /^[eE]/) n = 224; + else if (inp ~ /^[fF]/) n = 240; + if (inp ~ /^.[0-9]/) n += substr(inp, 2, 1); + else if (inp ~ /^.[aA]/) n += 10; + else if (inp ~ /^.[bB]/) n += 11; + else if (inp ~ /^.[cC]/) n += 12; + else if (inp ~ /^.[dD]/) n += 13; + else if (inp ~ /^.[eE]/) n += 14; + else if (inp ~ /^.[fF]/) n += 15; + out = out sprintf("%c", n); + inp = substr(inp, 3); + } else { + out = out inp; + break; + } + return out; +} + +function HTML( text ) { + gsub( /&/, "\\&", text ); + gsub( //, "\\>", text ); + gsub( /"/, "\\"", text ); + gsub( /'/, "\\'", text ); + gsub( /\[/, "\\[", text ); + gsub( /\]/, "\\]", text ); + gsub( /\r/, "\\ ", text ); + gsub( /\n/, "\\ ", text ); + gsub( /\\/, "\\\", text ); + return text; +} + +function URL( text ) { + gsub( /&/, "%26", text ); + gsub( /"/, "%22", text ); + gsub( /'/, "%27", text ); + gsub( /`/, "%60", text ); + gsub( /\?/, "%3F", text ); + gsub( /#/, "%23", text ); + gsub( /\[/, "%5B", text ); + gsub( /\]/, "%5D", text ); + gsub( / /, "%20", text ); + gsub( /\t/, "%09", text ); + gsub( /\r/, "%0D", text ); + gsub( /\n/, "%0A", text ); + gsub( /%/, "%25", text ); + gsub( /\\/, "%5C", text ); + return text; +} + +function _cgilite_urldecode( str, arr, spl, form, k, n, key) { + if (! spl) spl="&" + split(str, form, spl); + for ( k in form ) { + key = form[k]; sub(/=.*$/, "", key); + sub(/^[^=]*=/, "", form[k]); + if ( key in arr ) { + n = 1; while ( (key, n) in arr ) n++; + arr[key,n] = HEX_DECODE( "%", form[k]); + } else { + arr[key] = HEX_DECODE( "%", form[k]); + } + } +} + +function _cgilite_request( key, val) { + # Read request from client connection + + # Read Headers + getline; REQUEST_METHOD = $1; REQUEST_URI = $2; SERVER_PROTOCOL = $3; + while ( getline ) { + if ($0 ~ /^\r?$/) break; + else if ($0 ~ /^[a-zA-Z][0-9a-zA-Z_-]+: .*/) { + key = toupper($0); + sub(/:.*$/, "", key); + gsub(/-/, "_", key); + _HEADER[key] = $0; + sub(/^[^:]:[\t ]*/, "", _HEADER[key]); + sub(/[\t ]*\r?$/, "", _HEADER[key]); + } + } + CONTENT_LENGTH = _HEADER["CONTENT_LENGTH"]; + CONTENT_TYPE = _HEADER["CONTENT_TYPE"]; + + PATH_INFO = REQUEST_URI; gsub(/\?.*$/, "", PATH_INFO) + PATH_INFO = PATH( HEX_DECODE( "%", PATH_INFO ) ); + QUERY_STRING = REQUEST_URI; + if ( !gsub(/^[^?]+\?/, "", QUERY_STRING) ) QUERY_STRING = ""; + + # Set up _GET[]-Array + _cgilite_urldecode(QUERY_STRING, _GET); + + if ( _HEADER["CONTENT_TYPE"] == "application/x-www-form-urlencoded" \ + && _HEADER["CONTENT_LENGTH"] ) { + # Set up _POST[]-Array + + val = ""; key = "head -c " _HEADER["CONTENT_LENGTH"]; + while (key |getline) val = val $0; close(key); + _cgilite_urldecode(val, _POST); + } + + if ( _HEADER["COOKIE"] ) { + # Set up _COOKIE[]-Array + _cgilite_urldecode(_HEADER["COOKIE"], _COOKIE, "; ?"); + } + + if ( _HEADER["REFERER"] ) { + key = HEADER["REFERER"]; + if (! sub(/^[^\?]+?/, "", key)) key = "" + _cgilite_urldecode(key, _REF); + } + +} + +function _cgilite_headers() { + # Import request data from webserver environment variables +} + +BEGIN { + REQUEST_METHOD=""; REQUEST_URI=""; SERVER_PROTOCOL=""; + PATH_INFO=""; QUERY_STRING=""; CONTENT_LENGTH=""; CONTENT_TYPE=""; + split("", _GET); split("", _POST); split("", _REF); + split("", _HEADER); split("", _COOKIE); + + if ( ENVIRON["REQUEST_METHOD"] ) + _cgilite_headers(); + else + _cgilite_request(); +} diff --git a/cgilite/cgilite.sh b/cgilite/cgilite.sh new file mode 100755 index 0000000..b2467c3 --- /dev/null +++ b/cgilite/cgilite.sh @@ -0,0 +1,356 @@ +#!/bin/sh + +# This is CGIlite. +# A collection of posix shell functions for writing CGI scripts. + +# Copyright 2017 - 2023 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[ -n "$include_cgilite" ] && return 0 +# guard set after webserver part + +# ksh and zsh workaround +# set -o posix # ksh, not portable +setopt -o OCTAL_ZEROES 2>&- + +# Integrated webserver request timeout +cgilite_timeout=2 + +# General environment variables +# $_EXEC - directory containing application itself +# $_DATA - direcotry where application data may be stored +# $_BASE - optional prefix for http path, e.g. "/myapp" +# +# Programmers should take care to use those variables throughout the +# application. +# Variables may be set via CLI argument, in environment, or left as default. + +for cgilite_arg in "$@"; do case $cgilite_arg in + --exec=*) _EXEC="${cgilite_arg#*=}";; + --data=*) _DATA="${cgilite_arg#*=}";; + --base=*) _BASE="${cgilite_arg#*=}";; +esac; done +unset cgilite_arg + +_EXEC="${_EXEC:-${0%/*}}" +_DATA="${_DATA:-.}" +_EXEC="${_EXEC%/}" _DATA="${_DATA%/}" _BASE="${_BASE%/}" + +export _EXEC _DATA _BASE + +# Carriage Return and Line Break characters for convenience +CR=" " +BR=' +' + +PATH(){ + local str seg out + # normalize path + # read from stdin if no arguments are provided + + [ $# -eq 0 ] && str="$(cat)" || str="$*" + while [ "$str" ]; do + seg=${str%%/*}; str="${str#*/}" + case $seg in + ..) out="${out%/}"; out="${out%/*}/";; + .|'') out="${out%/}/";; + *) out="${out%/}/${seg}";; + esac; + [ "$seg" = "$str" ] && break + done + [ "${str}" -a "${out}" ] && printf %s "$out" || printf %s/ "${out%/}" +} + +HEX_DECODE(){ + local pfx="$1" in="$2" out + # Print out Data encoded as Hex + # + # Arguments: + # pfx - required, prefix for a hex tupel, e.g. "\x", "%" "\", may be empty + # in - required, string to be decoded + # + # anything that does not constitute a tupel of valid Hex numerals + # will be copied to the output literally + + while [ "$in" ]; do + [ "$pfx" ] || case $in in + [0-9a-fA-F][0-9a-fA-F]*):;; + ?*) out="${out}${in%%"${in#?}"}" + in="${in#?}"; continue;; + esac + + case $in in + "$pfx"[0-9a-fA-F][0-9a-fA-F]*) in="${in#"${pfx}"}";; + \\*) in="${in#?}"; out="${out}\\\\"; continue;; + %*) in="${in#?}"; out="${out}%%"; continue;; + *) att="${in%%"${pfx}"*}"; att="${att%%%*}"; att="${att%%\\*}" + out="${out}${att}"; in="${in#"${att}"}"; continue;; + esac; + + # Hex escapes for printf (e.g. \x41) are not portable + # The portable way for Hex output is transforming Hex to Octal + # (e.g. \x41 = \101) + case $in in + [0123]?*) out="${out}\\0";; + [4567]?*) out="${out}\\1";; + [89aAbB]?*) out="${out}\\2";; + [c-fC-F]?*) out="${out}\\3";; + esac + case $in in + [048cC][0-7]*) out="${out}0";; + [048cC][89a-fA-F]*) out="${out}1";; + [159dD][0-7]*) out="${out}2";; + [159dD][89a-fA-F]*) out="${out}3";; + [26aAeE][0-7]*) out="${out}4";; + [26aAeE][89a-fA-F]*) out="${out}5";; + [37bBfF][0-7]*) out="${out}6";; + [37bBfF][89a-fA-F]*) out="${out}7";; + esac + case $in in + ?[08]*) out="${out}0";; + ?[19]*) out="${out}1";; + ?[2aA]*) out="${out}2";; + ?[3bB]*) out="${out}3";; + ?[4cC]*) out="${out}4";; + ?[5dD]*) out="${out}5";; + ?[6eE]*) out="${out}6";; + ?[7fF]*) out="${out}7";; + esac + in="${in#?}" + in="${in#?}" + done + printf -- "$out" +} + +if [ -z "$REQUEST_METHOD" ]; then + # no webserver variables means we are running via inetd / ncat + # so use builtin web server + + # Use env from inetd as webserver variables + REMOTE_ADDR="${TCPREMOTEIP}" + SERVER_NAME="${TCPLOCALIP}" + SERVER_PORT="${TCPLOCALPORT}" + + # Wait 2 seconds for request or kill connection through watchdog. + # Once Request is received the watchdog will be suspended (killed). + # At the end of the loop the watchdog will be restarted to enable + # timeout for the subsequent request. + + (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$! + while read REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL; do + unset PATH_INFO QUERY_STRING cgilite_headers CONTENT_LENGTH CONTENT_TYPE + + [ "${SERVER_PROTOCOL#HTTP/1.[01]${CR}}" ] && break + kill $cgilite_watchdog + + SERVER_PROTOCOL="${SERVER_PROTOCOL%${CR}}" + PATH_INFO="$(HEX_DECODE % "${REQUEST_URI%\?*}" |PATH)" + [ "${REQUEST_URI}" = "${REQUEST_URI#*\?}" ] \ + && QUERY_STRING='' \ + || QUERY_STRING="${REQUEST_URI#*\?}" + while read -r hl; do + hl="${hl%${CR}}"; [ "$hl" ] || break + case $hl in + 'Content-Length: '*) CONTENT_LENGTH="${hl#*: }";; + 'Content-Type: '*) CONTENT_TYPE="${hl#*: }";; + esac + cgilite_headers="${cgilite_headers}${hl}${BR}" + done + + export REMOTE_ADDR SERVER_NAME SERVER_PORT REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL \ + PATH_INFO QUERY_STRING CONTENT_TYPE CONTENT_LENGTH cgilite_headers + + # Try to serve multiple requests, provided that script serves a + # Content-Length header. + # Without Content-Length header, connection will terminate after + # script. + + cgilite_status='200 OK'; cgilite_response=''; cgilite_cl="Connection: close${CR}${BR}"; + . "$0" | while read -r l; do case $l in + Status:*) + cgilite_status="${l#Status: }";; + Content-Length:*) + cgilite_cl="" + cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";; + Connection:*) + cgilite_cl="${l}${BR}";; + $CR) printf '%s %s\r\n%s%s\r\n' \ + 'HTTP/1.1' "${cgilite_status%${CR}}" \ + "${cgilite_response}${cgilite_response:+${BR}}" "${cgilite_cl}" + cat || kill $$ + [ "${cgilite_cl#Connection}" = "${cgilite_cl}" ]; exit;; + *) cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";; + esac; done || exit 0; + (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$! + done + kill $cgilite_watchdog + exit 0 +fi + +include_cgilite="$0" + +if [ "${REQUEST_METHOD}" = POST -a "${CONTENT_LENGTH:-0}" -gt 0 -a \ + "${CONTENT_TYPE}" = "application/x-www-form-urlencoded" ]; then + cgilite_post="$(head -c "$CONTENT_LENGTH")" +fi + +PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")" + +debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } +[ "${DEBUG+x}" ] && env >&2 + +# general helper functions, see GET, POST, and REF below + +cgilite_count(){ + printf %s "&$1" \ + | grep -oE '&'"$2"'=[^&]*' \ + | wc -l +} + +cgilite_value(){ + local str="&$1" name="$2" cnt="${3:-1}" + while [ $cnt -gt 0 ]; do + [ "${str}" = "${str#*&${name}=}" ] && return 1 + str="${str#*&${name}=}" + cnt=$((cnt - 1)) + done + HEX_DECODE % "$(printf %s "${str%%&*}" |tr + \ )" +} + +cgilite_keys(){ + local str="&$1" + while [ "${str#*&}" != "${str}" ]; do + str="${str#*&}" + printf '%s\n' "${str%%=*}" + done \ + | sort -u +} + +# Read arguments from GET, POST, or the query string of the referrer (REF). +# Example: +# GET varname n +# +# where n is number for the Nth occurence of a variable and defaults to 1 +# +# *_COUNT varname +# -> returns number of ocurences +# *_KEYS +# -> returns list of available varnames + +GET(){ cgilite_value "${QUERY_STRING}" "$@"; } +GET_COUNT(){ cgilite_count "${QUERY_STRING}" $1; } +GET_KEYS(){ cgilite_keys "${QUERY_STRING}"; } + +POST(){ cgilite_value "${cgilite_post}" "$@"; } +POST_COUNT(){ cgilite_count "${cgilite_post}" $1; } +POST_KEYS(){ cgilite_keys "${cgilite_post}"; } + +REF(){ cgilite_value "${HTTP_REFERER#*\?}" "$@"; } +REF_COUNT(){ cgilite_count "${HTTP_REFERER#*\?}" $1; } +REF_KEYS(){ cgilite_keys "${HTTP_REFERER#*\?}"; } + +HEADER(){ + # Read value of header line. Use this instead of + # referencing HTTP_* environment variables. + if [ -n "${cgilite_headers+x}" ]; then + local str="${BR}${cgilite_headers}" + [ "${str}" = "${str#*${BR}${1}: }" ] && return 1 + str="${str#*${BR}${1}: }" + printf %s "${str%%${BR}*}" + else + local var="HTTP_$(printf %s "$1" |tr a-z- A-Z_)" + eval "[ \"\$$var\" ] && printf %s \"\$$var\" || return 1" + # eval "printf %s \"\$HTTP_$(printf %s "${1}" |tr a-z A-Z |tr -c A-Z _)\"" + fi +} + +COOKIE(){ + # Read value of cookie + HEX_DECODE % "$( + HEADER Cookie \ + | grep -oE '(^|; ?)'"$1"'=[^;]*' \ + | sed -En "${2:-1}"'{s;^[^=]+=;;; s;\+; ;g; p;}' + )" +} + +HTML(){ + # Escape HTML cahracters + # Also escape [, ], and \n for use in html-sh + local str out + [ $# -eq 0 ] && str="$(cat)" || str="$*" + while [ "$str" ]; do case $str in + \&*) out="${out}&"; str="${str#?}";; + \<*) out="${out}<"; str="${str#?}";; + \>*) out="${out}>"; str="${str#?}";; + \"*) out="${out}""; str="${str#?}";; + \'*) out="${out}'"; str="${str#?}";; + \[*) out="${out}["; str="${str#?}";; + \]*) out="${out}]"; str="${str#?}";; + "${CR}"*) out="${out} "; str="${str#?}";; + "${BR}"*) out="${out} "; str="${str#?}";; + *) out="${out}${str%%[]&<>\"\'${CR}${BR}[]*}"; str="${str#"${str%%[]&<>\"\'${CR}${BR}[]*}"}";; + esac; done + printf %s "$out" +} + +URL(){ + # Escape pathes, so they can be used in link tags and HTTP Headers + local str out + [ $# -eq 0 ] && str="$(cat)" || str="$*" + while [ "$str" ]; do case $str in + \&*) out="${out}%26"; str="${str#?}";; + \"*) out="${out}%22"; str="${str#?}";; + \'*) out="${out}%27"; str="${str#?}";; + \`*) out="${out}%60"; str="${str#?}";; + \?*) out="${out}%3F"; str="${str#?}";; + \#*) out="${out}%23"; str="${str#?}";; + \[*) out="${out}%5B"; str="${str#?}";; + \]*) out="${out}%5D"; str="${str#?}";; + \ *) out="${out}%20"; str="${str#?}";; + " "*) out="${out}%09"; str="${str#?}";; + "${CR}"*) out="${out}%0D"; str="${str#?}";; + "${BR}"*) out="${out}%0A"; str="${str#?}";; + %*) out="${out}%25"; str="${str#?}";; + *) out="${out}${str%%[]&\"\'\?# ${CR}${BR}%[]*}"; str="${str#"${str%%[]&\"\'\?# ${CR}${BR}%[]*}"}";; + esac; done + printf %s "$out" +} + +SET_COOKIE(){ + # Param: session | +seconds | [date] + # Param: name=value + # Param: Path= | Domain= | Secure + local expire cookie + case "$1" in + ''|0|session) expire='';; + [+-][0-9]*) expire="$(date -R -d @$(($(date +%s) + $1)))";; + *) expire="$(date -R -d "$1")";; + esac + cookie="$2" + + printf 'Set-Cookie: %s; HttpOnly; SameSite=Lax' "$cookie" + [ -n "$expire" ] && printf '; Expires=%s' "${expire%+????}${expire:+GMT}" + [ $# -ge 3 ] && shift 2 && printf '; %s' "$@" + printf '\r\n' +} + +REDIRECT(){ + # Trigger redirct and terminate script + printf '%s: %s\r\n' \ + Status "303 See Other" \ + Content-Length 0 \ + Location "$*" + printf '\r\n' + exit 0 +} diff --git a/common.css b/cgilite/common.css similarity index 71% rename from common.css rename to cgilite/common.css index f9b17ad..30c3942 100644 --- a/common.css +++ b/cgilite/common.css @@ -16,12 +16,20 @@ body { color: #000; background: #FFF; } -ul, ol, dl, table, p { margin-bottom: .5em; } +ul, ol, dl, table, pre, p { margin-bottom: .5em; } +p:only-child { margin-bottom: 0; } + +table { + max-width: 100%; + overflow-x: auto; +} +th, td { padding: .25em .75em; } a { font-style: italic; text-decoration: underline; color: #068; + word-break: break-word; } a.button { font-style: inherit; @@ -40,10 +48,27 @@ b, strong { font-weight: bolder; } tt, code, var, samp, kbd { font-family: monospace; } kbd { font-style: italic; } -ul, ol { margin-left: 1.125em; } +blockquote { + background-color: #EEE; + margin: .5em 0; + padding: 1em 2em; + white-space: pre-line; +} + +ul, ol { padding-left: 1.5em; } dl dt { font-weight: bolder; } +dl dd { + margin: 0 2em; + background-color: #EEE; +} table th { font-weight: bold; } +li p + ul, li p + ol { + margin-top: -.25em; +} + +hr { border-bottom: 1pt solid; } + h1, h2, h3 { font-weight: bold; margin-top: .75em; @@ -55,7 +80,10 @@ h4, h5, h6, form legend { margin-bottom: .25em; } -h1 { font-size: 1.5em; } +h1 { + text-align: center; + font-size: 1.5em; +} h2 { font-size: 1.125em; } select, input, button, textarea, a.button { @@ -68,6 +96,7 @@ select, input, button, textarea, a.button { border-radius: 2pt; } select { padding: .375em 0; } +textarea { min-height: 7em; } input[type=radio], input[type=checkbox] { vertical-align: baseline; @@ -87,11 +116,27 @@ input + label { margin-left: .375em; } +input[type="search"] + button.search, +input.search + button.search { + width: 2.5em; + color: transparent; + background-color: #CCC; + margin-left: -2pt; + border-left: none; + border-radius: 0 2pt 2pt 0; + white-space: nowrap; + overflow: hidden; +} +input[type="search"] + button.search:before, +input.search + button.search:before { + content: '\1f50d'; + color: #000; + font-weight: bold; +} + @media print { @page { margin: 20mm; } - h1 { text-align: center; } - h1, h2, h3, h4, h5, h6, form legend { page-break-inside: avoid; page-break-after: avoid; @@ -108,7 +153,8 @@ input + label { *[tooltip]:hover:after { display: block; position: absolute; - bottom: -100%; left: 50%; transform: translate(-50%, 0); + min-width: 12em; + bottom: 100%; left: 50%; transform: translate(-50%, 0); content: attr(tooltip); padding: .5em; color: #000; background-color: #FFC; @@ -138,4 +184,8 @@ input[type=radio].tab ~ *.tab { box-shadow: .125em .125em .125em #888; } +/* Markdown line-block */ +.line-block { white-space: pre-wrap; } +.line-block br { display: none; } + /* ======= End Common Styles ======= */ diff --git a/cgilite/db23.sh b/cgilite/db23.sh new file mode 100755 index 0000000..e8a0d64 --- /dev/null +++ b/cgilite/db23.sh @@ -0,0 +1,114 @@ +#!/bin/sh + +# Copyright 2023, 2024 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[ -n "$include_db23" ] && return 0 +include_db23="$0" + +. "${_EXEC:-.}/cgilite/storage.sh" + +DB2() { + local call data file key val seq + data="${BR}${1}${BR}" call="$2" + shift 2 + + case $call in + new|discard) + printf '' + ;; + open|load) file="$1" + cat "$file" || return 1 + ;; + check|contains) key="$(STRING "$1")" val='' + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] && return 1 + ;; + count) key="$(STRING "$1")" val='' seq=0 + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] || val="${val} " + while [ "$val" != '' ]; do + seq=$((seq + 1)) val="${val#* }" + done + printf "%i\n" "$seq" + [ $seq = 0 ] && return 1 + ;; + get) key="$(STRING "$1")" seq="${2:-1}" + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] && return 1 || val="${val} " + while [ $seq -gt 1 ]; do + seq=$((seq - 1)) val="${val#* }" + done + [ "$val" = '' ] && return 1 + UNSTRING "${val%% *}" + ;; + iterate|raw) key="$(STRING "$1")" + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + [ "$val" = '' ] && return 1 + printf '%s\n' $val + ;; + delete|remove) key="$(STRING "$1")" + val="${data#*"${BR}${key}" *"${BR}"}" + key="${data%"${BR}${key}" *"${BR}"*}" + [ "${key}${BR}${val}" = "${data}" ] && return 1 + printf '%s' "${key#"${BR}"}${BR}${val%"${BR}"}" + ;; + set|store) key="$(STRING "$1")" val="" + shift 1 + val="$(for v in "$@"; do STRING "$v"; printf \\t; done)" + if [ "${data#*"${BR}${key}" *}" != "$data" ]; then + data="${data%"${BR}${key}" *"${BR}"*}${BR}${key} ${val% }${BR}${data#*"${BR}${key}" *"${BR}"}" + data="${data#"${BR}"}" data="${data%"${BR}"}" + else + data="${data#"${BR}"}${key} ${val% }${BR}" + data="${data#"${BR}"}" + fi + printf %s\\n "${data}" + ;; + append) key="$(STRING "$1")" val="" + val="${data##*"${BR}${key}" }" val="${val%%"${BR}"*}" + if [ "$val" = '' ]; then + printf %s\\n "${data}" + return 1 + else + shift 1 + val="${val}$(for v in "$@"; do printf \\t; STRING "$v"; done)" + data="${data%"${BR}${key}" *"${BR}"*}${BR}${key} ${val% }${BR}${data#*"${BR}${key}" *"${BR}"}" + data="${data#"${BR}"}" data="${data%"${BR}"}" + printf %s\\n "${data}" + fi + ;; + flush|save|write) file="$1" + data="${data#"${BR}"}" data="${data%"${BR}"}" + printf '%s\n' "$data" >"$file" || return 1 + ;; + esac + return 0 +} + +DB3() { + # wrapper function that allows easyer use of DB2 + # by always keeping file data in $db3_data + + case "$1" in + new|discard|open|load|delete|remove|set|store|append) + db3_data="$(DB2 "$db3_data" "$@")" + return "$?" + ;; + get|count|check|contains|iterate|raw|flush|save|write) + DB2 "$db3_data" "$@" + return "$?" + ;; + esac +} diff --git a/file.sh b/cgilite/file.sh similarity index 60% rename from file.sh rename to cgilite/file.sh index 04a8ef6..c66b17d 100755 --- a/file.sh +++ b/cgilite/file.sh @@ -1,69 +1,76 @@ #!/bin/sh -# Copyright 2016 - 2019 Paul Hänsch -# -# This file is part of cgilite. +# Copyright 2016 - 2024 Paul Hänsch # -# cgilite 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. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. # -# cgilite 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 cgilite. If not, see . +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. [ -n "$include_fileserve" ] && return 0 include_fileserve="$0" file_type(){ case ${1##*.} in - html|html) printf 'text/html';; css) printf 'text/css';; - js) printf 'text/javascript';; - txt) printf 'text/plain';; - sh) printf 'text/shellscript';; + gif) printf 'image/gif';; + html|html) printf 'text/html';; jpg|jpeg) printf 'image/jpeg';; + js) printf 'text/javascript';; + m3u8) printf 'application/x-mpegURL';; + m4a) printf 'audio/mp4';; + m4s) printf 'video/iso.segment';; + m4v|mp4) printf 'video/mp4';; + mpd) printf 'application/dash+xml';; + ogg) printf 'audio/ogg';; + pdf) printf 'application/pdf';; png) printf 'image/png';; + sh) printf 'text/x-shellscript';; svg) printf 'image/svg+xml';; - gif) printf 'image/gif';; + tex) printf 'text/x-tex';; + txt) printf 'text/plain';; + short) printf 'text/prs.shorthand';; + ts) printf 'video/MP2T';; webm) printf 'video/webm';; - mp4|m4v) printf 'video/mp4';; - m4a) printf 'audio/mp4';; - ogg) printf 'audio/ogg';; xml) printf 'application/xml';; - m3u8) printf 'application/x-mpegURL';; - ts) printf 'video/MP2T';; - mpd) printf 'application/dash+xml';; - m4s) printf 'video/iso.segment';; *) printf 'application/octet-stream';; esac } FILE(){ - local file file_size file_date http_date cachedate range mime - file="$1" mime="$2" + local file="$1" mime="$2" + local file_size file_date http_date cachedate range if ! [ -f "$file" ]; then printf 'Content-Length: 0\r\nStatus: 404 Not Found\r\n\r\n' - exit 0 + return 0 elif ! [ -r "$file" ]; then printf 'Content-Length: 0\r\nStatus: 403 Forbidden\r\n\r\n' - exit 0 + return 0 fi - file_size="$(stat -Lc %s "$file")" - file_date="$(stat -Lc %Y "$file")" - http_date="$(date -uRd @$file_date)" - http_date="${http_date%+0000}GMT" + read file_size file_date <<-EOF + $(stat -Lc "%s %Y" "$file") + EOF + http_date="$(date -ud "@$file_date" +"%a, %d %b %Y %T GMT")" + + [ ! "$HTTP_IF_MODIFIED_SINCE" -a "$cgilite_headers" ] \ + && HTTP_IF_MODIFIED_SINCE="$(HEADER If-Modified-Since)" + [ ! "$HTTP_RANGE" -a "$cgilite_headers" ] \ + && HTTP_RANGE="$(HEADER Range)" + cachedate="$( # Parse the allowable date formats from Section 3.3.1 of # https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html - HEADER If-Modified-Since \ + # HEADER If-Modified-Since \ + printf %s "$HTTP_IF_MODIFIED_SINCE" \ | sed -E 's;^[^ ]+, ([0-9]{2}) (...) ([0-9]{4}) (..:..:..) GMT$;\3-\2-\1 \4;; s;^[^ ]+, ([0-9]{2})-(...)-([789][0-9]) (..:..:..) GMT$;19\3-\2-\1 \4;; s;^[^ ]+, ([0-9]{2})-(...)-([0-6][0-9]) (..:..:..) GMT$;20\3-\2-\1 \4;; @@ -74,14 +81,25 @@ FILE(){ | xargs -r0 date +%s -ud 2>&- )" - range="$(HEADER Range |sed -nE 's;^bytes=([0-9]+-[0-9]*|-[0-9]+)$;\1;p;q;')" + range="${HTTP_RANGE#bytes=}" case "$range" in - *-) range="${range}$((file_size - 1))";; - -*) [ ${range#-} -le $file_size ] \ - && range="$((file_size - ${range#-}))-$((file_size - 1))" \ - || range="0-$((file_size - 1))";; - *-*) [ ${range#*-} -ge $file_size ] \ - && range="${range%-*}-$((file_size - 1))";; + *[!0-9]*-*|*-*[!0-9]*) + range="" + ;; + *-) + range="${range}$((file_size - 1))" + ;; + -*) + [ ${range#-} -le $file_size ] \ + && range="$((file_size - ${range#-}))-$((file_size - 1))" \ + || range="0-$((file_size - 1))" + ;; + *-*) + [ ${range#*-} -ge $file_size ] \ + && range="${range%-*}-$((file_size - 1))" + ;; + *) range="" + ;; esac if [ "$file_date" -lt "$cachedate" ] 2>&-; then diff --git a/html-sh.sed b/cgilite/html-sh.sed similarity index 72% rename from html-sh.sed rename to cgilite/html-sh.sed index 8d7b61c..1a0f2b4 100755 --- a/html-sh.sed +++ b/cgilite/html-sh.sed @@ -1,5 +1,19 @@ #!/bin/sed -nEf +# Copyright 2018 - 2019 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + :Escapes s,\\\\,\\,g; s,\\&,\&,g; s,\\<,\<,g; s,\\>,\>,g; diff --git a/cgilite/json.sh b/cgilite/json.sh new file mode 100755 index 0000000..12afdc4 --- /dev/null +++ b/cgilite/json.sh @@ -0,0 +1,360 @@ +#!/bin/sh + +[ -n "$include_json" ] && return 0 +include_json="$0" + +. "${_EXEC:-.}/cgilite/db23.sh" + +# debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; } + +json_except() { + printf '%s\n' "$@" >&2; + printf 'Exc: %s\n' "$json_document" >&2 +} + +json_space() { + while true; do case "$json_document" in + [" ${BR}${CR} "]*) json_document="${json_document#?}";; + *) break ;; + esac; done +} + +json_string() { + local string json_document="$json_document" end=0 + + json_space + case $json_document in + \"*) json_document="${json_document#?}" + ;; + *) json_except "Expected string specifyer starting with (\")" + return 1 + ;; + esac + while [ "$json_document" ]; do case $json_document in + \\?*) + string="${string}${json_document%"${json_document#??}"}" + json_document="${json_document#??}" + ;; + \"*) + json_document="${json_document#?}" + end=1 + break + ;; + *) + string="${string}${json_document%"${json_document#?}"}" + json_document="${json_document#?}" + ;; + esac; done + + if [ $end -eq 0 ]; then + json_except "Document ended mid-string" + return 1 + fi + + printf "%s %s\n" "$(STRING "$string")" "$json_document" +} + +json_key() { + local key json_document="$json_document" + + json_space + case $json_document in + \"*) + key="$(json_string)" || return 1 + json_document="${key#* }" + key="${key%% *}" + ;; + *) json_except "Expected key specifyer starting with '\"'" + return 1 + ;; + esac + json_space + case $json_document in + :*) json_document="${json_document#?}" + ;; + *) json_except "Expected value separator \":\"" + return 1 + ;; + esac + + printf '%s %s\n' "$key" "$json_document" +} + +json_number() { + local number json_document="$json_document" + + json_space + number="${json_document%%[" ${BR}${CR} ,}]"]*}" + json_document="${json_document#"$number"}" + if ! number="$(printf %f "$number")"; then + json_except "Invalid number format" + return 1 + fi + + printf '%s %s\n' "${number%.000000}" "$json_document" +} + +json_array() { + local struct="$(DB2 "" new)" value json_document="$json_document" + + json_space + case $json_document in + "["*) json_document="${json_document#?}" + ;; + *) json_except "Expected array starting with \"[\"" + return 1 + ;; + esac + + json_space + case $json_document in + "]"*) + printf "%s %s\n" "" "${json_document#?}" + return 0 + ;; + esac + + while :; do + json_space + + value="$(json_value)" || return 1 + json_document="${value#* }" + value="$(UNSTRING "${value%% *}")" + + struct="$(DB2 "$struct" append "@" "$value")" \ + || struct="$(DB2 "$struct" set "@" "$value")" + + json_space + case $json_document in + ,*) json_document="${json_document#?}" + ;; + "]"*) json_document="${json_document#?}" + break + ;; + *) json_except "Unexpected character mid-array" + return 1 + ;; + esac + done + + printf "%s %s\n" "$(STRING "$struct")" "$json_document" +} + +json_object() { + local struct="$(DB2 "" new)" key value json_document="$json_document" + + json_space + case $json_document in + "{"*) json_document="${json_document#?}" + ;; + *) json_except "Expected object starting with \"{\"" + return 1 + ;; + esac + + json_space + case $json_document in + "}"*) + printf "%s %s\n" "" "${json_document#?}" + return 0 + ;; + esac + + while :; do + json_space + + key="$(json_key)" || return 1 + json_document="${key#* }" + key="$(UNSTRING "${key%% *}")" + + value="$(json_value)" || return 1 + json_document="${value#* }" + value="$(UNSTRING "${value%% *}")" + + struct="$(DB2 "$struct" set "$key" "$value")" + + json_space + case $json_document in + ,*) json_document="${json_document#?}" + ;; + "}"*) json_document="${json_document#?}" + break + ;; + *) json_except "Unexpected character mid-object" + return 1 + ;; + esac + done + + printf "%s %s\n" "$(STRING "$struct")" "$json_document" +} + +json_value() { + local value json_document="$json_document" + json_type="" + + json_space + case $json_document in + \"*) + value="$(json_string)" || return 1 + json_document="${value#* }" + value="str:${value%% *}" + json_type=string + ;; + [+-.0-9]*) + value="$(json_number)" || return 1 + json_document="${value#* }" + value="num:${value%% *}" + json_type=number + ;; + "{"*) + value="$(json_object)" || return 1 + json_document="${value#* }" + value="obj:${value%% *}" + json_type=object + ;; + "["*) + value="$(json_array)" || return 1 + json_document="${value#* }" + value="arr:${value%% *}" + json_type=array + ;; + null*) + json_document="${json_document#null}" + value="null" + json_type=null + ;; + true*) + json_document="${json_document#true}" + value="true" + json_type=boolean + ;; + false*) + json_document="${json_document#false}" + value="false" + json_type=boolean + ;; + esac + + printf "%s %s\n" "$value" "$json_document" +} + +json_load() { + local json_document="$1" json + + json_value |UNSTRING +} + +json_get() { + local json="$1" jpath="${2#.}" key idx + json_type='' + + case $json in + str:*) json_type="string";; + arr:*) json_type="array";; + obj:*) json_type="object";; + num:*) json_type="number";; + true|false) + json_type="boolean";; + null) json_type="null";; + esac + + case $jpath in + "") + printf %s\\n "${json#???:}" + return 0 + ;; + "["[0-9]*"]"*) + idx="${jpath%%"]"*}" idx="${idx#"["}" + jpath="${jpath#"["*"]"}" + ;; + "['"*"']"*) + key="${jpath%%"']"*}" key="${key#"['"}" + jpath="${jpath#"['"*"']"}" + ;; + "$"*) + jpath="${jpath#?}" + ;; + *) key="${jpath%%[".["]*}" + jpath="${jpath#"$key"}" + ;; + esac + + if [ "$key" -a "$json_type" = object ]; then + if ! json="$(DB2 "${json#obj:}" get "$key")"; then + debug "Key not found: \"$key\"" + return 1 + fi + elif [ "$idx" -a "$json_type" = array ]; then + if ! json="$(DB2 "${json#arr:}" get @ "$(( idx + 1 ))")"; then + debug "Array index not found: \"$idx\"" + return 1 + fi + elif [ "$key" ]; then + debug "Cannot select key (\"$key\") from value of type \"$json_type\"" + return 1 + elif [ "$idx" ]; then + debug "Cannot select index ($idx) from value of type \"$json_type\"" + return 1 + fi + json_get "$json" "$jpath" + return $? +} + +json_dump_string() { + local in="$1" out='' + while [ "$in" ]; do case $in in + \\*) out="${out}\\\\"; in="${in#\\}" ;; + "$BR"*) out="${out}\\n"; in="${in#${BR}}" ;; + "$CR"*) out="${out}\\r"; in="${in#${CR}}" ;; + " "*) out="${out}\\t"; in="${in# }" ;; + \"*) out="${out}\\\""; in="${in#\"}" ;; + *) out="${out}${in%%[\\${CR}${BR} \"]*}"; in="${in#"${in%%[\\${BR}${CR} \"]*}"}" ;; + esac; done + printf '"%s"' "${out}" +} + +json_dump_array() { + local json="$1" value out='' + + for value in $(DB2 "$json" iterate @); do + out="${out},$(json_dump "$(UNSTRING "$value")")" + done + printf '[%s]' "${out#,}" +} + +json_dump_object() { + local json="$1" key value out='' + + while read -r key value; do + out="${out},$(json_dump_string "$(UNSTRING "$key")"):$(json_dump "$(UNSTRING "$value")")" + done <<-EOF + ${json} + EOF + printf '{%s}' "${out#,}" +} + +json_dump() { + local json="$1" + + case $json in + str:*) + json_dump_string "${json#str:}" + ;; + arr:*) + json_dump_array "${json#arr:}" + ;; + obj:*) + json_dump_object "${json#obj:}" + ;; + num:*) + printf "${json#num:}" + ;; + true|false|null) + printf %s\\n "$json" + ;; + *) + json_dump_string "${json}" + ;; + esac +} diff --git a/logging.sh b/cgilite/logging.sh similarity index 100% rename from logging.sh rename to cgilite/logging.sh diff --git a/cgilite/markdown.awk b/cgilite/markdown.awk new file mode 100755 index 0000000..34879d2 --- /dev/null +++ b/cgilite/markdown.awk @@ -0,0 +1,866 @@ +#!/bin/awk -f +#!/opt/busybox/awk -f + +# EXPERIMENTAL Markdown processor with minimal dependencies. +# Meant to support all features of John Grubers basic Markdown +# + a number of common extensions, mostly inspired by Pandoc Markdown + +# Copyright 2021 - 2023 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# Supported Features / TODO: +# ========================== +# [x] done [ ] todo [-] not planned ? unsure +# +# Basic Markdown - Block elements: +# ------------------------------- +# - [x] Paragraphs +# - [x] Double space line breaks +# - [x] Proper block element nesting +# - [x] Headings +# - [x] ATX-Style Headings +# - [x] Blockquotes +# - [x] Lists (ordered, unordered) +# - [x] Code blocks (using indention) +# - [x] Horizontal rules +# - [x] Verbatim HTML block (disabled by default) +# +# Basic Markdown - Inline elements: +# --------------------------------- +# - [x] Links +# - [x] Reference style links +# - [x] Emphasis *em*/**strong** (*Asterisk*, _Underscore_) +# - [x] `code`, also ``code containing `backticks` `` +# - [x] Images / reference style images +# - [x] +# - [x] backslash escapes +# - [x] Verbatim HTML inline (disabled by default) +# - [x] HTML escaping +# +# NOTE: Set the environment variable MD_HTML=true to enable verbatim HTML +# +# Extensions - Block elements: +# ---------------------------- +# - [x] Automatic
-wrapping (custom) +# - ? Heading identifiers (php md, pandoc) +# - [x] Heading attributes (custom) +# - [ ]
terminates section +# - [x] Automatic heading identifiers (custom) +# - [x] Fenced code blocks (php md, pandoc) +# - [x] Fenced code attributes +# - [x] Images (as block elements,
-wrapped) (custom) +# - [x] reference style block images +# - [/] Tables +# - ? Simple table (pandoc) +# - ? Multiline table (pandoc) +# - [x] Grid table (pandoc) +# - [x] Headerless +# - [x] Pipe table (php md, pandoc) +# - [x] Line blocks (pandoc) +# - [x] Task lists (pandoc, custom) +# - [x] Definition lists (php md, pandoc) +# - [-] Numbered example lists (pandoc) +# - [-] Metadata blocks (pandoc) +# - [x] Metadata blocks (custom) +# - [x] Fenced Divs (pandoc) +# +# Extensions - Inline elements: +# ---------------------------- +# - [x] Ignore embedded_underscores (php md, pandoc) +# - [x] ~~strikeout~~ (pandoc) +# - [x] ^Superscript^ ~Subscript~ (pandoc) +# - [-] Bracketed spans (pandoc) +# - [-] Inline attributes (pandoc) +# - [x] Image attributes (custom, pandoc inspired, not for reference style) +# - [x] Wiki style links [[PageName]] / [[PageName|Link Text]] +# - [-] TEX-Math (pandoc) +# - ? Footnotes (php md) +# - ? Abbreviations (php md) +# - ? "Curly quotes" (smartypants) +# - [ ] em-dashes (--) (smartypants old) +# - ? ... three-dot ellipsis (smartypants) +# - [-] en-dash (smartypants) +# - [ ] Automatic em-dash / en-dash +# - [x] Automatic -> Arrows <- (custom) + +function debug(text) { printf "\n---\n%s\n---\n", text > "/dev/stderr"; } + +function HTML ( text ) { + gsub( /&/, "\\&", text ); + gsub( //, "\\>", text ); + gsub( /"/, "\\"", text ); + gsub( /'/, "\\'", text ); + gsub( /\\/, "\\\", text ); + return text; +} + +function URL ( text, sharp ) { + gsub( /&/, "%26", text ); + gsub( /"/, "%22", text ); + gsub( /'/, "%27", text ); + gsub( /`/, "%60", text ); + gsub( /\?/, "%3F", text ); + if (sharp) gsub( /#/, "%23", text ); + gsub( /\[/, "%5B", text ); + gsub( /\]/, "%5D", text ); + gsub( / /, "%20", text ); + gsub( / /, "%09", text ); + gsub( /\\/, "%5C", text ); + return text; +} + +function inline( line, LOCAL, len, text, code, href, guard ) { + if ( line ~ /^$/ ) { # Recursion End + return ""; + + # omit processing of escaped characters + } else if ( line ~ /^\\./) { + return HTML(substr(line, 2, 1)) inline( substr(line, 3) ); + + # hard brakes + } else if ( match(line, /^ \n/) ) { + return "
\n" inline( substr(line, RLENGTH + 1) ); + + # ``code spans`` + } else if ( match( line, /^`+/) ) { + len = RLENGTH + guard = substr( line, 1, len ) + if ( match(line, guard ".*" guard) ) { + code = substr( line, len + 1, match( substr(line, len + 1), guard ) - 1) + len = 2 * length(guard) + length(code) + # strip single surrounding white spaces + gsub( /^ | $/, "", code) + # escape HTML within code span + gsub( /&/, "\\&", code ); gsub( //, "\\>", code ); + return "" code "" inline( substr( line, len + 1 ) ) + } + + # Macros + } else if ( match( line, /^<<([^>]|>[^>])+>>/ ) ) { + len = RLENGTH; + return "" HTML( substr( line, 3, len - 4 ) ) "" inline(substr(line, len + 1)); + + # Wiki style links + } else if ( match( line, /^\[\[([^]|]+)(\|[^]]+)?\]\]/) ) { + len = RLENGTH; + href = gensub(/^\[\[([^]|]+)(\|([^]]+))?\]\]/, "\\1", 1, substr(line, 1, len) ); + text = gensub(/^\[\[([^]|]+)(\|([^]]+))?\]\]/, "\\3", 1, substr(line, 1, len) ); + if ( ! text ) text = href; + return "" HTML(text) "" inline( substr( line, len + 1) ); + + # quick links ("automatic links" in md doc) + } else if ( match( line, /^<[a-zA-Z]+:\/\/([-\.[:alnum:]]+)(:[0-9]*)?(\/[^>]*)?>/ ) ) { + len = RLENGTH; + href = HTML( substr( line, 2, len - 2) ); + return "" href "" inline( substr( line, len + 1) ); + + # quick link email + } else if ( match( line, /^<[a-zA-Z0-9.!#$%&'\''*+\/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>/ ) ) { + len = RLENGTH; + href = HTML( substr( line, 2, len - 2) ); + return "" href "" inline( substr( line, len + 1) ); + + # Verbatim inline HTML + } else if ( AllowHTML && match( line, /^(|<\?([^\?]|\?[^>])*\?>|]*>|])*\]\]>|<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)/) ) { + len = RLENGTH; + return substr( line, 1, len) inline(substr(line, len + 1)); + + # inline links + } else if ( match(line, "^" lii "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)") ) { + len = RLENGTH; + text = href = title = substr( line, 1, len); + sub("^\\[", "", text); sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)$", "", text); + sub("^" lii "\\([\n\t ]*", "", href); sub("([\n\t ]+" lit ")?[\n\t ]*\\)$", "", href); + sub("^" lii "\\([\n\t ]*" lid, "", title); sub("[\n\t ]*\\)$", "", title); sub("^[\n\t ]+", "", title); + + if ( match(href, /^<.*>$/) ) { sub(/^$/, "", href); } + if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); } + else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); } + else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); } + + gsub(/\\/, "", href); gsub(/\\/, "", title); gsub(/[\n\t]+/, " ", title); + + return "" \ + inline( text ) "" inline( substr( line, len + 1) ); + + # reference style links + } else if ( match(line, /^\[([^]]+)\] ?\[([^]]*)\]/ ) ) { + len = RLENGTH; + text = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, substr(line, 1, len) ); + id = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, substr(line, 1, len) ); + if ( ! id ) id = text; + if ( rl_href[id] && rl_title[id] ) { + return "" inline(text) "" inline( substr( line, len + 1) ); + } else if ( rl_href[id] ) { + return "" inline(text) "" inline( substr( line, len + 1) ); + } else { + return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) ); + } + + # inline images + } else if ( match(line, "^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?") ) { + len = RLENGTH; text = href = title = attrib = substr( line, 1, len); + + sub("^!\\[", "", text); + sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", text); + + sub("^!" lix "\\([\n\t ]*", "", href); + sub("([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", href); + + sub("^!" lix "\\([\n\t ]*" lid, "", title); + sub("[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", title); + sub("^[\n\t ]+", "", title); + + sub("^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)", "", attrib); + sub(/^\{[ \t]*/, "", attrib); sub(/[ \t]*\}$/, "", attrib); gsub(/[ \t]+/, " ", attrib); + + if ( match(href, /^<.*>$/) ) { sub(/^$/, "", href); } + if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); } + else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); } + else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); } + + gsub(/^[\t ]+$/, "", text); gsub(/\\/, "", href); + gsub(/\\/, "", title); gsub(/[\n\t]+/, " ", title); + + return "\""" inline( substr( line, len + 1) ); + + # reference style images + } else if ( match(line, /^!\[([^]]*)\] ?\[([^]]*)\]/ ) ) { + len = RLENGTH; + text = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\].*/, "\\1", 1, substr(line, 1, len) ); + id = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\].*/, "\\2", 1, substr(line, 1, len) ); + if ( ! id ) id = text; + if ( rl_href[id] && rl_title[id] ) { + return "\""" \ + inline( substr( line, len + 1) ); + } else if ( rl_href[id] ) { + return "\""" \ + inline( substr( line, len + 1) ); + } else { + return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) ); + } + + # ~~strikeout~~ (pandoc) + } else if ( match(line, /^~~([[:graph:]]|[[:graph:]]([^~]|~[^~])*[[:graph:]])~~/) ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # ^superscript^ (pandoc) + } else if ( match(line, /^\^([^[:space:]^]|\\[ ^])+\^/) ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # ~subscript~ (pandoc) + } else if ( match(line, /^~([^[:space:]~]|\\[ ~])+~/) ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # ignore embedded underscores (pandoc, php md) + } else if ( match(line, "^[[:alnum:]](__|_)") ) { + return HTML(substr( line, 1, RLENGTH)) inline( substr(line, RLENGTH + 1) ); + + # __strong__$ + } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__$") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # __strong__ + } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__[[:space:][:punct:]]") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 5 ) ) "" inline( substr( line, len) ); + + # **strong** + } else if ( match(line, "^\\*\\*(([^\\*[:space:]]|" iea ")|([^\\*[:space:]]|" iea ")(" na "|" iea ")*([^\\*[:space:]]|" iea "))\\*\\*") ) { + len = RLENGTH; + return "" inline( substr( line, 3, len - 4 ) ) "" inline( substr( line, len + 1 ) ); + + # _em_$ + } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_$") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # _em_ + } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_[[:space:][:punct:]]") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 3 ) ) "" inline( substr( line, len ) ); + + # *em* + } else if ( match(line, "^\\*(([^\\*[:space:]]|" isa ")|([^\\*[:space:]]|" isa ")(" na "|" isa ")*([^\\*[:space:]]|" isa "))\\*") ) { + len = RLENGTH; + return "" inline( substr( line, 2, len - 2 ) ) "" inline( substr( line, len + 1 ) ); + + # Literal HTML entities + } else if ( match( line, /^&([a-zA-Z]{2,32}|#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6});/) ) { + len = RLENGTH; + return substr( line, 1, len ) inline(substr(line, len + 1)); + + # Arrows + } else if ( line ~ /^-->( |$)/) { # ignore multidash-arrow + return "-->" inline( substr(line, 4) ); + } else if ( line ~ /^<-( |$)/) { + return "←" inline( substr(line, 3) ); + } else if ( line ~ /^->( |$)/) { + return "→" inline( substr(line, 3) ); + + # Escape lone HTML character + } else if ( match( line, /^[&<>"']/) ) { + return HTML(substr(line, 1, 1)) inline(substr(line, 2)); + + # continue walk over string + } else { + return substr(line, 1, 1) inline( substr(line, 2) ); + } +} + +function headline( hlvl, htxt, attrib, LOCAL, sec, n, HL) { + match(hstack, /([0-9]+( [0-9]+){5})$/); split( substr(hstack, RSTART), HL); + + for ( n = hlvl; n <= 6; n++ ) { sec = sec (HL[n]?"
":""); } + HL[hlvl]++; for ( n = hlvl + 1; n <= 6; n++) { HL[n] = 0;} + + hid = ""; for ( n = 2; n <= blvl; n++) { hid = hid BL[n] "/"; } + hid = hid HL[1]; for ( n = 2; n <= hlvl; n++) { hid = hid "." HL[n] ; } + hid = hid ":" URL(htxt, 1); + + sub(/([0-9]+( [0-9]+){5})$/, "", hstack); + hstack = hstack HL[1] " " HL[2] " " HL[3] " " HL[4] " " HL[5] " " HL[6]; + + return sec "
" \ + "" inline( htxt ) \ + "" \ + "\n"; +} + +# Nested Block, resets heading counters +function _nblock( block, LOCAL, sec, n ) { + hstack = hstack " 0 0 0 0 0 0"; + + # Block Level + blvl++; BL[blvl]++; + for ( n = blvl + 1; n in BL; n++) { delete BL[n]; } + + block = _block( block ); + match(hstack, /([0-9]+( [0-9]+){5})$/); split( substr(hstack, RSTART), HL); + sec = ""; for ( n = 1; n <= 6; n++ ) { sec = sec (HL[n]?"
":""); } + + sub("( +[0-9]+){6} *$", "", hstack); blvl--; + return block sec; +} + +function _block( block, LOCAL, st, len, text, title, attrib, href, guard, code, indent, list ) { + gsub( "(^\n+|\n+$)", "", block ); + + if ( block == "" ) { + return ""; + + # HTML #2 #3 #4 $5 + } else if ( AllowHTML && match( block, /(^|\n) ? ? ?(|$)|<\?([^\?]|\?[^>])*(\?>|$)|]*(>|$)|])*(\]\]>|$))/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #6 + } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<\/?(address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)([[:space:]\n>]|\/>)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #1 + } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<(script|pre|style)([[:space:]\n>]).*(<\/script>|<\/pre>|<\/style>|$)/) ) { + len = RLENGTH; st = RSTART; + match( tolower(substr(block, st, len)), /(<\/script>|<\/pre>|<\/style>)/); + len = RSTART + RLENGTH; + return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len)); + + # HTML #7 + } else if ( AllowHTML && match( block, /^ ? ? ?(<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)([[:space:]]*\n)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) { + len = RLENGTH; st = RSTART; + return substr(block, st, len) _block(substr(block, st + len)); + + # Metadata (custom, block starting with %something) + # Metadata is ignored but can be interpreted externally + } else if ( match(block, /^%[a-zA-Z-]+([[:space:]][^\n]*)?(\n|$)(%[a-zA-Z-]+([[:space:]][^\n]*)?(\n|$)|%([[:space:]][^\n]*)?(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) { + len = RLENGTH; st = RSTART; + return _block( substr( block, len + 1) ); + + # Blockquote (leading >) + } else if ( match( block, /^> /) ) { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match(block, /$/); + len = RLENGTH; st = RSTART; + text = substr(block, 1, st - 1); gsub( /(^|\n)> /, "\n", text ); + text = _nblock( text ); gsub( /^\n|\n$/, "", text ) + return "
" text "
\n\n" _block( substr(block, st + len) ); + + # Pipe Tables (pandoc / php md / gfm ) + } else if ( match(block, "^((\\|)?([^\n]+\\|)+[^\n]+(\\|)?)\n" \ + "((\\|)?(:?-+:?[\\|+])+:?-+:?(\\|)?)\n" \ + "((\\|)?([^\n]+\\|)+[^\n]+(\\|)?(\n|$))+" ) ) { + len = RLENGTH; st = RSTART; + #initialize empty arrays + split("", talign); split("", tarray); + cols = 0; cnt=0; ttext = ""; + + # table header and alignment + split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tarray, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + cols = split( \ + gensub( /(^\||\|$)/, "", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + ), talign, /[+\|]/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + + for( cnt = 1; cnt < cols; cnt++ ) { + if (match(talign[cnt], /:-+:/)) talign[cnt]="center"; + else if (match(talign[cnt], /-+:/)) talign[cnt]="right"; + else if (match(talign[cnt], /:-+/)) talign[cnt]="left"; + else talign[cnt]=""; + } + + ttext = "\n" + for (cnt = 1; cnt < cols; cnt++) + ttext = ttext "" inline(tarray[cnt]) "" + ttext = ttext "\n\n" + + while ( match(block, "^((\\|)?([^\n]+\\|)+[^\n]+(\\|)?(\n|$))+" ) ){ + split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tarray, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + + ttext = ttext "" + for (cnt = 1; cnt < cols; cnt++) + ttext = ttext "" inline(tarray[cnt]) "" + ttext = ttext "\n" + } + return "" ttext "
\n" _block(block); + + # Grid Tables (pandoc) + # (with, and without header) + } else if ( match( block, "^\\+(-+\\+)+\n" \ + "(\\|([^\n]+\\|)+\n)+" \ + "(\\+(:?=+:?\\+)+)\n" \ + "((\\|([^\n]+\\|)+\n)+" \ + "\\+(-+\\+)+(\n|$))+" \ + ) || \ + match( block, "^()()()" \ + "(\\+(:?-+:?\\+)+)\n" \ + "((\\|([^\n]+\\|)+\n)+" \ + "\\+(-+\\+)+(\n|$))+" \ + ) ) { + len = RLENGTH; st = RSTART; + #initialize empty arrays + split("", talign); split("", tarray); split("", tread); + cols = 0; cnt=0; ttext = ""; + + # Column Count + cols = split( gensub( "^(\\+(:?-+:?\\+)+)(\n.*)*$", "\\1", 1, block), tread, /\+/) - 2; + # debug(" Cols: " gensub( "^(\\+(:?-+:?\\+)+)(\n.*)*$", "\\1", 1, block )); + + # table alignment + split( gensub( "^(.*\n)?\\+((:?=+:?\\+|(:-+|-+:|:-+:)\\+)+)(\n.*)$", "\\2", "g", block ), talign, /\+/ ); + # debug("Align: " gensub( "^(.*\n)?\\+((:?=+:?\\+|(:-+|-+:|:-+:)\\+)+)(\n.*)$", "\\2", "g", block )); + + for (cnt = 1; cnt <= cols; cnt++) { + if (match(talign[cnt], /:(-+|=+):/)) talign[cnt]="center"; + else if (match(talign[cnt], /(-+|=+):/)) talign[cnt]="right"; + else if (match(talign[cnt], /:(-+|=+)/ )) talign[cnt]="left"; + else talign[cnt]=""; + } + + if ( match(block, "^\\+(-+\\+)+\n" \ + "(\\|([^\n]+\\|)+\n)+" \ + "\\+(:?=+:?\\+)+\n" \ + "((\\|([^\n]+\\|)+\n)+" \ + "\\+(-+\\+)+(\n|$))+" \ + ) ) { + # table header + block = substr(block, match(block, /(\n|$)/) + 1 ); + while ( match(block, "^\\|([^\n]+\\|)+\n") ) { + split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tread, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + for (cnt = 1; cnt <= cols; cnt++) + tarray[cnt] = tarray[cnt] "\n" tread[cnt]; + } + + ttext = "\n" + for (cnt = 1; cnt <= cols; cnt++) + ttext = ttext "" _nblock(tarray[cnt]) "" + ttext = ttext "\n" + } + + # table body + block = substr(block, match(block, /(\n|$)/) + 1 ); + ttext = ttext "\n" + + while ( match(block, /^((\|([^\n]+\|)+\n)+\+(-+\+)+(\n|$))+/ ) ){ + split("", tarray); + while ( match(block, /^\|([^\n]+\|)+\n/) ) { + split( gensub( /(^\||\|$)/, "", "g", \ + gensub( /(^|[^\\])\\\|/, "\\1\\|", "g", \ + substr(block, 1, match(block, /(\n|$)/)) \ + )), tread, /\|/); + block = substr(block, match(block, /(\n|$)/) + 1 ); + for (cnt = 1; cnt <= cols; cnt++) + tarray[cnt] = tarray[cnt] "\n" tread[cnt]; + } + block = substr(block, match(block, /(\n|$)/) + 1 ); + + ttext = ttext "" + for (cnt = 1; cnt <= cols; cnt++) + ttext = ttext "" _nblock(tarray[cnt]) "" + ttext = ttext "\n" + } + return "" ttext "
\n" _nblock(block); + + # Line Blocks (pandoc) + } else if ( match(block, /^\| [^\n]*(\n|$)(\| [^\n]*(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) { + len = RLENGTH; st = RSTART; + + text = substr(block, 1, len); gsub(/\n[[:space:]]+/, " ", text); + gsub(/\n\| /, "\n", text); gsub(/^\| |\n$/, "", text); + text = inline(text); gsub(/\n/, "
\n", text); + + return "
" text "
\n" _block( substr( block, len + 1) ); + + # Indented Code Block + } else if ( match(block, /^( |\t)( *\t*[^ \t\n]+ *\t*)+(\n|$)(( |\t)[^\n]+(\n|$)|[ \t]*(\n|$))*/) ) { + len = RLENGTH; st = RSTART; + code = substr(block, 1, len); + gsub(/(^|\n)( |\t)/, "\n", code); + gsub(/^\n|\n+$/, "", code); + return "
" HTML( code ) "
\n" \ + _block( substr( block, len + 1 ) ); + + # Fenced Divs (pandoc, custom) + } else if ( match( block, /^(:::+)/ ) ) { + guard = substr( block, 1, RLENGTH ); + code = block; sub(/^[^\n]+\n/, "", code); + attrib = gensub(/^:::+[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\1", 1, block); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); + gsub(/(^ | $)/, "", attrib); + if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) { + len = RLENGTH; st = RSTART; + return "
" _nblock( substr(code, 1, st - 1) ) "
\n" \ + _block( substr( code, st + len ) ); + } else { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ ); + len = RLENGTH; st = RSTART; + return "

" inline( substr(block, 1, st - 1) ) "

\n" \ + _block( substr(block, st + len) ); + } + + # Fenced Code Block (pandoc) + } else if ( match( block, /^(~~~+|```+)/ ) ) { + guard = substr( block, 1, RLENGTH ); + code = gensub(/^[^\n]+\n/, "", 1, block); + attrib = gensub(/^(~~~+|```+)[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\2", 1, block); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); + gsub(/(^ | $)/, "", attrib); + if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) { + len = RLENGTH; st = RSTART; + return "
" HTML( substr(code, 1, st - 1) ) "
\n" \ + _block( substr( code, st + len ) ); + } else { + match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ ); + len = RLENGTH; st = RSTART; + return "

" inline( substr(block, 1, st - 1) ) "

\n" \ + _block( substr(block, st + len) ); + } + + # First Order Heading H1 + Attrib + } else if ( match( block, /^([^\n]+)([ \t]*\{([^\}\n]+)\})\n===+(\n|$)/ ) ) { + len = RLENGTH; text = attrib = block; + sub(/([ \t]*\{([^\}\n]+)\})\n===+(\n.*)?$/, "", text); + sub(/\}\n===+(\n.*)?$/, "", attrib); sub(/^([^\n]+)[ \t]*\{/, "", attrib); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib); + + return headline(1, text, attrib) _block( substr( block, len + 1 ) ); + + # First Order Heading H1 + } else if ( match( block, /^([^\n]+)\n===+(\n|$)/ ) ) { + len = RLENGTH; text = substr(block, 1, len); + sub(/\n===+(\n.*)?$/, "", text); + + return headline(1, text, 0) _block( substr( block, len + 1 ) ); + + # Second Order Heading H2 + Attrib + } else if ( match( block, /^([^\n]+)([ \t]*\{([^\}\n]+)\})\n---+(\n|$)/ ) ) { + len = RLENGTH; text = attrib = block; + sub(/([ \t]*\{([^\}\n]+)\})\n---+(\n.*)?$/, "", text); + sub(/\}\n---+(\n.*)?$/, "", attrib); sub(/^([^\n]+)[ \t]*\{/, "", attrib); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib); + + return headline(2, text, attrib) _block( substr( block, len + 1) ); + + # Second Order Heading H2 + } else if ( match( block, /^([^\n]+)\n---+(\n|$)/ ) ) { + len = RLENGTH; text = substr(block, 1, len); + sub(/\n---+(\n.*)?$/, "", text); + + return headline(2, text, 0) _block( substr( block, len + 1) ); + + # Nth Order Heading H1 H2 H3 H4 H5 H6 + Attrib + } else if ( match( block, /^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*([ \t]*\{([a-zA-Z \t-]*)\})(\n|$)/ ) ) { + len = RLENGTH; text = attrib = substr(block, 1, len); + match(block, /^#{1,6}/); n = RLENGTH; + + sub(/^(#{1,6})[ \t]*/, "", text); sub(/[ \t]*#*([ \t]*\{([a-zA-Z \t-]*)\})(\n.*)?$/, "", text); + sub(/^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*[ \t]*\{/, "", attrib); + sub(/\})(\n.*)?$/, "", attrib); + gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib); + + return headline( n, text, attrib ) _block( substr( block, len + 1) ); + + # Nth Order Heading H1 H2 H3 H4 H5 H6 + } else if ( match( block, /^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*(\n|$)/ ) ) { + len = RLENGTH; text = substr(block, 1, len); + match(block, /^#{1,6}/); n = RLENGTH; + sub(/^(#{1,6})[ \t]*/, "", text); sub(/[ \t]*#*(\n.*)?$/, "", text); + + return headline( n, text, 0 ) _block( substr( block, len + 1) ); + + # block images (wrapped in
) + } else if ( match(block, "^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n|$)") ) { + len = RLENGTH; text = href = title = attrib = substr( block, 1, len); + + sub("^!\\[", "", text); + sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", text); + + sub("^!" lix "\\([\n\t ]*", "", href); + sub("([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", href); + + sub("^!" lix "\\([\n\t ]*" lid, "", title); + sub("[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", title); + sub("^[\n\t ]+", "", title); + + sub("^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)", "", attrib); + sub("(\n.*)?$", "", attrib); + sub(/^\{[ \t]*/, "", attrib); sub(/[ \t]*\}$/, "", attrib); gsub(/[ \t]+/, " ", attrib); + + if ( match(href, /^<.*>$/) ) { sub(/^$/, "", href); } + if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); } + else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); } + else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); } + + gsub(/^[\t ]+$/, "", text); gsub(/\\/, "", href); + + return "
" \ + "\""" \ + (title?"
" inline(title) "
":"") \ + "
\n\n" \ + _block( substr( block, len + 1) ); + + # reference style images (block) + } else if ( match(line, /^!\[([^]]*)\] ?\[([^]]*)\](\n|$)/ ) ) { + len = RLENGTH; + text = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\](\n.*)?$/, "\\1", 1, block); + id = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\](\n.*)?$/, "\\2", 1, block); + if ( ! id ) id = text; + if ( rl_href[id] && rl_title[id] ) { + return "
" \ + "\""" \ + "
" inline(rl_title[id]) "
" \ + "
\n\n" \ + _block( substr( block, len + 1) ); + } else if ( rl_href[id] ) { + return "
" \ + "\""" \ + "
\n\n" \ + _block( substr( block, len + 1) ); + } else { + return "

" HTML(substr(block, 1, len)) "

\n" _block( substr(block, len + 1) ); + } + + # Macros (standalone <> calls handled as block, so they are not wrapped in paragraph) + } else if ( match( block, /^<<(([^>]|>[^>])+)>>(\n|$)/ ) ) { + len = RLENGTH; + text = gensub(/^<<(([^>]|>[^>])+)>>(\n.*)?$/, "\\1", 1, block); + return "" HTML(text) "" _block(substr(block, len + 1) ); + + # Definition list + } else if (match( block, "^(([ \t]*\n)*[^:\n \t][^\n]+\n" \ + "([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \ + "(([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \ + "|[^:\n \t][^\n]+(\n|$)" \ + "|( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|([ \t]*\n)+( ? ? ?\t| +)[^\n]+(\n|$))*)+" \ + )) { + list = substr( block, 1, RLENGTH); block = substr( block, RLENGTH + 1); + return "\n
\n" _dlist( list ) "
\n" _block( block ); + + # Unordered list types + } else if ( text = _startlist( block, "ul", "-", "([+*•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ul", "\\+", "([-*•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ul", "\\*", "([-+•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ul", "•", "([-+*]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) { + return text; + + # Ordered list types + } else if ( text = _startlist( block, "ol", "[0-9]+\\.", "([-+*•]|#\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ol", "[0-9]+\\)", "([-+*•]|[0-9]+\\.|#\\.|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ol", "#\\.", "([-+*•]|[0-9]+\\.|[0-9]+\\)|#\\))") ) { + return text; + } else if ( text = _startlist( block, "ol", "#\\)", "([-+*•]|[0-9]+\\.|#\\.|[0-9]+\\))") ) { + return text; + + # Split paragraphs + } else if ( match( block, /(^|\n)[[:space:]]*(\n|$)/) ) { + len = RLENGTH; st = RSTART; + return _block( substr(block, 1, st - 1) ) "\n" \ + _block( substr(block, st + len) ); + + # Horizontal rule + } else if ( match( block, /(^|\n) ? ? ?((\* *){3,}|(- *){3,}|(_ *){3,})($|\n)/) ) { + len = RLENGTH; st = RSTART; + return _block(substr(block, 1, st - 1)) "
\n" _block(substr(block, st + len)); + + # Plain paragraph + } else { + return "

" inline(block) "

\n"; + } +} + +function _startlist(block, type, mark, exclude, LOCAL, st, len, list, indent, text) { + if (match( block, "(^|\n) ? ? ?" mark "[ \t][^\n]+(\n|$)" \ + "(([ \t]*\n)* ? ? ?" mark "[ \t][^\n]+(\n|$)" \ + "|([ \t]*\n)*( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|[^\n \t][^\n]+(\n|$))*" ) ) { + st = RSTART; len = RLENGTH; list = substr( block, st, len); + + sub("^\n", "", list); match(list, "^ ? ? ?"); indent = RLENGTH; + gsub( "(^|\n) {0," indent "}", "\n", list); sub("^\n", "", list); + + text = substr(block, 1, st - 1); block = substr(block, st + len); + if (match(text, /\n[[:space:]]*\n/)) return 0; + if (match(text, "(^|\n) ? ? ?" exclude "[ \t][^\n]+")) return 0; + if (match( list, "\n" exclude "[ \t]" )) { + block = substr(list, RSTART + 1) block; + list = substr(list, 1, RSTART); + } + + return _block( text ) "<" type ">\n" _list( list, mark ) "\n" _block( block ); + } else return 0; +} + +function _list (block, mark, p, LOCAL, len, st, text, indent, task) { + if ( match(block, "^([ \t]*\n)*$")) return; + + match(block, "^" mark "[ \t]"); indent = RLENGTH; + sub("^" mark "[ \t]", "", block); + + if (match(block, /\n[ \t]*\n/)) p = 1; + + match( block, "\n" mark "[ \t][^\n]+(\n|$)" ); + st = (RLENGTH == -1) ? length(block) + 1 : RSTART; + text = substr(block, 1, st); block = substr(block, st + 1); + + gsub("\n {0," indent "}", "\n", text); + + task = match( text, /^\[ \]/ ) ? "
  • " : \ + match( text, /^\[-\]/ ) ? "
  • " : \ + match( text, /^\[\/\]/ ) ? "
  • " : \ + match( text, /^\[\?\]/ ) ? "
  • " : \ + match( text, /^\[[xX]\]/) ? "
  • " : "
  • "; + sub(/^\[[-? \/xX]\]/, "", text); + + text = _nblock( text ); + if ( ! p && match( text, "^

    (]|\n$" )) + gsub( "(^

    |

    \n$)", "", text); + + return task text "
  • \n" _list(block, mark, p); +} + +function _dlist (block, LOCAL, len, st, text, indent, p) { + if (match( block, "^([ \t]*\n)*[^:\n \t][^\n]+\n" )) { + len = RLENGTH; text = substr(block, 1, len); + gsub( "(^\n*|\n*$)", "", text ); + return "
    " inline( text ) "
    \n" _dlist( substr(block, len + 1) ); + } else if (match( block, "^([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \ + "([^:\n \t][^\n]+(\n|$)" \ + "|( ? ? ?\t| +)[^\n]+(\n|$)" \ + "|([ \t]*\n)+( ? ? ?\t| +)[^\n]+(\n|$))*" \ + )) { + len = RLENGTH; text = substr(block, 1, len); + sub( "^([ \t]*\n)*", "", text); + match(text, "^ ? ? ?:(\t| +)"); indent = RLENGTH; + sub( "^ ? ? ?:(\t| +)", "", text); + gsub( "(^|\n) {0," indent "}", "\n", text ); + + text = _nblock(text); + if (match( text, "^

    (]|\n$" )) + gsub( "(^

    |

    \n$)", "", text); + + return "
    " text "
    \n" _dlist( substr(block, len + 1) ); + } +} + +BEGIN { + # Global Vars + file = ""; rl_href[""] = ""; rl_title[""] = ""; + if (ENVIRON["MD_HTML"] == "true") { AllowHTML = "true"; } + HL[1] = 0; HL[2] = 0; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0; + # hls = "0 0 0 0 0 0"; + + # Universal Patterns + nu = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\_]|_[[:alnum:]])*" # not underline (except when escaped) + na = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\\\*])*" # not asterisk (except when escaped) + ieu = "_([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])_" # inner (underline) + isu = "__([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])__" # inner (underline) + iea = "\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*" # inner (asterisk) + isa = "\\*\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*\\*" # inner (asterisk) + + lix="\\[(\\\\[^\n]|[^]\n\\\\[])*\\]" # link text + lid="(<(\\\\[^\n]|[^\n<>\\\\])*>|(\\\\.|[^()\"'\\\\])+|([^<\n\t ()\\\\]|\\\\[^\n])(\\\\[\n]|[^\n\t \\(\\)\\\\])*)" # link dest + lit="(\"(\\\\.|[^\"\\\\])*\"|'(\\\\.|[^'\\\\])*'|\\((\\\\.|[^\\(\\)\\\\])*\\))" # link text + # link text with image def + lii="\\[(\\\\[^\n]|[^]\n\\\\[])*(!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\))?(\\\\[^\n]|[^]\n\\\\[])*\\]" + + # Buffering of full file ist necessary, e.g. to find reference links + while (getline) { file = file $0 "\n"; } + # Clean up MS-DOS line breaks + gsub(/\r\n/, "\n", file); + + # Fill array of reference links + f = file; rl_id; + re_reflink = "(^|\n) ? ? ?\\[([^]\n]+)\\]: ([^ \t\n]+)(\n?[ \t]+(\"([^\"]+)\"|'([^']+)'|\\(([^)]+)\\)))?(\n|$)"; + # /(^|\n) ? ? ?\[([^]\n]+)\]: ([^ \t\n]+)(\n?[ \t]+("([^"]+)"|'([^']+)'|\(([^)]+)\)))?(\n|$)/ + while ( match(f, re_reflink ) ) { + rl_id = gensub( re_reflink, "\\2", 1, substr(f, RSTART, RLENGTH) ); + rl_href[rl_id] = gensub( re_reflink, "\\3", 1, substr(f, RSTART, RLENGTH) ); + rl_title[rl_id] = gensub( re_reflink, "\\5", 1, substr(f, RSTART, RLENGTH) ); + f = substr(f, RSTART + RLENGTH); + rl_title[rl_id] = substr( rl_title[rl_id], 2, length(rl_title[rl_id]) - 2 ); + if ( rl_href[rl_id] ~ /<.*>/ ) rl_href[rl_id] = substr( rl_href[rl_id], 2, length(rl_href[rl_id]) - 2 ); + } + # Clear reflinks from File + while( gsub(re_reflink, "\n", file ) ); + # for (n in rl_href) { debug(n " | " rl_href[n] " | " rl_title[n] ); } + + # Run Block Processing -> The Actual Markdown! + printf "%s", _nblock( file ); +} diff --git a/cgilite/session.sh b/cgilite/session.sh new file mode 100755 index 0000000..c3a44e8 --- /dev/null +++ b/cgilite/session.sh @@ -0,0 +1,152 @@ +#!/bin/sh + +# Copyright 2018 - 2022 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[ -n "$include_session" ] && return 0 +include_session="$0" + +export _DATE="$(date +%s)" +SESSION_TIMEOUT="${SESSION_TIMEOUT:-7200}" + +if ! which uuencode >/dev/null; then + uuencode() { busybox uuencode "$@"; } +fi +if ! which sha256sum >/dev/null; then + sha256sum() { busybox sha256sum "$@"; } +fi + +if which openssl >/dev/null; then + session_mac(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | openssl dgst -sha1 -hmac "$(server_key)" -binary |slopecode; } +else + # Gonzo MAC if openssl is unavailable + session_mac(){ + { server_key | dd status=none bs=256 count=1 skip=1 + { server_key | dd status=none bs=256 count=1 + [ $# -gt 0 ] && printf %s "$*" || cat + } \ + | sha256sum -; + } \ + | sha256sum | cut -d\ -f1 + } +fi + +server_key(){ + IDFILE="${IDFILE:-${_DATA:-.}/serverkey}" + if [ "$(stat -c %s "$IDFILE")" -ne 512 ] || ! cat "$IDFILE"; then + dd count=1 bs=512 if=/dev/urandom \ + | tee "$IDFILE" + fi 2>&- +} + +slopecode(){ + # 6-Bit Code that retains sort order of input data, while beeing safe to use + # in ascii transmissions, unix file names, HTTP URLs, and HTML attributes + + { [ $# -gt 0 ] && printf %s "$*" || cat; } \ + | uuencode -m - | sed ' + 1d;$d; + y;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/;0123456789:=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; + ' +} + +randomid(){ + dd bs=12 count=1 if=/dev/urandom 2>&- \ + | slopecode +} + +timeid(){ + d=$(($_DATE % 4294967296)) + { printf "$( + printf \\%o \ + $((d / 16777216 % 256)) \ + $((d / 65536 % 256)) \ + $((d / 256 % 256)) \ + $((d % 256)) + )" + dd bs=8 count=1 if=/dev/urandom 2>&- + } | slopecode +} + +transid(){ + # transaction ID to modify a given file + local file="$1" + session_mac "$(stat -c %F%i%n%N%s%Y "$file" 2>&-)" "$SESSION_ID" +} + +checkid(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | grep -m 1 -xE '[0-9a-zA-Z:=]{16}'; } + +update_session(){ + local session sid time sig checksig + unset SESSION_KEY SESSION_ID + + read -r sid time sig <<-END + $(POST session_key || COOKIE session) + END + + checksig="$(session_mac "$sid" "$time")" + + if [ "$checksig" = "$sig" \ + -a "$time" -ge "$_DATE" \ + -a "$(checkid "$sid")" ] 2>&- + then + time=$(( $_DATE + $SESSION_TIMEOUT )) + sig="$(session_mac "$sid" "$time")" + + SESSION_KEY="${sid} ${time} ${sig}" + SESSION_ID="${sid}" + return 0 + else + return 1 + fi + +} + +new_session(){ + local sid time sig + + debug "Setting up new session" + sid="$(randomid)" + time=$(( $_DATE + $SESSION_TIMEOUT )) + sig="$(session_mac "$sid" "$time")" + + SESSION_KEY="${sid} ${time} ${sig}" + SESSION_ID="${sid}" +} + +SESSION_BIND() { + # Set tamper-proof authenticated cookie + local key="$1" value="$2" + SET_COOKIE session "$key"="${value} $(session_mac "$value" "$SESSION_ID")" Path="/${_BASE#/}" SameSite=Strict HttpOnly +} + +SESSION_VAR() { + # read authenticated cookie + # fail if value has been tampered with + local key="$1" value sig + value="$(COOKIE "$key")" + sig="${value##* }" value="${value% *}" + if [ "$sig" = "$(session_mac "$value" "$SESSION_ID")" ]; then + printf %s\\n "$value" + else + return 1 + fi +} + +SESSION_COOKIE() { + [ "$1" = new ] && new_session + SET_COOKIE 0 session="$SESSION_KEY" Path="/${_BASE#/}" SameSite=Strict HttpOnly +} + +update_session || new_session diff --git a/storage.sh b/cgilite/storage.sh similarity index 65% rename from storage.sh rename to cgilite/storage.sh index 355bd56..17ea0d0 100755 --- a/storage.sh +++ b/cgilite/storage.sh @@ -1,21 +1,18 @@ #!/bin/sh -# Copyright 2018, 2019 Paul Hänsch -# -# This is a file format helper, part of CGIlite. +# Copyright 2018 - 2021 Paul Hänsch # -# CGIlite 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. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. # -# CGIlite 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 CGIlite. If not, see . +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. [ -n "$include_storage" ] && return 0 include_storage="$0" @@ -25,41 +22,40 @@ BR=' ' LOCK(){ - local lock timeout block - lock="${1}.lock" - timeout="${2-20}" - if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -d "$lock" ]; then + local lock="${1}.lock" timeout="${2-20}" block + + if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -f "$lock" ]; then debug "Impossible to get lock: $lock" return 1 fi - while ! mkdir "$lock" 2>&-; do - block="$(cat "$lock/pid" || printf 1)" - if ! { ps -eo pid |grep -qwF "$block"; }; then - debug "Overriding stale lock: $lock" - break - fi - if [ $timeout -le 0 ]; then - debug "Timeout while trying to get lock: $lock" - return 1 + while [ $timeout -gt 0 ]; do + printf '%i\n' $$ >>"${lock}" + read block <"$lock" + if [ "$block" = $$ ]; then + return 0 + elif ! { ps -eo pid |grep -qwF "$block"; }; then + debug "Trying to override stale lock: $lock" + if LOCK "$lock" 1; then + rm -- "$lock" + RELEASE "$lock" + fi + else + timeout=$((timeout - 1)) + [ $timeout -gt 0 ] && sleep 1 fi - timeout=$((timeout - 1)) - sleep 1 done - printf '%i\n' $$ >"${lock}/pid" - return 0 + + debug "Timeout while trying to get lock: $lock" + return 1 } RELEASE(){ - local lock - lock="${1}.lock" - if [ "$(cat "$lock/pid")" = "$$" ]; then - rm "$lock/pid" - if ! rmdir "$lock"; then - debug "Cannot remove tainted lock: $lock" - printf '%i\n' $$ >"${lock}/pid" - return 1 - fi + local lock="${1}.lock" block + + read block <"$lock" + if [ "$block" = $$ ]; then + rm -- "$lock" return 0 else debug "Refusing to release foreign lock: $lock" @@ -67,11 +63,6 @@ RELEASE(){ fi } -# STRING=' -# s;\\;\\\\;g; s;\t;\\t;g; -# s;\n;\\n;g; s;\r;\\r;g; -# s;\+;\\+;g; s; ;+;g; -# ' STRING(){ local in out='' [ $# -gt 0 ] && in="$*" || in="$(cat)" @@ -84,19 +75,9 @@ STRING(){ " "*) out="${out}+"; in="${in# }" ;; *) out="${out}${in%%[\\${CR}${BR} + ]*}"; in="${in#"${in%%[\\${BR}${CR} + ]*}"}" ;; esac; done - printf '%s' "$out" + printf '%s' "${out:-\\}" } -UNSTRING=' - :UNSTRING_X - s;((^|[^\\])(\\\\)*)\\n;\1\n;g; - s;((^|[^\\])(\\\\)*)\\t;\1\t;g; - s;((^|[^\\])(\\\\)*)\\r;\1\r;g; - s;((^|[^\\])(\\\\)*)\+;\1 ;g; - tUNSTRING_X; - s;((^|[^\\])(\\\\)*)\\\+;\1+;g; - s;\\\\;\\;g; -' UNSTRING(){ local in out='' [ $# -gt 0 ] && in="$*" || in="$(cat)" @@ -110,7 +91,7 @@ UNSTRING(){ \\*) in="${in#\\}" ;; *) out="${out}${in%%[\\+]*}"; in="${in#"${in%%[\\+]*}"}" ;; esac; done - printf '%s' "$out" + printf '%s\n' "$out" } DBM() { @@ -161,7 +142,7 @@ DBM() { update|replace) k="$1" key="$(STRING "$1")" value="$(STRING "$2")" LOCK "$file" || return 1 - if ! DBM check "$k"; then + if ! DBM "$file" check "$k"; then RELEASE "$file" return 1 fi @@ -178,7 +159,7 @@ DBM() { append) key="$(STRING "$1")" value="$(STRING "$2")" LOCK "$file" || return 1 - if ! DBM check "$1"; then + if ! DBM "$file" check "$1"; then RELEASE "$file" return 1 fi diff --git a/cgilite/users.sh b/cgilite/users.sh new file mode 100755 index 0000000..32299ff --- /dev/null +++ b/cgilite/users.sh @@ -0,0 +1,661 @@ +#!/bin/sh + +# Copyright 2021 - 2024 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[ -n "$include_users" ] && return 0 +include_users="$0" + +. "${_EXEC:-.}/cgilite/session.sh" +. "${_EXEC:-.}/cgilite/storage.sh" + +SENDMAIL=${SENDMAIL-sendmail} + +USER_REGISTRATION="${USER_REGISTRATION-true}" +USER_REQUIREEMAIL="${USER_REQUIREEMAIL-true}" +USER_ACCOUNTPAGE="${USER_ACCOUNTPAGE}" + +USER_ACCOUNTEXPIRE="${USER_ACCOUNTEXPIRE:-$((86400 * 730))}" +USER_CONFIRMEXPIRE="${USER_CONFIRMEXPIRE:-86400}" + +HTTP_HOST="$(HEADER Host)" +MAILFROM="noreply@${HTTP_HOST%:*}" + +[ "$HTTPS" ] && SCHEMA=https || SCHEMA=http + +# == FILE FORMAT == +# UID UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE +# (pending|active|deleted) + +# == GLOBALS == +UNSET_USER='unset \ + USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE +' + +LOCAL_USER='local \ + USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE +' + +# == TRANSLATIONS == +# override all functions marked with "TRANSLATION" +# sed -n '/TRANSLATION$/,/^}/p;' "${user_db}.$$" + mv -- "${user_db}.$$" "$user_db" + RELEASE "$user_db" + else + return 1 + fi +} + +new_user(){ + local user="${1:-$(timeid)}" + shift 1 + + if LOCK "$user_db"; then + if grep -q "^${user} " "$user_db"; then + RELEASE "$user_db" + return 1 + fi + printf '%s \\ %s \\ \\ \\ %i \\ \\\n' \ + "$user" "pending" "$(( _DATE + USER_CONFIRMEXPIRE ))" >>"$user_db" + else + return 1 + fi + + if [ $# -eq 0 ]; then + RELEASE "$user_db" + return 0 + elif update_user "$user" "$@"; then + return 0 + else + RELEASE "$user_db" + return 1 + fi +} + +user_idmap(){ + local uid="$1" ret + eval "$LOCAL_USER" + + if [ ! "$USER_IDMAP" ]; then + while read_user; do + USER_IDMAP="${USER_IDMAP}${USER_ID} ${USER_NAME}${BR}" + done <"$user_db" + fi + if [ "$uid" -a "$USER_IDMAP" != "${USER_IDMAP##*${uid} }" ]; then + ret="${USER_IDMAP##*${uid} }"; ret="${ret%%${BR}*}"; + printf '%s\n' "$ret" + return 0 + elif [ "$uid" ]; then + return 1 + else + printf '%s' "$USER_IDMAP" + return 0 + fi +} + +user_idof(){ + local name="$(STRING "$1")" ret + [ "$USER_IDMAP" ] || user_idmap >/dev/null + + if [ "${name%\\}" -a "$USER_IDMAP" != "${USER_IDMAP% ${name}${BR}*}" ]; then + ret="${USER_IDMAP% ${name}${BR}*}"; ret="${ret##*${BR}}" + printf '%s\n' "$ret" + return 0 + else + return 1 + fi +} + +user_checkname(){ + { [ $# -gt 0 ] && printf %s "$*" || cat; } \ + | sed -nE ' + :X; $!{N;bX;} + s;[ \t\r\n]+; ;g; + s;^ ;;; s; $;;; + /@/d; + /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d; + p; + ' +} + +user_checkemail(){ + { [ $# -gt 0 ] && printf %s "$*" || cat; } \ + | sed -nE ' + # W3C recommended email regex + # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email) + /^[a-zA-Z0-9.!#$%&'\''*+\/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/p; + ' +} + +user_nameexist(){ + local uname="$(STRING "$1")" + local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + [ -f "$user_db" -a -r "$user_db" ] \ + && while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do + [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0 + done <"$user_db" + return 1 +} + +user_emailexist(){ + local email="$(STRING "$1")" + local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + [ -f "$user_db" -a -r "$user_db" ] \ + && while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do + [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0 + done <"$user_db" + return 1 +} + +user_pwhash(){ + local salt="$1" secret="$2" hash + hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)" + printf '%s\n' "${hash%% *}" +} + +user_register_email() { # TRANSLATION + "$SENDMAIL" -t -f "$MAILFROM" <<-EOF + From: ${MAILFROM} + To: ${email} + Subject: Your account registration at ${HTTP_HOST%:*} + + Someone tried to sign up for a user account using this email address. + + You can activate your account using this link: + + ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid") + + This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours. + + If you did not request an account at ${HTTP_HOST%:*}, then someone else + probably entered your email address by accident. In this case you shoud + simply ignore this message and we will remove your email address from + our database within the next day. + + This is an automatic email. Any direct reply will not be received. + Your Account Registration Robot. + EOF +} + +user_register(){ + # reserve account, send registration mail + # preliminary uid, expiration, signature + local uid="$(timeid)" + local uname="$(POST uname |user_checkname)" + local email="$(POST email |user_checkemail)" + local pwsalt="$(randomid)" + local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)" + + if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED" + fi + + if [ "$USER_REQUIREEMAIL" = true ]; then + if [ ! "$email" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID" + elif user_emailexist "$email"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS" + elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then + debug "Sending Activation Link:" \ + "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")" + user_register_email + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi + + elif [ "$USER_REQUIREEMAIL" != true ]; then + if [ ! "$uname" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID" + elif user_nameexist "$uname"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS" + elif [ ! "$pw" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT" + elif [ "$pw" != "$pwconfirm" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH" + elif new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + USER_ACCOUNTEXPIRE))"; then + SESSION_COOKIE new + SESSION_BIND user_id "$uid" + + if [ "$USER_ACCOUNTPAGE" ]; then + REDIRECT "${USER_ACCOUNTPAGE}" + else + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + fi + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi + fi +} + +user_invite_email(){ # TRANSLATION + "$SENDMAIL" -t -f "$MAILFROM" <<-EOF + From: ${MAILFROM} + To: ${email} + Subject: You have been invited to ${HTTP_HOST%:*} + + ${USER_NAME:-Someone} has offered an invitation to this email address. + + ${message} + + You can create your account using this link: + + ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid") + + This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours. + + If you do not know what this is about, then someone else probably + entered your email address by accident. In this case you shoud + simply ignore this message and we will remove your email address from + our database within the next day. + + This is an automatic email. Any direct reply will not be received. + Your Account Registration Robot. + EOF +} + +user_invite(){ + local uid="$(timeid)" + local email="$(POST email |user_checkemail)" + local message="$(POST message)" + + if [ ! "$email" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID" + elif user_emailexist "$email"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS" + elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then + debug "Sending Invitation Link:" \ + "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")" + user_invite_email + REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi +} + +user_confirm(){ + # enable account + eval "$LOCAL_USER" + local uid="$(POST uid |checkid || printf invalid)" + local signature="$(POST signature)" + local uname="$(POST uname |user_checkname)" + local pwsalt="$(randomid)" + local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)" + + read_user "${uid}" + + if [ "$signature" != "$(session_mac "$uid")" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID" + elif [ ! "$uname" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID" + elif user_nameexist "$uname"; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS" + elif [ ! "$pw" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT" + elif [ "$pw" != "$pwconfirm" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH" + elif [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then + REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID" + elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then + SESSION_COOKIE new + SESSION_BIND user_id "$USER_ID" + if [ "$USER_ACCOUNTPAGE" ]; then + REDIRECT "${USER_ACCOUNTPAGE}" + else + REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM" + fi + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK" + fi +} + +user_login(){ + # set cookie + # keep logged in - device cookie? + # initialize new session! + local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + local uname="$(POST uname |STRING)" pw="$(POST pw)" + + [ -f "$user_db" -a -r "$user_db" ] \ + && while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE; do + if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then + if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then + SESSION_COOKIE new + SESSION_BIND user_id "$UID_" + REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN" + fi + fi + done <"$user_db" + REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN" +} + +user_logout(){ + # destroy cookie, destroy session + # keep device cookie + new_session + SESSION_COOKIE new + SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly + REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT" +} + +user_update(){ + # todo: username update, email update / email confirm + local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + # local uname="$(POST uname |STRING)" + local uid oldpw pw pwconfirm + + uid="$(POST uid)" + oldpw="$(POST oldpw)" + pw="$(POST pw |grep -m1 -xE '.{6,}')" + pwconfirm="$(POST pwconfirm)" + + + read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE <<-EOF + $(grep "^${uid} " "$user_db") + EOF + + if [ "$UID_" = "$USER_ID" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$oldpw")" ]; then + if [ "$pw" -a "$pw" = "$pwconfirm" ]; then + update_user "${uid}" password="$pw" + REDIRECT "${_BASE}${PATH_INFO}#UPDATE_SUCCESS" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH" + fi + elif [ "$UID_" = "$USER_ID" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_AUTH_PASSWORD" + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_NOTLOGGEDIN" + fi +} + +user_recover(){ + # send recover link + : +} +user_disable(){ + : +} + +read_user "$(SESSION_VAR user_id)" +[ "$USER_STATUS" -a "$USER_STATUS" != active ] && eval $UNSET_USER + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + user_register) user_register ;; + user_confirm) user_confirm ;; + user_invite) user_invite ;; + user_login) user_login ;; + user_logout) user_logout ;; + user_update) user_update ;; + user_recover) + :;; + user_disable) + :;; +esac + +export USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \ + USER_EXPIRE USER_DEVICES USER_FUTUREUSE + + +w_user_update(){ + if [ ! "$USER_ID" ]; then + cat <<-EOF + [div #user_update .nouser + This page can only be used by registered users + ] + EOF + else + cat <<-EOF + [form #user_update method=POST + [hidden "uid" "$USER_ID"] + [p .username Logged in as $USER_NAME] + [input type=password name=oldpw placeholder="Current Passphrase"] + [input type=password name=pw placeholder="New Passphrase" pattern=".{6,}"] + [input type=password name=pwconfirm placeholder="Confirm New Passphrase" pattern=".{6,}"] + [submit "action" "user_update" Update Passphrase] + ] + EOF + fi +} + +w_user_register_disabled(){ # TRANSLATION + cat <<-EOF + [div #user_register .disabled + User Registration is disabled. + ] + EOF +} +w_user_register_sendmail(){ # TRANSLATION + cat <<-EOF + [form #user_register .registeremail method=POST + [p We will send an activation mail to your email address. + You can continue the signup process when you click on the + activation link in this email.] + [input type=email name=email placeholder="Email"] + [submit "action" "user_register" Sign Up] + ] + EOF +} +w_user_register_direct(){ # TRANSLATION + cat <<-EOF + [form #user_register .registername method=POST + [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off] + [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"] + [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"] + [submit "action" "user_register" Sign Up] + ] + EOF +} + +w_user_register(){ + if [ "$(GET user_confirm)" ]; then + w_user_confirm + elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then + w_user_register_disabled + elif [ "$USER_REQUIREEMAIL" = true ]; then + w_user_register_sendmail + elif [ "$USER_REQUIREEMAIL" != true ]; then + w_user_register_direct + fi +} + +w_user_confirm_proceed(){ # TRANSLATION + cat <<-EOF + [form #user_confirm method=POST + [input type=hidden name=uid value="${uid}"] + [input type=hidden name=signature value="${signature}"] + $([ "$EMAIL" != '\' ] && printf \ + '[input disabled=disabled value="%s" placeholder="Email"]' "$(UNSTRING "$EMAIL" |HTML)" + ) + [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off] + [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"] + [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"] + [submit "action" "user_confirm" Finish Registration] + ] + EOF +} +w_user_confirm_expired(){ # TRANSLATION + cat <<-EOF + [div #user_confirm .expired + [p This activation link is not valid anymore.] + ] + EOF +} +w_user_confirm_invalid(){ # TRANSLATION + cat <<-EOF + [div #user_confirm .invalid + [p This activation link is invalid. Make sure you copied the whole activation link from your email and be careful not to include any line breaks.] + ] + EOF +} + +w_user_confirm(){ + local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE + local user_confirm="$(GET user_confirm)" + local uid="${user_confirm% *}" signature="${user_confirm#* }" + + if [ "$signature" = "$(session_mac "$uid")" ]; then + read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE <<-EOF + $(grep "^${uid} " "$user_db") + EOF + if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then + w_user_confirm_proceed + else + w_user_confirm_expired + fi + else + w_user_confirm_invalid + fi +} + +w_user_invite_email(){ # TRANSLATION + cat <<-EOF + [form #user_invite method=POST + [input placeholder="Email Recipient" name=email autocomplete=off] + [textarea name="message" placeholder="Message to recipient" . ] + [submit "action" "user_invite" Send Invitation] + ] + EOF +} +w_user_invite_link(){ # TRANSLATION + cat <<-EOF + [div #user_invite .link + [p An anonymous user account has been set up. Send the following link to the intended user, so they may claim their account. The link will remain valid for $((USER_CONFIRMEXPIRE / 3600)) hours.] + [a href="$(HTML "$invlink")" . $(HTML "$invlink")] + + [p [a href="#" . Set up another account]] + ] + EOF +} +w_user_invite_deny(){ # TRANSLATION + cat <<-EOF + [div #user_invite .notallowed + Only registered users may send an invitation to another user. + ] + EOF +} + +w_user_invite(){ + local uid invlink + + if [ "$(GET user_confirm)" ]; then + w_user_confirm + elif [ "$USER_ID" -a "$USER_REQUIREEMAIL" = true ]; then + w_user_invite_email + elif [ "$USER_ID" ]; then + uid="$(timeid)" + new_user "$uid" status=pending expire="$((_DATE + USER_CONFIRMEXPIRE))" + invlink="${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")" + debug "New Invitation Link: $invlink" + w_user_invite_link + else + w_user_invite_deny + fi +} + +w_user_login_logon(){ # TRANSLATION + cat <<-EOF + [form #user_login .login method=POST + [input name=uname placeholder="Username or Email"] + [input type=password name=pw placeholder="Passphrase"] + [submit "action" "user_login" Login] + ] + EOF +} +w_user_login_logoff(){ # TRANSLATION + cat <<-EOF + [form #user_login .logout method=POST + [p Logged in as [span . $(HTML ${USER_NAME})]] + [submit "action" "user_logout" Logout] + ] + EOF +} + +w_user_login(){ + if [ ! "$USER_ID" ]; then + w_user_login_logon + elif [ "$USER_ID" ]; then + w_user_login_logoff + fi +} diff --git a/courses/edit_course.sh b/courses/edit_course.sh new file mode 100755 index 0000000..e8ce0c6 --- /dev/null +++ b/courses/edit_course.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# Copyright 2014, 2019, 2020 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +locktimeout=900 +. "$_EXEC"/session_lock.sh + +course="$(GET course |PATH)" +coursefile="$_DATA/ical/${course##*/}" + +if tempfile="$(SLOCK "$coursefile" "$locktimeout")"; then + REDIRECT "${_BASE}/courses/?e=${course}" +elif [ -f "$tempfile" ]; then + SET_COOKIE session message="SESSLOCK" + REDIRECT "${_BASE}/courses/#${course}" +else + SET_COOKIE session message="EDITLOCK" + REDIRECT "${_BASE}/courses/#${course}" +fi diff --git a/courses/export_ical.sh b/courses/export_ical.sh new file mode 100755 index 0000000..3649ed5 --- /dev/null +++ b/courses/export_ical.sh @@ -0,0 +1,28 @@ +#!/bin/zsh + +# Copyright 2014,2015,2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +course="$(GET course |PATH)" +coursefile="$_DATA/ical/${course##*/}" + +. $_EXEC/pdiread.sh +. $_EXEC/cgilite/file.sh + +printf 'Content-Disposition: inline; filename="%s.ics"\r\n' "$(pdi_value "$(pdi_load "$coursefile")" SUMMARY)" + +FILE "$coursefile" "text/calendar; charset=utf-8" diff --git a/courses/export_pdf.sh b/courses/export_pdf.sh new file mode 100755 index 0000000..92e10c5 --- /dev/null +++ b/courses/export_pdf.sh @@ -0,0 +1,132 @@ +#!/bin/sh + +. "${_EXEC}/pdiread.sh" +. "$_EXEC/cards/l10n.sh" + +coursefile="${_DATA}/ical/$(GET course)" + +if [ ! -r "$coursefile" ]; then + SET_COOKIE 0 message="Cannot read course file" + REDIRECT "${_BASE}/courses/" + return 0 +elif ! mkdir -p "$_DATA/export"; then + SET_COOKIE 0 message="Cannot create export directory" + REDIRECT "${_BASE}/courses/" + return 0 +fi + +ics="$(pdi_load "$coursefile")" +htmlfile="${_DATA}/export/$(pdi_value "$ics" SUMMARY |unescape |tr \\n/ __).html" +pdffile=${htmlfile%.html}.pdf + +pdi_date() { + local pdt y m d H M S Z + [ $# -eq 0 ] && read pdt || pdt="$*" + + case $pdt in + *T*Z) + Z=UTC; pdt="${pdt%Z}";; + TZID=*:*T*) + Z="${pdt%%:*}"; Z=${Z#TZID=}; pdt=${pdt#TZID=*:};; + esac + + y="${pdt%%????T*}" pdt=${pdt#????} + m="${pdt%%??T*}" pdt=${pdt#??} + d="${pdt%%T*}" pdt=${pdt#??T} + H="${pdt%%????}" pdt=${pdt#??} + M="${pdt%%??}" pdt=${pdt#??} + S="${pdt}" pdt='' + + case Z in + UTC) date -d "${y}-${m}-${d} ${H}:${M}:${S} UTC" +%s;; + '') date -d "${y}-${m}-${d} ${H}:${M}:${S}" +%s;; + *) date -d "TZ=\"${Z}\" ${y}-${m}-${d} ${H}:${M}:${S}" +%s;; + esac +} + +get_dates() { + local dts_date rrule rr_int rr_freq rec today="$(date +%Y%m%d)" + + dts_date="$(pdi_value "$ics" DTSTART || printf %s "$today")" + dts_date="${dts_date#TZID=*:}" dts_date="${dts_date%%T*}" + rrule="$(pdi_value "$ics" RRULE)" + rr_int="${rrule##*INTERVAL=}" rr_int="${rr_int%%;*}" + rr_freq="${rrule##*FREQ=}" rr_freq="${rr_freq%%;*}" + + [ "$rr_int" -ge 0 ] || rr_int=1 2>/dev/null + case "$rr_freq" in + YEARLY) rec="$rr_int year";; + MONTHLY) rec="$rr_int month";; + DAILY) rec="$rr_int day";; + WEEKLY) rec="$rr_int week";; + *) rec="$rr_int week";; + esac + + while [ "$dts_date" -lt "$today" ]; do dts_date="$(date -d "${dts_date} + ${rec}" +%Y%m%d)"; done + for n in 1 2 3 4 5 6 7 8 9 10; do + LANG=de_DE.UTF-8 date -d "$dts_date" +"%d. %b." + dts_date="$(date -d "${dts_date} + ${rec}" +%Y%m%d)" + done +} + +# some table styles need to be inline, because this is how libreoffice works +style_td='style="border: 1pt solid; padding: 1mm 2mm; vertical-align: top;"' + +"$_EXEC/cgilite/html-sh.sed" <<-EOF >"$htmlfile" + +[html [head + [meta http-equiv="content-type" content="text/html; charset=utf-8"] + [title] + [meta name="generator" content="Confetti"] + [meta name="created" content="$(date +%FT%T)"] + [meta name="changed" content="$(date +%FT%T)"] + [style type="text/css" + @page { size: 29.7cm 21cm; margin: 1.5cm; } + * { background: inherit; } + body { background: transparent; font-family: Liberation Sans, Sans-Serif; } + + th { white-space: pre; } + th, td { text-align: left; } + ] +][body lang="de_DE" + [table width="100%" style="page-break-after: always;" + [col width=10*] [col width=5*] [col width=10*] [col width=15*] + [thead + [tr [th $style_td . $(l10n N)] [th $style_td . $(l10n BDAY)] [th $style_td . $(l10n TEL)] [th $style_td . $(l10n NOTE)]] + ][tbody + $(grep -F "${coursefile##*/} " "$_DATA/mappings/attendance" |while read discard each; do + vcf="$(pdi_load "$_DATA/vcard/$each")" + tel="$( seq 1 $(pdi_count "$vcf" TEL) |while read n; do + type="$(pdi_attrib "$vcf" TEL $n TYPE)" + [ "$type" ] && type="$(l10n "TYPE=$type"):" + printf '%s %s
    ' "$type" "$(pdi_value "$vcf" TEL $n)" + done )" + printf '[tr valign=top [td %s .N . %s] [td %s .BDAY . %s] [td %s .TEL . %s] [td %s .NOTE . %s]]\n' \ + "$style_td" "$(pdi_value "$vcf" FN |unescape |HTML)" \ + "$style_td" "$(pdi_value "$vcf" BDAY |unescape |HTML)" \ + "$style_td" "$tel" \ + "$style_td" "$(pdi_value "$vcf" NOTE |unescape |HTML)" + done |sort)] + ] + [table width="100%" + [col width=30*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] + [thead + [tr [th $style_td ] $(get_dates |xargs -d\\n printf "[th $style_td . %s]")] + ][tbody + $(grep -F "${coursefile##*/} " "$_DATA/mappings/attendance" |while read discard each; do + vcf="$(pdi_load "$_DATA/vcard/$each")" + printf '[tr [td %s .N . %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s]]\n' \ + "$style_td" "$(pdi_value "$vcf" FN |unescape |HTML)" \ + "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" + done |sort)] + ] +]] +EOF + +export HOME="$_DATA/" +export XDG_CONFIG_HOME="$_DATA/xdg_config" +export XDG_CACHE_HOME="$_DATA/xdg_cache" +export XDG_DATA_HOME="$_DATA/xdg_local" + +lowriter --convert-to pdf --outdir "$_DATA/export/" "$htmlfile" >/dev/null +REDIRECT "$(URL "${_BASE}/export/${pdffile##*/}")" diff --git a/courses/index.cgi b/courses/index.cgi new file mode 100755 index 0000000..57761ad --- /dev/null +++ b/courses/index.cgi @@ -0,0 +1,24 @@ +#!/bin/sh + +. $_EXEC/pdiread.sh +. $_EXEC/courses/l10n.sh +. $_EXEC/courses/widgets.sh +. $_EXEC/courses/list.sh + +order="$(GET o |grep -m1 -xE 'DOW|TOD')" +edit="$(GET e |PATH)" + +[ "$order" ] || order=DOW +edit="${edit##*/}" + +{ w_sort_courses + printf ' + [form .newcourses action="%s/courses/new_course.sh" method="POST" + [button type="submit" %s] + ]' "${_BASE}" "$(l10n newcourse)" + + [ "$edit" ] && edit_course "$edit" + printf '[div .courselist\n' + list_courses + printf ']' +} | yield_page courses #/courses/courses.css diff --git a/courses/l10n.sh b/courses/l10n.sh new file mode 100755 index 0000000..f98529f --- /dev/null +++ b/courses/l10n.sh @@ -0,0 +1,50 @@ +# Copyright 2014, 2016, 2019 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +l10n(){ + local word + [ $# -eq 0 ] && read -r word || word="$*" + + case $word in + newcourse) printf "Neuen Kurs anlegen";; + time) printf "Uhrzeit";; + + edit_dtscal) printf "✓";; + edit) printf "Bearbeiten";; + ics_export) printf "ICal exportieren";; + courselist) printf "Kursliste (PDF)";; + + course_mail) printf "Mail an Teilnehmende";; + + sort_order) printf "Sortierung";; + order_DOW) printf "Wochentag";; + order_TOD) printf "Uhrzeit";; + order_apply) printf "Sortieren";; + + t_every) printf "Alle";; + t_eternal) printf "ewig";; + t_times) printf "mal";; + t_until) printf "Bis";; + t_oclock) printf "Uhr";; + + "Mon Tue Wed Thu Fri Sat Sun") printf "Mo Di Mi Do Fr Sa So";; + "January February March April May June July August September October November December") + printf "Januar Februar März April Mai Juni Juli August September Oktober November Dezember";; + + *) l10n_global "$word";; + esac +} diff --git a/courses/list.sh b/courses/list.sh new file mode 100755 index 0000000..95857ec --- /dev/null +++ b/courses/list.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +. "${_EXEC}"/pdiread.sh + +SUP_FIELDS="COMMENT" +MAILTO="${MAILTO:-confetti@confetti}" + +edit_course(){ + local coursefile="$_DATA/ical/$1" + local tempfile course + + . $_EXEC/session_lock.sh + + if ! tempfile="$(CHECK_SLOCK "$coursefile")"; then + printf '[div .message %s]' "$(l10n "This course is not set up for editing within this session.")" + else + course="$(pdi_load "$tempfile")" + cat <<-EOF + [form .course #${coursefile##*/} action="${_BASE}/courses/update_course.sh" method="POST" + [input type="hidden" name="course" value="${coursefile##*/}"] + [input type="hidden" name="tid" value="$(transid ${tempfile})"] + [div .section .basic . $( + edit_item "$course" SUMMARY COMMENT + )] + [div .section .dtstart . $( + edit_item "$course" DTSTART + )] + [div .section .recur . $( + edit_item "$course" RRULE + )] + [div .section .attendance . $( + edit_item "$course" attendance + )] + [div .control + [!-- select .item name=newfield + [option disabled="disabled" selected . $(l10n edit_addfieldtext)] + $(for f in $SUP_FIELDS; do printf '[option value="%s" . %s]\n' "$f" "$(l10n "$f")"; done) + ] + [button .item type="submit" name="action" value="addfield" . $(l10n edit_addfield) --] + [button .item type="submit" name="action" value="update" . $(l10n edit_update)] + [button .item type="submit" name="action" value="cancel" . $(l10n edit_cancel)] + [div .item .delete label="$(l10n edit_delete)" + [input type="checkbox" #delete] + [label for="delete" $(l10n edit_delete)] + [button type="submit" name="action" value="delete" $(l10n edit_delete)] + ] + ] + ] + EOF + fi +} + +print_course(){ + local coursefile="$1" + local course="$(pdi_load "$coursefile")" + + cat <<-EOF + [div .course #${coursefile##*/} + [div .section .basic . $( + cal_item "$course" SUMMARY DTSTART RRULE + )] + [div .section .COMMENT . $(cal_item "$course" COMMENT)] + [div .section .attendance [h3 $(l10n course_attendance) ] [ul . + $(grep -F "${coursefile##*/} " "$_DATA/mappings/attendance" |while read discard each; do + printf '[li [a .item .attendance href="%s/cards/#%s" . %s]]\n' \ + "${_BASE}" "$each" \ + "$(pdi_value "$(pdi_load "$_DATA/vcard/$each")" FN |unescape |HTML)" + done |sort -k7)] + ] + [div .control + [a .button .item href="${_BASE}/courses/edit_course.sh?course=${coursefile##*/}" $(l10n edit)] + [a .button .item href="${_BASE}/courses/export_pdf.sh?course=${coursefile##*/}" target="blank" $(l10n courselist)] + [a .button .item href="${_BASE}/courses/export_ical.sh?course=${coursefile##*/}" $(l10n ics_export)] + [a .button .item href="mailto:${MAILTO}?bcc=$(course_mail "${coursefile##*/}" |HTML)" $(l10n course_mail)] + ] + ] + EOF +} + +course_mail() { + course="$1" + grep -F "${course} " "$_DATA/mappings/attendance" |while read junk card; do + cat "${_DATA}/vcard/${card}" + done \ + | pdi_load - \ + | sed -nE 's;^EMAIL(\;[^:]*)*:(.+)\r?$;\2,;p' \ + | tr -d \\n \ + | unescape +} + +print_courses(){ + local calfile cachefile date size name ldate=0 lsize lname + + while read calfile; do + cachefile="${_DATA}/cache/${calfile##*/}.cache" + if [ -s "$cachefile" -a "$cachefile" -nt "$calfile" ]; then + cat "$cachefile" + elif [ -s "$calfile" ]; then + print_course "$calfile" |tee "$cachefile" + fi + done +} + +order_courses() { + local calfile course + + while read calfile; do + icstime="$(pdi_value "$(pdi_load "$calfile")" DTSTART |cal_date)" + case $order in + DOW) printf '%s %s\n' "$(date -d "$icstime" "+%u %H:%M:%S")" "$calfile";; + TOD) printf '%s %s\n' "$(date -d "$icstime" "+%H:%M:%S")" "$calfile";; + esac + done \ + | sort \ + | sed -E 's;^.*\t;;g' +} + +list_courses(){ + printf '%s\n' ${_DATA}/ical/*.ics \ + | order_courses \ + | print_courses +} diff --git a/courses/new_course.sh b/courses/new_course.sh new file mode 100755 index 0000000..36de913 --- /dev/null +++ b/courses/new_course.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +# Copyright 2014, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +locktimeout=900 +. "$_EXEC"/session_lock.sh + +uid="$(timeid)$(randomid)" # 32 Octets UID, starting with timestamp +course="${uid}.ics" + +tzid="$(cat /etc/timezone)" +tstamp="$(TZ="$tzid" date +%Y%m%dT%H%M%S)" + +coursefile="$_DATA/ical/$course" + +if tempfile="$(SLOCK "$coursefile")"; then + cat >"$tempfile" <<-EOF + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:Berlin RAW Confetti + BEGIN:VEVENT + UID:$uid + DTSTAMP:TZID=${tzid}:${tstamp} + DTSTART:TZID=${tzid}:${tstamp} + DURATION: + RRULE: + SUMMARY: + COMMENT: + END:VEVENT + END:VCARD + EOF + REDIRECT "${_BASE}/courses/?e=${course}" +else + SET_COOKIE session message="EDITLOCK" + REDIRECT "${_BASE}/courses/" +fi diff --git a/courses/update_course.sh b/courses/update_course.sh new file mode 100755 index 0000000..aa9632c --- /dev/null +++ b/courses/update_course.sh @@ -0,0 +1,153 @@ +#!/bin/sh + +# Copyright 2014, 2015, 2020, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +. "$_EXEC/pdiread.sh" +. "$_EXEC/session_lock.sh" +. "$_EXEC/cgilite/storage.sh" + +unset coursefile attfile tempfile + +course="$(POST course |PATH)"; course="${course##*/}" +coursefile="$_DATA/ical/$course" +attfile="$_DATA/mappings/attendance" + +if ! tempfile="$(CHECK_SLOCK "$coursefile")"; then + SET_COOKIE 0 message="NO VALID FILE LOCK" + REDIRECT "${_BASE}/courses/?e=${course}" + exit 0 +elif [ "$(POST tid)" != "$(transid "$tempfile")" ]; then + SET_COOKIE 0 message="INVALID TRANSACTION ID" + REDIRECT "${_BASE}/courses/?e=${course}" + exit 0 +fi + +vcf_escape(){ + for each in "$@"; do + printf %s\\n "$each" \ + | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;' + done \ + | sed -E ':X;$!{N;bX}; s;\n;\;;g' +} + +ics="$(pdi_load "$tempfile")" + +tzid=$(cat /etc/timezone) + +ics="$(pdi_update_attrib "$ics" DTSTAMP 1 "TZID=${tzid}")" +ics="$(pdi_update_value "$ics" DTSTAMP 1 "$(TZ="$tzid" date +%Y%m%dT%H%M%S)")" + +dts_year="$( POST DTS_YEAR |grep -m1 -xE '[0-9]{4}' || date +%Y)" +dts_month="$( POST DTS_MONTH |grep -m1 -xE '0[1-9]|1[012]' || date +%m)" +dts_day="$( POST DTS_DAY |grep -m1 -xE '0[1-9]|[12][0-9]|3[01]' || date +%d)" +dts_hour="$( POST DTS_HOUR |grep -m1 -xE '[0-9]|1[0-9]|2[0-3]' || date +%H)" +dts_minute="$(POST DTS_MINUTE |grep -m1 -xE '[0-9]|[1-5][0-9]' || date +%M)" +[ ${#dts_hour} -eq 1 ] && dts_minute="0$dts_hour" +[ ${#dts_minute} -eq 1 ] && dts_minute="0$dts_minute" +DTSTART="${dts_year}${dts_month}${dts_day}T${dts_hour}${dts_minute}00" + +ics="$(pdi_update_attrib "$ics" DTSTART 1 "TZID=${tzid}")" +ics="$(pdi_update_value "$ics" DTSTART 1 "$DTSTART")" + +rr_int=$( POST RRULE_INTERVAL |grep -m1 -xE '[0-9]+' || printf 1) +rr_count=$(POST RRULE_COUNT |grep -m1 -xE '[0-9]+' || printf 1) +rr_freq=$( POST RRULE_FREQ |grep -m1 -xE 'DAILY|WEEKLY|MONTHLY|YEARLY' || printf MONTHLY) +rr_uy=$( POST RRULE_UYEAR |grep -m1 -xE '[0-9]{4}' || date +%Y) +rr_um=$( POST RRULE_UMONTH |grep -m1 -xE '[1-9]|1[012]' || date +%m) +rr_ud=$( POST RRULE_UDAY |grep -m1 -xE '[1-9]|[12][0-9]|3[01]' || date +%d) +[ ${#rr_um} -eq 1 ] && rr_um="0$rr_um" +[ ${#rr_ud} -eq 1 ] && rr_ud="0$rr_ud" + +case $(POST RRULE_LIMIT) in + COUNT) RRULE="FREQ=$rr_freq;INTERVAL=$rr_int;COUNT=$rr_count";; + UNTIL) RRULE="FREQ=$rr_freq;INTERVAL=$rr_int;UNTIL=${rr_uy}${rr_um}${rr_ud}T000000Z";; + ETERN|*) RRULE="FREQ=$rr_freq;INTERVAL=$rr_int";; +esac + +ics="$(pdi_update_value "$ics" RRULE 1 "$RRULE")" + +for field in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*'); do + for cnt in $(seq 1 $(POST_COUNT "$field")); do + case "$field" in + *) + ics="$(pdi_update_value "$ics" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")" + ;; + esac +done; done + +# delete fields, first mark for deletion using delete_key +# this way the field enumeration is preserved during the process +# finally filter marked lines +delete_key="$(randomid)" +for delete in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*_delete_[0-9]+'); do + f="${delete%%_*}"; c="${delete##*_}"; + [ "$(POST "$delete")" = "true" ] && ics="$(pdi_update_value "$ics" "$f" "$c" "delete=${delete_key}")" +done +ics="$(printf '%s\n' "$ics" |sed -E "/^[^:]+:delete=${delete_key}\$/d")" + +case "$(POST action)" in + addfield) + newfield="$(POST newfield |grep -m 1 -xE '[A-Z][A-Z0-9-]*')" + ics="$(pdi_update_value "$ics" "$newfield" $(( $(pdi_count "$ics" "$newfield") + 1 )) '')" + printf '%s' "$ics" |grep -vx '' >"$tempfile" + REDIRECT "${_BASE}/courses/?e=${course}" + ;; + addfield\ [A-Z]*) + newfield="$(POST action |sed -nE '1s;^addfield ([A-Z][A-Z0-9-]*)$;\1;p')" + ics="$(pdi_update_value "$ics" "$newfield" $(( $(pdi_count "$ics" "$newfield") + 1 )) '')" + printf '%s' "$ics" |grep -vx '' >"$tempfile" + REDIRECT "${_BASE}/courses/?e=${course}" + ;; + update) + if LOCK "$attfile"; then + grep -F "${course} " "$attfile" |while read junk card; do + touch "$_DATA/vcard/${card}" + done + sed -E -i "/^${course} .+\$/d" "$attfile" + seq 1 $(POST_COUNT attendance) |while read n; do + printf '%s %s\n' "$course" "$(POST attendance $n)" + done >>"$attfile" + grep -F "${course} " "$attfile" |while read junk card; do + touch "$_DATA/vcard/${card}" + done + RELEASE "$attfile" + else + SET_COOKIE 0 message="COULD NOT UPDATE COURSE MAPPINGS" + fi + + printf '%s' "$ics" |grep -vx '' >"${tempfile}.cp" + mv "${tempfile}.cp" "$coursefile" + RELEASE_SLOCK "$coursefile" + REDIRECT "${_BASE}/courses/#${course}" + ;; + cancel) + RELEASE_SLOCK "$coursefile" + [ -f "$coursefile" ] \ + && REDIRECT "${_BASE}/courses/#${course}" \ + || REDIRECT "${_BASE}/courses/" + ;; + delete) + rm "$coursefile" + RELEASE_SLOCK "$coursefile" + REDIRECT "${_BASE}/courses/" + ;; + *) + printf '%s' "$ics" |grep -vx '' >"$tempfile" + REDIRECT "${_BASE}/courses/?e=${course}" + ;; +esac diff --git a/courses/widgets.sh b/courses/widgets.sh new file mode 100755 index 0000000..fee7690 --- /dev/null +++ b/courses/widgets.sh @@ -0,0 +1,237 @@ +# Copyright 2014, 2019, 2020 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +check(){ + [ "$1" = "$2" ] && printf 'checked="checked"' +} + +edit="$(GET e)" +order="$(GET o |grep -m1 -xE 'DOW|TOD')" + +w_sort_courses(){ + cat <<-EOF + [form .sort .search action="?" method="GET" + [fieldset .order [legend $(l10n sort_order):] + [radio "order" "DOW" $(check $order DOW) $(l10n order_DOW)] + [radio "order" "TOD" $(check $order TOD) $(l10n order_TOD)] + ] + [submit "" "" $(l10n order_apply)] + ] + EOF +} + +cal_date(){ + { [ $# -eq 0 ] && cat || printf %s "$*"; } |sed -nE ' + 2q + s/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$/\1-\2-\3 \4:\5:\6 UTC/p;t + s/^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$/TZ="\1" \2-\3-\4 \5:\6:\7/p;t + s/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$/\1-\2-\3 \4:\5:\6/p;t + ' +} + +cal_item(){ + local course="$1" + local item cnt c + shift 1 + + for item in $@; do + cnt="$(pdi_count "$course" "$item")" + + case $item in + SUMMARY) + printf '[h2 . %s]' "$(pdi_value "$course" SUMMARY)" + ;; + DTSTART) + printf '[span .text .DTSTART %s %s ]' \ + "$(LANG=de_DE.UTF-8 date -d "$(pdi_value "$course" DTSTART |cal_date)" '+%A, %d. %B %Y - %H:%M')" \ + "$(l10n t_oclock)" + ;; + RRULE) + dts_date="$(pdi_value "$course" DTSTART |cal_date)" + rrule=" $(pdi_value "$course" RRULE)" + rr_int="${rrule##*INTERVAL=}"; rr_int="${rr_int%%;*}" + rr_count="${rrule##*COUNT=}"; rr_count="${rr_count%%;*}" + rr_freq="${rrule##*FREQ=}"; rr_freq="${rr_freq%%;*}" + rr_until="${rrule##*UNTIL=}"; rr_until="${rr_until%%;*}" + rr_until="$(cal_date "${rr_until}")" + + [ "$rr_int" -eq 1 ] \ + && printf '[span .text .RRULE %s]' "$(l10n "s$rr_freq")" \ + || printf '[span .text .RRULE %s %s %s]' "$(l10n t_every)" "${rr_int}" "$(l10n $rr_freq)" + case "$rrule $rr_freq" in + *COUNT*DAILY*) + printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) day" "+%A %B %d, %Y - %H:%M")" + ;; + *COUNT*WEEKLY*) + printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) week" "+%A %B %d, %Y - %H:%M")" + ;; + *COUNT*MONTHLY*) + printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) month" "+%A %B %d, %Y - %H:%M")" + ;; + *COUNT*YEARLY*) + printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) year" "+%A %B %d, %Y - %H:%M")" + ;; + *UNTIL*) + printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$rr_until" "+%A %B %d, %Y - %H:%M")" + ;; + esac + ;; + attendance);; + COMMENT)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")" + seq 1 $cnt |while read c; do + printf '[p .item .%s . %s]' "$item" \ + "$(pdi_value "$course" "$item" $c |unescape |HTML)" + done + ;; + *)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")" + seq 1 $cnt |while read c; do + printf '[span .item .%s . %s]' "$item" \ + "$(pdi_value "$course" "$item" $c |unescape |HTML)" + done + ;; + esac + done +} + +edit_item(){ + local course="$1" + local item cnt c + shift 1 + + for item in $@; do + cnt="$(pdi_count "$course" "$item")" + [ "$cnt" -lt 1 ] && cnt=1 + + case $item in + DTSTART) + local dtstart="$(pdi_value "$course" DTSTART |cal_date)" + local ystart="${dtstart%%-*}"; ystart="${ystart##* }" + local mstart="${dtstart#*-}"; mstart="${mstart%%-*}" + local dstart="${dtstart##*-}"; dstart="${dstart%% *}" + local hhstart="${dtstart##* }"; hhstart="${hhstart%%:*}" + local mmstart="${dtstart##* }"; mmstart="${mmstart#*:}"; mmstart="${mmstart%:*}" + local m mn cdow d + + cat <<-EOF + [h3 . $(l10n DTSTART)] + [input type="number" name="DTS_YEAR" value="${ystart}" placeholder="$(l10n YYYY)"] + [select name="DTS_MONTH" onchange="this.form.submit();" + $(m=1; for mn in $(l10n January February March April May June July August September October November December); do + printf ' [option value="%02i" %s . %s]\n' $m "$(selected $m $mstart)" "$mn" + m=$((m+1)) + done) + ][submit "DTS" "update" . $(l10n edit_dtscal)] + [table .dtscalt + [tr $(printf '[th . %s]' $(l10n Mon Tue Wed Thu Fri Sat Sun))] + [tr $( + local cdow d + cdow="$(date -d ${ystart}-${mstart}-1 +%u)" + seq 2 $cdow |xargs -n1 printf '[td .padding .%s]' + d=1; while [ "$d" -lt 29 ] || [ "$(date -d ${ystart}-${mstart}-${d} +%m)" -eq "$mstart" ]; do + [ $cdow -eq 1 -a $d -ne 1 ] && printf ']\n [tr ' + printf '[td [input type="radio" name="DTS_DAY" #DTSCAL_%i value="%02i" %s][label for="DTSCAL_%i" %i]]' \ + $d $d "$(checked $d $dstart)" $d $d + d=$((d + 1)); cdow=$(((cdow + 1) % 7)) + done 2>/dev/null + )] + ] + [label .DTSTIME $(l10n time):] + [input type="number" name="DTS_HOUR" value="$hhstart" min="0" max="23"]:[input type="number" name="DTS_MINUTE" value="$mmstart" min="0" max="59"] + EOF + ;; + RRULE) + local dtstart="$(pdi_value "$course" DTSTART |cal_date)" + local ystart="${dtstart%%-*}"; ystart="${ystart##* }" + local mstart="${dtstart#*-}"; mstart="${mstart%%-*}" + local dstart="${dtstart##*-}"; dstart="${dstart%% *}" + + local rrule="$(pdi_value "$course" RRULE)" + local rr_int="$(printf %s "$rrule" |sed -nE 's;^(.*\;[ ]*)?INTERVAL=([0-9]+)(\;.*)?$;\2;p')" + local rr_count="$(printf %s "$rrule" |sed -nE 's;^(.*\;[ ]*)?COUNT=([0-9]+)(\;.*)?$;\2;p')" + local rr_freq="$(printf %s "$rrule" |sed -nE 's;^(.*\;[ ]*)?FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)(\;.*)?$;\2;p')" + local rr_until="$(printf %s "$rrule" |sed -nE 's;^(.*\;[ ]*)?UNTIL=([0-9]{8}T[0-9]{6}Z)(\;.*)?$;\2;p')" + local rr_uyear="${rr_until%????T??????Z}" + local rr_umonth=${rr_until#????}; rr_umonth="${rr_umonth%??T??????Z}" + local rr_uday=${rr_until#??????}; rr_uday="${rr_uday%T??????Z}" + local rr_limit="ETERN" + [ "$rr_count" ] && [ "$rr_count" -ge 0 ] && rr_limit="COUNT" + [ "$rr_uyear" ] && [ "$rr_uyear" -ge 0 ] && rr_limit="UNTIL" + + cat <<-EOF + [h3 . $(l10n "$item")] + [span .item . $(l10n t_every) + [input type="number" .RRULE .INTERVAL name="RRULE_INTERVAL" placeholder="#N" value="${rr_int:-1}" min="1"] + [select .RRULE .FREQ name="RRULE_FREQ" + $(for f in DAILY WEEKLY MONTHLY YEARLY; do + printf ' [option value="%s" %s . %s]\n' "$f" "$(selected $f "$rr_freq")" "$(l10n $f)" + done) + ]] + [label .item [input type="radio" name="RRULE_LIMIT" value="ETERN" $(checked "$rr_limit" ETERN)] $(l10n t_eternal)] + [label .item + [input type="radio" name="RRULE_LIMIT" value="COUNT" $(checked "$rr_limit" COUNT)] + [input type="number" .RRULE .COUNT name="RRULE_COUNT" placeholder="#N" value="${rr_count:-1}" min="1"] $(l10n t_times) + ] + [label .item + [input type="radio" name="RRULE_LIMIT" value="UNTIL" $(checked "$rr_limit" UNTIL)] $(l10n t_until) + [input type="number" .RRULE .UYEAR name="RRULE_UYEAR" placeholder="$(l10n YYYY)" value="${rr_uyear:-$ystart}" min="$ystart"] + [input type="number" .RRULE .UMONTH name="RRULE_UMONTH" placeholder="$(l10n MM)" value="${rr_umonth:-$mstart}" min="1" max="12"] + [input type="number" .RRULE .UDAY name="RRULE_UDAY" placeholder="$(l10n DD)" value="${rr_uday:-$dstart}" min="1" max="31"] + ] + EOF + ;; + COMMENT) + printf '[h3 %s]' "$(l10n "$item")" + seq 1 $cnt |while read c; do + printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \ + "$item" $c "$item" $c "$item" $c "$(l10n delete)" + printf '' \ + "$item" "$item" "$(pdi_value "$course" "$item" $c |unescape |HTML)" + done + printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)" + ;; + attendance) + printf '[h3 %s]' "$(l10n course_attendance)" + printf '[div .attendance\n' + for vcf in "$_DATA"/vcard/*.vcf; do + fn="$(pdi_value "$(pdi_load "$vcf")" FN)" + printf '%s/%s\n' "${vcf##*/}" "$fn" + done \ + | sort -t/ -k2 \ + | while IFS=/ read -r vcf fn; do + printf '[span .item [input type="checkbox" id="att%s" name="attendance" value="%s" %s][label for="att%s" . %s]]' \ + "$vcf" "$vcf" "$(grep -qxF "${coursefile##*/} $vcf" "$_DATA/mappings/attendance" && printf 'checked="checked"')" "$vcf" "$fn" + done + printf ']' + ;; + SUMMARY) + printf '[h3 %s]' "$(l10n "$item")" + printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \ + "$item" "$item" "$(pdi_value "$course" "$item" |unescape |HTML)" "$(l10n "$item")" + ;; + *) + printf '[h3 %s]' "$(l10n "$item")" + seq 1 $cnt |while read c; do + printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \ + "$item" $c "$item" $c "$item" $c "$(l10n delete)" + printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \ + "$item" "$item" "$(pdi_value "$course" "$item" $c |unescape |HTML)" "$(l10n "$item")" + done + printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)" + ;; + esac + done +} diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..71edd85 --- /dev/null +++ b/index.cgi @@ -0,0 +1,104 @@ +#!/bin/sh + +for n in "$@"; do case ${n%%=*} in + data) _DATA="${n#data=}";; + exec) _EXEC="${n#exec=}";; + base) _BASE="${n#base=}";; + debug) DEBUG="${n#debug=}";; +esac; done + +[ ! "${_EXEC%/}" ] && _EXEC="$(realpath "${0%/*}")" || _EXEC="${_EXEC%/}" +[ ! "${_DATA%/}" ] && _DATA="${PWD%/}" || _DATA="${_DATA%/}" +_BASE="${_BASE%/}" +[ "$DEBUG" ] && exec 2>>"$DEBUG" + +mkdir -p "${_DATA}/cache" "${_DATA}/mappings" "${_DATA}/export" "${_DATA}/lock" "${_DATA}/ical" "${_DATA}/vcard" + +debug() { + local dbg=/dev/stderr + if [ ! "$DEBUG" ]; then + [ "$#" -gt 0 ] && : || cat; + elif [ "$#" -gt 0 ]; then + printf '%s\n' "$@" >>"$dbg" + else + tee -a "$dbg" + fi +} + +. "$_EXEC/cgilite/cgilite.sh" +. "$_EXEC/cgilite/session.sh" + +. "$_EXEC/l10n.sh" + +_PATH="$(PATH "/${PATH_INFO}")" +_PATH="${_PATH#${_BASE}}" +ACTION="$(GET a)" + +SESSION_COOKIE + +message="$(COOKIE message)" +[ "$message" ] && SET_COOKIE 0 message='' + +checked(){ + if [ "$1" = "$2" ] || [ "$1" -eq "$2" ]; then + printf 'checked="checked"' + fi 2>/dev/null +} +selected(){ + if [ "$1" = "$2" ] || [ "$1" -eq "$2" ]; then + printf 'selected="selected"' + fi 2>/dev/null +} + +yield_page() { + local class="$1" style="$2" + printf 'Content-Type: text/html; charset=utf-8\r\n\r\n' + { printf ' + [html [head + [title Confetti] + [meta name="viewport" content="width=device-width"] + [link rel="stylesheet" type="text/css" href="%s/cgilite/common.css"] + [link rel="stylesheet" type="text/css" href="%s/style.css"] + ' "${_BASE}" "${_BASE}" + [ -n "$style" ] && printf ' + [link rel="stylesheet" type="text/css" href="%s"] + ' "$style" + printf ' + ] [body #top class="%s" + ' "$class" + printf '[ul .menu [li [a "%s/cards/" . %s]][li [a "%s/courses/" . %s]][li [a "%s/ledgers/" . %s]]]' \ + "${_BASE}" "$(l10n cards)" "${_BASE}" "$(l10n courses)" "${_BASE}" "$(l10n ledgers)" + [ "$message" ] && printf '[p #message\n%s\n]' "$(l10n "$message")" + cat + printf '] ]' + } \ + | "${_EXEC}/cgilite/html-sh.sed" +} + +topdir="${_PATH#/}" +topdir="/${topdir%%/*}" + +case ${_PATH} in + "/") REDIRECT "${_BASE}/cards/" + ;; + "/export"/*.pdf) . "$_EXEC/cgilite/file.sh" + FILE "${_DATA}/${_PATH}" "application/pdf" + ;; + "/export"/*) . "$_EXEC/cgilite/file.sh" + FILE "${_DATA}/${_PATH}" + ;; + *) + if [ -d "${_EXEC}/${_PATH}" -a -x "${_EXEC}/${_PATH}/index.cgi" ]; then + . "${_EXEC}/${_PATH}/index.cgi" + elif [ -f "${_EXEC}/${_PATH}" -a -x "${_EXEC}/${_PATH}" ]; then + . "${_EXEC}/${_PATH}" + elif [ -f "${_EXEC}/${_PATH}" -a -r "${_EXEC}/${_PATH}" ]; then + . "$_EXEC/cgilite/file.sh" + FILE "${_EXEC}/${_PATH}" + elif [ -d "${_EXEC}/${topdir}" -a -x "${_EXEC}/${topdir}/index.cgi" ]; then + . "${_EXEC}/${topdir}/index.cgi" + else + printf '%s\r\n' 'Status: 404 Not Found' 'Content-Length: 0' '' + fi + ;; +esac diff --git a/l10n.sh b/l10n.sh new file mode 100755 index 0000000..d9e01d0 --- /dev/null +++ b/l10n.sh @@ -0,0 +1,179 @@ +# Copyright 2014, 2016, 2019, 2021 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +l10n(){ + local word + [ $# -eq 0 ] && read -r word || word="$*" + l10n_global "$word" +} + +l10n_global() { + case $1 in + # Nav Menu + cards) printf %s "Teil­neh­mende";; + courses) printf %s "Kurse";; + ledgers) printf %s "Bei­trä­ge";; + + # VCF Default + PHOTO) printf %s "Foto";; + LOGO) printf %s "Logo";; + FN) printf %s "Voller Name";; + N) printf %s "Name";; + n_pre) printf %s "Titel";; + n_first) printf %s "Vorname";; + n_middle) printf %s "Mittel­namen";; + n_last) printf %s "Nachname";; + n_post) printf %s "Zusätze";; + NICKNAME) printf %s "Spitz­name";; + SOUND) printf %s "Aus­sprache";; + GENDER) printf %s "Ge­schlecht";; + KIND) printf %s "Typ";; + TITLE) printf %s "Beruf";; + ROLE) printf %s "Position";; + ORG) printf %s "Orga­ni­sation";; + MEMBER) printf %s "Mitglied";; + CATEGORIES) printf %s "Kategorien";; + ANNIVERSARY) printf %s "Jubiläum";; + BDAY) printf %s "Geburtstag";; + EMAIL) printf %s "E-Mail";; + TEL) printf %s "Telefon";; + teltype) printf %s "Anschluss­typ:";; + TYPE=HOME) printf %s "Privat";; + TYPE=WORK) printf %s "Geschäft­lich";; + TYPE=CELL) printf %s "Mobil";; + TYPE=FAX) printf %s "Fax";; + IMPP) printf %s "Chat";; + ADR) printf %s "Anschrift";; + URL) printf %s "Webseite";; + LANG) printf %s "Sprache";; + NOTE) printf %s "Notiz";; + RELATED) printf %s "Kontakte";; + + # ICS Default + SUMMARY) printf "Bezeichnung";; + COMMENT) printf "Kommentar";; + DTSTART) printf "Beginn";; + DURATION) printf "Dauer";; + RRULE) printf "Regelmäßigkeit";; + DAILY) printf "Tage";; + WEEKLY) printf "Wochen";; + MONTHLY) printf "Monate";; + YEARLY) printf "Jahre";; + sDAILY) printf "Täglich";; + sWEEKLY) printf "Wöchentlich";; + sMONTHLY) printf "Monatlich";; + sYEARLY) printf "Jährlich";; + + # UI labels + year) printf %s "Jahr";; + month) printf %s "Monat";; + day) printf %s "Tag";; + edit) printf %s "Bearbeiten";; + edit_categories) printf %s "Kategorien Bearbeiten";; + vcf_export) printf %s "Vcard Exportieren";; + control) printf %s "Aktionen";; + delete) printf %s "entfernen";; + edit_update) printf %s "Daten übernehmen";; + edit_cancel) printf %s "Abbrechen";; + edit_delete) printf %s "Eintrag löschen";; + edit_addfieldtext) printf %s "Neues Feld";; + edit_addfield) printf %s "+";; + edit_deletefield) printf %s "X";; + + filter_label) printf %s "Filter";; + filter_item) printf %s "Eingrenzung nach";; + filter_placeholder) printf %s "Begriffe zur Eingrenzung eingeben";; + filter_type) printf %s "Filter­typ";; + filter_order) printf %s "Sortie­rung";; + filter_any) printf %s "Alles";; + filter_name) printf %s "Name";; + filter_firstname) printf %s "Vor­name";; + filter_lastname) printf %s "Nach­name";; + filter_street) printf %s "Straße";; + filter_zip) printf %s "PLZ.";; + filter_TEL) printf %s "Tele­fon";; + filter_BDAY) printf %s "Geburts­jahr";; + filter_bdate) printf %s "Geburts­datum";; + filter_course) printf %s "Kurs";; + filter_CATEGORIES) printf %s "Kate­go­rien";; + filter_more) printf %s "+ mehr Filter";; + filter_apply) printf %s "Filtern";; + filter_cancel) printf %s "Filter löschen";; + export_csv) printf %s "Liste als CSV-Datei";; + + # UI Labels Special + course_attendance) printf %s "Kurs­teil­nahme";; + vcf_seed_label) printf "Anmeld. Vorn. Nachn. Geb.Tag Geb.Monat Geb.Jahr Tel. Mobil () EMail () Notiz";; + '(unnamed course)') printf '(Unbe\302\255nannter Kurs)';; + + gender_none) printf %s "keine Angabe";; + gender_female) printf %s "Weiblich";; + gender_male) printf %s "Männlich";; + gender_other) printf %s "Sonstiges";; + + female) printf %s "♀";; + male) printf %s "♂";; + other) printf %s "⚥";; + none) printf %s "⚪";; + + # Fallback + *) printf %s "$word";; + esac +} + +l10n_time() { + [ $# -eq 0 ] && read -r time || time="$*" + printf '%s\n' "$time" |sed -E ' + s;Monday;Mon\­\;tag;g; s;Mon\.;Mo.;g; + s;Tuesday;Diens\­\;tag;g; s;Tue\.;Di.;g; + s;Wednesday;Mitt\­\;woch;g; s;Wed\.;Mi.;g; + s;Thursday;Don\­\;ners\­\;tag;g; s;Thu\.;Do.;g; + s;Friday;Frei\­\;tag;g; s;Fri\.;Fr.;g; + s;Saturday;Sams\­\;tag;g; s;Sat\.;Sa.;g; + s;Sunday;Sonn\­\;tag;g; s;Sun\.;So.;g; + + s;January;Ja\­\;nu\­\;ar;g; s;Jan\.;Jan.;g; + s;February;Fe\­\;bru\­\;ar;g; s;Feb\.;Feb.;g; + s;March;März;g; s;Mar\.;Mär.;g; + s;April;April;g; s;Apr\.;Apr.;g; + s;May;Mai;g; s;May\.;Mai.;g; + s;June;Juni;g; s;Jun\.;Jun.;g; + s;July;Juli;g; s;Jul\.;Jul.;g; + s;August;Au\­\;gust;g; s;Aug\.;Aug.;g; + s;September;Sep\­\;tem\­\;ber;g; s;Sep\.;Sep.;g; + s;October;Ok\­\;to\­\;ber;g; s;Oct\.;Okt.;g; + s;November;No\­\;vem\­\;ber;g; s;Nov\.;Nov.;g; + s;December;De\­\;zem\­\;ber;g; s;Dec\.;Dez.;g; + ' +} + +parse_date() { + [ $# -eq 0 ] && read -r date || date="$*" + + case $date in + *[0-9].*[0-9].*[0-9]) + d="${date%%.*}" + y="${date##*.}" + m="${date%.*}" + m="${m#*.}" + [ $y -lt 100 ] && y="$((y + 2000))" + date -d "$(printf '%04i-%02i-%02i' "$y" "$m" "$d")" +%F + ;; + *) date -d "$date" +%F + ;; + esac +} diff --git a/ledgers/csv_upload.awk b/ledgers/csv_upload.awk new file mode 100755 index 0000000..04de4d5 --- /dev/null +++ b/ledgers/csv_upload.awk @@ -0,0 +1,100 @@ +#!/bin/awk -f + +function STRING( inp ) { + gsub(/\\/, "\\\\", inp); + gsub(/\n/, "\\n", inp); + gsub(/\r/, "\\r", inp); + gsub(/\t/, "\\t", inp); + gsub(/\+/, "\\+", inp); + gsub(/ /, "+", inp); + return inp ? inp : "\\"; +} + +function UNSTRING( inp, out, tmp ) { + while ( inp ) { + if ( inp ~ /^\\\\/) { out = out "\\"; sub(/^\\\\/, "", inp); } + else if ( inp ~ /^\\n/) { out = out "\n"; sub(/^\\n/, "", inp); } + else if ( inp ~ /^\\r/) { out = out "\r"; sub(/^\\r/, "", inp); } + else if ( inp ~ /^\\t/) { out = out "\t"; sub(/^\\t/, "", inp); } + else if ( inp ~ /^\\+/) { out = out "+"; sub(/^\\+/, "", inp); } + else if ( inp ~ /^\+/) { out = out " "; sub(/^\+/, "", inp); } + else if ( inp ~ /^\\/) { out = out ""; sub(/^\+/, "", inp); } + else { tmp = inp; sub(/[\\+].*$/, "", tmp); out = out tmp; sub(/^[^\\+]*/, "", inp); } + } + return out; +} + +function isdate( date, dt, y, m, d ) { + if ( match( date, + /^[0-9]{4}-((01|03|05|07|08|10|12)-(0[1-9]|[12][0-9]|3[01])|(04|06|09|11)-(0[1-9]|[12][0-9]|30)|02-(0[1-9]|[12][0-9]))$/ )) { + split( date, dt, "-"); + y = dt[1]; m = dt[2]; d = dt[3]; + + } else if ( match( date, + /^((0?1|0?3|0?5|0?7|0?8|10|12)\/(0?[1-9]|[12][0-9]|3[01])|(0?4|0?6|0?9|11)\/(0?[1-9]|[12][0-9]|30)|0?2\/(0[1-9]|[12][0-9]))\/([0-9]{2}|[0-9]{4})$/ )) { + split( date, dt, "/"); + m = dt[1]; d = dt[2]; y = dt[3]; + + } else if ( match( date, + /^((0?[1-9]|[12][0-9]|3[01])[\.\/](0?1|0?3|0?5|0?7|0?8|10|12)|(0?[1-9]|[12][0-9]|30)[\.\/](0?4|0?6|0?9|11)|(0[1-9]|[12][0-9])[\.\/]0?2)[\.\/]([0-9]{2}|[0-9]{4})$/ )) { + split( date, dt, /[\.\/]/); + d = dt[1]; m = dt[2]; y = dt[3]; + + } else return ""; + + if ( y < 100 && y > 50 ) y = y + 1900; + if ( y <= 50 ) y = y + 2000; + + # leap year + if ( m == 2 && d == 29 ) { + if ( y % 400 == 0 ) y = y; + else if ( y % 100 == 0 ) return ""; + else if ( y % 4 == 0 ) y = y; + else return ""; + } + + return sprintf("%04i-%02i-%02i", y, m, d); +} + +function cents( val ) { + gsub(/\./, "", val); sub(/,/, ".", val); + return val * 100; +} + +BEGIN { + FS = ";"; + dtrange_end = dt_from = dt_to = balance_start = balance_end = ""; + split("", rec); + rec[0] = "Date DateU IBAN Name Subject Amount" +} + +/^([012]?[0-9]|30|31).(0?[1-9]|1[012]).[0-9]{4} - ([012]?[0-9]|30|31).(0?[1-9]|1[012]).[0-9]{4}$/ { + dtrange_end = $0; sub(/^.* - /, "", dtrange_end); + dt_from = $0; sub(/ - .*$/, "", dt_from); dt_from = isdate(dt_from); + dt_to = $0; sub(/^.* - /, "", dt_to ); dt_to = isdate(dt_to ); +} + +/^Letzter Kontostand;;;;[0-9\.,]+;EUR$/ { + balance_start = cents($5); +} + +/Kontostand;[^;]+;;;[0-9\.,]+;EUR/ { + if ( $2 = dtrange_end ) balance_end = cents($5) +} + +$18 == "EUR" { + rec_date = isdate($1); gsub(/-/, " ", rec_date); rec_date = mktime(rec_date " 00 00 00", "UTC"); + rec[length(rec)] = sprintf("%s %i %s %s %s %i", + isdate($1), rec_date, $6 ? $6 : "\\", STRING($4), STRING($5), cents($12)); +} + +END { + if ( dt_from && dt_to ) { + dtu_from = dt_from; gsub(/-/, " ", dtu_from); dtu_from = mktime( dtu_from " 00 00 00", "UTC"); + dtu_to = dt_to ; gsub(/-/, " ", dtu_to ); dtu_to = mktime( dtu_to " 00 00 00", "UTC"); + + printf "%i %s %i %s %i %i\n", + dtu_from, dt_from, dtu_to, dt_to, balance_start, balance_end; + for ( k = 1; k < length(rec); k++ ) print rec[k]; + } +} diff --git a/ledgers/csv_upload.sh b/ledgers/csv_upload.sh new file mode 100755 index 0000000..4d25b5c --- /dev/null +++ b/ledgers/csv_upload.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Copyright 2024 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +if [ "${CONTENT_TYPE%%;*}" != "multipart/form-data" ]; then + SET_COOKIE 0 message="Not an upload" + REDIRECT "${_BASE}/ledgers/" +fi + +. "$_EXEC/multipart.sh" +multipart_cache + +# Validate session id from form to prevent CSRF +if [ "$(multipart session_id)" != "$SESSION_ID" ]; then + rm -- "$multipart_cachefile" + SET_COOKIE 0 message="INVALID SESSION ID IN FORM" + REDIRECT "${_BASE}/ledgers/" +fi + +mkdir -p "$_DATA/ledgers/" +CSV="$(multipart "csv" 1 | "$_EXEC/ledgers/csv_upload.awk")" +rm -- "$multipart_cachefile" + +read dtu_start dt_start dtu_end dt_end balance_start balance_end <<-EOF + ${CSV%%${BR}*} +EOF + +if [ ! "$dtu_end" -o ! "$dtu_start" ] || [ "$dtu_end" -lt "$dtu_start" ]; then + SET_COOKIE 0 message="No valid date range in upload" +else + num=0; while [ ! "$filename" -o -f "$_DATA/ledgers/$filename" ]; do + num=$((num + 1)); filename="${dt_start} - ${dt_end} - $(printf '%04i' $num).tbl" + done + printf '%s\n' "$CSV" >"$_DATA/ledgers/$filename" +fi + +REDIRECT "${_BASE}/ledgers/" diff --git a/ledgers/iban_assign.awk b/ledgers/iban_assign.awk new file mode 100755 index 0000000..68a5727 --- /dev/null +++ b/ledgers/iban_assign.awk @@ -0,0 +1,90 @@ +#!/bin/awk -f + +function STRING( inp ) { + gsub(/\\/, "\\\\", inp); + gsub(/\n/, "\\n", inp); + gsub(/\r/, "\\r", inp); + gsub(/\t/, "\\t", inp); + gsub(/\+/, "\\+", inp); + gsub(/ /, "+", inp); + return inp ? inp : "\\"; +} + +function UNSTRING( inp, out, tmp ) { + while ( inp ) { + if ( inp ~ /^\\\\/) { out = out "\\"; sub(/^\\\\/, "", inp); } + else if ( inp ~ /^\\n/) { out = out "\n"; sub(/^\\n/, "", inp); } + else if ( inp ~ /^\\r/) { out = out "\r"; sub(/^\\r/, "", inp); } + else if ( inp ~ /^\\t/) { out = out "\t"; sub(/^\\t/, "", inp); } + else if ( inp ~ /^\\+/) { out = out "+"; sub(/^\\+/, "", inp); } + else if ( inp ~ /^\+/) { out = out " "; sub(/^\+/, "", inp); } + else if ( inp ~ /^\\/) { out = out ""; sub(/^\+/, "", inp); } + else { tmp = inp; sub(/[\\+].*$/, "", tmp); out = out tmp; sub(/^[^\\+]*/, "", inp); } + } + return out; +} + +function rx( regex ) { + gsub(/[].*+?^${}()|\\[]/, "\\\\&", regex); + return regex; +} + +BEGIN { + FS = " "; ledger = 0; + fn = n = uid = iban = tmp = ""; + split("", uid_n); split("", uid_fn); split("", uid_iban); + split("", iban_uid); split("", ibans); split("", uids); + + split("", sure); split("", unsure); split("", unknown); + split("", unsure_rec) +} + +/^BEGIN;:VCARD$/ { fn = n = id = iban = tmp = ""; } + + /^UID;[^:]*:/ { uid = $0; sub(/^[^;]+;[^:]*:/, "", uid); } + /^FN;[^:]*:/ { fn = $0; sub(/^[^;]+;[^:]*:/, "", fn); } + /^N;[^:]*:/ { n = $0; sub(/^[^;]+;[^:]*:/, "", n); sub(/;.*$/, "", n); } +/^X-IBAN;[^:]*:/ { iban = $0; sub(/^[^;]+;[^:]*:/, "", iban); ibans[length(ibans)] = iban; } + +/^END;:VCARD$/ { + uid_n[uid] = n; uid_fn[uid] = fn; uid_iban[uid] = iban; + for (iban in ibans) iban_uid[ibans[iban]] = iban_uid[ibans[iban]] ? iban_uid[ibans[iban]] " " uid : uid; + fn = n = uid = iban = tmp = ""; split("", ibans); +} + +strftime("%Y-%m-%d", $1, "UTC") == $2 && strftime("%Y-%m-%d", $3, "UTC") == $4 { ledger = 1; } + +ledger && strftime("%Y-%m-%d", $2, "UTC") == $1 { + if ($3 in iban_uid) { + sure[$3] = iban_uid[$3]; + } else { + for (uid in uid_fn) if ( match(UNSTRING($5), rx(uid_fn[uid])) ) { + if (! match(unsure[$3], rx(uid))) unsure[$3] = unsure[$3] ? unsure[$3] " " uid : uid; + unsure_rec[$3] = $0 + } + for (uid in uid_n) if ( uid_n[uid] && match(UNSTRING($4), rx(uid_n[uid])) ) { + if (! match(unsure[$3], rx(uid))) unsure[$3] = unsure[$3] ? unsure[$3] " " uid : uid; + unsure_rec[$3] = $0 + } + } + if (!($3 in sure) && !($3 in unsure)) unknown[$3] = $0; +} + +END { + for (iban in sure) { + line = "sure " iban; + split(sure[iban], uids, / /); + for (k in uids) line = line " " STRING(uids[k] "/" uid_fn[uids[k]]); + print line; + } + for (iban in unsure) { + line = "guess " iban " " STRING(unsure_rec[iban]); + split(unsure[iban], uids, / /); + for (k in uids) line = line " " STRING(uids[k] "/" uid_fn[uids[k]]); + print line; + } + for (iban in unknown) { + line = "unknown " iban " " unknown[iban]; + print line; + } +} diff --git a/ledgers/index.cgi b/ledgers/index.cgi new file mode 100755 index 0000000..dbf8ee8 --- /dev/null +++ b/ledgers/index.cgi @@ -0,0 +1,98 @@ +#!/bin/sh + +. "$_EXEC/cgilite/storage.sh" +. "$_EXEC/pdiread.sh" + +credit() { + printf '%03i\n' "$1" \ + | sed -E 's;[0-9]{2}$;d&;; :0 s;([0-9])([0-9]{3}[dm]);\1m\2;; t0; y;dm;,.;' +} + +{ printf ' + [form .upload action="%s/ledgers/csv_upload.sh" method="POST" enctype="multipart/form-data" + [label for=ledger_upload . %s:] + [input #ledger_upload type="file" name="csv" accept=".csv,text/csv"] + [input type=hidden name=session_id value="%s"] + [button type="submit" %s] + ]' \ + "${_BASE}" "$(l10n "Postbank CSV")" "$SESSION_ID" "$(l10n Upload)" + printf ' + [form .ledgers action="%s/ledgers/delete.sh" method=POST + [input type=hidden name=session_id value="%s"] + [h3 . %s] + ' "${_BASE}" "$SESSION_ID" "$(l10n Ledgers)" + for ledger in "$_DATA"/ledgers/????-??-??\ -\ ????-??-??\ -\ ????.tbl; do + ledger="${ledger##*/}" + [ "$ledger" = "????-??-?? - ????-??-?? - ????.tbl" ] && continue + printf '[p .ledger . %s [button type=submit name=delete value="%s" . %s]]' \ + "$(HTML "${ledger% - ????.tbl}")" "$(HTML "$ledger")" "$(l10n delete)" + done + printf ' ]' + printf ' + [form .ibanassign action="%s/ledgers/iban_assign.sh" method=POST + [input type=hidden name=session_id value="%s"] + [h3 . %s] + ' "${_BASE}" "$SESSION_ID" "$(l10n "IBAN Assignments")" + printf '[datalist id=lattendants .' + pdi_load "${_DATA}"/vcard/*.vcf |sed -n '/^FN\;:/!b; s;^FN\;:;;; p;' \ + | while read name; do + printf '[option value="%s"]' "$(HTML "$name")" + done + printf ']' + l10n_attendant="$(l10n attendant)" + { pdi_load "${_DATA}"/vcard/*.vcf + cat "${_DATA}"/ledgers/????-??-??\ -\ ????-??-??\ -\ ????.tbl + } | "${_EXEC}"/ledgers/iban_assign.awk \ + | while read -r state iban data; do + printf '[fieldset .iban .%s [legend . %s ]' \ + "$state" "$iban" + if [ $state = sure ]; then + for card in $data; do + uid="${card%%/*}" name="$(UNSTRING "${card#*/}")" + printf '[span .card . %s]' "$(HTML "${name}")" + done + : + elif [ $state = guess ]; then + record="$(UNSTRING "${data%% *}")" + cards="${data#* }" + date="${record%% *}" + principal="${record#* * * }" principal="${principal%% *}" + subject="${record#* * * * }" subject="${subject%% *}" + amount="${record#* * * * * }" amount="${amount%% *}" + printf '[p .principal . %s][p .date %s][p .amount %s][p .subject . %s]' \ + "$(UNSTRING "$principal" |HTML)" "$date" "$(credit "$amount")" "$(UNSTRING "$subject" |HTML)" + n=0; for card in $cards; do + n=$((n+1)); uid="${card%%/*}" name="$(UNSTRING "${card#*/}")" + cat <<-EOF + [input type=checkbox id="check_${iban}_$n" name="check_${iban}_$n" value=true checked=checked] + [input .card name="fn_${iban}_$n" value="$(HTML "$name")" disabled=disabled] + [label .del for="check_${iban}_$n" . -] + EOF + done + for m in 1 2 3 4 5 6 7 8; do + cat <<-EOF + [input type=checkbox id="check_${iban}_$((n+m))" name="check_${iban}_$((n+m))" value=false] + [input .card name="fn_${iban}_$((n+m))" value="" placeholder="${l10n_attendant}" list="lattendants"] + [label .add for="check_${iban}_$((n+m))" . +] + EOF + done + elif [ $state = unknown ]; then + date="${data%% *}" + principal="${data#* * * }" principal="${principal%% *}" + subject="${data#* * * * }" subject="${subject%% *}" + amount="${data#* * * * * }" amount="${amount%% *}" + printf '[p .principal . %s][p .date %s][p .amount %s][p .subject . %s]' \ + "$(UNSTRING "$principal" |HTML)" "$date" "$(credit "$amount")" "$(UNSTRING "$subject" |HTML)" + printf '[input name="check_" type=hidden][input type=hidden][label .del]' + n=0; for m in 1 2 3 4 5 6 7 8; do + cat <<-EOF + [input type=checkbox id="check_${iban}_$((n+m))" name="check_${iban}_$((n+m))" value=false] + [input .card name="fn_${iban}_$((n+m))" value="" placeholder="${l10n_attendant}" list="lattendants"] + [label .add for="check_${iban}_$((n+m))" . +] + EOF + done + fi + printf ']' + done + printf ' ]' +} | yield_page ledgers diff --git a/multipart.sh b/multipart.sh new file mode 100755 index 0000000..02f7dfb --- /dev/null +++ b/multipart.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +[ "$include_multipart" ] && return 0 +inlude_multipart="$0" + +# Copyright 2022 - 2023 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +if [ "${CONTENT_TYPE}" -a ! "${CONTENT_TYPE##multipart/form-data;*}" ]; then + multipart_boundary="${CONTENT_TYPE#*; boundary=}" + multipart_boundary="${multipart_boundary%%;*}" + multipart_boundary="${multipart_boundary%${CR}}" +fi +multipart_cachefile="/tmp/multipart.$$" + +readbytes(){ + # read n bytes, like `head -c` but do not consume input + local size="$1" block + + for block in 65536 32768 16384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1; do + if [ $size -ge $block ]; then + dd status=none bs="$block" count="$((size / block))" + size="$((size % block))" + fi + done +} + +multipart_cache() { + multipart_cachefile="${1:-${multipart_cachefile}}" # global + + if [ "${multipart_boundary}" ]; then + # readbytes "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}" + head -c "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}" + else + return 1 + fi +} + +multipart(){ + local name="$1" count="${2:-1}" + local formdata state=begin + + while IFS='' read -r formdata; do case "$formdata" in + "--${multipart_boundary}--${CR}") + [ $state = data ] && return 0 \ + || return 1 + ;; + "--${multipart_boundary}${CR}") + [ $state = data ] && return 0 \ + || state=header + ;; + "Content-Disposition: form-data; name=\"${name}\""*"${CR}") + [ $state = header -a $count -eq 1 ] && state=dheader + [ $state = header -a $count -gt 1 ] && count=$((count - 1)) + [ $state = data ] && printf "%s\n" "$formdata" + ;; + "${CR}") + if [ $state = dheader ]; then + # Do not use `sed -n` (or busybox sed will "convert" NULL to LF) + sed "/--${multipart_boundary}\(--\)\?${CR}/{x;q;}" \ + | head -c-3 + return 0; + fi + [ $state = header ] && state=junk + ;; + esac; done <"${multipart_cachefile}" +} + +multipart_filename(){ + local name="$1" count="${2:-1}" + local formdata state=begin + + while read -r formdata; do case "$formdata" in + "--${multipart_boundary}--${CR}") + return 1 + ;; + "--${multipart_boundary}${CR}") + state=header + ;; + "Content-Disposition: form-data; name=\"${name}\"; filename=\""*"\""*"${CR}") + [ $state = header -a $count -eq 1 ] && break + [ $state = header -a $count -gt 1 ] && count=$((count - 1)) + ;; + "${CR}") + [ $state = header ] && state=junk + ;; + esac; done <"${multipart_cachefile}" + + filename="${formdata#*; filename=\"}" + filename="${filename%%\"${CR}}" + filename="${filename%%\";*}" + + HEX_DECODE % "$filename" +} diff --git a/pdiread.sh b/pdiread.sh new file mode 100755 index 0000000..e19ceb8 --- /dev/null +++ b/pdiread.sh @@ -0,0 +1,197 @@ +#!/bin/zsh + +# Copyright 2014 - 2018, 2023 Paul Hänsch +# +# This file is part of Confetti. +# +# Confetti 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. +# +# Confetti 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 Confetti. If not, see . + +# This is a parsing library for the Personal Data Interchange format (PDI) +# PDI is the format for encoding VCard (.vcf) and iCalendar (.ics) files + +[ -n "$include_pdi" ] && return 0 +include_pdi="$0" + +BR=' +' +unescape(){ + local in out='' + [ $# -gt 0 ] && in="$*" || in="$(cat)" + while [ "$in" ]; do case $in in + \\\\*) out="${out}\\"; in="${in#\\\\}" ;; + \\n*) out="${out}${BR}"; in="${in#\\n}" ;; + \\*) in="${in#\\}" ;; + *) out="${out}${in%%[\\]*}"; in="${in#"${in%%[\\]*}"}" ;; + esac; done + printf '%s\n' "$out" + } + +pdi_load() { + # normalise PDI file for processing with pdi_* functions + # functions in this library can only be applied to normalised data + # Usage example: + # data="$(pdi_load file.vcf)" + + sed -srn ' + # === Read entire file into buffer === + :X $bY; N; bX; :Y s;^.*$;\n&\n;; + + # === Join continuing lines, strip trailing CRs === + s;\r*\n[ \t];;g; + s;\r*\n;\n;g; + + # === turn property names to upper case, strip group names === + s;\n([^;:\.\n]+\.)([^;:\n]+);\n\2;g; + :upcase + s;(\n[^;:]*)a;\1A;g; s;(\n[^;:]*)b;\1B;g; s;(\n[^;:]*)c;\1C;g; s;(\n[^;:]*)d;\1D;g; s;(\n[^;:]*)e;\1E;g; + s;(\n[^;:]*)f;\1F;g; s;(\n[^;:]*)g;\1G;g; s;(\n[^;:]*)h;\1H;g; s;(\n[^;:]*)i;\1I;g; s;(\n[^;:]*)j;\1J;g; + s;(\n[^;:]*)k;\1K;g; s;(\n[^;:]*)l;\1L;g; s;(\n[^;:]*)m;\1M;g; s;(\n[^;:]*)n;\1N;g; s;(\n[^;:]*)o;\1O;g; + s;(\n[^;:]*)p;\1P;g; s;(\n[^;:]*)q;\1Q;g; s;(\n[^;:]*)r;\1R;g; s;(\n[^;:]*)s;\1S;g; s;(\n[^;:]*)t;\1T;g; + s;(\n[^;:]*)u;\1U;g; s;(\n[^;:]*)v;\1V;g; s;(\n[^;:]*)w;\1W;g; s;(\n[^;:]*)x;\1X;g; s;(\n[^;:]*)y;\1Y;g; + s;(\n[^;:]*)z;\1Z;g; + t upcase; + + # === Insert empty attribute fields where no attributes are present === + s;\n([^;:]+):;\n\1\;:;g; + + # === Unscramble aggregated fields === + :disag + s;\n([^:\n]+:)(([^\n]*[^\])?(\\\\)*),;\n\1\2\n\1;; + t disag; + + # === Insert FN when only N is present === + /\nFN[;:]/!{ + s,\nN(;[^:]*)?:([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);?\n,&FN;:\5 \3 \4 \2 \6\n,; + :despace + s,(\nFN;:[^\n]*) ([^\n]*\n),\1 \2,; + s,(\nFN;:) ([^\n]*\n),\1\2,; + s,(\nFN;:[^\n]*) (\n),\1\2,; + t despace; + } + /\nFN[;:]/!{ s,\n(N[;:][^\n]*)\n,&F\1\n,; } # Fallback + + # === Normalise various known vendor properties === + s;\nX-MS-CARDPICTURE(\;|:);\nPHOTO\1;g; + s;\nX-GENDER(\;|:);\nGENDER\1;g; + s;\nX-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g; + s;\nX-EVOLUTION-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g; + s;\nX-KADDRESSBOOK-X-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g; + s;\nX-EVOLUTION-BLOG-URL(\;|:);\nURL\1;g; + s;\nAGENT(\;|:);\nRELATED\;VALUE=text\;TYPE=agent\1;g; + s;\nX-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g; + s;\nX-EVOLUTION-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g; + s;\nX-KADDRESSBOOK-X-ASSISTANTSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g; + s;\nX-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g; + s;\nX-EVOLUTION-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g; + s;\nX-KADDRESSBOOK-X-MANAGERSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g; + s;\nX-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g; + s;\nX-EVOLUTION-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g; + s;\nX-KADDRESSBOOK-X-SPOUSENAME(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g; + + # === Normalise obsolete vendor IM properties === + s;\nX-AIM((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g; + s;\nX-ICQ((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g; + s;\nX-GOOGLE-TALK((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g; + s;\nX-JABBER((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g; + s;\nX-MSN((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:msn:;g; + s;\nX-YAHOO((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:ymsgr:;g; + s;\nX-SIP((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(sip:)?;\nIMPP\1:sip:;g; + + # === Update obsolete LABEL property === + s;\nLABEL((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)\n;\nADR\1\;LABEL="\5":\n;g; + + p;' "$@" +} + +pdi_count(){ + local card="$1" name="$2" rc='' cnt=0 + while rc="${card#*${BR}${name};}"; do + [ "${rc}" != "${card}" ] || break + card="$rc" + cnt=$(($cnt + 1)) + done + printf %i\\n $cnt +} + +pdi_attrib(){ + local card=":$1" name="$2" cnt="${3:-1}" attr="$4" + while [ $cnt -gt 0 ]; do + [ "${card#*${BR}${name};}" = "$card" ] && return 1 + card="${card#*${BR}${name};}" + cnt=$((cnt - 1)) + done + card="${card%%:*}" + if [ "$attr" ]; then + case $card in + *\;"$attr"=*) card="${card#*;${attr}=}";; + "$attr"=*) card="${card#${attr}=}";; + "$attr"|*\;"$attr"|"$attr"\;*|*\;"$attr"\;*) return 0;; + *) return 1;; + esac + case $card in + \"*\"\;*|\'*\'\;*) + card="${card#[\"\']}"; card="${card%%[\"\'];*}" + ;; + \"*\"|\'*\') + card="${card#[\"\']}"; card="${card%%[\"\']}" + ;; + *\;*) card="${card%%;*}";; + esac + fi + printf %s\\n "${card}" +} + +pdi_value(){ + local card="${BR}$1" name="$2" cnt="${3:-1}" + while [ "$cnt" -gt 0 ]; do + [ "${card#*${BR}${name};*:}" = "$card" ] && return 1 + card="${card#*${BR}${name};*:}" + cnt=$((cnt - 1)) + done + printf %s\\n "${card%%${BR}*}" +} + +pdi_update_value(){ + local card="${BR}$1" name="$2" cnt="$3" val="$4" + while [ "$cnt" -gt 0 ]; do + if [ "${card#*${BR}${name};*:}" = "${card}" ]; then + printf '%s\n%s;:' "${card%${BR}END;:VCARD*}" "${name}" + card="${BR}END;:VCARD" + break; + else + printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}" + card="${card#*${BR}${name};}" + printf '%s:' "${card%%:*}" + card="${card#*:}" + fi + cnt=$((cnt - 1)) + done + printf '%s\n%s\n' "$val" "${card#*${BR}}" +} + +pdi_update_attrib(){ + local card="${BR}$1" name="$2" cnt="$3" val="$4" + while [ "$cnt" -gt 0 ]; do + if [ "${card#*${BR}${name};*:}" = "${card}" ]; then + printf '%s\n%s;' "${card%${BR}END;:VCARD*}" "${name}" + card=":${BR}END;:VCARD" + break; + else + printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}" + card="${card#*${BR}${name};}" + fi + cnt=$((cnt - 1)) + done + printf '%s:%s\n' "$val" "${card#*:}" +} diff --git a/session.sh b/session.sh deleted file mode 100755 index b9cef4d..0000000 --- a/session.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/bin/sh - -[ -n "$include_session" ] && return 0 -include_session="$0" - -if ! which uuencode >/dev/null; then - uuencode() { busybox uuencode "$@"; } -fi -if ! which sha256sum >/dev/null; then - sha256sum() { busybox sha256sum "$@"; } -fi - -_DATE="$(date +%s)" -SESSION_TIMEOUT="${SESSION_TIMEOUT:-7200}" - -server_key(){ - IDFILE="${IDFILE:-${_DATA:-.}/serverkey}" - if [ "$(stat -c %s "$IDFILE")" -ne 512 ] || ! cat "$IDFILE"; then - dd count=1 bs=512 if=/dev/urandom \ - | tee "$IDFILE" - fi 2>&- -} - -slopecode(){ - # 6-Bit Code that retains sort order of input data, while beeing safe to use - # in ascii transmissions, unix file names, HTTP URLs, and HTML attributes - - uuencode -m - | sed ' - 1d;$d; - y;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/;0123456789:=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz; - ' -} - -session_mac(){ - local info - [ $# -eq 0 ] && info="$(cat)" || info="$*" - - if which openssl >/dev/null; then - printf %s "$info" |openssl dgst -sha1 -hmac "$(server_key)" -binary |slopecode - else - { printf %s "$info"; server_key; } |sha256sum |cut -d\ -f1 - fi -} - -randomid(){ - dd bs=12 count=1 if=/dev/urandom 2>&- \ - | slopecode -} - -timeid(){ - d=$(($_DATE % 4294967296)) - { printf "$( - printf \\%o \ - $((d / 16777216 % 256)) \ - $((d / 65536 % 256)) \ - $((d / 256 % 256)) \ - $((d % 256)) - )" - dd bs=8 count=1 if=/dev/urandom 2>&- - } | slopecode -} - -checkid(){ grep -m 1 -xE '[0-9a-zA-Z:=]{16}'; } - -transid(){ - # transaction ID to modify a given file - local file="$1" - session_mac "$(stat -c %F%i%n%N%s%Y "$file" 2>&-)" "$SESSION_ID" -} - -update_session(){ - local session sid time sig checksig - - read -r sid time sig <<-END - $(POST session_key || COOKIE session) - END - - checksig="$(session_mac "$sid" "$time")" - - if ! [ "$checksig" = "$sig" \ - -a "$time" -ge "$_DATE" \ - -a "$(printf %s "$sid" |checkid)" ] 2>&- - then - debug "Setting up new session" - sid="$(randomid)" - fi - - time=$(( $_DATE + $SESSION_TIMEOUT )) - sig="$(session_mac "$sid" "$time")" - printf %s\\n "${sid} ${time} ${sig}" -} - -SESSION_KEY="$(update_session)" -SET_COOKIE 0 session="$SESSION_KEY" Path=/ SameSite=Strict HttpOnly -SESSION_ID="${SESSION_KEY%% *}" - -SESSION_BIND() { - local key="$1" value="$2" - SET_COOKIE session "$key"="${value} $(session_mac "$value" "$SESSION_ID")" -} - -SESSION_VAR() { - local key="$1" - local value sig - value="$(COOKIE "$key")" - sig="${value##* }" value="${value% *}" - if [ "$sig" = "$(session_mac "$value" "$SESSION_ID")" ]; then - printf %s\\n "$value" - else - return 1 - fi -} diff --git a/session_lock.sh b/session_lock.sh new file mode 100644 index 0000000..de1641a --- /dev/null +++ b/session_lock.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +[ "$include_session_lock" ] && return 0 +include_session_lock="$0" + +SLOCK(){ + local file="$1"; + local timeout="${2-900}" + local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}" + local ovlock="${lockdir%/*}/delete.${lockdir##*/}" + local tempfile="$lockdir/${SESSION_ID}" + local lockexpire=$(( $(date +%s) - timeout )) + + mkdir -p "${lockdir%/*}" + + if [ -e "$lockdir" ] \ + && [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \ + && mkdir "$ovlock"; then + [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \ + && rm -r "$lockdir" + rmdir "$ovlock" + fi + + printf '%s\n' "$tempfile" + if mkdir "$lockdir" 2>&-; then + cp "$file" "$tempfile" + return 0 + else + return 1 + fi +} + +CHECK_SLOCK(){ + local file="$1"; + local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}" + local tempfile="$lockdir/${SESSION_ID}" + + printf '%s\n' "$tempfile" + if [ -f "$tempfile" ]; then + touch "$lockdir" + return 0 + else + return 1 + fi +} + +RELEASE_SLOCK(){ + local file="$1"; + local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}" + local ovlock="${lockdir%/*}/delete.${lockdir##*/}" + local tempfile="$lockdir/${SESSION_ID}" + + if [ -f "$tempfile" ] && mkdir "$ovlock"; then + [ -f "$tempfile" ] && rm -r "$lockdir" + rmdir "$ovlock" + return 0 + else + return 1 + fi +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..742163b --- /dev/null +++ b/style.css @@ -0,0 +1,437 @@ +h1:first-child, h2:first-child, h3:first-child, +p + h1, p + h2, p + h3 { + margin-top: 3pt; +} + +/* ====== COMMON ELEMENTS ======*/ + +body > ul.menu { + display: block; + height: 1.75em; + margin: 0; -padding: 0 .5em; + list-style: none; + color: #CCC; + background-color: #444; + box-shadow: inset 0 -.25em .25em #000; + overflow: hidden; + z-index: 3; +} + +body > .menu li { + display: inline-block; +} +body > .menu a { + color: inherit; + padding: .5em 3em; + box-shadow: inset 0 0 .5em #000; +} +body.ledgers > .menu a[href$="/ledgers/"], +body.courses > .menu a[href$="/courses/"], +body.cards > .menu a[href$="/cards/"] { + color: #000; + background-color: #FFF; + box-shadow: none; +} + +/* =========== FILTER AND SEARCH Headers ========= */ + +form.upload, form.categories, +form.search, form.sort, form.filter, form.newcard, form.newcourses { + margin-top: 1em; padding: .125em 1em 0 1em; + z-index: 1; +} +form.filter > h1 { display: none; } + +form.filter fieldset { margin-top: .5em; } +form.filter fieldset.item + fieldset.item legend { display: none; } + +form.filter fieldset.item input[value=course] ~ input[type=text], +form.filter fieldset.item input[value=CATEGORIES] ~ input[type=text] { display: block; } +form.filter fieldset.item input[value=course]:checked ~ input[type=text], +form.filter fieldset.item input[value=CATEGORIES]:checked ~ input[type=text] { display: none; } +form.filter fieldset.item input[value=course]:checked ~ fieldset.courses, +form.filter fieldset.item input[value=CATEGORIES]:checked ~ fieldset.categories { display: block; } + +form.filter fieldset.order legend { + float: left; margin-right: 1em; +} + +form.filter fieldset label, +form.filter fieldset a { white-space: pre;} +form.filter button[type=submit] { + margin-top: .5em; margin-bottom: .5em; +} + +form.filter button[value=export_csv] { margin-left: 1em; } + +body.courses form .order { display: inline-block; margin-right: 2em;} +body.courses form.search.sort fieldset { margin-top: .5em; } + +body.cards form.newcard { display: flex; } +body.cards form.newcard input[name=seed] { flex: 1; } + +form.upload label { + display: block; + font-weight: bold; + margin-top: .5em; +} + + +/* ============ LIST ITEMS, Generic ============= */ + +body > form, +div.card, +div.course { + position: relative; + width: 98%; width: calc(100% - 2em); + margin-left: auto; margin-right: auto; + margin-bottom: 1em; + box-shadow: .125em .125em .25em; + z-index: 1; +} + +/* HACK: put anchor point 10em above card and highlight target element */ + div:target { box-shadow: none; z-index: 0; } + div:target:before { + content: ''; + display: block; + margin-top: -10em; + height: 10em; + visibility: hidden; + } + div:target:after { + content: ''; + display: block; + position: absolute; + left: 0; right: 0; + top: 10em; bottom: 0; + box-shadow: .125em .125em .25em; + animation: highlight 4s; + z-index: -1; + } + @keyframes highlight { from { background-color: #FF0; } to { background-color: transparent; } } +/**/ + +div .section, form .section { + display: block; + vertical-align: top; + padding: 0 1em; + overflow: hidden; + word-break: break-word; +} + +div .section :last-child, form .section :last-child { + margin-bottom: 1em; +} + +div .section h2, form .section h2, +div .section h3, form .section h3 { + border-bottom: 1pt solid #EEE; +} +div .control, form .control { + background-color: #EEE; + padding: .25em; + text-align: right; +} + +@media(min-width: 60em) { + div .section, form .section { + display: table-cell; + width: calc(100% / 10); + } + div .section :last-child { margin-bottom: 0; } + div .control, form .control { + background-color: transparent; + } + div .section:nth-of-type(2n) { + background-color: #EEE; + } +} +@media(min-width: 80em) { + div .control, form .control { + display: table-cell; + width: calc(100% / 10); + } + div .control .item, form .control .item { + display: block; + margin-bottom: .25em; + } +} + +div .section .item, form .section .item, +form .section.attendance > label { + display: block; + width: 100%; +} + +div .section .item.NOTE { + white-space: pre-wrap; +} + +form .section .item { + margin-bottom: .25em; +} + +form .section button[value^=addfield] { + font-size: .75em; + margin-top: .5em; padding: 0 .375em; +} + +/* HACK: "responsive" Delete Button above each field */ + form input.delete { display: none; } + form input.delete + label { + float: right; + font-size: .75em; + line-height: 1; + max-width: 1.75em; height: 1.125em; overflow: hidden; + color: #FBB; background-color: #444; + margin: 0; padding: .125em .5em 0 .5em; + border-radius: 4pt 4pt 0 0; + transition: max-width .3s; + } + form input.delete + label:before { content: '\274c '; margin-right: .5em; } + form input.delete + label:hover { max-width: 10em; } + -form input.delete + label:hover:before { content: ''; } + -form input.delete + label:hover:after { content: ' \274c'; } + form input.delete:checked + label, + form input.delete:checked + label + *, + form input.delete:checked + label + .teltype + .TEL { + display: none; + } +/**/ + + +/* ====== right hand Control Buttons on list items ====== */ + +form .control { + position: relative; + padding-left: 11em; + padding-top: 1.5em; +} +form .control .item { + display: inline-block; + margin-bottom: .25em; + vertical-align: text-bottom; +} + +/* Combined Select/Submit Box */ + form .control .item.newfield { box-shadow: .125em .125em .25em; } + form .control .item.newfield select { margin-right: -1pt; } + form .control .item.newfield button { box-shadow: none; } +/**/ + +/* HACK: Delete Checkbox before delete Button */ + form .control .item.delete { + position: absolute; + bottom: .375em; left: .25em; width: auto; + padding-bottom: calc(2.25em + 2pt); + } + + form .control .item.delete input + label + button { + display: none; + position: absolute; + bottom: 0; width: 100%; + color: #800; + background-color: #FEE; + z-index: 1; + } + form .control .item.delete:after { + content: attr(label); + display: block; + position: absolute; + bottom: 0; width: 100%; + text-align: center; + color: #BAA; + padding: .25em 0; + border: 1pt solid; + box-shadow: .125em .125em .25em; + } + form .control .item.delete input:checked + label + button { display: block; } +/**/ + +@media(min-width: 80em) { + form .control { padding: .25em; min-height: 16em; } + form .control .item { width: 100%; } + form .control .item.newfield select { width: calc(100% - 2.5em); } + form .control .item.delete { bottom: .125em; right: .25em; } +} + + +/* ======= LIST ITEMS, Cards ======= */ + +form.card .attendance div.attendance, +form.card .attendance div.categories { + max-height: 10em; + overflow-y: auto; +} +form.card .attendance label { + display: inline-block; + width: calc(100% - 2em); + vertical-align: top; + margin-bottom: 0; +} +form.card .attendance input { margin-top: .375em; } + + +/* ======= LIST ITEMS, Courses ======= */ + +form.course .dtstart input[name=DTS_YEAR], +form.course .dtstart select[name=DTS_MONTH] { width: calc(50% - 1.25em); } +form.course .dtstart input[name=DTS_YEAR] { margin-right: -.375em; } +form.course .dtstart table { width: 100%; margin: 1em 0; } +form.course .dtstart table td { text-align: right; -border: .5pt solid; } +form.course .dtstart table input[type=radio] { display: none; } +form.course .dtstart table input[type=radio] + label { + display: block; + width: 100%; + margin: 0; padding: 0 3pt; +} +form.course .dtstart table input[type=radio]:checked + label { + font-weight: bold; + padding: 0 2pt; + box-shadow: .125em .125em .25em; +} + +form.course .dtstart label.DTSTIME { + display: inline-block; + font-weight: bold; + margin: 0; + width: calc(100% - 7.875em); +} +form.course .dtstart input[name=DTS_HOUR], +form.course .dtstart input[name=DTS_MINUTE] { + vertical-align: baseline; + width: 3.5em; + margin-bottom: 0; +} + +form.course .recur .item { white-space: nowrap; } +form.course .recur .item > * { margin-bottom: 0; vertical-align: baseline; } +form.course .recur input[name=RRULE_INTERVAL], +form.course .recur input[name=RRULE_COUNT], +form.course .recur input[name=RRULE_UMONTH], +form.course .recur input[name=RRULE_UDAY] { width: 3.5em; } +form.course .recur input[name=RRULE_UYEAR] { width: 4.5em; } +form.course .recur input[name=RRULE_UYEAR], +form.course .recur input[name=RRULE_UMONTH], +form.course .recur input[name=RRULE_UDAY] { + margin-right: -.375em; +} + +form.course .attendance div.attendance { + max-height: 16em; + overflow-y: scroll; +} +form.course .attendance label { + display: inline-block; + max-width: calc(100% - 2em); + vertical-align: top; + margin-bottom: 0; +} +form.course .attendance input { margin-top: .375em; } + +/* ======== Categories Page ======== */ + +body.categories form ul { list-style: none; margin: 0; } + +form.categories li { + display: inline-block; + background-color: #EEE; + margin-right: .5em; margin-bottom: .5em; + padding-left: .5em; + box-shadow: .125em .125em .25em; +} +form.categories li button[name=remove] { + font-size: .75em; + width: 2.5em; + background-color: #FBB; + overflow: hidden; + white-space: pre; +} +form.categories li button[name=remove]:before { + content: '\274C '; + margin-right: 3em; +} + +form.categories li:last-child { padding-left: 0 } + +body.categories form.namelist ul.namelist > li:nth-of-type(2n + 1) { background-color: #EEE; } +body.categories form.namelist ul.namelist > li h2, +body.categories form.namelist ul.namelist > li ul { + display: inline-block; +} +body.categories form.namelist ul.namelist > li h2 { + width: 20%; + min-width: 10em; +} +body.categories form.namelist ul.namelist > li ul li { + display: inline-block; +} + + +/* ======== Ledgers Page ======== */ + +form.ibanassign, +form.ledgers { + padding: .125em 1em 0 1em; +} + +.ibanassign fieldset.iban.sure { background-color: #DFD; } +.ibanassign fieldset.iban.guess { background-color: #FFD; } +.ibanassign fieldset.iban.unknown { background-color: #FDD; } + +.ibanassign fieldset.iban { + padding: 0 .75em; + margin-top: -.5em; + box-shadow: .25em .25em .25em #AAA; +} +.ibanassign fieldset.iban legend { + top: .75em; +} +.ibanassign fieldset.iban p.principal { + font-size: .875em; +} +.ibanassign fieldset.iban p.date, +.ibanassign fieldset.iban p.amount { + font-size: .875em; + display: inline-block; + vertical-align: top; + margin-right: .75em; + margin-bottom: 0; +} +.ibanassign fieldset.iban p.amount { + font-weight: bold; +} + +.ibanassign fieldset.iban input[name^="check_"], +.ibanassign fieldset.iban input[name^="check_"] + input, +.ibanassign fieldset.iban input[name^="check_"] + input + label { + display: none; +} +.ibanassign fieldset.iban input[name^="check_"]:checked + input, +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label.del, +.ibanassign fieldset.iban input[name^="check_"] + input + label.del + input + input + label.add, +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label + input + input + label.add { + display: inline; +} +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label.add, +.ibanassign fieldset.iban input[name^="check_"] + input + label.del + input:checked + input + label.add, +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label + input:checked + input + label.add { + display: none; +} + +.ibanassign fieldset.iban input[name^="check_"] + input + label { + vertical-align: bottom; + line-height: 2.5em; + padding: .375em .625em; + border: .5pt solid; +} +.ibanassign fieldset.iban input[name^="check_"] + input + label.add { + background-color: #DFD; + border-radius: 2pt; +} +.ibanassign fieldset.iban input[name^="check_"] + input + label.del { + margin: 0 .5em 0 -.25em; + background-color: #FDD; + border-radius: 0 2pt 2pt 0; +}